summary refs log tree commit diff stats
path: root/lib/packages/docutils
diff options
context:
space:
mode:
Diffstat (limited to 'lib/packages/docutils')
-rw-r--r--lib/packages/docutils/rst.nim168
-rw-r--r--lib/packages/docutils/rstast.nim14
-rw-r--r--lib/packages/docutils/rstgen.nim135
3 files changed, 218 insertions, 99 deletions
diff --git a/lib/packages/docutils/rst.nim b/lib/packages/docutils/rst.nim
index 8d16edc61..698b973e2 100644
--- a/lib/packages/docutils/rst.nim
+++ b/lib/packages/docutils/rst.nim
@@ -7,10 +7,19 @@
 #    distribution, for details about the copyright.
 #
 
-## This module implements a `reStructuredText`:idx: (RST) parser. A large
-## subset is implemented. Some features of the `markdown`:idx: syntax are
-## also supported. Nim can output the result to HTML (command ``rst2html``)
-## or Latex (command ``rst2tex``).
+## ==================================
+## rst: Nim-flavored reStructuredText
+## ==================================
+##
+## This module implements a `reStructuredText`:idx: (RST) parser.
+## A large subset is implemented with some limitations_ and
+## `Nim-specific features`_.
+## A few `extra features`_ of the `Markdown`:idx: syntax are
+## also supported.
+##
+## Nim can output the result to HTML (commands ``nim doc`` for
+## ``*.nim`` files and ``nim rst2html`` for ``*.rst`` files) or
+## Latex (command ``nim rst2tex`` for ``*.rst``).
 ##
 ## If you are new to RST please consider reading the following:
 ##
@@ -18,6 +27,9 @@
 ## 2) an `RST reference`_: a comprehensive cheatsheet for RST
 ## 3) a more formal 50-page `RST specification`_.
 ##
+## Features
+## --------
+##
 ## Supported standard RST features:
 ##
 ## * body elements
@@ -43,27 +55,33 @@
 ##   + comments
 ## * inline markup
 ##   + *emphasis*, **strong emphasis**,
-##     ``inline literals``, hyperlink references, substitution references,
-##     standalone hyperlinks
+##     ``inline literals``, hyperlink references (including embedded URI),
+##     substitution references, standalone hyperlinks,
+##     internal links (inline and outline)
 ##   + \`interpreted text\` with roles ``:literal:``, ``:strong:``,
 ##     ``emphasis``, ``:sub:``/``:subscript:``, ``:sup:``/``:supscript:``
 ##     (see `RST roles list`_ for description).
+##   + inline internal targets
+##
+## .. _`Nim-specific features`:
 ##
-## Additional features:
+## Additional Nim-specific features:
 ##
 ## * directives: ``code-block``, ``title``, ``index``
 ## * ***triple emphasis*** (bold and italic) using \*\*\*
 ## * ``:idx:`` role for \`interpreted text\` to include the link to this
 ##   text into an index (example: `Nim index`_).
 ##
+## .. _`extra features`:
+##
 ## Optional additional features, turned on by ``options: RstParseOption`` in
 ## `rstParse proc <#rstParse,string,string,int,int,bool,RstParseOptions,FindFileHandler,MsgHandler>`_:
 ##
 ## * emoji / smiley symbols
-## * markdown tables
-## * markdown code blocks
-## * markdown links
-## * markdown headlines
+## * Markdown tables
+## * Markdown code blocks
+## * Markdown links
+## * Markdown headlines
 ## * using ``1`` as auto-enumerator in enumerated lists like RST ``#``
 ##   (auto-enumerator ``1`` can not be used with ``#`` in the same list)
 ##
@@ -73,7 +91,8 @@
 ## .. warning:: Using Nim-specific features can cause other RST implementations
 ##   to fail on your document.
 ##
-## Limitations:
+## Limitations
+## -----------
 ##
 ## * no Unicode support in character width calculations
 ## * body elements
@@ -89,10 +108,21 @@
 ##     - no ``role`` directives and no custom interpreted text roles
 ##     - some standard roles are not supported (check `RST roles list`_)
 ##   - no footnotes & citations support
-##   - no inline internal targets
 ## * inline markup
 ##   - no simple-inline-markup
-##   - no embedded URI and aliases
+##   - no embedded aliases
+##
+## Usage
+## -----
+##
+## See `Nim DocGen Tools Guide <docgen.html>`_ for the details about
+## ``nim doc``, ``nim rst2html`` and ``nim rst2tex`` commands.
+##
+## See `packages/docutils/rstgen module <rstgen.html>`_ to know how to
+## generate HTML or Latex strings to embed them into your documents.
+##
+## .. Tip:: Import ``packages/docutils/rst`` to use this module
+##    programmatically.
 ##
 ## .. _quick introduction: https://docutils.sourceforge.io/docs/user/rst/quickstart.html
 ## .. _RST reference: https://docutils.sourceforge.io/docs/user/rst/quickref.html
