summary refs log tree commit diff stats
path: root/compiler/docgen.nim
diff options
context:
space:
mode:
Diffstat (limited to 'compiler/docgen.nim')
-rw-r--r--compiler/docgen.nim1579
1 files changed, 1061 insertions, 518 deletions
diff --git a/compiler/docgen.nim b/compiler/docgen.nim
index cb8c77e87..8e5f5e4e7 100644
--- a/compiler/docgen.nim
+++ b/compiler/docgen.nim
@@ -7,43 +7,96 @@
 #    distribution, for details about the copyright.
 #
 
-# This is the documentation generator. It is currently pretty simple: No
-# semantic checking is done for the code. Cross-references are generated
-# by knowing how the anchors are going to be named.
+## This is the Nim documentation generator. Cross-references are generated
+## by knowing how the anchors are going to be named.
+##
+## .. importdoc:: ../docgen.md
+##
+## For corresponding users' documentation see [Nim DocGen Tools Guide].
 
 import
-  ast, strutils, strtabs, options, msgs, os, ropes, idents,
-  wordrecg, syntaxes, renderer, lexer, packages/docutils/rstast,
-  packages/docutils/rst, packages/docutils/rstgen,
-  json, xmltree, cgi, trees, types,
-  typesrenderer, astalgo, lineinfos, intsets,
-  pathutils, trees, tables, nimpaths, renderverbatim, osproc
+  ast, options, msgs, idents,
+  wordrecg, syntaxes, renderer, lexer,
+  packages/docutils/[rst, rstidx, rstgen, dochelpers],
+  trees, types,
+  typesrenderer, astalgo, lineinfos,
+  pathutils, nimpaths, renderverbatim, packages
+import packages/docutils/rstast except FileIndex, TLineInfo
+
+import std/[os, strutils, strtabs, algorithm, json, osproc, tables, intsets, xmltree, sequtils]
+from std/uri import encodeUrl
+from nodejs import findNodeJs
+
+when defined(nimPreviewSlimSystem):
+  import std/[assertions, syncio]
 
-from std/private/globs import nativeToUnixPath
 
 const
   exportSection = skField
   docCmdSkip = "skip"
+  DocColOffset = "## ".len  # assuming that a space was added after ##
 
 type
-  TSections = array[TSymKind, Rope]
+  ItemFragment = object  ## A fragment from each item will be eventually
+                         ## constructed by converting `rst` fields to strings.
+    case isRst: bool
+    of true:
+      rst: PRstNode
+    of false:            ## contains ready markup e.g. from runnableExamples
+      str: string
+  ItemPre = seq[ItemFragment]  ## A pre-processed item.
+  Item = object        ## Any item in documentation, e.g. symbol
+                       ## entry. Configuration variable ``doc.item``
+                       ## is used for its HTML rendering.
+    descRst: ItemPre     ## Description of the item (may contain
+                         ## runnableExamples).
+    substitutions: seq[string]    ## Variable names in `doc.item`...
+    sortName: string    ## The string used for sorting in output
+    info: rstast.TLineInfo  ## place where symbol was defined (for messages)
+    anchor: string  ## e.g. HTML anchor
+    name: string  ## short name of the symbol, not unique
+                  ## (includes backticks ` if present)
+    detailedName: string  ## longer name like `proc search(x: int): int`
+  ModSection = object  ## Section like Procs, Types, etc.
+    secItems: Table[string, seq[Item]]
+                         ## Map basic name -> pre-processed items.
+    finalMarkup: string  ## The items, after RST pass 2 and rendering.
+  ModSections = array[TSymKind, ModSection]
+  TocItem = object  ## HTML TOC item
+    content: string
+    sortName: string
+  TocSectionsFinal = array[TSymKind, string]
   ExampleGroup = ref object
     ## a group of runnableExamples with same rdoccmd
     rdoccmd: string ## from 1st arg in `runnableExamples(rdoccmd): body`
-    docCmd: string ## from user config, eg --doccmd:-d:foo
+    docCmd: string ## from user config, e.g. --doccmd:-d:foo
     code: string ## contains imports; each import contains `body`
     index: int ## group index
+  JsonItem = object  # pre-processed item: `rst` should be finalized
+    json: JsonNode
+    rst: PRstNode
+    rstField: string
   TDocumentor = object of rstgen.RstGenerator
-    modDesc: Rope       # module description
+    modDescPre: ItemPre   # module description, not finalized
+    modDescFinal: string  # module description, after RST pass 2 and rendering
     module: PSym
-    modDeprecationMsg: Rope
-    toc, section: TSections
+    modDeprecationMsg: string
+    section: ModSections     # entries of ``.nim`` file (for `proc`s, etc)
+    tocSimple: array[TSymKind, seq[TocItem]]
+      # TOC entries for non-overloadable symbols (e.g. types, constants)...
+    tocTable:  array[TSymKind, Table[string, seq[TocItem]]]
+      # ...otherwise (e.g. procs)
+    toc2: TocSectionsFinal  # TOC `content`, which is probably wrapped
+                            # in `doc.section.toc2`
+    toc: TocSectionsFinal  # final TOC (wrapped in `doc.section.toc`)
     indexValFilename: string
     analytics: string  # Google Analytics javascript, "" if doesn't exist
     seenSymbols: StringTableRef # avoids duplicate symbol generation for HTML.
-    jArray: JsonNode
+    jEntriesPre: seq[JsonItem] # pre-processed RST + JSON content
+    jEntriesFinal: JsonNode    # final JSON after RST pass 2 and rendering
     types: TStrTable
-    isPureRst: bool
+    sharedState: PRstSharedState
+    standaloneDoc: bool        # is markup (.rst/.md) document?
     conf*: ConfigRef
     cache*: IdentCache
     exampleCounter: int
@@ -52,10 +105,77 @@ type
     thisDir*: AbsoluteDir
     exampleGroups: OrderedTable[string, ExampleGroup]
     wroteSupportFiles*: bool
+    nimToRstFid: Table[lineinfos.FileIndex, rstast.FileIndex]
+      ## map Nim FileIndex -> RST one, it's needed because we keep them separate
 
   PDoc* = ref TDocumentor ## Alias to type less.
 
-proc presentationPath*(conf: ConfigRef, file: AbsoluteFile, isTitle = false): RelativeFile =
+proc add(dest: var ItemPre, rst: PRstNode) = dest.add ItemFragment(isRst: true, rst: rst)
+proc add(dest: var ItemPre, str: string) = dest.add ItemFragment(isRst: false, str: str)
+
+proc addRstFileIndex(d: PDoc, fileIndex: lineinfos.FileIndex): rstast.FileIndex =
+  let invalid = rstast.FileIndex(-1)
+  result = d.nimToRstFid.getOrDefault(fileIndex, default = invalid)
+  if result == invalid:
+    let fname = toFullPath(d.conf, fileIndex)
+    result = addFilename(d.sharedState, fname)
+    d.nimToRstFid[fileIndex] = result
+
+proc addRstFileIndex(d: PDoc, info: lineinfos.TLineInfo): rstast.FileIndex =
+  addRstFileIndex(d, info.fileIndex)
+
+proc cmpDecimalsIgnoreCase(a, b: string): int =
+  ## For sorting with correct handling of cases like 'uint8' and 'uint16'.
+  ## Also handles leading zeros well (however note that leading zeros are
+  ## significant when lengths of numbers mismatch, e.g. 'bar08' > 'bar8' !).
+  runnableExamples:
+    doAssert cmpDecimalsIgnoreCase("uint8", "uint16") < 0
+    doAssert cmpDecimalsIgnoreCase("val00032", "val16suffix") > 0
+    doAssert cmpDecimalsIgnoreCase("val16suffix", "val16") > 0
+    doAssert cmpDecimalsIgnoreCase("val_08_32", "val_08_8") > 0
+    doAssert cmpDecimalsIgnoreCase("val_07_32", "val_08_8") < 0
+    doAssert cmpDecimalsIgnoreCase("ab8", "ab08") < 0
+    doAssert cmpDecimalsIgnoreCase("ab8de", "ab08c") < 0 # sanity check
+  let aLen = a.len
+  let bLen = b.len
+  var
+    iA = 0
+    iB = 0
+  while iA < aLen and iB < bLen:
+    if isDigit(a[iA]) and isDigit(b[iB]):
+      var
+        limitA = iA  # index after the last (least significant) digit
+        limitB = iB
+      while limitA < aLen and isDigit(a[limitA]): inc limitA
+      while limitB < bLen and isDigit(b[limitB]): inc limitB
+      var pos = max(limitA-iA, limitB-iA)
+      while pos > 0:
+        if limitA-pos < iA:  # digit in `a` is 0 effectively
+          result = ord('0') - ord(b[limitB-pos])
+        elif limitB-pos < iB:  # digit in `b` is 0 effectively
+          result = ord(a[limitA-pos]) - ord('0')
+        else:
+          result = ord(a[limitA-pos]) - ord(b[limitB-pos])
+        if result != 0: return
+        dec pos
+      result = (limitA - iA) - (limitB - iB)  # consider 'bar08' > 'bar8'
+      if result != 0: return
+      iA = limitA
+      iB = limitB
+    else:
+      result = ord(toLowerAscii(a[iA])) - ord(toLowerAscii(b[iB]))
+      if result != 0: return
+      inc iA
+      inc iB
+  result = (aLen - iA) - (bLen - iB)
+
+proc prettyString(a: object): string =
+  # xxx pending std/prettyprint refs https://github.com/nim-lang/RFCs/issues/203#issuecomment-602534906
+  result = ""
+  for k, v in fieldPairs(a):
+    result.add k & ": " & $v & "\n"
+
+proc presentationPath*(conf: ConfigRef, file: AbsoluteFile): RelativeFile =
   ## returns a relative file that will be appended to outDir
   let file2 = $file
   template bail() =
@@ -80,7 +200,7 @@ proc presentationPath*(conf: ConfigRef, file: AbsoluteFile, isTitle = false): Re
     if result.isEmpty: bail()
   elif conf.docRoot.len > 0:
     # we're (currently) requiring `isAbsolute` to avoid confusion when passing
-    # a relative path (would it be relative wrt $PWD or to projectfile)
+    # a relative path (would it be relative with regard to $PWD or to projectfile)
     conf.globalAssert conf.docRoot.isAbsolute, arg=conf.docRoot
     conf.globalAssert conf.docRoot.dirExists, arg=conf.docRoot
     # needed because `canonicalizePath` called on `file`
@@ -89,10 +209,7 @@ proc presentationPath*(conf: ConfigRef, file: AbsoluteFile, isTitle = false): Re
     bail()
   if isAbsolute(result.string):
     result = file.string.splitPath()[1].RelativeFile
-  if isTitle:
-    result = result.string.nativeToUnixPath.RelativeFile
-  else:
-    result = result.string.replace("..", dotdotMangle).RelativeFile
+  result = result.string.replace("..", dotdotMangle).RelativeFile
   doAssert not result.isEmpty
   doAssert not isAbsolute(result.string)
 
@@ -100,12 +217,16 @@ proc whichType(d: PDoc; n: PNode): PSym =
   if n.kind == nkSym:
     if d.types.strTableContains(n.sym):
       result = n.sym
+    else:
+      result = nil
   else:
+    result = nil
     for i in 0..<n.safeLen:
       let x = whichType(d, n[i])
       if x != nil: return x
 
 proc attachToType(d: PDoc; p: PSym): PSym =
+  result = nil
   let params = p.ast[paramsPos]
   template check(i) =
     result = whichType(d, params[i])
@@ -117,7 +238,7 @@ proc attachToType(d: PDoc; p: PSym): PSym =
   if params.len > 0: check(0)
   for i in 2..<params.len: check(i)
 
-template declareClosures =
+template declareClosures(currentFilename: AbsoluteFile, destFile: string) =
   proc compilerMsgHandler(filename: string, line, col: int,
                           msgKind: rst.MsgKind, arg: string) {.gcsafe.} =
     # translate msg kind:
@@ -125,16 +246,35 @@ template declareClosures =
     case msgKind
     of meCannotOpenFile: k = errCannotOpenFile
     of meExpected: k = errXExpected
-    of meGridTableNotImplemented: k = errGridTableNotImplemented
-    of meNewSectionExpected: k = errNewSectionExpected
-    of meGeneralParseError: k = errGeneralParseError
-    of meInvalidDirective: k = errInvalidDirectiveX
-    of mwRedefinitionOfLabel: k = warnRedefinitionOfLabel
-    of mwUnknownSubstitution: k = warnUnknownSubstitutionX
-    of mwUnsupportedLanguage: k = warnLanguageXNotSupported
-    of mwUnsupportedField: k = warnFieldXNotSupported
+    of meMissingClosing: k = errRstMissingClosing
+    of meGridTableNotImplemented: k = errRstGridTableNotImplemented
+    of meMarkdownIllformedTable: k = errRstMarkdownIllformedTable
+    of meIllformedTable: k = errRstIllformedTable
+    of meNewSectionExpected: k = errRstNewSectionExpected
+    of meGeneralParseError: k = errRstGeneralParseError
+    of meInvalidDirective: k = errRstInvalidDirectiveX
+    of meInvalidField: k = errRstInvalidField
+    of meFootnoteMismatch: k = errRstFootnoteMismatch
+    of meSandboxedDirective: k = errRstSandboxedDirective
+    of mwRedefinitionOfLabel: k = warnRstRedefinitionOfLabel
+    of mwUnknownSubstitution: k = warnRstUnknownSubstitutionX
+    of mwAmbiguousLink: k = warnRstAmbiguousLink
+    of mwBrokenLink: k = warnRstBrokenLink
+    of mwUnsupportedLanguage: k = warnRstLanguageXNotSupported
+    of mwUnsupportedField: k = warnRstFieldXNotSupported
+    of mwUnusedImportdoc: k = warnRstUnusedImportdoc
+    of mwRstStyle: k = warnRstStyle
     {.gcsafe.}:
-      globalError(conf, newLineInfo(conf, AbsoluteFile filename, line, col), k, arg)
+      let errorsAsWarnings = (roPreferMarkdown in d.sharedState.options) and
+          not d.standaloneDoc  # not tolerate errors in .rst/.md files
+      if whichMsgClass(msgKind) == mcError and errorsAsWarnings:
+        liMessage(conf, newLineInfo(conf, AbsoluteFile filename, line, col),
+                  k, arg, doNothing, instLoc(), ignoreError=true)
+        # when our Markdown parser fails, we currently can only terminate the
+        # parsing (and then we will return monospaced text instead of markup):
+        raiseRecoverableError("")
+      else:
+        globalError(conf, newLineInfo(conf, AbsoluteFile filename, line, col), k, arg)
 
   proc docgenFindFile(s: string): string {.gcsafe.} =
     result = options.findFile(conf, s).string
@@ -142,13 +282,30 @@ template declareClosures =
       result = getCurrentDir() / s
       if not fileExists(result): result = ""
 
-proc parseRst(text, filename: string,
-              line, column: int, hasToc: var bool,
-              rstOptions: RstParseOptions;
-              conf: ConfigRef): PRstNode =
-  declareClosures()
-  result = rstParse(text, filename, line, column, hasToc, rstOptions,
-                    docgenFindFile, compilerMsgHandler)
+  proc docgenFindRefFile(targetRelPath: string):
+         tuple[targetPath: string, linkRelPath: string] {.gcsafe.} =
+    let fromDir = splitFile(destFile).dir  # dir where we reference from
+    let basedir = os.splitFile(currentFilename.string).dir
+    let outDirPath: RelativeFile =
+        presentationPath(conf, AbsoluteFile(basedir / targetRelPath))
+          # use presentationPath because `..` path can be be mangled to `_._`
+    result = (string(conf.outDir / outDirPath), "")
+    if not fileExists(result.targetPath):
+      # this can happen if targetRelPath goes to parent directory `OUTDIR/..`.
+      # Trying it, this may cause ambiguities, but allows us to insert
+      # "packages" into each other, which is actually used in Nim repo itself.
+      let destPath = fromDir / targetRelPath
+      if destPath != result.targetPath and fileExists(destPath):
+        result.targetPath = destPath
+
+    result.linkRelPath = relativePath(result.targetPath.splitFile.dir,
+                                      fromDir).replace('\\', '/')
+
+
+proc parseRst(text: string,
+              line, column: int,
+              conf: ConfigRef, sharedState: PRstSharedState): PRstNode =
+  result = rstParsePass1(text, line, column, sharedState)
 
 proc getOutFile2(conf: ConfigRef; filename: RelativeFile,
                  ext: string, guessTarget: bool): AbsoluteFile =
