/ doc / rstPreproc.nim
rstPreproc.nim
  1  ## This is a tool for preprocessing rst files. Lines starting with ``%`` will
  2  ## get substituted by nicely layouted nim and yaml code included from file in
  3  ## the snippets tree.
  4  ##
  5  ## The syntax of substituted lines is ``'%' path '%' level``. *path* shall be
  6  ## a path relative to the *snippets* directory. *level* shall be the level depth
  7  ## of the first title that should be produced.
  8  ##
  9  ## Usage:
 10  ##
 11  ##     rstPreproc -o:path <infile>
 12  ##
 13  ## *path* is the output path. If omitted, it will be equal to infile with its
 14  ## suffix substituted by ``.rst``. *infile* is the source rst file.
 15  ##
 16  ## The reason for this complex approach is to have all snippets used in the docs
 17  ## available as source files for automatic testing. This way, we can make sure
 18  ## that the code in the docs actually works.
 19  
 20  import parseopt, streams, tables, strutils, unicode, os, options, algorithm
 21  
 22  var
 23    infile = ""
 24    path = none(string)
 25  for kind, key, val in getopt():
 26    case kind
 27    of cmdArgument:
 28      if infile == "":
 29        if key == "":
 30          echo "invalid input file with empty name!"
 31          quit 1
 32        infile = key
 33      else:
 34        echo "Only one input file is supported!"
 35        quit 1
 36    of cmdLongOption, cmdShortOption:
 37      case key
 38      of "out", "o":
 39        if path.isNone: path = some(val)
 40        else:
 41          echo "Duplicate output path!"
 42          quit 1
 43      else:
 44        echo "Unknown option: ", key
 45        quit 1
 46    of cmdEnd: assert(false) # cannot happen
 47  
 48  if infile == "":
 49    echo "Missing input file!"
 50    quit 1
 51  
 52  if path.isNone:
 53    for i in countdown(infile.len - 1, 0):
 54      if infile[i] == '.':
 55        if infile[i..^1] == ".rst": path = some(infile & ".rst")
 56        else: path = some(infile[0..i] & "rst")
 57        break
 58    if path.isNone: path = some(infile & ".rst")
 59  
 60  var tmpOut = newFileStream(path.get(), fmWrite)
 61  
 62  proc append(s: string) =
 63    tmpOut.writeLine(s)
 64  
 65  const headingChars = ['=', '-', '^', ':', '\'']
 66  
 67  proc outputExamples(curPath: string, level: int = 0) =
 68    let titlePath = curPath / "title"
 69    if fileExists(titlePath):
 70      let titleFile = open(titlePath, fmRead)
 71      defer: titleFile.close()
 72      var title = ""
 73      if titleFile.readLine(title):
 74        let headingChar = if level >= headingChars.len: headingChars[^1] else:
 75            headingChars[level]
 76        append(title)
 77        append(repeat(headingChar, title.len) & '\l')
 78  
 79    # process content files under this directory
 80  
 81    var subdirs = newSeq[string]()
 82    var codeFiles = newSeq[string]()
 83    for kind, filePath in walkDir(curPath, true):
 84      if kind == pcFile:
 85        if filePath != "title": codeFiles.add(filePath)
 86      elif kind == pcDir:
 87        subdirs.add(filePath)
 88    codeFiles.sort()
 89    case codeFiles.len
 90    of 0: discard
 91    of 1:
 92      let (_, _, extension) = codeFiles[0].splitFile()
 93      append(".. code:: " & extension[1..^1])
 94      append("   :file: " & (curPath / codeFiles[0]) & '\l')
 95    of 2:
 96      append(".. raw:: html")
 97      append("  <table class=\"quickstart-example\"><thead><tr>")
 98      for codeFile in codeFiles:
 99        append("    <th>" & codeFile[3..^1] & "</th>")
100      append("  </th></tr></thead><tbody><tr><td>\n")
101  
102      var first = true
103      for codeFile in codeFiles:
104        if first: first = false
105        else: append(".. raw:: html\n  </td>\n  <td>\n")
106        let (_, _, extension) = codeFile.splitFile()
107        append(".. code:: " & extension[1..^1])
108        append("   :file: " & (curPath / codeFile) & '\l')
109  
110      append(".. raw:: html")
111      append("  </td></tr></tbody></table>\n")
112    else:
113      echo "Unexpected number of files in ", curPath, ": ", codeFiles.len
114  
115    # process child directories
116  
117    subdirs.sort()
118    for dirPath in subdirs:
119      outputExamples(curPath / dirPath, level + 1)
120  
121  var lineNum = 0
122  for line in infile.lines():
123    if line.len > 0 and line[0] == '%':
124      var
125        srcPath = none(string)
126        level = 0
127      for i in 1..<line.len:
128        if line[i] == '%':
129          srcPath = some(line[1 .. i - 1])
130          level = parseInt(line[i + 1 .. ^1])
131          break
132      if srcPath.isNone:
133        echo "Second % missing in line " & $lineNum & "! content:\n"
134        echo line
135        quit 1
136      outputExamples("snippets" / srcPath.get(), level)
137    else:
138      append(line)