@@ -100,13 +130,6 @@
 ## .. _RST directives list: https://docutils.sourceforge.io/docs/ref/rst/directives.html
 ## .. _RST roles list: https://docutils.sourceforge.io/docs/ref/rst/roles.html
 ## .. _Nim index: https://nim-lang.org/docs/theindex.html
-##
-## See `Nim DocGen Tools Guide <docgen.html>`_ for the details about
-## ``nim doc``, ``nim rst2html`` and ``nim rst2tex`` commands.
-##
-## .. note:: Import ``packages/docutils/rst`` to use this module.
-##
-## See also `packages/docutils/rstgen module <rstgen.html>`_.
 
 import
   os, strutils, rstast
@@ -118,7 +141,7 @@ type
     roSupportSmilies,         ## make the RST parser support smilies like ``:)``
     roSupportRawDirective,    ## support the ``raw`` directive (don't support
                               ## it for sandboxing)
-    roSupportMarkdown         ## support additional features of markdown
+    roSupportMarkdown         ## support additional features of Markdown
 
   RstParseOptions* = set[RstParseOption]
 
@@ -131,7 +154,7 @@ type
     meCannotOpenFile = "cannot open '$1'",
     meExpected = "'$1' expected",
     meGridTableNotImplemented = "grid table is not implemented",
-    meMarkdownIllformedTable = "illformed delimiter row of a markdown table",
+    meMarkdownIllformedTable = "illformed delimiter row of a Markdown table",
     meNewSectionExpected = "new section expected",
     meGeneralParseError = "general parse error",
     meInvalidDirective = "invalid directive: '$1'",
@@ -379,12 +402,16 @@ type
   Substitution = object
     key*: string
     value*: PRstNode
+  AnchorSubst = tuple
+    mainAnchor: string
+    aliases: seq[string]
 
   SharedState = object
     options: RstParseOptions    # parsing options
     uLevel, oLevel: int         # counters for the section levels
     subs: seq[Substitution]     # substitutions
     refs: seq[Substitution]     # references
+    anchors: seq[AnchorSubst]   # internal target substitutions
     underlineToLevel: LevelMap  # Saves for each possible title adornment
                                 # character its level in the
                                 # current document.
@@ -405,6 +432,7 @@ type
     filename*: string
     line*, col*: int
     hasToc*: bool
+    curAnchor*: string          # variable to track latest anchor in s.anchors
 
   EParseError* = object of ValueError
 
@@ -577,6 +605,38 @@ proc findRef(p: var RstParser, key: string): PRstNode =
     if key == p.s.refs[i].key:
       return p.s.refs[i].value
 
+proc addAnchor(p: var RstParser, refn: string, reset: bool) =
+  ## add anchor `refn` to anchor aliases and update last anchor ``curAnchor``
+  if p.curAnchor == "":
+    p.s.anchors.add (refn, @[refn])
+  else:
+    p.s.anchors[^1].mainAnchor = refn
+    p.s.anchors[^1].aliases.add refn
+  if reset:
+    p.curAnchor = ""
+  else:
+    p.curAnchor = refn
+
+proc findMainAnchor(p: RstParser, refn: string): string =
+  for subst in p.s.anchors:
+    if subst.mainAnchor == refn:  # no need to rename
+      result = subst.mainAnchor
+      break
+    var toLeave = false
+    for anchor in subst.aliases:
+      if anchor == refn:  # this anchor will be named as mainAnchor
+        result = subst.mainAnchor
+        toLeave = true
+    if toLeave:
+      break
+
+proc newRstNodeA(p: var RstParser, kind: RstNodeKind): PRstNode =
+  ## create node and consume the current anchor
+  result = newRstNode(kind)
+  if p.curAnchor != "":
+    result.anchor = p.curAnchor
+    p.curAnchor = ""
+
 proc newLeaf(p: var RstParser): PRstNode =
   result = newRstNode(rnLeaf, currentTok(p).symbol)
 
@@ -629,7 +689,10 @@ proc isInlineMarkupEnd(p: RstParser, markup: string): bool =
 proc isInlineMarkupStart(p: RstParser, markup: string): bool =
   # rst rules: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#inline-markup-recognition-rules
   var d: char
-  result = currentTok(p).symbol == markup
+  if markup != "_`":
+    result = currentTok(p).symbol == markup
+  else:  # _` is a 2 token case
+    result = currentTok(p).symbol == "_" and nextTok(p).symbol == "`"
   if not result: return
   # Rule 6:
   result = p.idx == 0 or prevTok(p).kind in {tkIndent, tkWhite} or
@@ -873,7 +936,7 @@ proc parseMarkdownCodeblock(p: var RstParser): PRstNode =
       inc p.idx
   var lb = newRstNode(rnLiteralBlock)
   lb.add(n)
-  result = newRstNode(rnCodeBlock)
+  result = newRstNodeA(p, rnCodeBlock)
   result.add(args)
   result.add(PRstNode(nil))
   result.add(lb)
@@ -918,6 +981,13 @@ proc parseInline(p: var RstParser, father: PRstNode) =
       var n = newRstNode(rnEmphasis)
       parseUntil(p, n, "*", true)
       father.add(n)