@@ -161,17 +318,39 @@ proc getOutFile2(conf: ConfigRef; filename: RelativeFile,
   else:
     result = getOutFile(conf, filename, ext)
 
-proc newDocumentor*(filename: AbsoluteFile; cache: IdentCache; conf: ConfigRef, outExt: string = HtmlExt, module: PSym = nil): PDoc =
-  declareClosures()
+proc isLatexCmd(conf: ConfigRef): bool =
+  conf.cmd in {cmdRst2tex, cmdMd2tex, cmdDoc2tex}
+
+proc newDocumentor*(filename: AbsoluteFile; cache: IdentCache; conf: ConfigRef,
+                    outExt: string = HtmlExt, module: PSym = nil,
+                    standaloneDoc = false, preferMarkdown = true,
+                    hasToc = true): PDoc =
+  let destFile = getOutFile2(conf, presentationPath(conf, filename), outExt, false).string
   new(result)
+  let d = result  # pass `d` to `declareClosures`:
+  declareClosures(currentFilename = filename, destFile = destFile)
   result.module = module
   result.conf = conf
   result.cache = cache
-  result.outDir = conf.outDir
-  initRstGenerator(result[], (if conf.cmd != cmdRst2tex: outHtml else: outLatex),
-                   conf.configVars, filename.string, {roSupportRawDirective, roSupportMarkdown},
+  result.outDir = conf.outDir.string
+  result.standaloneDoc = standaloneDoc
+  var options= {roSupportRawDirective, roSupportMarkdown, roSandboxDisabled}
+  if preferMarkdown:
+    options.incl roPreferMarkdown
+  if not standaloneDoc: options.incl roNimFile
+  # (options can be changed dynamically in `setDoctype` by `{.doctype.}`)
+  result.hasToc = hasToc
+  result.sharedState = newRstSharedState(
+      options, filename.string,
+      docgenFindFile, docgenFindRefFile, compilerMsgHandler, hasToc)
+  initRstGenerator(result[], (if conf.isLatexCmd: outLatex else: outHtml),
+                   conf.configVars, filename.string,
                    docgenFindFile, compilerMsgHandler)
 
+  if conf.configVars.hasKey("doc.googleAnalytics") and
+      conf.configVars.hasKey("doc.plausibleAnalytics"):
+    raiseAssert "Either use googleAnalytics or plausibleAnalytics"
+
   if conf.configVars.hasKey("doc.googleAnalytics"):
     result.analytics = """
 <script>
@@ -185,22 +364,27 @@ proc newDocumentor*(filename: AbsoluteFile; cache: IdentCache; conf: ConfigRef,
 
 </script>
     """ % [conf.configVars.getOrDefault"doc.googleAnalytics"]
+  elif conf.configVars.hasKey("doc.plausibleAnalytics"):
+    result.analytics = """
+    <script defer data-domain="$1" src="https://plausible.io/js/plausible.js"></script>
+    """ % [conf.configVars.getOrDefault"doc.plausibleAnalytics"]
   else:
     result.analytics = ""
 
   result.seenSymbols = newStringTable(modeCaseInsensitive)
   result.id = 100
-  result.jArray = newJArray()
-  initStrTable result.types
+  result.jEntriesFinal = newJArray()
+  result.types = initStrTable()
   result.onTestSnippet =
-    proc (gen: var RstGenerator; filename, cmd: string; status: int; content: string) =
+    proc (gen: var RstGenerator; filename, cmd: string; status: int; content: string) {.gcsafe.} =
       if conf.docCmd == docCmdSkip: return
       inc(gen.id)
-      var d = TDocumentor(gen)
+      var d = (ptr TDocumentor)(addr gen)
       var outp: AbsoluteFile
       if filename.len == 0:
         let nameOnly = splitFile(d.filename).name
-        outp = getNimcacheDir(conf) / RelativeDir(nameOnly) /
+        # "snippets" needed, refs bug #17183
+        outp = getNimcacheDir(conf) / "snippets".RelativeDir / RelativeDir(nameOnly) /
                RelativeFile(nameOnly & "_snippet_" & $d.id & ".nim")
       elif isAbsolute(filename):
         outp = AbsoluteFile(filename)
@@ -211,17 +395,21 @@ proc newDocumentor*(filename: AbsoluteFile; cache: IdentCache; conf: ConfigRef,
       # Make sure the destination directory exists
       createDir(outp.splitFile.dir)
       # Include the current file if we're parsing a nim file
-      let importStmt = if d.isPureRst: "" else: "import \"$1\"\n" % [d.filename.replace("\\", "/")]
+      let importStmt = if d.standaloneDoc: "" else: "import \"$1\"\n" % [d.filename.replace("\\", "/")]
       writeFile(outp, importStmt & content)
 
       proc interpSnippetCmd(cmd: string): string =
         # backward compatibility hacks; interpolation commands should explicitly use `$`
         if cmd.startsWith "nim ": result = "$nim " & cmd[4..^1]
         else: result = cmd
+        # factor with D20210224T221756
         result = result.replace("$1", "$options") % [
           "nim", os.getAppFilename().quoteShell,
+          "libpath", quoteShell(d.conf.libpath),
+          "docCmd", d.conf.docCmd,
           "backend", $d.conf.backend,
           "options", outp.quoteShell,
+            # xxx `quoteShell` seems buggy if user passes options = "-d:foo somefile.nim"
         ]
       let cmd = cmd.interpSnippetCmd
       rawMessage(conf, hintExecuting, cmd)
@@ -229,11 +417,12 @@ proc newDocumentor*(filename: AbsoluteFile; cache: IdentCache; conf: ConfigRef,
       if gotten != status:
         rawMessage(conf, errGenerated, "snippet failed: cmd: '$1' status: $2 expected: $3 output: $4" % [cmd, $gotten, $status, output])
   result.emitted = initIntSet()
-  result.destFile = getOutFile2(conf, presentationPath(conf, filename), outExt, false)
-  result.thisDir = result.destFile.splitFile.dir
+  result.destFile = destFile
+  result.thisDir = result.destFile.AbsoluteFile.splitFile.dir
 
-template dispA(conf: ConfigRef; dest: var Rope, xml, tex: string, args: openArray[Rope]) =
-  if conf.cmd != cmdRst2tex: dest.addf(xml, args)
+template dispA(conf: ConfigRef; dest: var string, xml, tex: string,
+               args: openArray[string]) =
+  if not conf.isLatexCmd: dest.addf(xml, args)
   else: dest.addf(tex, args)
 
 proc getVarIdx(varnames: openArray[string], id: string): int =
@@ -242,93 +431,35 @@ proc getVarIdx(varnames: openArray[string], id: string): int =
       return i
   result = -1
 
-proc ropeFormatNamedVars(conf: ConfigRef; frmt: FormatStr,
-                         varnames: openArray[string],
-                         varvalues: openArray[Rope]): Rope =
-  var i = 0
-  result = nil
-  var num = 0
-  while i < frmt.len:
-    if frmt[i] == '$':
-      inc(i)                  # skip '$'
-      case frmt[i]
-      of '#':
-        result.add(varvalues[num])
-        inc(num)
-        inc(i)
-      of '$':
-        result.add("$")
-        inc(i)
-      of '0'..'9':
-        var j = 0
-        while true:
-          j = (j * 10) + ord(frmt[i]) - ord('0')
-          inc(i)
-          if (i > frmt.len + 0 - 1) or not (frmt[i] in {'0'..'9'}): break
-        if j > high(varvalues) + 1:
-          rawMessage(conf, errGenerated, "Invalid format string; too many $s: " & frmt)
-        num = j
-        result.add(varvalues[j - 1])
-      of 'A'..'Z', 'a'..'z', '\x80'..'\xFF':
-        var id = ""
-        while true:
-          id.add(frmt[i])
-          inc(i)
-          if not (frmt[i] in {'A'..'Z', '_', 'a'..'z', '\x80'..'\xFF'}): break
-        var idx = getVarIdx(varnames, id)
-        if idx >= 0: result.add(varvalues[idx])
-        else: rawMessage(conf, errGenerated, "unknown substition variable: " & id)
-      of '{':
-        var id = ""
-        inc(i)
-        while i < frmt.len and frmt[i] != '}':
-          id.add(frmt[i])
-          inc(i)
-        if i >= frmt.len:
-          rawMessage(conf, errGenerated, "expected closing '}'")
-        else:
-          inc(i)                # skip }
-        # search for the variable:
-        let idx = getVarIdx(varnames, id)
-        if idx >= 0: result.add(varvalues[idx])
-        else: rawMessage(conf, errGenerated, "unknown substition variable: " & id)
-      else:
-        result.add("$")
-    var start = i
-    while i < frmt.len:
-      if frmt[i] != '$': inc(i)
-      else: break
-    if i - 1 >= start: result.add(substr(frmt, start, i - 1))
-
-proc genComment(d: PDoc, n: PNode): string =
-  result = ""
-  var dummyHasToc: bool
+proc genComment(d: PDoc, n: PNode): PRstNode =
   if n.comment.len > 0:
-    var comment2 = n.comment
-    when false:
-      # RFC: to preseve newlines in comments, this would work:
-      comment2 = comment2.replace("\n", "\n\n")
-    renderRstToOut(d[], parseRst(comment2, toFullPath(d.conf, n.info),
-                               toLinenumber(n.info), toColumn(n.info),
-                               dummyHasToc, d.options, d.conf), result)
+    d.sharedState.currFileIdx = addRstFileIndex(d, n.info)
+    try:
+      result = parseRst(n.comment,
+                        toLinenumber(n.info),
+                        toColumn(n.info) + DocColOffset,
+                        d.conf, d.sharedState)
+    except ERecoverableError:
+      result = newRstNode(rnLiteralBlock, @[newRstLeaf(n.comment)])
+  else:
+    result = nil
 
-proc genRecCommentAux(d: PDoc, n: PNode): Rope =
+proc genRecCommentAux(d: PDoc, n: PNode): PRstNode =
   if n == nil: return nil
-  result = genComment(d, n).rope
+  result = genComment(d, n)
   if result == nil:
-    if n.kind in {nkStmtList, nkStmtListExpr, nkTypeDef, nkConstDef,
-                  nkObjectTy, nkRefTy, nkPtrTy, nkAsgn, nkFastAsgn, nkHiddenStdConv}:
+    if n.kind in {nkStmtList, nkStmtListExpr, nkTypeDef, nkConstDef, nkTypeClassTy,
+                  nkObjectTy, nkRefTy, nkPtrTy, nkAsgn, nkFastAsgn, nkSinkAsgn, nkHiddenStdConv}:
       # notin {nkEmpty..nkNilLit, nkEnumTy, nkTupleTy}:
       for i in 0..<n.len:
         result = genRecCommentAux(d, n[i])
         if result != nil: return
   else:
-    when defined(nimNoNilSeqs): n.comment = ""
-    else: n.comment = nil
+    n.comment = ""
 
-proc genRecComment(d: PDoc, n: PNode): Rope =
+proc genRecComment(d: PDoc, n: PNode): PRstNode =
   if n == nil: return nil
-  result = genComment(d, n).rope
+  result = genComment(d, n)
   if result == nil:
     if n.kind in {nkProcDef, nkFuncDef, nkMethodDef, nkIteratorDef,
                   nkMacroDef, nkTemplateDef, nkConverterDef}:
@@ -342,18 +473,15 @@ proc getPlainDocstring(n: PNode): string =
   ## You need to call this before genRecComment, whose side effects are removal
   ## of comments from the tree. The proc will recursively scan and return all
   ## the concatenated ``##`` comments of the node.
-  result = ""
-  if n == nil: return
-  if startsWith(n.comment, "##"):
+  if n == nil: result = ""
+  elif startsWith(n.comment, "##"):
     result = n.comment
-  if result.len < 1:
+  else:
+    result = ""
     for i in 0..<n.safeLen:
       result = getPlainDocstring(n[i])
       if result.len > 0: return
 
-proc belongsToPackage(conf: ConfigRef; module: PSym): bool =
-  result = module.kind == skModule and module.getnimblePkgId == conf.mainPackageId
-
 proc externalDep(d: PDoc; module: PSym): string =
   if optWholeProject in d.conf.globalOptions or d.conf.docRoot.len > 0:
     let full = AbsoluteFile toFullPath(d.conf, FileIndex module.position)
@@ -362,15 +490,15 @@ proc externalDep(d: PDoc; module: PSym): string =
   else:
     result = extractFilename toFullPath(d.conf, FileIndex module.position)
 
-proc nodeToHighlightedHtml(d: PDoc; n: PNode; result: var Rope; renderFlags: TRenderFlags = {};
-                           procLink: Rope) =
-  var r: TSrcGen
+proc nodeToHighlightedHtml(d: PDoc; n: PNode; result: var string;
+                           renderFlags: TRenderFlags = {};
+                           procLink: string) =
+  var r: TSrcGen = initTokRender(n, renderFlags)
   var literal = ""
-  initTokRender(r, n, renderFlags)
   var kind = tkEof
   var tokenPos = 0
   var procTokenPos = 0
-  template escLit(): untyped = rope(esc(d.target, literal))
+  template escLit(): untyped = esc(d.target, literal)
   while true:
     getNextTok(r, kind, literal)
     inc tokenPos
@@ -384,11 +512,11 @@ proc nodeToHighlightedHtml(d: PDoc; n: PNode; result: var Rope; renderFlags: TRe
       if kind in {tkProc, tkMethod, tkIterator, tkMacro, tkTemplate, tkFunc, tkConverter}:
         procTokenPos = tokenPos
       dispA(d.conf, result, "<span class=\"Keyword\">$1</span>", "\\spanKeyword{$1}",
-            [rope(literal)])
+            [literal])
     of tkOpr:
       dispA(d.conf, result, "<span class=\"Operator\">$1</span>", "\\spanOperator{$1}",
             [escLit])
-    of tkStrLit..tkTripleStrLit:
+    of tkStrLit..tkTripleStrLit, tkCustomLit:
       dispA(d.conf, result, "<span class=\"StringLit\">$1</span>",
             "\\spanStringLit{$1}", [escLit])
     of tkCharLit:
@@ -403,37 +531,37 @@ proc nodeToHighlightedHtml(d: PDoc; n: PNode; result: var Rope; renderFlags: TRe
     of tkSymbol:
       let s = getTokSym(r)
       # -2 because of the whitespace in between:
-      if procTokenPos == tokenPos-2 and procLink != nil:
+      if procTokenPos == tokenPos-2 and procLink != "":
         dispA(d.conf, result, "<a href=\"#$2\"><span class=\"Identifier\">$1</span></a>",
               "\\spanIdentifier{$1}", [escLit, procLink])
       elif s != nil and s.kind in {skType, skVar, skLet, skConst} and
            sfExported in s.flags and s.owner != nil and
-           belongsToPackage(d.conf, s.owner) and d.target == outHtml:
+           belongsToProjectPackage(d.conf, s.owner) and d.target == outHtml:
         let external = externalDep(d, s.owner)
         result.addf "<a href=\"$1#$2\"><span class=\"Identifier\">$3</span></a>",
-          [rope changeFileExt(external, "html"), rope literal,
+          [changeFileExt(external, "html"), literal,
            escLit]
       else:
         dispA(d.conf, result, "<span class=\"Identifier\">$1</span>",
               "\\spanIdentifier{$1}", [escLit])
     of tkSpaces, tkInvalid:
       result.add(literal)
-    of tkCurlyDotLe:
+    of tkHideableStart:
       template fun(s) = dispA(d.conf, result, s, "\\spanOther{$1}", [escLit])
       if renderRunnableExamples in renderFlags: fun "$1"
-      else: fun: "<span>" & # This span is required for the JS to work properly
-        """<span class="Other">{</span><span class="Other pragmadots">...</span><span class="Other">}</span>
+      else:
+        # 1st span is required for the JS to work properly
+        fun """
+<span>
+<span class="Other pragmadots">...</span>
 </span>
-<span class="pragmawrap">
-<span class="Other">$1</span>
-<span class="pragma">""".replace("\n", "")  # Must remove newlines because wrapped in a <pre>
-    of tkCurlyDotRi:
+<span class="pragmawrap">""".replace("\n", "")  # Must remove newlines because wrapped in a <pre>
+    of tkHideableEnd:
       template fun(s) = dispA(d.conf, result, s, "\\spanOther{$1}", [escLit])
       if renderRunnableExamples in renderFlags: fun "$1"
-      else: fun """
-</span>
-<span class="Other">$1</span>
-</span>""".replace("\n", "")
+      else: fun "</span>"
+    of tkCurlyDotLe: dispA(d.conf, result, "$1", "\\spanOther{$1}", [escLit])
+    of tkCurlyDotRi: dispA(d.conf, result, "$1", "\\spanOther{$1}", [escLit])
     of tkParLe, tkParRi, tkBracketLe, tkBracketRi, tkCurlyLe, tkCurlyRi,
        tkBracketDotLe, tkBracketDotRi, tkParDotLe,
        tkParDotRi, tkComma, tkSemiColon, tkColon, tkEquals, tkDot, tkDotDot,
@@ -445,22 +573,9 @@ proc nodeToHighlightedHtml(d: PDoc; n: PNode; result: var Rope; renderFlags: TRe
 
 proc exampleOutputDir(d: PDoc): AbsoluteDir = d.conf.getNimcacheDir / RelativeDir"runnableExamples"
 
-proc writeExample(d: PDoc; ex: PNode, rdoccmd: string) =
-  if d.conf.errorCounter > 0: return
-  let outputDir = d.exampleOutputDir
-  createDir(outputDir)
-  inc d.exampleCounter
-  let outp = outputDir / RelativeFile(extractFilename(d.filename.changeFileExt"" &
-      "_examples" & $d.exampleCounter & ".nim"))
-  #let nimcache = outp.changeFileExt"" & "_nimcache"
-  renderModule(ex, d.filename, outp.string, conf = d.conf)
-  if rdoccmd notin d.exampleGroups: d.exampleGroups[rdoccmd] = ExampleGroup(rdoccmd: rdoccmd, docCmd: d.conf.docCmd, index: d.exampleGroups.len)
-  d.exampleGroups[rdoccmd].code.add "import r\"$1\"\n" % outp.string
-
 proc runAllExamples(d: PDoc) =
-  let backend = d.conf.backend
   # This used to be: `let backend = if isDefined(d.conf, "js"): "js"` (etc), however
-  # using `-d:js` (etc) cannot work properly, eg would fail with `importjs`
+  # using `-d:js` (etc) cannot work properly, e.g. would fail with `importjs`
   # since semantics are affected by `config.backend`, not by isDefined(d.conf, "js")
   let outputDir = d.exampleOutputDir
   for _, group in d.exampleGroups:
@@ -470,23 +585,38 @@ proc runAllExamples(d: PDoc) =
     writeFile(outp, group.code)
     # most useful semantics is that `docCmd` comes after `rdoccmd`, so that we can (temporarily) override
     # via command line
-    let cmd = "$nim $backend -r --warning:UnusedImport:off --path:$path --nimcache:$nimcache $rdoccmd $docCmd $file" % [
-      "nim", os.getAppFilename(),
+    # D20210224T221756:here
+    var pathArgs = "--path:$path" % [ "path", quoteShell(d.conf.projectPath) ]
+    for p in d.conf.searchPaths:
+      pathArgs = "$args --path:$path" % [ "args", pathArgs, "path", quoteShell(p) ]
+    let cmd = "$nim $backend -r --lib:$libpath --warning:UnusedImport:off $pathArgs --nimcache:$nimcache $rdoccmd $docCmd $file" % [
+      "nim", quoteShell(os.getAppFilename()),
       "backend", $d.conf.backend,
-      "path", quoteShell(d.conf.projectPath),
+      "pathArgs", pathArgs,
+      "libpath", quoteShell(d.conf.libpath),
       "nimcache", quoteShell(outputDir),
       "file", quoteShell(outp),
       "rdoccmd", group.rdoccmd,
       "docCmd", group.docCmd,
     ]
-    if os.execShellCmd(cmd) != 0:
-      quit "[runnableExamples] failed: generated file: '$1' group: '$2' cmd: $3" % [outp.string, $group[], cmd]
+    if d.conf.backend == backendJs and findNodeJs() == "":
+      discard "ignore JS runnableExample"
+    elif os.execShellCmd(cmd) != 0:
+      d.conf.quitOrRaise "[runnableExamples] failed: generated file: '$1' group: '$2' cmd: $3" % [outp.string, group[].prettyString, cmd]
     else:
       # keep generated source file `outp` to allow inspection.
       rawMessage(d.conf, hintSuccess, ["runnableExamples: " & outp.string])
       # removeFile(outp.changeFileExt(ExeExt)) # it's in nimcache, no need to remove
 
-proc prepareExample(d: PDoc; n: PNode): tuple[rdoccmd: string, code: string] =
+proc quoted(a: string): string =
+  result = ""
+  result.addQuoted(a)
+
+proc toInstantiationInfo(conf: ConfigRef, info: TLineInfo): (string, int, int) =
+  # xxx expose in compiler/lineinfos.nim
+  (conf.toMsgFilename(info), info.line.int, info.col.int + ColOffset)
+
+proc prepareExample(d: PDoc; n: PNode, topLevel: bool): tuple[rdoccmd: string, code: string] =
   ## returns `rdoccmd` and source code for this runnableExamples
   var rdoccmd = ""
   if n.len < 2 or n.len > 3: globalError(d.conf, n.info, "runnableExamples invalid")
@@ -496,19 +626,66 @@ proc prepareExample(d: PDoc; n: PNode): tuple[rdoccmd: string, code: string] =
     if n1.kind notin nkStrKinds: globalError(d.conf, n1.info, "string litteral expected")
     rdoccmd = n1.strVal
 
-  var docComment = newTree(nkCommentStmt)
+  let useRenderModule = false
   let loc = d.conf.toFileLineCol(n.info)
+  let code = extractRunnableExamplesSource(d.conf, n)
 
-  docComment.comment = "autogenerated by docgen\nloc: $1\nrdoccmd: $2" % [loc, rdoccmd]
-  var runnableExamples = newTree(nkStmtList,
-      docComment,
-      newTree(nkImportStmt, newStrNode(nkStrLit, d.filename)))
-  runnableExamples.info = n.info
-  let ret = extractRunnableExamplesSource(d.conf, n)
-  for a in n.lastSon: runnableExamples.add a
-  # we could also use `ret` instead here, to keep sources verbatim
-  writeExample(d, runnableExamples, rdoccmd)
-  result = (rdoccmd, ret)
+  if d.conf.errorCounter > 0:
+    return (rdoccmd, code)
+
+  let comment = "autogenerated by docgen\nloc: $1\nrdoccmd: $2" % [loc, rdoccmd]
+  let outputDir = d.exampleOutputDir
+  createDir(outputDir)
+  inc d.exampleCounter
+  let outp = outputDir / RelativeFile("$#_examples_$#.nim" % [d.filename.extractFilename.changeFileExt"", $d.exampleCounter])
+
+  if useRenderModule:
+    var docComment = newTree(nkCommentStmt)
+    docComment.comment = comment
+    var runnableExamples = newTree(nkStmtList,
+        docComment,
+        newTree(nkImportStmt, newStrNode(nkStrLit, "std/assertions")),
+        newTree(nkImportStmt, newStrNode(nkStrLit, d.filename)))
+    runnableExamples.info = n.info
+    for a in n.lastSon: runnableExamples.add a
+
+    # buggy, refs bug #17292
+    # still worth fixing as it can affect other code relying on `renderModule`,
+    # so we keep this code path here for now, which could still be useful in some
+    # other situations.
+    renderModule(runnableExamples, outp.string, conf = d.conf)
+
+  else:
+    var code2 = code
+    if code.len > 0 and "codeReordering" notin code:
+      let codeIndent = extractRunnableExamplesSource(d.conf, n, indent = 2)
+      # hacky but simplest solution, until we devise a way to make `{.line.}`
+      # work without introducing a scope
+      code2 = """
+{.line: $#.}:
+$#
+""" % [$toInstantiationInfo(d.conf, n.info), codeIndent]
+    code2 = """
+#[
+$#
+]#
+import std/assertions
+import $#
+$#
+""" % [comment, d.filename.quoted, code2]
+    writeFile(outp.string, code2)
+
+  if rdoccmd notin d.exampleGroups:
+    d.exampleGroups[rdoccmd] = ExampleGroup(rdoccmd: rdoccmd, docCmd: d.conf.docCmd, index: d.exampleGroups.len)
+  d.exampleGroups[rdoccmd].code.add "import $1\n" % outp.string.quoted
+
+  var codeShown: string
+  if topLevel: # refs https://github.com/nim-lang/RFCs/issues/352
+    let title = canonicalImport(d.conf, AbsoluteFile d.filename)
+    codeShown = "import $#\n$#" % [title, code]
+  else:
+    codeShown = code
+  result = (rdoccmd, codeShown)
   when false:
     proc extractImports(n: PNode; result: PNode) =
       if n.kind in {nkImportStmt, nkImportExceptStmt, nkFromStmt}:
@@ -522,27 +699,15 @@ proc prepareExample(d: PDoc; n: PNode): tuple[rdoccmd: string, code: string] =
     for imp in imports: runnableExamples.add imp
     runnableExamples.add newTree(nkBlockStmt, newNode(nkEmpty), copyTree savedLastSon)
 
-proc renderNimCodeOld(d: PDoc, n: PNode, dest: var Rope) =
-  ## this is a rather hacky way to get rid of the initial indentation
-  ## that the renderer currently produces:
-  # deadcode
-  var i = 0
-  var body = n.lastSon
-  if body.len == 1 and body.kind == nkStmtList and
-      body.lastSon.kind == nkStmtList:
-    body = body.lastSon
-  for b in body:
-    if i > 0: dest.add "\n"
-    inc i
-    nodeToHighlightedHtml(d, b, dest, {renderRunnableExamples}, nil)
-
 type RunnableState = enum
   rsStart
   rsComment
   rsRunnable
   rsDone
 
-proc getAllRunnableExamplesImpl(d: PDoc; n: PNode, dest: var Rope, state: RunnableState): RunnableState =
+proc getAllRunnableExamplesImpl(d: PDoc; n: PNode, dest: var ItemPre,
+                                state: RunnableState, topLevel: bool):
+                               RunnableState =
   ##[
   Simple state machine to tell whether we render runnableExamples and doc comments.
   This is to ensure that we can interleave runnableExamples and doc comments freely;
@@ -566,23 +731,25 @@ proc getAllRunnableExamplesImpl(d: PDoc; n: PNode, dest: var Rope, state: Runnab
       return rsComment
   of nkCallKinds:
     if isRunnableExamples(n[0]) and
-        n.len >= 2 and n.lastSon.kind == nkStmtList and state in {rsStart, rsComment, rsRunnable}:
-      let (rdoccmd, code) = prepareExample(d, n)
-      var msg = "Example:"
-      if rdoccmd.len > 0: msg.add " cmd: " & rdoccmd
-      dispA(d.conf, dest, "\n<p><strong class=\"examples_text\">$1</strong></p>\n",
-          "\n\\textbf{$1}\n", [msg.rope])
-      inc d.listingCounter
-      let id = $d.listingCounter
-      dest.add(d.config.getOrDefault"doc.listing_start" % [id, "langNim"])
-      when true:
+        n.len >= 2 and n.lastSon.kind == nkStmtList:
+      if state in {rsStart, rsComment, rsRunnable}:
+        let (rdoccmd, code) = prepareExample(d, n, topLevel)
+        var msg = "Example:"
+        if rdoccmd.len > 0: msg.add " cmd: " & rdoccmd
+        var s: string = ""
+        dispA(d.conf, s, "\n<p><strong class=\"examples_text\">$1</strong></p>\n",
+            "\n\n\\textbf{$1}\n", [msg])
+        dest.add s
+        inc d.listingCounter
+        let id = $d.listingCounter
+        dest.add(d.config.getOrDefault"doc.listing_start" % [id, "langNim", ""])
         var dest2 = ""
-        renderNimCode(dest2, code, isLatex = d.conf.cmd == cmdRst2tex)
+        renderNimCode(dest2, code, d.target)
         dest.add dest2
+        dest.add(d.config.getOrDefault"doc.listing_end" % id)
+        return rsRunnable
       else:
-        renderNimCodeOld(d, n, dest)
-      dest.add(d.config.getOrDefault"doc.listing_end" % id)
-      return rsRunnable
+        localError(d.conf, n.info, errUser, "runnableExamples must appear before the first non-comment statement")
   else: discard
   return rsDone
     # change this to `rsStart` if you want to keep generating doc comments
@@ -611,27 +778,29 @@ proc getRoutineBody(n: PNode): PNode =
   (0 or more) doc comments and runnableExamples.
   ]##
   result = n[bodyPos]
-  if result.kind == nkAsgn and n.len > bodyPos+1 and n[bodyPos+1].kind == nkSym:
-    doAssert result[0].kind == nkSym
+
+  # This won't be transformed: result.id = 10. Namely result[0].kind != nkSym.
+  if result.kind == nkAsgn and result[0].kind == nkSym and
+                               n.len > bodyPos+1 and n[bodyPos+1].kind == nkSym:
     doAssert result.len == 2
     result = result[1]
 
-proc getAllRunnableExamples(d: PDoc, n: PNode, dest: var Rope) =
+proc getAllRunnableExamples(d: PDoc, n: PNode, dest: var ItemPre) =
   var n = n
   var state = rsStart
-  template fn(n2) =
-    state = getAllRunnableExamplesImpl(d, n2, dest, state)
-  dest.add genComment(d, n).rope
+  template fn(n2, topLevel) =
+    state = getAllRunnableExamplesImpl(d, n2, dest, state, topLevel)
+  dest.add genComment(d, n)
   case n.kind
   of routineDefs:
     n = n.getRoutineBody
     case n.kind
-    of nkCommentStmt, nkCallKinds: fn(n)
+    of nkCommentStmt, nkCallKinds: fn(n, topLevel = false)
     else:
       for i in 0..<n.safeLen:
-        fn(n[i])
-        if state == rsDone: return
-  else: fn(n)
+        fn(n[i], topLevel = false)
+        if state == rsDone: discard # check all sons
+  else: fn(n, topLevel = true)
 
 proc isVisible(d: PDoc; n: PNode): bool =
   result = false
@@ -652,21 +821,24 @@ proc isVisible(d: PDoc; n: PNode): bool =
   elif n.kind == nkPragmaExpr:
     result = isVisible(d, n[0])
 
-proc getName(d: PDoc, n: PNode, splitAfter = -1): string =
+proc getName(n: PNode): string =
   case n.kind
-  of nkPostfix: result = getName(d, n[1], splitAfter)
-  of nkPragmaExpr: result = getName(d, n[0], splitAfter)
-  of nkSym: result = esc(d.target, n.sym.renderDefinitionName, splitAfter)
-  of nkIdent: result = esc(d.target, n.ident.s, splitAfter)
+  of nkPostfix: result = getName(n[1])
+  of nkPragmaExpr: result = getName(n[0])
+  of nkSym: result = n.sym.renderDefinitionName
+  of nkIdent: result = n.ident.s
   of nkAccQuoted:
-    result = esc(d.target, "`")
-    for i in 0..<n.len: result.add(getName(d, n[i], splitAfter))
-    result.add esc(d.target, "`")
-  of nkOpenSymChoice, nkClosedSymChoice:
-    result = getName(d, n[0], splitAfter)
+    result = "`"
+    for i in 0..<n.len: result.add(getName(n[i]))
+    result = "`"
+  of nkOpenSymChoice, nkClosedSymChoice, nkOpenSym:
+    result = getName(n[0])
   else:
     result = ""
 
+proc getNameEsc(d: PDoc, n: PNode): string =
+  esc(d.target, getName(n))
+
 proc getNameIdent(cache: IdentCache; n: PNode): PIdent =
   case n.kind
   of nkPostfix: result = getNameIdent(cache, n[1])
@@ -677,7 +849,7 @@ proc getNameIdent(cache: IdentCache; n: PNode): PIdent =
     var r = ""
     for i in 0..<n.len: r.add(getNameIdent(cache, n[i]).s)
     result = getIdent(cache, r)
-  of nkOpenSymChoice, nkClosedSymChoice:
+  of nkOpenSymChoice, nkClosedSymChoice, nkOpenSym:
     result = getNameIdent(cache, n[0])
   else:
     result = nil
@@ -686,12 +858,12 @@ proc getRstName(n: PNode): PRstNode =
   case n.kind
   of nkPostfix: result = getRstName(n[1])
   of nkPragmaExpr: result = getRstName(n[0])
-  of nkSym: result = newRstNode(rnLeaf, n.sym.renderDefinitionName)
-  of nkIdent: result = newRstNode(rnLeaf, n.ident.s)
+  of nkSym: result = newRstLeaf(n.sym.renderDefinitionName)
+  of nkIdent: result = newRstLeaf(n.ident.s)
   of nkAccQuoted:
     result = getRstName(n[0])
     for i in 1..<n.len: result.text.add(getRstName(n[i]).text)
-  of nkOpenSymChoice, nkClosedSymChoice:
+  of nkOpenSymChoice, nkClosedSymChoice, nkOpenSym:
     result = getRstName(n[0])
   else:
     result = nil
@@ -725,7 +897,7 @@ proc complexName(k: TSymKind, n: PNode, baseName: string): string =
   ## types will be added with a preceding dash. Return types won't be added.
   ##
   ## If you modify the output of this proc, please update the anchor generation
-  ## section of ``doc/docgen.txt``.
+  ## section of ``doc/docgen.rst``.
   result = baseName
   case k
   of skProc, skFunc: discard
@@ -735,20 +907,12 @@ proc complexName(k: TSymKind, n: PNode, baseName: string): string =
   of skTemplate: result.add(".t")
   of skConverter: result.add(".c")
   else: discard
-  if n.len > paramsPos and n[paramsPos].kind == nkFormalParams:
+  if n.safeLen > paramsPos and n[paramsPos].kind == nkFormalParams:
     let params = renderParamTypes(n[paramsPos])
     if params.len > 0:
       result.add(defaultParamSeparator)
       result.add(params)
 
-proc isCallable(n: PNode): bool =
-  ## Returns true if `n` contains a callable node.
-  case n.kind
-  of nkProcDef, nkMethodDef, nkIteratorDef, nkMacroDef, nkTemplateDef,
-    nkConverterDef, nkFuncDef: result = true
-  else:
-    result = false
-
 proc docstringSummary(rstText: string): string =
   ## Returns just the first line or a brief chunk of text from a rst string.
   ##
@@ -765,7 +929,7 @@ proc docstringSummary(rstText: string): string =
   result = rstText.substr(2).strip
   var pos = result.find('\L')
   if pos > 0:
-    result.delete(pos, result.len - 1)
+    result.setLen(pos - 1)
     result.add("…")
   if pos < maxDocstringChars:
     return
@@ -773,156 +937,255 @@ proc docstringSummary(rstText: string): string =
   pos = result.find({'.', ',', ':'})
   let last = result.len - 1
   if pos > 0 and pos < last:
-    result.delete(pos, last)
+    result.setLen(pos - 1)
     result.add("…")
 
-proc genDeprecationMsg(d: PDoc, n: PNode): Rope =
+proc genDeprecationMsg(d: PDoc, n: PNode): string =
   ## Given a nkPragma wDeprecated node output a well-formatted section
   if n == nil: return
 
   case n.safeLen:
   of 0: # Deprecated w/o any message
-    result = ropeFormatNamedVars(d.conf,
-      getConfigVar(d.conf, "doc.deprecationmsg"), ["label", "message"],
-      [~"Deprecated", nil])
+    result = getConfigVar(d.conf, "doc.deprecationmsg") % [
+       "label" , "Deprecated", "message", ""]
   of 2: # Deprecated w/ a message
     if n[1].kind in {nkStrLit..nkTripleStrLit}:
-      result = ropeFormatNamedVars(d.conf,
-        getConfigVar(d.conf, "doc.deprecationmsg"), ["label", "message"],
-        [~"Deprecated:", rope(xmltree.escape(n[1].strVal))])
+      result = getConfigVar(d.conf, "doc.deprecationmsg") % [
+          "label", "Deprecated:", "message", xmltree.escape(n[1].strVal)]
+    else:
+      result = ""
   else:
-    doAssert false
+    raiseAssert "unreachable"
 
 type DocFlags = enum
   kDefault
   kForceExport
 
-proc genItem(d: PDoc, n, nameNode: PNode, k: TSymKind, docFlags: DocFlags) =
+proc genSeeSrc(d: PDoc, path: string, line: int): string =
+  result = ""
+  let docItemSeeSrc = getConfigVar(d.conf, "doc.item.seesrc")
+  if docItemSeeSrc.len > 0:
+    let path = relativeTo(AbsoluteFile path, AbsoluteDir getCurrentDir(), '/')
+    when false:
+      let cwd = canonicalizePath(d.conf, getCurrentDir())
+      var path = path
+      if path.startsWith(cwd):
+        path = path[cwd.len+1..^1].replace('\\', '/')
+    let gitUrl = getConfigVar(d.conf, "git.url")
+    if gitUrl.len > 0:
+      let defaultBranch =
+        if NimPatch mod 2 == 1: "devel"
+        else: "version-$1-$2" % [$NimMajor, $NimMinor]
+      let commit = getConfigVar(d.conf, "git.commit", defaultBranch)
+      let develBranch = getConfigVar(d.conf, "git.devel", "devel")
+      dispA(d.conf, result, "$1", "", [docItemSeeSrc % [
+          "path", path.string, "line", $line, "url", gitUrl,
+          "commit", commit, "devel", develBranch]])
+
+proc symbolPriority(k: TSymKind): int =
+  result = case k
+    of skMacro: -3
+    of skTemplate: -2
+    of skIterator: -1
+    else: 0  # including skProc which have higher priority
+    # documentation itself has even higher priority 1
+
+proc getTypeKind(n: PNode): string =
+  case n[2].kind
+  of nkEnumTy: "enum"
+  of nkObjectTy: "object"
+  of nkTupleTy: "tuple"
+  else: ""
+
+proc toLangSymbol(k: TSymKind, n: PNode, baseName: string): LangSymbol =
+  ## Converts symbol info (names/types/parameters) in `n` into format
+  ## `LangSymbol` convenient for ``rst.nim``/``dochelpers.nim``.
+  result = LangSymbol(name: baseName.nimIdentNormalize,
+      symKind: k.toHumanStr
+  )
+  if k in routineKinds:
+    var
+      paramTypes: seq[string] = @[]
+    renderParamTypes(paramTypes, n[paramsPos], toNormalize=true)
+    let paramNames = renderParamNames(n[paramsPos], toNormalize=true)
+    # In some rare cases (system.typeof) parameter type is not set for default:
+    doAssert paramTypes.len <= paramNames.len
+    for i in 0 ..< paramNames.len:
+      if i < paramTypes.len:
+        result.parameters.add (paramNames[i], paramTypes[i])
+      else:
+        result.parameters.add (paramNames[i], "")
+    result.parametersProvided = true
+
+    result.outType = renderOutType(n[paramsPos], toNormalize=true)
+
+  if k in {skProc, skFunc, skType, skIterator}:
+    # Obtain `result.generics`
+    # Use `n[miscPos]` since n[genericParamsPos] does not contain constraints
+    var genNode: PNode = nil
+    if k == skType:
+      genNode = n[1]  # FIXME: what is index 1?
+    else:
+      if n[miscPos].kind != nkEmpty:
+        genNode = n[miscPos][1]   # FIXME: what is index 1?
+    if genNode != nil:
+      var literal = ""
+      var r: TSrcGen = initTokRender(genNode, {renderNoBody, renderNoComments,
+        renderNoPragmas, renderNoProcDefs, renderExpandUsing, renderNoPostfix})
+      var kind = tkEof
+      while true:
+        getNextTok(r, kind, literal)
+        if kind == tkEof:
+          break
+        if kind != tkSpaces:
+          result.generics.add(literal.nimIdentNormalize)
+
+  if k == skType: result.symTypeKind = getTypeKind(n)
+
+proc genItem(d: PDoc, n, nameNode: PNode, k: TSymKind, docFlags: DocFlags, nonExports: bool = false) =
   if (docFlags != kForceExport) and not isVisible(d, nameNode): return
   let
-    name = getName(d, nameNode)
-    nameRope = name.rope
+    name = getName(nameNode)
+    nameEsc = esc(d.target, name)
   var plainDocstring = getPlainDocstring(n) # call here before genRecComment!
-  var result: Rope = nil
+  var result = ""
   var literal, plainName = ""
   var kind = tkEof
-  var comm: Rope = nil
+  var comm: ItemPre = default(ItemPre)
   if n.kind in routineDefs:
     getAllRunnableExamples(d, n, comm)
   else:
     comm.add genRecComment(d, n)
 
-  var r: TSrcGen
   # Obtain the plain rendered string for hyperlink titles.
-  initTokRender(r, n, {renderNoBody, renderNoComments, renderDocComments,
-    renderNoPragmas, renderNoProcDefs})
+  var r: TSrcGen = initTokRender(n, {renderNoBody, renderNoComments, renderDocComments,
+    renderNoPragmas, renderNoProcDefs, renderExpandUsing, renderNoPostfix})
   while true:
     getNextTok(r, kind, literal)
     if kind == tkEof:
       break
     plainName.add(literal)
 
-  var pragmaNode: PNode = nil
-  if n.isCallable and n[pragmasPos].kind != nkEmpty:
-    pragmaNode = findPragma(n[pragmasPos], wDeprecated)
+  var pragmaNode = getDeclPragma(n)
+  if pragmaNode != nil: pragmaNode = findPragma(pragmaNode, wDeprecated)
 
   inc(d.id)
   let
-    plainNameRope = rope(xmltree.escape(plainName.strip))
+    plainNameEsc = esc(d.target, plainName.strip)
+    typeDescr =
+      if k == skType and getTypeKind(n) != "": getTypeKind(n)
+      else: k.toHumanStr
+    detailedName = typeDescr & " " & (
+        if k in routineKinds: plainName else: name)
+    uniqueName = if k in routineKinds: plainNameEsc else: nameEsc
+    sortName = if k in routineKinds: plainName.strip else: name
     cleanPlainSymbol = renderPlainSymbolName(nameNode)
     complexSymbol = complexName(k, n, cleanPlainSymbol)
-    plainSymbolRope = rope(cleanPlainSymbol)
-    plainSymbolEncRope = rope(encodeUrl(cleanPlainSymbol))
-    itemIDRope = rope(d.id)
+    plainSymbolEnc = encodeUrl(cleanPlainSymbol, usePlus = false)
     symbolOrId = d.newUniquePlainSymbol(complexSymbol)
-    symbolOrIdRope = symbolOrId.rope
-    symbolOrIdEncRope = encodeUrl(symbolOrId).rope
-    deprecationMsgRope = genDeprecationMsg(d, pragmaNode)
-
-  nodeToHighlightedHtml(d, n, result, {renderNoBody, renderNoComments,
-    renderDocComments, renderSyms}, symbolOrIdEncRope)
-
-  var seeSrcRope: Rope = nil
-  let docItemSeeSrc = getConfigVar(d.conf, "doc.item.seesrc")
-  if docItemSeeSrc.len > 0:
-    let path = relativeTo(AbsoluteFile toFullPath(d.conf, n.info), AbsoluteDir getCurrentDir(), '/')
-    when false:
-      let cwd = canonicalizePath(d.conf, getCurrentDir())
-      var path = toFullPath(d.conf, n.info)
-      if path.startsWith(cwd):
-        path = path[cwd.len+1..^1].replace('\\', '/')
-    let gitUrl = getConfigVar(d.conf, "git.url")
-    if gitUrl.len > 0:
-      let defaultBranch =
-        if NimPatch mod 2 == 1: "devel"
-        else: "version-$1-$2" % [$NimMajor, $NimMinor]
-      let commit = getConfigVar(d.conf, "git.commit", defaultBranch)
-      let develBranch = getConfigVar(d.conf, "git.devel", "devel")
-      dispA(d.conf, seeSrcRope, "$1", "", [ropeFormatNamedVars(d.conf, docItemSeeSrc,
-          ["path", "line", "url", "commit", "devel"], [rope path.string,
-          rope($n.info.line), rope gitUrl, rope commit, rope develBranch])])
-
-  d.section[k].add(ropeFormatNamedVars(d.conf, getConfigVar(d.conf, "doc.item"),
-    ["name", "header", "desc", "itemID", "header_plain", "itemSym",
-      "itemSymOrID", "itemSymEnc", "itemSymOrIDEnc", "seeSrc", "deprecationMsg"],
-    [nameRope, result, comm, itemIDRope, plainNameRope, plainSymbolRope,
-      symbolOrIdRope, plainSymbolEncRope, symbolOrIdEncRope, seeSrcRope,
-      deprecationMsgRope]))
-
-  let external = d.destFile.relativeTo(d.conf.outDir, '/').changeFileExt(HtmlExt).string
-
-  var attype: Rope
-  if k in routineKinds and nameNode.kind == nkSym:
+    symbolOrIdEnc = encodeUrl(symbolOrId, usePlus = false)
+    deprecationMsg = genDeprecationMsg(d, pragmaNode)
+    rstLangSymbol = toLangSymbol(k, n, cleanPlainSymbol)
+    symNameNode =
+      if nameNode.kind == nkPostfix: nameNode[1]
+      else: nameNode
+
+  # we generate anchors automatically for subsequent use in doc comments
+  let lineinfo = rstast.TLineInfo(
+      line: nameNode.info.line, col: nameNode.info.col,
+      fileIndex: addRstFileIndex(d, nameNode.info))
+  addAnchorNim(d.sharedState, external = false, refn = symbolOrId,
+               tooltip = detailedName, langSym = rstLangSymbol,
+               priority = symbolPriority(k), info = lineinfo,
+               module = addRstFileIndex(d, FileIndex d.module.position))
+
+  var renderFlags = {renderNoBody, renderNoComments, renderDocComments,
+    renderSyms, renderExpandUsing, renderNoPostfix}
+  if nonExports:
+    renderFlags.incl renderNonExportedFields
+  nodeToHighlightedHtml(d, n, result, renderFlags, symbolOrIdEnc)
+
+  let seeSrc = genSeeSrc(d, toFullPath(d.conf, n.info), n.info.line.int)
+
+  d.section[k].secItems.mgetOrPut(cleanPlainSymbol, newSeq[Item]()).add Item(
+    descRst: comm,
+    sortName: sortName,
+    info: lineinfo,
+    anchor: symbolOrId,
+    detailedName: detailedName,
+    name: name,
+    substitutions: @[
+     "uniqueName", uniqueName,
+     "header", result, "itemID", $d.id,
+     "header_plain", plainNameEsc, "itemSym", cleanPlainSymbol,
+     "itemSymEnc", plainSymbolEnc,
+     "itemSymOrIDEnc", symbolOrIdEnc, "seeSrc", seeSrc,
+     "deprecationMsg", deprecationMsg])
+
+  let external = d.destFile.AbsoluteFile.relativeTo(d.conf.outDir, '/').changeFileExt(HtmlExt).string
+
+  var attype = ""
+  if k in routineKinds and symNameNode.kind == nkSym:
     let att = attachToType(d, nameNode.sym)
     if att != nil:
-      attype = rope esc(d.target, att.name.s)
-  elif k == skType and nameNode.kind == nkSym and nameNode.sym.typ.kind in {tyEnum, tyBool}:
-    let etyp = nameNode.sym.typ
+      attype = esc(d.target, att.name.s)
+  elif k == skType and symNameNode.kind == nkSym and
+      symNameNode.sym.typ.kind in {tyEnum, tyBool}:
+    let etyp = symNameNode.sym.typ
     for e in etyp.n:
       if e.sym.kind != skEnumField: continue
       let plain = renderPlainSymbolName(e)
       let symbolOrId = d.newUniquePlainSymbol(plain)
-      setIndexTerm(d[], external, symbolOrId, plain, nameNode.sym.name.s & '.' & plain,
-        xmltree.escape(getPlainDocstring(e).docstringSummary))
-
-  d.toc[k].add(ropeFormatNamedVars(d.conf, getConfigVar(d.conf, "doc.item.toc"),
-    ["name", "header", "desc", "itemID", "header_plain", "itemSym",
-      "itemSymOrID", "itemSymEnc", "itemSymOrIDEnc", "attype"],
-    [rope(getName(d, nameNode, d.splitAfter)), result, comm,
-      itemIDRope, plainNameRope, plainSymbolRope, symbolOrIdRope,
-      plainSymbolEncRope, symbolOrIdEncRope, attype]))
-
-  # Ironically for types the complexSymbol is *cleaner* than the plainName
-  # because it doesn't include object fields or documentation comments. So we
-  # use the plain one for callable elements, and the complex for the rest.
-  var linkTitle = changeFileExt(extractFilename(d.filename), "") & ": "
-  if n.isCallable: linkTitle.add(xmltree.escape(plainName.strip))
-  else: linkTitle.add(xmltree.escape(complexSymbol.strip))
-
-  setIndexTerm(d[], external, symbolOrId, name, linkTitle,
-    xmltree.escape(plainDocstring.docstringSummary))
-  if k == skType and nameNode.kind == nkSym:
-    d.types.strTableAdd nameNode.sym
-
-proc genJsonItem(d: PDoc, n, nameNode: PNode, k: TSymKind): JsonNode =
+      setIndexTerm(d[], ieNim, htmlFile = external, id = symbolOrId,
+                   term = plain, linkTitle = symNameNode.sym.name.s & '.' & plain,
+                   linkDesc = xmltree.escape(getPlainDocstring(e).docstringSummary),
+                   line = n.info.line.int)
+
+  d.tocSimple[k].add TocItem(
+    sortName: sortName,
+    content: getConfigVar(d.conf, "doc.item.toc") % [
+      "name", name, "header_plain", plainNameEsc,
+      "itemSymOrIDEnc", symbolOrIdEnc])
+
+  d.tocTable[k].mgetOrPut(cleanPlainSymbol, newSeq[TocItem]()).add TocItem(
+    sortName: sortName,
+    content: getConfigVar(d.conf, "doc.item.tocTable") % [
+      "name", name, "header_plain", plainNameEsc,
+      "itemSymOrID", symbolOrId.replace(",", ",<wbr>"),
+      "itemSymOrIDEnc", symbolOrIdEnc])
+
+  setIndexTerm(d[], ieNim, htmlFile = external, id = symbolOrId, term = name,
+               linkTitle = detailedName,
+               linkDesc = xmltree.escape(plainDocstring.docstringSummary),
+               line = n.info.line.int)
+  if k == skType and symNameNode.kind == nkSym:
+    d.types.strTableAdd symNameNode.sym
+
+proc genJsonItem(d: PDoc, n, nameNode: PNode, k: TSymKind, nonExports = false): JsonItem =
   if not isVisible(d, nameNode): return
   var
-    name = getName(d, nameNode)
-    comm = $genRecComment(d, n)
+    name = getNameEsc(d, nameNode)
+    comm = genRecComment(d, n)
     r: TSrcGen
-  initTokRender(r, n, {renderNoBody, renderNoComments, renderDocComments})
-  result = %{ "name": %name, "type": %($k), "line": %n.info.line.int,
-                 "col": %n.info.col}
-  if comm.len > 0:
-    result["description"] = %comm
+    renderFlags = {renderNoBody, renderNoComments, renderDocComments,
+      renderExpandUsing, renderNoPostfix}
+  if nonExports:
+    renderFlags.incl renderNonExportedFields
+  r = initTokRender(n, renderFlags)
+  result = JsonItem(json: %{ "name": %name, "type": %($k), "line": %n.info.line.int,
+                   "col": %n.info.col}
+  )
+  if comm != nil:
+    result.rst = comm
+    result.rstField = "description"
   if r.buf.len > 0:
-    result["code"] = %r.buf
+    result.json["code"] = %r.buf
   if k in routineKinds:
-    result["signature"] = newJObject()
+    result.json["signature"] = newJObject()
     if n[paramsPos][0].kind != nkEmpty:
-      result["signature"]["return"] = %($n[paramsPos][0])
+      result.json["signature"]["return"] = %($n[paramsPos][0])
     if n[paramsPos].len > 1:
-      result["signature"]["arguments"] = newJArray()
+      result.json["signature"]["arguments"] = newJArray()
     for paramIdx in 1 ..< n[paramsPos].len:
       for identIdx in 0 ..< n[paramsPos][paramIdx].len - 2:
         let
@@ -930,22 +1193,61 @@ proc genJsonItem(d: PDoc, n, nameNode: PNode, k: TSymKind): JsonNode =
           paramType = $n[paramsPos][paramIdx][^2]
         if n[paramsPos][paramIdx][^1].kind != nkEmpty:
           let paramDefault = $n[paramsPos][paramIdx][^1]
-          result["signature"]["arguments"].add %{"name": %paramName, "type": %paramType, "default": %paramDefault}
+          result.json["signature"]["arguments"].add %{"name": %paramName, "type": %paramType, "default": %paramDefault}
         else:
-          result["signature"]["arguments"].add %{"name": %paramName, "type": %paramType}
+          result.json["signature"]["arguments"].add %{"name": %paramName, "type": %paramType}
     if n[pragmasPos].kind != nkEmpty:
-      result["signature"]["pragmas"] = newJArray()
+      result.json["signature"]["pragmas"] = newJArray()
       for pragma in n[pragmasPos]:
-        result["signature"]["pragmas"].add %($pragma)
+        result.json["signature"]["pragmas"].add %($pragma)
     if n[genericParamsPos].kind != nkEmpty:
-      result["signature"]["genericParams"] = newJArray()
+      result.json["signature"]["genericParams"] = newJArray()
       for genericParam in n[genericParamsPos]:
         var param = %{"name": %($genericParam)}
-        if genericParam.sym.typ.sons.len > 0:
+        if genericParam.sym.typ.len > 0:
           param["types"] = newJArray()
-        for kind in genericParam.sym.typ.sons:
-          param["types"].add %($kind)
-        result["signature"]["genericParams"].add param
+          param["types"] = %($genericParam.sym.typ.elementType)
+        result.json["signature"]["genericParams"].add param
+  if optGenIndex in d.conf.globalOptions:
+    genItem(d, n, nameNode, k, kForceExport)
+
+proc setDoctype(d: PDoc, n: PNode) =
+  ## Processes `{.doctype.}` pragma changing Markdown/RST parsing options.
+  if n == nil:
+    return
+  if n.len != 2:
+    localError(d.conf, n.info, errUser,
+      "doctype pragma takes exactly 1 argument"
+    )
+    return
+  var dt = ""
+  case n[1].kind
+  of nkStrLit:
+    dt = toLowerAscii(n[1].strVal)
+  of nkIdent:
+    dt = toLowerAscii(n[1].ident.s)
+  else:
+    localError(d.conf, n.info, errUser,
+      "unknown argument type $1 provided to doctype" % [$n[1].kind]
+    )
+    return
+  case dt
+  of "markdown":
+    d.sharedState.options.incl roSupportMarkdown
+    d.sharedState.options.incl roPreferMarkdown
+  of "rstmarkdown":
+    d.sharedState.options.incl roSupportMarkdown
+    d.sharedState.options.excl roPreferMarkdown
+  of "rst":
+    d.sharedState.options.excl roSupportMarkdown
+    d.sharedState.options.excl roPreferMarkdown
+  else:
+    localError(d.conf, n.info, errUser,
+      (
+        "unknown doctype value \"$1\", should be from " &
+        "\"RST\", \"Markdown\", \"RstMarkdown\""
+      ) % [dt]
+    )
 
 proc checkForFalse(n: PNode): bool =
   result = n.kind == nkIdent and cmpIgnoreStyle(n.ident.s, "false") == 0
@@ -962,38 +1264,44 @@ proc traceDeps(d: PDoc, it: PNode) =
     for x in it[2]:
       a[2] = x
       traceDeps(d, a)
-  elif it.kind == nkSym and belongsToPackage(d.conf, it.sym):
+  elif it.kind == nkSym and belongsToProjectPackage(d.conf, it.sym):
     let external = externalDep(d, it.sym)
-    if d.section[k] != nil: d.section[k].add(", ")
-    dispA(d.conf, d.section[k],
+    if d.section[k].finalMarkup != "": d.section[k].finalMarkup.add(", ")
+    dispA(d.conf, d.section[k].finalMarkup,
           "<a class=\"reference external\" href=\"$2\">$1</a>",
-          "$1", [rope esc(d.target, external.prettyLink),
-          rope changeFileExt(external, "html")])
+          "$1", [esc(d.target, external.prettyLink),
+                 changeFileExt(external, "html")])
 
 proc exportSym(d: PDoc; s: PSym) =
   const k = exportSection
-  if s.kind == skModule and belongsToPackage(d.conf, s):
+  if s.kind == skModule and belongsToProjectPackage(d.conf, s):
     let external = externalDep(d, s)
-    if d.section[k] != nil: d.section[k].add(", ")
-    dispA(d.conf, d.section[k],
+    if d.section[k].finalMarkup != "": d.section[k].finalMarkup.add(", ")
+    dispA(d.conf, d.section[k].finalMarkup,
           "<a class=\"reference external\" href=\"$2\">$1</a>",
-          "$1", [rope esc(d.target, external.prettyLink),
-          rope changeFileExt(external, "html")])
+          "$1", [esc(d.target, external.prettyLink),
+                 changeFileExt(external, "html")])
   elif s.kind != skModule and s.owner != nil:
     let module = originatingModule(s)
-    if belongsToPackage(d.conf, module):
-      let external = externalDep(d, module)
-      if d.section[k] != nil: d.section[k].add(", ")
+    if belongsToProjectPackage(d.conf, module):
+      let
+        complexSymbol = complexName(s.kind, s.ast, s.name.s)
+        symbolOrId = d.newUniquePlainSymbol(complexSymbol)
+        external = externalDep(d, module)
+      if d.section[k].finalMarkup != "": d.section[k].finalMarkup.add(", ")
       # XXX proper anchor generation here
-      dispA(d.conf, d.section[k],
-            "<a href=\"$2#$1\"><span class=\"Identifier\">$1</span></a>",
-            "$1", [rope esc(d.target, s.name.s),
-            rope changeFileExt(external, "html")])
+      dispA(d.conf, d.section[k].finalMarkup,
+            "<a href=\"$2#$3\"><span class=\"Identifier\">$1</span></a>",
+            "$1", [esc(d.target, s.name.s),
+                   changeFileExt(external, "html"),
+                   symbolOrId])
 
 proc documentNewEffect(cache: IdentCache; n: PNode): PNode =
   let s = n[namePos].sym
   if tfReturnsNew in s.typ.flags:
     result = newIdentNode(getIdent(cache, "new"), n.info)
+  else:
+    result = nil
 
 proc documentEffect(cache: IdentCache; n, x: PNode, effectType: TSpecialWord, idx: int): PNode =
   let spec = effectSpec(x, effectType)
@@ -1003,18 +1311,21 @@ proc documentEffect(cache: IdentCache; n, x: PNode, effectType: TSpecialWord, id
     let actual = s.typ.n[0]
     if actual.len != effectListLen: return
     let real = actual[idx]
-
+    if real == nil: return
+    let realLen = real.len
     # warning: hack ahead:
-    var effects = newNodeI(nkBracket, n.info, real.len)
-    for i in 0..<real.len:
+    var effects = newNodeI(nkBracket, n.info, realLen)
+    for i in 0..<realLen:
       var t = typeToString(real[i].typ)
       if t.startsWith("ref "): t = substr(t, 4)
       effects[i] = newIdentNode(getIdent(cache, t), n.info)
       # set the type so that the following analysis doesn't screw up:
       effects[i].typ = real[i].typ
 
-    result = newNode(nkExprColonExpr, n.info, @[
-      newIdentNode(getIdent(cache, specialWords[effectType]), n.info), effects])
+    result = newTreeI(nkExprColonExpr, n.info,
+      newIdentNode(getIdent(cache, $effectType), n.info), effects)
+  else:
+    result = nil
 
 proc documentWriteEffect(cache: IdentCache; n: PNode; flag: TSymFlag; pragmaName: string): PNode =
   let s = n[namePos].sym
@@ -1026,8 +1337,10 @@ proc documentWriteEffect(cache: IdentCache; n: PNode; flag: TSymFlag; pragmaName
       effects.add params[i]
 
   if effects.len > 0:
-    result = newNode(nkExprColonExpr, n.info, @[
-      newIdentNode(getIdent(cache, pragmaName), n.info), effects])
+    result = newTreeI(nkExprColonExpr, n.info,
+      newIdentNode(getIdent(cache, pragmaName), n.info), effects)
+  else:
+    result = nil
 
 proc documentRaises*(cache: IdentCache; n: PNode) =
   if n[namePos].kind != nkSym: return
@@ -1037,8 +1350,9 @@ proc documentRaises*(cache: IdentCache; n: PNode) =
   let p3 = documentWriteEffect(cache, n, sfWrittenTo, "writes")
   let p4 = documentNewEffect(cache, n)
   let p5 = documentWriteEffect(cache, n, sfEscapes, "escapes")
+  let p6 = documentEffect(cache, n, pragmas, wForbids, forbiddenEffects)
 
-  if p1 != nil or p2 != nil or p3 != nil or p4 != nil or p5 != nil:
+  if p1 != nil or p2 != nil or p3 != nil or p4 != nil or p5 != nil or p6 != nil:
     if pragmas.kind == nkEmpty:
       n[pragmasPos] = newNodeI(nkPragma, n.info)
     if p1 != nil: n[pragmasPos].add p1
@@ -1046,21 +1360,25 @@ proc documentRaises*(cache: IdentCache; n: PNode) =
     if p3 != nil: n[pragmasPos].add p3
     if p4 != nil: n[pragmasPos].add p4
     if p5 != nil: n[pragmasPos].add p5
+    if p6 != nil: n[pragmasPos].add p6
 
-proc generateDoc*(d: PDoc, n, orig: PNode, docFlags: DocFlags = kDefault) =
+proc generateDoc*(d: PDoc, n, orig: PNode, config: ConfigRef, docFlags: DocFlags = kDefault) =
+  ## Goes through nim nodes recursively and collects doc comments.
+  ## Main function for `doc`:option: command,
+  ## which is implemented in ``docgen2.nim``.
   template genItemAux(skind) =
     genItem(d, n, n[namePos], skind, docFlags)
+  let showNonExports = optShowNonExportedFields in config.globalOptions
   case n.kind
   of nkPragma:
     let pragmaNode = findPragma(n, wDeprecated)
     d.modDeprecationMsg.add(genDeprecationMsg(d, pragmaNode))
-  of nkCommentStmt: d.modDesc.add(genComment(d, n))
-  of nkProcDef:
+    let doctypeNode = findPragma(n, wDoctype)
+    setDoctype(d, doctypeNode)
+  of nkCommentStmt: d.modDescPre.add(genComment(d, n))
+  of nkProcDef, nkFuncDef:
     when useEffectSystem: documentRaises(d.cache, n)
     genItemAux(skProc)
-  of nkFuncDef:
-    when useEffectSystem: documentRaises(d.cache, n)
-    genItemAux(skFunc)
   of nkMethodDef:
     when useEffectSystem: documentRaises(d.cache, n)
     genItemAux(skMethod)
@@ -1077,46 +1395,188 @@ proc generateDoc*(d: PDoc, n, orig: PNode, docFlags: DocFlags = kDefault) =
       if n[i].kind != nkCommentStmt:
         # order is always 'type var let const':
         genItem(d, n[i], n[i][0],
-                succ(skType, ord(n.kind)-ord(nkTypeSection)), docFlags)
+                succ(skType, ord(n.kind)-ord(nkTypeSection)), docFlags, showNonExports)
   of nkStmtList:
-    for i in 0..<n.len: generateDoc(d, n[i], orig)
+    for i in 0..<n.len: generateDoc(d, n[i], orig, config)
   of nkWhenStmt:
     # generate documentation for the first branch only:
     if not checkForFalse(n[0][0]):
-      generateDoc(d, lastSon(n[0]), orig)
+      generateDoc(d, lastSon(n[0]), orig, config)
   of nkImportStmt:
     for it in n: traceDeps(d, it)
   of nkExportStmt:
     for it in n:
-      if it.kind == nkSym:
+      # bug #23051; don't generate documentation for exported symbols again
+      if it.kind == nkSym and sfExported notin it.sym.flags:
         if d.module != nil and d.module == it.sym.owner:
-          generateDoc(d, it.sym.ast, orig, kForceExport)
-        else:
+          generateDoc(d, it.sym.ast, orig, config, kForceExport)
+        elif it.sym.ast != nil:
           exportSym(d, it.sym)
   of nkExportExceptStmt: discard "transformed into nkExportStmt by semExportExcept"
   of nkFromStmt, nkImportExceptStmt: traceDeps(d, n[0])
   of nkCallKinds:
-    var comm: Rope = nil
+    var comm: ItemPre = default(ItemPre)
     getAllRunnableExamples(d, n, comm)
-    if comm != nil: d.modDesc.add(comm)
+    if comm.len != 0: d.modDescPre.add(comm)
   else: discard
 
-proc add(d: PDoc; j: JsonNode) =
-  if j != nil: d.jArray.add j
+proc overloadGroupName(s: string, k: TSymKind): string =
+  ## Turns a name like `f` into anchor `f-procs-all`
+  s & "-" & k.toHumanStr & "s-all"
+
+proc setIndexTitle(d: PDoc, useMetaTitle: bool) =
+  let titleKind = if d.standaloneDoc: ieMarkupTitle else: ieNimTitle
+  let external = AbsoluteFile(d.destFile)
+    .relativeTo(d.conf.outDir, '/')
+    .changeFileExt(HtmlExt)
+    .string
+  var term, linkTitle: string
+  if useMetaTitle and d.meta[metaTitle].len != 0:
+    term = d.meta[metaTitleRaw]
+    linkTitle = d.meta[metaTitleRaw]
+  else:
+    let filename = extractFilename(d.filename)
+    term =
+      if d.standaloneDoc: filename  # keep .rst/.md extension
+      else: changeFileExt(filename, "")  # rm .nim extension
+    linkTitle =
+      if d.standaloneDoc: term  # keep .rst/.md extension
+      else: canonicalImport(d.conf, AbsoluteFile d.filename)
+  if not d.standaloneDoc:
+    linkTitle = "module " & linkTitle
+  setIndexTerm(d[], titleKind, htmlFile = external, id = "",
+               term = term, linkTitle = linkTitle)
+
+proc finishGenerateDoc*(d: var PDoc) =
+  ## Perform 2nd RST pass for resolution of links/footnotes/headings...
+  # copy file map `filenames` to ``rstgen.nim`` for its warnings
+  d.filenames = d.sharedState.filenames
+
+  # Main title/subtitle are allowed only in the first RST fragment of document
+  var firstRst = PRstNode(nil)
+  for fragment in d.modDescPre:
+    if fragment.isRst:
+      firstRst = fragment.rst
+      break
+  d.hasToc = d.hasToc or d.sharedState.hasToc
+  # in --index:only mode we do NOT want to load other .idx, only write ours:
+  let importdoc = optGenIndexOnly notin d.conf.globalOptions and
+                  optNoImportdoc notin d.conf.globalOptions
+  preparePass2(d.sharedState, firstRst, importdoc)
+
+  if optGenIndexOnly in d.conf.globalOptions:
+    # Top-level doc.comments may contain titles and :idx: statements:
+    for fragment in d.modDescPre:
+      if fragment.isRst:
+        traverseForIndex(d[], fragment.rst)
+    setIndexTitle(d, useMetaTitle = d.standaloneDoc)
+    # Symbol-associated doc.comments may contain :idx: statements:
+    for k in TSymKind:
+      for _, overloadChoices in d.section[k].secItems:
+        for item in overloadChoices:
+          for fragment in item.descRst:
+            if fragment.isRst:
+              traverseForIndex(d[], fragment.rst)
+
+  # add anchors to overload groups before RST resolution
+  for k in TSymKind:
+    if k in routineKinds:
+      for plainName, overloadChoices in d.section[k].secItems:
+        if overloadChoices.len > 1:
+          let refn = overloadGroupName(plainName, k)
+          let tooltip = "$1 ($2 overloads)" % [
+                      k.toHumanStr & " " & plainName, $overloadChoices.len]
+          let name = nimIdentBackticksNormalize(plainName)
+          # save overload group to ``.idx``
+          let external = d.destFile.AbsoluteFile.relativeTo(d.conf.outDir, '/').
+                         changeFileExt(HtmlExt).string
+          setIndexTerm(d[], ieNimGroup, htmlFile = external, id = refn,
+                       term = name, linkTitle = k.toHumanStr,
+                       linkDesc = "", line = overloadChoices[0].info.line.int)
+          if optGenIndexOnly in d.conf.globalOptions: continue
+          addAnchorNim(d.sharedState, external=false, refn, tooltip,
+                       LangSymbol(symKind: k.toHumanStr,
+                                  name: name,
+                                  isGroup: true),
+                       priority = symbolPriority(k),
+                       # select index `0` just to have any meaningful warning:
+                       info = overloadChoices[0].info,
+                       module = addRstFileIndex(d, FileIndex d.module.position))
+
+  if optGenIndexOnly in d.conf.globalOptions:
+    return
+
+  # Finalize fragments of ``.nim`` or ``.rst`` file
+  proc renderItemPre(d: PDoc, fragments: ItemPre, result: var string) =
+    for f in fragments:
+      case f.isRst:
+      of true:
+        var resolved = resolveSubs(d.sharedState, f.rst)
+        renderRstToOut(d[], resolved, result)
+      of false: result &= f.str
+  proc cmp(x, y: Item): int = cmpDecimalsIgnoreCase(x.sortName, y.sortName)
+  for k in TSymKind:
+    # add symbols to section for each `k`, while optionally wrapping
+    # overloadable items with the same basic name by ``doc.item2``
+    let overloadableNames = toSeq(keys(d.section[k].secItems))
+    for plainName in overloadableNames.sorted(cmpDecimalsIgnoreCase):
+      var overloadChoices = d.section[k].secItems[plainName]
+      overloadChoices.sort(cmp)
+      var nameContent = ""
+      for item in overloadChoices:
+        var itemDesc: string = ""
+        renderItemPre(d, item.descRst, itemDesc)
+        nameContent.add(
+          getConfigVar(d.conf, "doc.item") % (
+              item.substitutions & @[
+                "desc", itemDesc,
+                "name", item.name,
+                "itemSymOrID", item.anchor]))
+      if k in routineKinds:
+        let plainNameEsc1 = esc(d.target, plainName.strip)
+        let plainNameEsc2 = esc(d.target, plainName.strip, escMode=emUrl)
+        d.section[k].finalMarkup.add(
+          getConfigVar(d.conf, "doc.item2") % (
+            @["header_plain", plainNameEsc1,
+              "overloadGroupName", overloadGroupName(plainNameEsc2, k),
+              "content", nameContent]))
+      else:
+        d.section[k].finalMarkup.add(nameContent)
+    d.section[k].secItems.clear
+  renderItemPre(d, d.modDescPre, d.modDescFinal)
+  d.modDescPre.setLen 0
+
+  # Finalize fragments of ``.json`` file
+  for i, entry in d.jEntriesPre:
+    if entry.rst != nil:
+      let resolved = resolveSubs(d.sharedState, entry.rst)
+      var str: string = ""
+      renderRstToOut(d[], resolved, str)
+      entry.json[entry.rstField] = %str
+      d.jEntriesPre[i].rst = nil
 
-proc generateJson*(d: PDoc, n: PNode, includeComments: bool = true) =
+    d.jEntriesFinal.add entry.json # generates docs
+
+  setIndexTitle(d, useMetaTitle = d.standaloneDoc)
+  completePass2(d.sharedState)
+
+proc add(d: PDoc; j: JsonItem) =
+  if j.json != nil or j.rst != nil: d.jEntriesPre.add j
+
+proc generateJson*(d: PDoc, n: PNode, config: ConfigRef, includeComments: bool = true) =
   case n.kind
+  of nkPragma:
+    let doctypeNode = findPragma(n, wDoctype)
+    setDoctype(d, doctypeNode)
   of nkCommentStmt:
     if includeComments:
-      d.add %*{"comment": genComment(d, n)}
+      d.add JsonItem(rst: genComment(d, n), rstField: "comment",
+                     json: %Table[string, string]())
     else:
-      d.modDesc.add(genComment(d, n))
-  of nkProcDef:
+      d.modDescPre.add(genComment(d, n))
+  of nkProcDef, nkFuncDef:
     when useEffectSystem: documentRaises(d.cache, n)
     d.add genJsonItem(d, n, n[namePos], skProc)
-  of nkFuncDef:
-    when useEffectSystem: documentRaises(d.cache, n)
-    d.add genJsonItem(d, n, n[namePos], skFunc)
   of nkMethodDef:
     when useEffectSystem: documentRaises(d.cache, n)
     d.add genJsonItem(d, n, n[namePos], skMethod)
@@ -1135,20 +1595,20 @@ proc generateJson*(d: PDoc, n: PNode, includeComments: bool = true) =
       if n[i].kind != nkCommentStmt:
         # order is always 'type var let const':
         d.add genJsonItem(d, n[i], n[i][0],
-                succ(skType, ord(n.kind)-ord(nkTypeSection)))
+                succ(skType, ord(n.kind)-ord(nkTypeSection)), optShowNonExportedFields in config.globalOptions)
   of nkStmtList:
     for i in 0..<n.len:
-      generateJson(d, n[i], includeComments)
+      generateJson(d, n[i], config, includeComments)
   of nkWhenStmt:
     # generate documentation for the first branch only:
     if not checkForFalse(n[0][0]):
-      generateJson(d, lastSon(n[0]), includeComments)
+      generateJson(d, lastSon(n[0]), config, includeComments)
   else: discard
 
 proc genTagsItem(d: PDoc, n, nameNode: PNode, k: TSymKind): string =
-  result = getName(d, nameNode) & "\n"
+  result = getNameEsc(d, nameNode) & "\n"
 
-proc generateTags*(d: PDoc, n: PNode, r: var Rope) =
+proc generateTags*(d: PDoc, n: PNode, r: var string) =
   case n.kind
   of nkCommentStmt:
     if startsWith(n.comment, "##"):
@@ -1188,102 +1648,156 @@ proc generateTags*(d: PDoc, n: PNode, r: var Rope) =
       generateTags(d, lastSon(n[0]), r)
   else: discard
 
-proc genSection(d: PDoc, kind: TSymKind) =
+proc genSection(d: PDoc, kind: TSymKind, groupedToc = false) =
   const sectionNames: array[skModule..skField, string] = [
     "Imports", "Types", "Vars", "Lets", "Consts", "Vars", "Procs", "Funcs",
     "Methods", "Iterators", "Converters", "Macros", "Templates", "Exports"
   ]
-  if d.section[kind] == nil: return
-  var title = sectionNames[kind].rope
-  d.section[kind] = ropeFormatNamedVars(d.conf, getConfigVar(d.conf, "doc.section"), [
-      "sectionid", "sectionTitle", "sectionTitleID", "content"], [
-      ord(kind).rope, title, rope(ord(kind) + 50), d.section[kind]])
-  d.toc[kind] = ropeFormatNamedVars(d.conf, getConfigVar(d.conf, "doc.section.toc"), [
-      "sectionid", "sectionTitle", "sectionTitleID", "content"], [
-      ord(kind).rope, title, rope(ord(kind) + 50), d.toc[kind]])
-
-proc relLink(outDir: AbsoluteDir, destFile: AbsoluteFile, linkto: RelativeFile): Rope =
-  rope($relativeTo(outDir / linkto, destFile.splitFile().dir, '/'))
-
-proc genOutFile(d: PDoc): Rope =
+  if d.section[kind].finalMarkup == "": return
+  var title = sectionNames[kind]
+  d.section[kind].finalMarkup = getConfigVar(d.conf, "doc.section") % [
+      "sectionid", $ord(kind), "sectionTitle", title,
+      "sectionTitleID", $(ord(kind) + 50), "content", d.section[kind].finalMarkup]
+
+  proc cmp(x, y: TocItem): int = cmpDecimalsIgnoreCase(x.sortName, y.sortName)
+  if groupedToc:
+    let overloadableNames = toSeq(keys(d.tocTable[kind]))
+    for plainName in overloadableNames.sorted(cmpDecimalsIgnoreCase):
+      var overloadChoices = d.tocTable[kind][plainName]
+      overloadChoices.sort(cmp)
+      var content: string = ""
+      for item in overloadChoices:
+        content.add item.content
+      d.toc2[kind].add getConfigVar(d.conf, "doc.section.toc2") % [
+          "sectionid", $ord(kind), "sectionTitle", title,
+          "sectionTitleID", $(ord(kind) + 50),
+          "content", content, "plainName", plainName]
+  else:
+    for item in d.tocSimple[kind].sorted(cmp):
+      d.toc2[kind].add item.content
+
+  let sectionValues = @[
+     "sectionID", $ord(kind), "sectionTitleID", $(ord(kind) + 50),
+     "sectionTitle", title
+  ]
+
+  # Check if the toc has any children
+  if d.toc2[kind] != "":
+    # Use the dropdown version instead and store the children in the dropdown
+    d.toc[kind] = getConfigVar(d.conf, "doc.section.toc") % (sectionValues & @[
+       "content", d.toc2[kind]
+    ])
+  else:
+    # Just have the link
+    d.toc[kind] =  getConfigVar(d.conf, "doc.section.toc_item") % sectionValues
+
+proc relLink(outDir: AbsoluteDir, destFile: AbsoluteFile, linkto: RelativeFile): string =
+  $relativeTo(outDir / linkto, destFile.splitFile().dir, '/')
+
+proc genOutFile(d: PDoc, groupedToc = false): string =
   var
-    code, content: Rope
+    code, content: string = ""
     title = ""
   var j = 0
-  var tmp = ""
-  renderTocEntries(d[], j, 1, tmp)
-  var toc = tmp.rope
-  for i in low(TSymKind)..high(TSymKind):
-    genSection(d, i)
+  var toc = ""
+  renderTocEntries(d[], j, 1, toc)
+  for i in TSymKind:
+    var shouldSort = i in routineKinds and groupedToc
+    genSection(d, i, shouldSort)
     toc.add(d.toc[i])
-  if toc != nil:
-    toc = ropeFormatNamedVars(d.conf, getConfigVar(d.conf, "doc.toc"), ["content"], [toc])
-  for i in low(TSymKind)..high(TSymKind): code.add(d.section[i])
+  if toc != "" or d.target == outLatex:
+    # for Latex $doc.toc will automatically generate TOC if `d.hasToc` is set
+    toc = getConfigVar(d.conf, "doc.toc") % ["content", toc]
+  for i in TSymKind: code.add(d.section[i].finalMarkup)
 
   # Extract the title. Non API modules generate an entry in the index table.
   if d.meta[metaTitle].len != 0:
     title = d.meta[metaTitle]
-    let external = presentationPath(d.conf, AbsoluteFile d.filename).changeFileExt(HtmlExt).string.nativeToUnixPath
-    setIndexTerm(d[], external, "", title)
   else:
-    # Modules get an automatic title for the HTML, but no entry in the index.
-    # better than `extractFilename(changeFileExt(d.filename, ""))` as it disambiguates dups
-    title = $presentationPath(d.conf, AbsoluteFile d.filename, isTitle = true).changeFileExt("")
+    title = canonicalImport(d.conf, AbsoluteFile d.filename)
+  title = esc(d.target, title)
+  var subtitle = ""
+  if d.meta[metaSubtitle] != "":
+    dispA(d.conf, subtitle, "<h2 class=\"subtitle\">$1</h2>",
+        "\\\\\\vspace{0.5em}\\large $1", [esc(d.target, d.meta[metaSubtitle])])
 
   var groupsection = getConfigVar(d.conf, "doc.body_toc_groupsection")
-  let bodyname = if d.hasToc and not d.isPureRst:
+  let bodyname = if d.hasToc and not d.standaloneDoc and not d.conf.isLatexCmd:
                    groupsection.setLen 0
                    "doc.body_toc_group"
                  elif d.hasToc: "doc.body_toc"
                  else: "doc.body_no_toc"
-  content = ropeFormatNamedVars(d.conf, getConfigVar(d.conf, bodyname), ["title",
-      "tableofcontents", "moduledesc", "date", "time", "content", "deprecationMsg", "theindexhref", "body_toc_groupsection"],
-      [title.rope, toc, d.modDesc, rope(getDateStr()),
-      rope(getClockStr()), code, d.modDeprecationMsg, relLink(d.conf.outDir, d.destFile, theindexFname.RelativeFile), groupsection.rope])
+  let seeSrc = genSeeSrc(d, d.filename, 1)
+  content = getConfigVar(d.conf, bodyname) % [
+      "title", title, "subtitle", subtitle,
+      "tableofcontents", toc, "moduledesc", d.modDescFinal, "date", getDateStr(),
+      "time", getClockStr(), "content", code,
+      "deprecationMsg", d.modDeprecationMsg,
+      "theindexhref", relLink(d.conf.outDir, d.destFile.AbsoluteFile,
+                              theindexFname.RelativeFile),
+      "body_toc_groupsection", groupsection, "seeSrc", seeSrc]
   if optCompileOnly notin d.conf.globalOptions:
     # XXX what is this hack doing here? 'optCompileOnly' means raw output!?
-    code = ropeFormatNamedVars(d.conf, getConfigVar(d.conf, "doc.file"), [
-        "nimdoccss", "dochackjs",  "title", "tableofcontents", "moduledesc", "date", "time",
-        "content", "author", "version", "analytics", "deprecationMsg"],
-        [relLink(d.conf.outDir, d.destFile, nimdocOutCss.RelativeFile),
-        relLink(d.conf.outDir, d.destFile, docHackJsFname.RelativeFile),
-        title.rope, toc, d.modDesc, rope(getDateStr()), rope(getClockStr()),
-        content, d.meta[metaAuthor].rope, d.meta[metaVersion].rope, d.analytics.rope, d.modDeprecationMsg])
+    code = getConfigVar(d.conf, "doc.file") % [
+        "nimdoccss", relLink(d.conf.outDir, d.destFile.AbsoluteFile,
+                             nimdocOutCss.RelativeFile),
+        "dochackjs", relLink(d.conf.outDir, d.destFile.AbsoluteFile,
+                             docHackJsFname.RelativeFile),
+        "title", title, "subtitle", subtitle, "tableofcontents", toc,
+        "moduledesc", d.modDescFinal, "date", getDateStr(), "time", getClockStr(),
+        "content", content, "author", d.meta[metaAuthor],
+        "version", esc(d.target, d.meta[metaVersion]), "analytics", d.analytics,
+        "deprecationMsg", d.modDeprecationMsg, "nimVersion", $NimMajor & "." & $NimMinor & "." & $NimPatch]
   else:
     code = content
   result = code
 
+proc indexFile(d: PDoc): AbsoluteFile =
+  let dir = d.conf.outDir
+  result = dir / changeFileExt(presentationPath(d.conf,
+                                                AbsoluteFile d.filename),
+                               IndexExt)
+  let (finalDir, _, _) = result.string.splitFile
+  createDir(finalDir)
+
 proc generateIndex*(d: PDoc) =
   if optGenIndex in d.conf.globalOptions:
-    let dir = d.conf.outDir
-    createDir(dir)
-    let dest = dir / changeFileExt(presentationPath(d.conf, AbsoluteFile d.filename), IndexExt)
+    let dest = indexFile(d)
     writeIndexFile(d[], dest.string)
 
 proc updateOutfile(d: PDoc, outfile: AbsoluteFile) =
-  if d.module == nil or sfMainModule in d.module.flags: # nil for eg for commandRst2Html
+  if d.module == nil or sfMainModule in d.module.flags: # nil for e.g. for commandRst2Html
     if d.conf.outFile.isEmpty:
       d.conf.outFile = outfile.relativeTo(d.conf.outDir)
       if isAbsolute(d.conf.outFile.string):
         d.conf.outFile = splitPath(d.conf.outFile.string)[1].RelativeFile
 
-proc writeOutput*(d: PDoc, useWarning = false) =
+proc writeOutput*(d: PDoc, useWarning = false, groupedToc = false) =
+  if optGenIndexOnly in d.conf.globalOptions:
+    d.conf.outFile = indexFile(d).relativeTo(d.conf.outDir)  # just for display
+    return
   runAllExamples(d)
-  var content = genOutFile(d)
+  var content = genOutFile(d, groupedToc)
   if optStdout in d.conf.globalOptions:
-    writeRope(stdout, content)
+    write(stdout, content)
   else:
-    template outfile: untyped = d.destFile
+    template outfile: untyped = d.destFile.AbsoluteFile
     #let outfile = getOutFile2(d.conf, shortenDir(d.conf, filename), outExt)
     let dir = outfile.splitFile.dir
     createDir(dir)
     updateOutfile(d, outfile)
-    if not writeRope(content, outfile):
+    try:
+      writeFile(outfile, content)
+    except IOError:
       rawMessage(d.conf, if useWarning: warnCannotOpenFile else: errCannotOpenFile,
         outfile.string)
-    elif not d.wroteSupportFiles: # nimdoc.css + dochack.js
+    if not d.wroteSupportFiles: # nimdoc.css + dochack.js
       let nimr = $d.conf.getPrefixDir()
-      copyFile(docCss.interp(nimr = nimr), $d.conf.outDir / nimdocOutCss)
+      case d.target
+      of outHtml:
+        copyFile(docCss.interp(nimr = nimr), $d.conf.outDir / nimdocOutCss)
+      of outLatex:
+        copyFile(docCls.interp(nimr = nimr), $d.conf.outDir / nimdocOutCls)
       if optGenIndex in d.conf.globalOptions:
         let docHackJs2 = getDocHacksJs(nimr, nim = getAppFilename())
         copyFile(docHackJs2, $d.conf.outDir / docHackJs2.lastPathPart)
@@ -1291,24 +1805,26 @@ proc writeOutput*(d: PDoc, useWarning = false) =
 
 proc writeOutputJson*(d: PDoc, useWarning = false) =
   runAllExamples(d)
-  var modDesc: string
-  for desc in d.modDesc:
+  var modDesc: string = ""
+  for desc in d.modDescFinal:
     modDesc &= desc
   let content = %*{"orig": d.filename,
     "nimble": getPackageName(d.conf, d.filename),
     "moduleDescription": modDesc,
-    "entries": d.jArray}
+    "entries": d.jEntriesFinal}
   if optStdout in d.conf.globalOptions:
-    write(stdout, $content)
+    writeLine(stdout, $content)
   else:
-    var f: File
-    if open(f, d.destFile.string, fmWrite):
+    let dir = d.destFile.splitFile.dir
+    createDir(dir)
+    var f: File = default(File)
+    if open(f, d.destFile, fmWrite):
       write(f, $content)
       close(f)
-      updateOutfile(d, d.destFile)
+      updateOutfile(d, d.destFile.AbsoluteFile)
     else:
       localError(d.conf, newLineInfo(d.conf, AbsoluteFile d.filename, -1, -1),
-                 warnUser, "unable to open file \"" & d.destFile.string &
+                 warnUser, "unable to open file \"" & d.destFile &
                  "\" for writing")
 
 proc handleDocOutputOptions*(conf: ConfigRef) =
@@ -1319,93 +1835,120 @@ proc handleDocOutputOptions*(conf: ConfigRef) =
     conf.outDir = AbsoluteDir(conf.outDir / conf.outFile)
 
 proc commandDoc*(cache: IdentCache, conf: ConfigRef) =
+  ## implementation of deprecated ``doc0`` command (without semantic checking)
   handleDocOutputOptions conf
   var ast = parseFile(conf.projectMainIdx, cache, conf)
   if ast == nil: return
-  var d = newDocumentor(conf.projectFull, cache, conf)
-  d.hasToc = true
-  generateDoc(d, ast, ast)
+  var d = newDocumentor(conf.projectFull, cache, conf, hasToc = true)
+  generateDoc(d, ast, ast, conf)
+  finishGenerateDoc(d)
   writeOutput(d)
   generateIndex(d)
 
 proc commandRstAux(cache: IdentCache, conf: ConfigRef;
-                   filename: AbsoluteFile, outExt: string) =
+                   filename: AbsoluteFile, outExt: string,
+                   preferMarkdown: bool) =
   var filen = addFileExt(filename, "txt")
-  var d = newDocumentor(filen, cache, conf, outExt)
-
-  d.isPureRst = true
-  var rst = parseRst(readFile(filen.string), filen.string, 0, 1, d.hasToc,
-                     {roSupportRawDirective, roSupportMarkdown}, conf)
-  var modDesc = newStringOfCap(30_000)
-  renderRstToOut(d[], rst, modDesc)
-  d.modDesc = rope(modDesc)
-  writeOutput(d)
-  generateIndex(d)
-
-proc commandRst2Html*(cache: IdentCache, conf: ConfigRef) =
-  commandRstAux(cache, conf, conf.projectFull, HtmlExt)
-
-proc commandRst2TeX*(cache: IdentCache, conf: ConfigRef) =
-  commandRstAux(cache, conf, conf.projectFull, TexExt)
+  var d = newDocumentor(filen, cache, conf, outExt, standaloneDoc = true,
+                        preferMarkdown = preferMarkdown, hasToc = false)
+  try:
+    let rst = parseRst(readFile(filen.string),
+                      line=LineRstInit, column=ColRstInit,
+                      conf, d.sharedState)
+    d.modDescPre = @[ItemFragment(isRst: true, rst: rst)]
+    finishGenerateDoc(d)
+    writeOutput(d)
+    generateIndex(d)
+  except ERecoverableError:
+    discard "already reported the error"
+
+proc commandRst2Html*(cache: IdentCache, conf: ConfigRef,
+                      preferMarkdown=false) =
+  commandRstAux(cache, conf, conf.projectFull, HtmlExt, preferMarkdown)
+
+proc commandRst2TeX*(cache: IdentCache, conf: ConfigRef,
+                     preferMarkdown=false) =
+  commandRstAux(cache, conf, conf.projectFull, TexExt, preferMarkdown)
 
 proc commandJson*(cache: IdentCache, conf: ConfigRef) =
+  ## implementation of a deprecated jsondoc0 command
   var ast = parseFile(conf.projectMainIdx, cache, conf)
   if ast == nil: return
-  var d = newDocumentor(conf.projectFull, cache, conf)
+  var d = newDocumentor(conf.projectFull, cache, conf, hasToc = true)
   d.onTestSnippet = proc (d: var RstGenerator; filename, cmd: string;
-                          status: int; content: string) =
+                          status: int; content: string) {.gcsafe.} =
     localError(conf, newLineInfo(conf, AbsoluteFile d.filename, -1, -1),
                warnUser, "the ':test:' attribute is not supported by this backend")
-  d.hasToc = true
-  generateJson(d, ast)
-  let json = d.jArray
-  let content = rope(pretty(json))
+  generateJson(d, ast, conf)
+  finishGenerateDoc(d)
+  let json = d.jEntriesFinal
+  let content = pretty(json)
 
   if optStdout in d.conf.globalOptions:
-    writeRope(stdout, content)
+    write(stdout, content)
   else:
     #echo getOutFile(gProjectFull, JsonExt)
     let filename = getOutFile(conf, RelativeFile conf.projectName, JsonExt)
-    if not writeRope(content, filename):
+    try:
+      writeFile(filename, content)
+    except IOError:
       rawMessage(conf, errCannotOpenFile, filename.string)
 
 proc commandTags*(cache: IdentCache, conf: ConfigRef) =
   var ast = parseFile(conf.projectMainIdx, cache, conf)
   if ast == nil: return
-  var d = newDocumentor(conf.projectFull, cache, conf)
+  var d = newDocumentor(conf.projectFull, cache, conf, hasToc = true)
   d.onTestSnippet = proc (d: var RstGenerator; filename, cmd: string;
-                          status: int; content: string) =
+                          status: int; content: string) {.gcsafe.} =
     localError(conf, newLineInfo(conf, AbsoluteFile d.filename, -1, -1),
                warnUser, "the ':test:' attribute is not supported by this backend")
-  d.hasToc = true
   var
-    content: Rope
+    content = ""
   generateTags(d, ast, content)
 
   if optStdout in d.conf.globalOptions:
-    writeRope(stdout, content)
+    write(stdout, content)
   else:
     #echo getOutFile(gProjectFull, TagsExt)
     let filename = getOutFile(conf, RelativeFile conf.projectName, TagsExt)
-    if not writeRope(content, filename):
+    try:
+      writeFile(filename, content)
+    except IOError:
       rawMessage(conf, errCannotOpenFile, filename.string)
 
 proc commandBuildIndex*(conf: ConfigRef, dir: string, outFile = RelativeFile"") =
-  var content = mergeIndexes(dir).rope
+  if optGenIndexOnly in conf.globalOptions:
+    return
+  var content = mergeIndexes(dir)
 
   var outFile = outFile
   if outFile.isEmpty: outFile = theindexFname.RelativeFile.changeFileExt("")
   let filename = getOutFile(conf, outFile, HtmlExt)
 
-  let code = ropeFormatNamedVars(conf, getConfigVar(conf, "doc.file"), [
-      "nimdoccss", "dochackjs",
-      "title", "tableofcontents", "moduledesc", "date", "time",
-      "content", "author", "version", "analytics"],
-      [relLink(conf.outDir, filename, nimdocOutCss.RelativeFile),
-      relLink(conf.outDir, filename, docHackJsFname.RelativeFile),
-      rope"Index", nil, nil, rope(getDateStr()),
-      rope(getClockStr()), content, nil, nil, nil])
+  let code = getConfigVar(conf, "doc.file") % [
+      "nimdoccss", relLink(conf.outDir, filename, nimdocOutCss.RelativeFile),
+      "dochackjs", relLink(conf.outDir, filename, docHackJsFname.RelativeFile),
+      "title", "Index",
+      "subtitle", "", "tableofcontents", "", "moduledesc", "",
+      "date", getDateStr(), "time", getClockStr(),
+      "content", content, "author", "", "version", "", "analytics", "", "nimVersion", $NimMajor & "." & $NimMinor & "." & $NimPatch]
   # no analytics because context is not available
 
-  if not writeRope(code, filename):
+  try:
+    writeFile(filename, code)
+  except IOError:
+    rawMessage(conf, errCannotOpenFile, filename.string)
+
+proc commandBuildIndexJson*(conf: ConfigRef, dir: string, outFile = RelativeFile"") =
+  var (modules, symbols, docs) = readIndexDir(dir)
+  let documents = toSeq(keys(Table[IndexEntry, seq[IndexEntry]](docs)))
+  let body = %*({"documents": documents, "modules": modules, "symbols": symbols})
+
+  var outFile = outFile
+  if outFile.isEmpty: outFile = theindexFname.RelativeFile.changeFileExt("")
+  let filename = getOutFile(conf, outFile, JsonExt)
+
+  try:
+    writeFile(filename, $body)
+  except IOError:
     rawMessage(conf, errCannotOpenFile, filename.string)