+    elif isInlineMarkupStart(p, "_`"):
+      var n = newRstNode(rnInlineTarget)
+      inc p.idx
+      parseUntil(p, n, "`", false)
+      let refn = rstnodeToRefname(n)
+      p.s.anchors.add (refn, @[refn])
+      father.add(n)
     elif roSupportMarkdown in p.s.options and currentTok(p).symbol == "```":
       inc p.idx
       father.add(parseMarkdownCodeblock(p))
@@ -1049,7 +1119,7 @@ proc parseFields(p: var RstParser): PRstNode =
   if currentTok(p).kind == tkIndent and nextTok(p).symbol == ":" or
       atStart:
     var col = if atStart: currentTok(p).col else: currentTok(p).ival
-    result = newRstNode(rnFieldList)
+    result = newRstNodeA(p, rnFieldList)
     if not atStart: inc p.idx
     while true:
       result.add(parseField(p))
@@ -1090,7 +1160,7 @@ proc getArgument(n: PRstNode): string =
 
 proc parseDotDot(p: var RstParser): PRstNode {.gcsafe.}
 proc parseLiteralBlock(p: var RstParser): PRstNode =
-  result = newRstNode(rnLiteralBlock)
+  result = newRstNodeA(p, rnLiteralBlock)
   var n = newRstNode(rnLeaf, "")
   if currentTok(p).kind == tkIndent:
     var indent = currentTok(p).ival
@@ -1248,7 +1318,7 @@ proc parseLineBlock(p: var RstParser): PRstNode =
   result = nil
   if nextTok(p).kind in {tkWhite, tkIndent}:
     var col = currentTok(p).col
-    result = newRstNode(rnLineBlock)
+    result = newRstNodeA(p, rnLineBlock)
     while true:
       var item = newRstNode(rnLineBlockItem)
       if nextTok(p).kind == tkWhite:
@@ -1314,6 +1384,7 @@ proc parseHeadline(p: var RstParser): PRstNode =
     var c = nextTok(p).symbol[0]
     inc p.idx, 2
     result.level = getLevel(p.s.underlineToLevel, p.s.uLevel, c)
+  addAnchor(p, rstnodeToRefname(result), reset=true)
 
 type
   IntSeq = seq[int]
@@ -1349,7 +1420,7 @@ proc parseSimpleTable(p: var RstParser): PRstNode =
     c: char
     q: RstParser
     a, b: PRstNode
-  result = newRstNode(rnTable)
+  result = newRstNodeA(p, rnTable)
   cols = @[]
   row = @[]
   a = nil
@@ -1428,7 +1499,7 @@ proc parseMarkdownTable(p: var RstParser): PRstNode =
     colNum: int
     a, b: PRstNode
     q: RstParser
-  result = newRstNode(rnMarkdownTable)
+  result = newRstNodeA(p, rnMarkdownTable)
 
   proc parseRow(p: var RstParser, cellKind: RstNodeKind, result: PRstNode) =
     row = readTableRow(p)
@@ -1452,7 +1523,7 @@ proc parseMarkdownTable(p: var RstParser): PRstNode =
     parseRow(p, rnTableDataCell, result)
 
 proc parseTransition(p: var RstParser): PRstNode =
-  result = newRstNode(rnTransition)
+  result = newRstNodeA(p, rnTransition)
   inc p.idx
   if currentTok(p).kind == tkIndent: inc p.idx
   if currentTok(p).kind == tkIndent: inc p.idx
@@ -1475,13 +1546,14 @@ proc parseOverline(p: var RstParser): PRstNode =
   if currentTok(p).kind == tkAdornment:
     inc p.idx                # XXX: check?
     if currentTok(p).kind == tkIndent: inc p.idx
+  addAnchor(p, rstnodeToRefname(result), reset=true)
 
 proc parseBulletList(p: var RstParser): PRstNode =
   result = nil
   if nextTok(p).kind == tkWhite:
     var bullet = currentTok(p).symbol
     var col = currentTok(p).col
-    result = newRstNode(rnBulletList)
+    result = newRstNodeA(p, rnBulletList)
     pushInd(p, p.tok[p.idx + 2].col)
     inc p.idx, 2
     while true:
@@ -1497,7 +1569,7 @@ proc parseBulletList(p: var RstParser): PRstNode =
     popInd(p)
 
 proc parseOptionList(p: var RstParser): PRstNode =
-  result = newRstNode(rnOptionList)
+  result = newRstNodeA(p, rnOptionList)
   while true:
     if isOptionList(p):
       var a = newRstNode(rnOptionGroup)
@@ -1530,7 +1602,7 @@ proc parseDefinitionList(p: var RstParser): PRstNode =
   if j >= 1 and p.tok[j].kind == tkIndent and
       p.tok[j].ival > currInd(p) and p.tok[j - 1].symbol != "::":
     var col = currentTok(p).col
-    result = newRstNode(rnDefList)
+    result = newRstNodeA(p, rnDefList)
     while true:
       j = p.idx
       var a = newRstNode(rnDefName)
@@ -1568,7 +1640,7 @@ proc parseEnumList(p: var RstParser): PRstNode =
     wildToken: array[0..5, int] = [4, 3, 3, 4, 3, 3]  # number of tokens
     wildIndex: array[0..5, int] = [1, 0, 0, 1, 0, 0]
       # position of enumeration sequence (number/letter) in enumerator
-  result = newRstNode(rnEnumList)
+  result = newRstNodeA(p, rnEnumList)
   let col = currentTok(p).col
   var w = 0
   while w < wildcards.len:
@@ -1623,6 +1695,7 @@ proc sonKind(father: PRstNode, i: int): RstNodeKind =
   if i < father.len: result = father.sons[i].kind
 
 proc parseSection(p: var RstParser, result: PRstNode) =
+  ## parse top-level RST elements: sections, transitions and body elements.
   while true:
     var leave = false
     assert(p.idx >= 0)
@@ -1631,7 +1704,7 @@ proc parseSection(p: var RstParser, result: PRstNode) =
         inc p.idx
       elif currentTok(p).ival > currInd(p):
         pushInd(p, currentTok(p).ival)
-        var a = newRstNode(rnBlockQuote)
+        var a = newRstNodeA(p, rnBlockQuote)
         parseSection(p, a)
         result.add(a)
         popInd(p)
@@ -1667,7 +1740,7 @@ proc parseSection(p: var RstParser, result: PRstNode) =
       #InternalError("rst.parseSection()")
       discard
     if a == nil and k != rnDirective:
-      a = newRstNode(rnParagraph)
+      a = newRstNodeA(p, rnParagraph)
       parseParagraph(p, a)
     result.addIfNotNil(a)
   if sonKind(result, 0) == rnParagraph and sonKind(result, 1) != rnParagraph:
@@ -1703,7 +1776,7 @@ proc parseDirective(p: var RstParser, flags: DirFlags): PRstNode =
   ##
   ## Both rnDirArg and rnFieldList children nodes might be nil, so you need to
   ## check them before accessing.
-  result = newRstNode(rnDirective)
+  result = newRstNodeA(p, rnDirective)
   var args: PRstNode = nil
   var options: PRstNode = nil
   if hasArg in flags:
@@ -1981,7 +2054,10 @@ proc parseDotDot(p: var RstParser): PRstNode =
     var a = getReferenceName(p, ":")
     if currentTok(p).kind == tkWhite: inc p.idx
     var b = untilEol(p)
-    setRef(p, rstnodeToRefname(a), b)
+    if len(b) == 0 and b.text == "":  # set internal anchor
+      addAnchor(p, rstnodeToRefname(a), reset=false)
+    else:  # external hyperlink
+      setRef(p, rstnodeToRefname(a), b)
   elif match(p, p.idx, " |"):
     # substitution definitions:
     inc p.idx, 2
@@ -2009,6 +2085,7 @@ proc parseDotDot(p: var RstParser): PRstNode =
     result = parseComment(p)
 
 proc resolveSubs(p: var RstParser, n: PRstNode): PRstNode =
+  ## resolve substitutions and anchor aliases
   result = n
   if n == nil: return
   case n.kind
@@ -2022,12 +2099,20 @@ proc resolveSubs(p: var RstParser, n: PRstNode): PRstNode =
       if e != "": result = newRstNode(rnLeaf, e)
       else: rstMessage(p, mwUnknownSubstitution, key)
   of rnRef:
-    var y = findRef(p, rstnodeToRefname(n))
+    let refn = rstnodeToRefname(n)
+    var y = findRef(p, refn)
     if y != nil:
       result = newRstNode(rnHyperlink)
       n.kind = rnInner
       result.add(n)
       result.add(y)
+    else:
+      let s = findMainAnchor(p, refn)
+      if s != "":
+        result = newRstNode(rnInternalRef)
+        n.kind = rnInner
+        result.add(n)
+        result.add(newRstNode(rnLeaf, s))
   of rnLeaf:
     discard
   of rnContents:
@@ -2045,5 +2130,6 @@ proc rstParse*(text, filename: string,
   p.filename = filename
   p.line = line
   p.col = column + getTokens(text, roSkipPounds in options, p.tok)
-  result = resolveSubs(p, parseDoc(p))
+  let unresolved = parseDoc(p)
+  result = resolveSubs(p, unresolved)
   hasToc = p.hasToc
diff --git a/lib/packages/docutils/rstast.nim b/lib/packages/docutils/rstast.nim
index e4e192fa3..4e3ca9a2b 100644
--- a/lib/packages/docutils/rstast.nim
+++ b/lib/packages/docutils/rstast.nim
@@ -42,7 +42,7 @@ type
     rnLabel,                  # used for footnotes and other things
     rnFootnote,               # a footnote
     rnCitation,               # similar to footnote
-    rnStandaloneHyperlink, rnHyperlink, rnRef,
+    rnStandaloneHyperlink, rnHyperlink, rnRef, rnInternalRef,
     rnDirective,              # a general directive
     rnDirArg,                 # a directive argument (for some directives).
                               # here are directives that are not rnDirective:
@@ -62,6 +62,7 @@ type
     rnTripleEmphasis,         # "***"
     rnInterpretedText,        # "`"
     rnInlineLiteral,          # "``"
+    rnInlineTarget,           # "_`target`"
     rnSubstitutionReferences, # "|"
     rnSmiley,                 # some smiley
     rnLeaf                    # a leaf; the node's text field contains the
@@ -75,7 +76,9 @@ type
     text*: string             ## valid for leafs in the AST; and the title of
                               ## the document or the section; and rnEnumList
                               ## and rnAdmonition; and rnLineBlockItem
-    level*: int               ## valid for some node kinds
+    level*: int               ## valid for headlines/overlines only
+    anchor*: string           ## anchor, internal link target
+                              ## (aka HTML id tag, aka Latex label/hypertarget)
     sons*: RstNodeSeq        ## the node's sons
 
 proc len*(n: PRstNode): int =
@@ -330,8 +333,9 @@ proc renderRstToStr*(node: PRstNode, indent=0): string =
   if node == nil:
     result.add " ".repeat(indent) & "[nil]\n"
     return
-  result.add " ".repeat(indent) & $node.kind & "\t" &
-      (if node.text == "": "" else: "'" & node.text & "'") &
-      (if node.level == 0: "" else: "\tlevel=" & $node.level) & "\n"
+  result.add " ".repeat(indent) & $node.kind &
+      (if node.text == "":   "" else: "\t'" & node.text & "'") &
+      (if node.level == 0:   "" else: "\tlevel=" & $node.level) &
+      (if node.anchor == "": "" else: "\tanchor='" & node.anchor & "'") & "\n"
   for son in node.sons:
     result.add renderRstToStr(son, indent=indent+2)
diff --git a/lib/packages/docutils/rstgen.nim b/lib/packages/docutils/rstgen.nim
index 52125b52c..1dd299ee4 100644
--- a/lib/packages/docutils/rstgen.nim
+++ b/lib/packages/docutils/rstgen.nim
@@ -272,13 +272,25 @@ proc renderRstToOut*(d: var RstGenerator, n: PRstNode, result: var string)
 proc renderAux(d: PDoc, n: PRstNode, result: var string) =
   for i in countup(0, len(n)-1): renderRstToOut(d, n.sons[i], result)
 
-proc renderAux(d: PDoc, n: PRstNode, frmtA, frmtB: string, result: var string) =
+template idS(txt: string): string =
+  if txt == "": ""
+  else:
+    case d.target
+    of outHtml:
+      " id=\"" & txt & "\""
+    of outLatex:
+      "\\label{" & txt & "}\\hypertarget{" & txt & "}{}"
+        # we add \label for page number references via \pageref, while
+        # \hypertarget is for clickable links via \hyperlink.
+
+proc renderAux(d: PDoc, n: PRstNode, html, tex: string, result: var string) =
+  # formats sons of `n` as substitution variable $1 inside strings `html` and
+  # `tex`, internal target (anchor) is provided as substitute $2.
   var tmp = ""
   for i in countup(0, len(n)-1): renderRstToOut(d, n.sons[i], tmp)
-  if d.target != outLatex:
-    result.addf(frmtA, [tmp])
-  else:
-    result.addf(frmtB, [tmp])
+  case d.target
+  of outHtml:  result.addf(html, [tmp, n.anchor.idS])
+  of outLatex: result.addf(tex,  [tmp, n.anchor.idS])
 
 # ---------------- index handling --------------------------------------------
 
@@ -746,13 +758,13 @@ proc renderHeadline(d: PDoc, n: PRstNode, result: var string) =
     d.tocPart[length].n = n
     d.tocPart[length].header = tmp
 
-    dispA(d.target, result, "\n<h$1><a class=\"toc-backref\" " &
-      "id=\"$2\" href=\"#$2\">$3</a></h$1>", "\\rsth$4{$3}\\label{$2}\n",
-      [$n.level, d.tocPart[length].refname, tmp, $chr(n.level - 1 + ord('A'))])
+    dispA(d.target, result, "\n<h$1><a class=\"toc-backref\"" &
+      "$2 href=\"#$5\">$3</a></h$1>", "\\rsth$4{$3}$2\n",
+      [$n.level, refname.idS, tmp, $chr(n.level - 1 + ord('A')), refname])
   else:
-    dispA(d.target, result, "\n<h$1 id=\"$2\">$3</h$1>",
-                            "\\rsth$4{$3}\\label{$2}\n", [
-        $n.level, refname, tmp,
+    dispA(d.target, result, "\n<h$1$2>$3</h$1>",
+                            "\\rsth$4{$3}$2\n", [
+        $n.level, refname.idS, tmp,
         $chr(n.level - 1 + ord('A'))])
 
   # Generate index entry using spaces to indicate TOC level for the output HTML.
@@ -781,9 +793,9 @@ proc renderOverline(d: PDoc, n: PRstNode, result: var string) =
     var tmp = ""
     for i in countup(0, len(n) - 1): renderRstToOut(d, n.sons[i], tmp)
     d.currentSection = tmp
-    dispA(d.target, result, "<h$1 id=\"$2\"><center>$3</center></h$1>",
-                   "\\rstov$4{$3}\\label{$2}\n", [$n.level,
-        rstnodeToRefname(n), tmp, $chr(n.level - 1 + ord('A'))])
+    dispA(d.target, result, "<h$1$2><center>$3</center></h$1>",
+                   "\\rstov$4{$3}$2\n", [$n.level,
+        rstnodeToRefname(n).idS, tmp, $chr(n.level - 1 + ord('A'))])
 
 
 proc renderTocEntry(d: PDoc, e: TocEntry, result: var string) =
@@ -841,12 +853,12 @@ proc renderImage(d: PDoc, n: PRstNode, result: var string) =
   if arg.endsWith(".mp4") or arg.endsWith(".ogg") or
      arg.endsWith(".webm"):
     htmlOut = """
-      <video src="$1"$2 autoPlay='true' loop='true' muted='true'>
+      <video$3 src="$1"$2 autoPlay='true' loop='true' muted='true'>
       Sorry, your browser doesn't support embedded videos
       </video>
     """
   else:
-    htmlOut = "<img src=\"$1\"$2/>"
+    htmlOut = "<img$3 src=\"$1\"$2/>"
 
   # support for `:target:` links for images:
   var target = esc(d.target, getFieldValue(n, "target").strip())
@@ -859,8 +871,8 @@ proc renderImage(d: PDoc, n: PRstNode, result: var string) =
       "\\href{$2}{$1}", [htmlOut, target])
     htmlOut = htmlOutWithLink
 
-  dispA(d.target, result, htmlOut, "\\includegraphics$2{$1}",
-        [esc(d.target, arg), options])
+  dispA(d.target, result, htmlOut, "$3\\includegraphics$2{$1}",
+        [esc(d.target, arg), options, n.anchor.idS])
   if len(n) >= 3: renderRstToOut(d, n.sons[2], result)
 
 proc renderSmiley(d: PDoc, n: PRstNode, result: var string) =
@@ -925,7 +937,8 @@ proc parseCodeBlockParams(d: PDoc, n: PRstNode): CodeBlockParams =
   if result.langStr != "":
     result.lang = getSourceLanguage(result.langStr)
 
-proc buildLinesHtmlTable(d: PDoc; params: CodeBlockParams, code: string):
+proc buildLinesHtmlTable(d: PDoc; params: CodeBlockParams, code: string,
+                         idStr: string):
     tuple[beginTable, endTable: string] =
   ## Returns the necessary tags to start/end a code block in HTML.
   ##
@@ -937,21 +950,22 @@ proc buildLinesHtmlTable(d: PDoc; params: CodeBlockParams, code: string):
   let id = $d.listingCounter
   if not params.numberLines:
     result = (d.config.getOrDefault"doc.listing_start" %
-                [id, sourceLanguageToStr[params.lang]],
+                [id, sourceLanguageToStr[params.lang], idStr],
               d.config.getOrDefault"doc.listing_end" % id)
     return
 
   var codeLines = code.strip.countLines
   assert codeLines > 0
-  result.beginTable = """<table class="line-nums-table"><tbody><tr><td class="blob-line-nums"><pre class="line-nums">"""
+  result.beginTable = """<table$1 class="line-nums-table">""" % [idStr] &
+      """<tbody><tr><td class="blob-line-nums"><pre class="line-nums">"""
   var line = params.startLine
   while codeLines > 0:
     result.beginTable.add($line & "\n")
     line.inc
     codeLines.dec
-  result.beginTable.add("</pre></td><td>" & (
+  result.beginTable.add("</pre$3></td><td>" & (
       d.config.getOrDefault"doc.listing_start" %
-        [id, sourceLanguageToStr[params.lang]]))
+        [id, sourceLanguageToStr[params.lang], idStr]))
   result.endTable = (d.config.getOrDefault"doc.listing_end" % id) &
       "</td></tr></tbody></table>" & (
       d.config.getOrDefault"doc.listing_button" % id)
@@ -975,9 +989,10 @@ proc renderCodeBlock(d: PDoc, n: PRstNode, result: var string) =
   if params.testCmd.len > 0 and d.onTestSnippet != nil:
     d.onTestSnippet(d, params.filename, params.testCmd, params.status, m.text)
 
-  let (blockStart, blockEnd) = buildLinesHtmlTable(d, params, m.text)
-
-  dispA(d.target, result, blockStart, "\\begin{rstpre}\n", [])
+  let (blockStart, blockEnd) = buildLinesHtmlTable(d, params, m.text,
+                                                   n.anchor.idS)
+  dispA(d.target, result, blockStart,
+        "\\begin{rstpre}\n" & n.anchor.idS & "\n", [])
   if params.lang == langNone:
     if len(params.langStr) > 0:
       d.msgHandler(d.filename, 1, 0, mwUnsupportedLanguage, params.langStr)
@@ -1075,8 +1090,8 @@ proc renderEnumList(d: PDoc, n: PRstNode, result: var string) =
       if n.text[i1] != first:
         specStart = " start=\"$1\"" % [ $(ord(n.text[i1]) - ord(first) + 1) ]
       specifier = labelDef & specStart
-  renderAux(d, n, "<ol " & specifier & ">$1</ol>\n",
-            "\\begin{enumerate}" & specifier & "$1\\end{enumerate}\n",
+  renderAux(d, n, "<ol$2 " & specifier & ">$1</ol>\n",
+            "\\begin{enumerate}" & specifier & "$2$1\\end{enumerate}\n",
             result)
 
 proc renderAdmonition(d: PDoc, n: PRstNode, result: var string) =
@@ -1095,9 +1110,9 @@ proc renderAdmonition(d: PDoc, n: PRstNode, result: var string) =
   let txt = n.text.capitalizeAscii()
   let htmlHead = "<div class=\"admonition " & htmlCls & "\">"
   renderAux(d, n,
-      htmlHead & "<span class=\"" & htmlCls & "-text\"><b>" & txt &
+      htmlHead & "<span$2 class=\"" & htmlCls & "-text\"><b>" & txt &
         ":</b></span>\n" & "$1</div>\n",
-      "\n\n\\begin{mdframed}[linecolor=" & texColor & "]\n" &
+      "\n\n\\begin{mdframed}[linecolor=" & texColor & "]$2\n" &
         "{" & texSz & "\\color{" & texColor & "}{\\textbf{" & txt & ":}}} " &
         "$1\n\\end{mdframed}\n",
       result)
@@ -1108,33 +1123,33 @@ proc renderRstToOut(d: PDoc, n: PRstNode, result: var string) =
   of rnInner: renderAux(d, n, result)
   of rnHeadline: renderHeadline(d, n, result)
   of rnOverline: renderOverline(d, n, result)
-  of rnTransition: renderAux(d, n, "<hr />\n", "\\hrule\n", result)
-  of rnParagraph: renderAux(d, n, "<p>$1</p>\n", "$1\n\n", result)
+  of rnTransition: renderAux(d, n, "<hr$2 />\n", "\\hrule$2\n", result)
+  of rnParagraph: renderAux(d, n, "<p$2>$1</p>\n", "$2\n$1\n\n", result)
   of rnBulletList:
-    renderAux(d, n, "<ul class=\"simple\">$1</ul>\n",
-                    "\\begin{itemize}$1\\end{itemize}\n", result)
+    renderAux(d, n, "<ul$2 class=\"simple\">$1</ul>\n",
+                    "\\begin{itemize}\n$2\n$1\\end{itemize}\n", result)
   of rnBulletItem, rnEnumItem:
-    renderAux(d, n, "<li>$1</li>\n", "\\item $1\n", result)
+    renderAux(d, n, "<li$2>$1</li>\n", "\\item $2$1\n", result)
   of rnEnumList: renderEnumList(d, n, result)
   of rnDefList:
-    renderAux(d, n, "<dl class=\"docutils\">$1</dl>\n",
-                       "\\begin{description}$1\\end{description}\n", result)
+    renderAux(d, n, "<dl$2 class=\"docutils\">$1</dl>\n",
+                    "\\begin{description}\n$2\n$1\\end{description}\n", result)
   of rnDefItem: renderAux(d, n, result)
-  of rnDefName: renderAux(d, n, "<dt>$1</dt>\n", "\\item[$1] ", result)
-  of rnDefBody: renderAux(d, n, "<dd>$1</dd>\n", "$1\n", result)
+  of rnDefName: renderAux(d, n, "<dt$2>$1</dt>\n", "$2\\item[$1] ", result)
+  of rnDefBody: renderAux(d, n, "<dd$2>$1</dd>\n", "$2\n$1\n", result)
   of rnFieldList:
     var tmp = ""
     for i in countup(0, len(n) - 1):
       renderRstToOut(d, n.sons[i], tmp)
     if tmp.len != 0:
       dispA(d.target, result,
-          "<table class=\"docinfo\" frame=\"void\" rules=\"none\">" &
+          "<table$2 class=\"docinfo\" frame=\"void\" rules=\"none\">" &
           "<col class=\"docinfo-name\" />" &
           "<col class=\"docinfo-content\" />" &
           "<tbody valign=\"top\">$1" &
           "</tbody></table>",
-          "\\begin{description}$1\\end{description}\n",
-          [tmp])
+          "\\begin{description}\n$2\n$1\\end{description}\n",
+          [tmp, n.anchor.idS])
   of rnField: renderField(d, n, result)
   of rnFieldName:
     renderAux(d, n, "<th class=\"docinfo-name\">$1:</th>",
@@ -1144,8 +1159,8 @@ proc renderRstToOut(d: PDoc, n: PRstNode, result: var string) =
   of rnIndex:
     renderRstToOut(d, n.sons[2], result)
   of rnOptionList:
-    renderAux(d, n, "<table frame=\"void\">$1</table>",
-      "\\begin{description}\n$1\\end{description}\n", result)
+    renderAux(d, n, "<table$2 frame=\"void\">$1</table>",
+      "\\begin{description}\n$2\n$1\\end{description}\n", result)
   of rnOptionListItem:
     renderAux(d, n, "<tr>$1</tr>\n", "$1", result)
   of rnOptionGroup:
@@ -1155,16 +1170,17 @@ proc renderRstToOut(d: PDoc, n: PRstNode, result: var string) =
   of rnOption, rnOptionString, rnOptionArgument:
     doAssert false, "renderRstToOut"
   of rnLiteralBlock:
-    renderAux(d, n, "<pre>$1</pre>\n",
-                    "\\begin{rstpre}\n$1\n\\end{rstpre}\n", result)
+    renderAux(d, n, "<pre$2>$1</pre>\n",
+                    "\\begin{rstpre}\n$2\n$1\n\\end{rstpre}\n", result)
   of rnQuotedLiteralBlock:
     doAssert false, "renderRstToOut"
   of rnLineBlock:
     if n.sons.len == 1 and n.sons[0].text == "\n":
       # whole line block is one empty line, no need to add extra spacing
-      renderAux(d, n, "<p>$1</p> ", "\n\n$1", result)
+      renderAux(d, n, "<p$2>$1</p> ", "\n\n$2\n$1", result)
     else:  # add extra spacing around the line block for Latex
-      renderAux(d, n, "<p>$1</p>", "\n\\vspace{0.5em}\n$1\\vspace{0.5em}\n", result)
+      renderAux(d, n, "<p$2>$1</p>",
+        "\n\\vspace{0.5em}$2\n$1\\vspace{0.5em}\n", result)
   of rnLineBlockItem:
     if n.text.len == 0:  # normal case - no additional indentation
       renderAux(d, n, "$1<br/>", "\\noindent $1\n\n", result)
@@ -1176,13 +1192,13 @@ proc renderRstToOut(d: PDoc, n: PRstNode, result: var string) =
         "<span style=\"margin-left: " & indent & "\">$1</span><br/>",
         "\\noindent\\hspace{" & indent & "}$1\n\n", result)
   of rnBlockQuote:
-    renderAux(d, n, "<blockquote><p>$1</p></blockquote>\n",
-                    "\\begin{quote}$1\\end{quote}\n", result)
+    renderAux(d, n, "<blockquote$2><p>$1</p></blockquote>\n",
+                    "\\begin{quote}\n$2\n$1\\end{quote}\n", result)
   of rnAdmonition: renderAdmonition(d, n, result)
   of rnTable, rnGridTable, rnMarkdownTable:
     renderAux(d, n,
-      "<table border=\"1\" class=\"docutils\">$1</table>",
-      "\\begin{table}\\begin{rsttab}{" &
+      "<table$2 border=\"1\" class=\"docutils\">$1</table>",
+      "\\begin{table}\n$2\n\\begin{rsttab}{" &
         texColumns(n) & "|}\n\\hline\n$1\\end{rsttab}\\end{table}", result)
   of rnTableRow:
     if len(n) >= 1:
@@ -1217,6 +1233,12 @@ proc renderRstToOut(d: PDoc, n: PRstNode, result: var string) =
     renderAux(d, n,
       "<a class=\"reference external\" href=\"$1\">$1</a>",
       "\\href{$1}{$1}", result)
+  of rnInternalRef:
+    var tmp = ""
+    renderAux(d, n.sons[0], tmp)
+    dispA(d.target, result,
+      "<a class=\"reference internal\" href=\"#$2\">$1</a>",
+      "\\hyperlink{$2}{$1} (p.~\\pageref{$2})", [tmp, n.sons[1].text])
   of rnHyperlink:
     var tmp0 = ""
     var tmp1 = ""
@@ -1261,6 +1283,13 @@ proc renderRstToOut(d: PDoc, n: PRstNode, result: var string) =
     renderAux(d, n,
       "<tt class=\"docutils literal\"><span class=\"pre\">$1</span></tt>",
       "\\texttt{$1}", result)
+  of rnInlineTarget:
+    var tmp = ""
+    renderAux(d, n, tmp)
+    dispA(d.target, result,
+      "<span class=\"target\" id=\"$2\">$1</span>",
+      "\\label{$2}\\hypertarget{$2}{$1}",
+      [tmp, rstnodeToRefname(n)])
   of rnSmiley: renderSmiley(d, n, result)
   of rnLeaf: result.add(esc(d.target, n.text))
   of rnContents: d.hasToc = true
@@ -1396,7 +1425,7 @@ $moduledesc
 $content
 </div>
 """)
-  setConfigVar("doc.listing_start", "<pre class = \"listing\">")
+  setConfigVar("doc.listing_start", "<pre$3 class = \"listing\">")
   setConfigVar("doc.listing_end", "</pre>")
   setConfigVar("doc.listing_button", "</pre>")
   setConfigVar("doc.body_no_toc", "$moduledesc $content")