summary refs log tree commit diff stats
path: root/lib
diff options
context:
space:
mode:
authorAndrey Makarov <ph.makarov@gmail.com>2021-10-28 20:20:52 +0300
committerGitHub <noreply@github.com>2021-10-28 19:20:52 +0200
commit7ba2659f733b97db63b7552415ad048e34d4a11a (patch)
tree54eb85f0eabc927c6d15c2d69c45aefd09efa39d /lib
parentc80e2c173686bd12904e5487752dc0ce20cb8bcb (diff)
downloadNim-7ba2659f733b97db63b7552415ad048e34d4a11a.tar.gz
docgen: implement doc link resolution in current module (#18642)
Diffstat (limited to 'lib')
-rw-r--r--lib/packages/docutils/dochelpers.nim267
-rw-r--r--lib/packages/docutils/rst.nim321
-rw-r--r--lib/packages/docutils/rstast.nim3
-rw-r--r--lib/packages/docutils/rstgen.nim27
-rw-r--r--lib/pure/strutils.nim5
5 files changed, 545 insertions, 78 deletions
diff --git a/lib/packages/docutils/dochelpers.nim b/lib/packages/docutils/dochelpers.nim
new file mode 100644
index 000000000..c488c4d99
--- /dev/null
+++ b/lib/packages/docutils/dochelpers.nim
@@ -0,0 +1,267 @@
+#
+#
+#            Nim's Runtime Library
+#        (c) Copyright 2021 Nim contributors
+#
+#    See the file "copying.txt", included in this
+#    distribution, for details about the copyright.
+#
+
+## Integration helpers between ``docgen.nim`` and ``rst.nim``.
+##
+## Function `toLangSymbol(linkText)`_ produces a signature `docLink` of
+## `type LangSymbol`_ in ``rst.nim``, while `match(generated, docLink)`_
+## matches it with `generated`, produced from `PNode` by ``docgen.rst``.
+
+import rstast
+
+type
+  LangSymbol* = object       ## symbol signature in Nim
+    symKind*: string           ## "proc", "const", etc
+    name*: string              ## plain symbol name without any parameters
+    generics*: string          ## generic parameters (without brackets)
+    isGroup*: bool             ## is LangSymbol a group with overloads?
+    # the following fields are valid iff `isGroup` == false
+    # (always false when parsed by `toLangSymbol` because link like foo_
+    # can point to just a single symbol foo, e.g. proc).
+    parametersProvided*: bool  ## to disambiguate `proc f`_ and `proc f()`_
+    parameters*: seq[tuple[name: string, `type`: string]]
+                               ## name-type seq, e.g. for proc
+    outType*: string           ## result type, e.g. for proc
+
+func nimIdentBackticksNormalize*(s: string): string =
+  ## Normalizes the string `s` as a Nim identifier.
+  ##
+  ## Unlike `nimIdentNormalize` removes spaces and backticks.
+  ##
+  ## .. Warning:: No checking (e.g. that identifiers cannot start from
+  ##    digits or '_', or that number of backticks is even) is performed.
+  runnableExamples:
+    doAssert nimIdentBackticksNormalize("Foo_bar") == "Foobar"
+    doAssert nimIdentBackticksNormalize("FoO BAr") == "Foobar"
+    doAssert nimIdentBackticksNormalize("`Foo BAR`") == "Foobar"
+    doAssert nimIdentBackticksNormalize("` Foo BAR `") == "Foobar"
+    # not a valid identifier:
+    doAssert nimIdentBackticksNormalize("`_x_y`") == "_xy"
+  result = newString(s.len)
+  var firstChar = true
+  var j = 0
+  for i in 0..len(s) - 1:
+    if s[i] in {'A'..'Z'}:
+      if not firstChar:  # to lowercase
+        result[j] = chr(ord(s[i]) + (ord('a') - ord('A')))
+      else:
+        result[j] = s[i]
+        firstChar = false
+      inc j
+    elif s[i] notin {'_', ' ', '`'}:
+      result[j] = s[i]
+      inc j
+      firstChar = false
+    elif s[i] == '_' and firstChar:
+      result[j] = '_'
+      inc j
+      firstChar = false
+    else: discard  # just omit '`' or ' '
+  if j != s.len: setLen(result, j)
+
+proc toLangSymbol*(linkText: PRstNode): LangSymbol =
+  ## Parses `linkText` into a more structured form using a state machine.
+  ##
+  ## This proc is designed to allow link syntax with operators even
+  ## without escaped backticks inside::
+  ##   
+  ##   `proc *`_
+  ##   `proc []`_
+  ##
+  ## This proc should be kept in sync with the `renderTypes` proc from
+  ## ``compiler/typesrenderer.nim``.
+  assert linkText.kind in {rnRef, rnInner}
+
+  const NimDefs = ["proc", "func", "macro", "method", "iterator",
+                   "template", "converter", "const", "type", "var"]
+  type
+    State = enum
+      inBeginning
+      afterSymKind
+      beforeSymbolName  # auxiliary state to catch situations like `proc []`_ after space
+      atSymbolName
+      afterSymbolName
+      genericsPar
+      parameterName
+      parameterType
+      outType
+  var state = inBeginning
+  var curIdent = ""
+  template flushIdent() =
+    if curIdent != "":
+      case state
+      of inBeginning:  doAssert false, "incorrect state inBeginning"
+      of afterSymKind:  result.symKind = curIdent
+      of beforeSymbolName:  doAssert false, "incorrect state beforeSymbolName"
+      of atSymbolName: result.name = curIdent.nimIdentBackticksNormalize
+      of afterSymbolName: doAssert false, "incorrect state afterSymbolName"
+      of genericsPar: result.generics = curIdent
+      of parameterName: result.parameters.add (curIdent, "")
+      of parameterType:
+        for a in countdown(result.parameters.len - 1, 0):
+          if result.parameters[a].`type` == "":
+            result.parameters[a].`type` = curIdent
+      of outType: result.outType = curIdent
+      curIdent = ""
+  var parens = 0
+  let L = linkText.sons.len
+  template s(i: int): string = linkText.sons[i].text
+  var i = 0
+  template nextState =
+    case s(i)
+    of " ":
+      if state == afterSymKind:
+        flushIdent
+        state = beforeSymbolName
+    of "`":
+      curIdent.add "`"
+      inc i
+      while i < L:  # add contents between ` ` as a whole
+        curIdent.add s(i)
+        if s(i) == "`":
+          break
+        inc i
+      curIdent = curIdent.nimIdentBackticksNormalize
+      if state in {inBeginning, afterSymKind, beforeSymbolName}:
+        state = atSymbolName
+        flushIdent
+        state = afterSymbolName
+    of "[":
+      if state notin {inBeginning, afterSymKind, beforeSymbolName}:
+        inc parens
+      if state in {inBeginning, afterSymKind, beforeSymbolName}:
+        state = atSymbolName
+        curIdent.add s(i)
+      elif state in {atSymbolName, afterSymbolName} and parens == 1:
+        flushIdent
+        state = genericsPar
+        curIdent.add s(i)
+      else: curIdent.add s(i)
+    of "]":
+      if state notin {inBeginning, afterSymKind, beforeSymbolName, atSymbolName}:
+        dec parens
+      if state == genericsPar and parens == 0:
+        curIdent.add s(i)
+        flushIdent
+      else: curIdent.add s(i)
+    of "(":
+      inc parens
+      if state in {inBeginning, afterSymKind, beforeSymbolName}:
+        result.parametersProvided = true
+        state = atSymbolName
+        flushIdent
+        state = parameterName
+      elif state in {atSymbolName, afterSymbolName, genericsPar} and parens == 1:
+        result.parametersProvided = true
+        flushIdent
+        state = parameterName
+      else: curIdent.add s(i)
+    of ")":
+      dec parens
+      if state in {parameterName, parameterType} and parens == 0:
+        flushIdent
+        state = outType
+      else: curIdent.add s(i)
+    of "{":  # remove pragmas
+      while i < L:
+        if s(i) == "}":
+          break
+        inc i
+    of ",", ";":
+      if state in {parameterName, parameterType} and parens == 1:
+        flushIdent
+        state = parameterName
+      else: curIdent.add s(i)
+    of "*":  # skip export symbol
+      if state == atSymbolName:
+        flushIdent
+        state = afterSymbolName
+      elif state == afterSymbolName:
+        discard
+      else: curIdent.add "*"
+    of ":":
+      if state == outType: discard
+      elif state == parameterName:
+        flushIdent
+        state = parameterType
+      else: curIdent.add ":"
+    else:
+      let isPostfixSymKind = i > 0 and i == L - 1 and
+          result.symKind == "" and s(i) in NimDefs
+      if isPostfixSymKind:  # for links like `foo proc`_
+        result.symKind = s(i)
+      else:
+        case state
+        of inBeginning:
+          if s(i) in NimDefs:
+            state = afterSymKind
+          else:
+            state = atSymbolName
+          curIdent.add s(i)
+        of afterSymKind, beforeSymbolName:
+          state = atSymbolName
+          curIdent.add s(i)
+        of parameterType:
+          case s(i)
+          of "ref": curIdent.add "ref."
+          of "ptr": curIdent.add "ptr."
+          of "var": discard
+          else: curIdent.add s(i).nimIdentBackticksNormalize
+        of atSymbolName:
+          curIdent.add s(i)
+        else:
+          curIdent.add s(i).nimIdentBackticksNormalize
+  while i < L:
+    nextState
+    inc i
+  if state == afterSymKind:  # treat `type`_ as link to symbol `type`
+    state = atSymbolName
+  flushIdent
+  result.isGroup = false
+
+proc match*(generated: LangSymbol, docLink: LangSymbol): bool =
+  ## Returns true if `generated` can be a target for `docLink`.
+  ## If `generated` is an overload group then only `symKind` and `name`
+  ## are compared for success.
+  result = true
+  if docLink.symKind != "":
+    if generated.symKind == "proc":
+      result = docLink.symKind in ["proc", "func"]
+    else:
+      result = generated.symKind == docLink.symKind
+    if not result: return
+  result = generated.name == docLink.name
+  if not result: return
+  if generated.isGroup:
+    # if `()` were added then it's not a reference to the whole group:
+    return not docLink.parametersProvided
+  if docLink.generics != "":
+    result = generated.generics == docLink.generics
+    if not result: return
+  if docLink.outType != "":
+    result = generated.outType == docLink.outType
+    if not result: return
+  if docLink.parametersProvided:
+    result = generated.parameters.len == docLink.parameters.len
+    if not result: return
+    var onlyType = false
+    for i in 0 ..< generated.parameters.len:
+      let g = generated.parameters[i]
+      let d = docLink.parameters[i]
+      if i == 0:
+        if g.`type` == d.name:
+          onlyType = true  # only types, not names, are provided in `docLink`
+      if onlyType:
+        result = g.`type` == d.name:
+      else:
+        if d.`type` != "":
+          result = g.`type` == d.`type`
+          if not result: return
+        result = g.name == d.name
+      if not result: return
diff --git a/lib/packages/docutils/rst.nim b/lib/packages/docutils/rst.nim
index 29234f28b..2e908f4e5 100644
--- a/lib/packages/docutils/rst.nim
+++ b/lib/packages/docutils/rst.nim
@@ -114,7 +114,7 @@
 ## .. _`extra features`:
 ##
 ## Optional additional features, turned on by ``options: RstParseOption`` in
-## `rstParse proc <#rstParse,string,string,int,int,bool,RstParseOptions,FindFileHandler,MsgHandler>`_:
+## `proc rstParse`_:
 ##
 ## * emoji / smiley symbols
 ## * Markdown tables
@@ -196,7 +196,7 @@
 ## .. _Sphinx roles: https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html
 
 import
-  os, strutils, rstast, std/enumutils, algorithm, lists, sequtils,
+  os, strutils, rstast, dochelpers, std/enumutils, algorithm, lists, sequtils,
   std/private/miscdollars, tables
 from highlite import SourceLanguage, getSourceLanguage
 
@@ -231,6 +231,7 @@ type
     meFootnoteMismatch = "mismatch in number of footnotes and their refs: $1",
     mwRedefinitionOfLabel = "redefinition of label '$1'",
     mwUnknownSubstitution = "unknown substitution '$1'",
+    mwAmbiguousLink = "ambiguous doc link $1",
     mwBrokenLink = "broken link '$1'",
     mwUnsupportedLanguage = "language '$1' not supported",
     mwUnsupportedField = "field '$1' not supported",
@@ -473,12 +474,42 @@ type
     hasPeers: bool       # has headings on the same level of hierarchy?
   LevelMap = seq[LevelInfo]   # Saves for each possible title adornment
                               # style its level in the current document.
+  SubstitutionKind = enum
+    rstSubstitution = "substitution",
+    hyperlinkAlias = "hyperlink alias",
+    implicitHyperlinkAlias = "implicitly-generated hyperlink alias"
   Substitution = object
+    kind*: SubstitutionKind
     key*: string
     value*: PRstNode
-  AnchorSubst = tuple
-    mainAnchor: string
-    aliases: seq[string]
+    info*: TLineInfo   # place where the substitution was defined
+  AnchorRule = enum
+    arInternalRst,  ## For automatically generated RST anchors (from
+                    ## headings, footnotes, inline internal targets):
+                    ## case-insensitive, 1-space-significant (by RST spec)
+    arNim   ## For anchors generated by ``docgen.rst``: Nim-style case
+            ## sensitivity, etc. (see `proc normalizeNimName`_ for details)
+    arHyperlink,  ## For links with manually set anchors in
+                  ## form `text <pagename.html#anchor>`_
+  RstAnchorKind = enum
+    manualDirectiveAnchor = "manual directive anchor",
+    manualInlineAnchor = "manual inline anchor",
+    footnoteAnchor = "footnote anchor",
+    headlineAnchor = "implicitly-generated headline anchor"
+  AnchorSubst = object
+    mainAnchor: ref string  # A reference name that will be inserted directly
+                            # into HTML/Latex. It's declared as `ref` because
+                            # it can be shared between aliases.
+    info: TLineInfo         # where the anchor was defined
+    priority: int
+    case kind: range[arInternalRst .. arNim]
+    of arInternalRst:
+      anchorType: RstAnchorKind
+    of arNim:
+      tooltip: string       # displayed tooltip for Nim-generated anchors
+      langSym: LangSymbol
+  AnchorSubstTable = Table[string, seq[AnchorSubst]]
+                         # use `seq` to account for duplicate anchors
   FootnoteType = enum
     fnManualNumber,     # manually numbered footnote like [3]
     fnAutoNumber,       # auto-numbered footnote [#]
@@ -505,7 +536,8 @@ type
     currRoleKind: RstNodeKind   # ... and its node kind
     subs: seq[Substitution]     # substitutions
     refs*: seq[Substitution]    # references
-    anchors*: seq[AnchorSubst]  # internal target substitutions
+    anchors*: AnchorSubstTable
+                                # internal target substitutions
     lineFootnoteNum: seq[TLineInfo]     # footnote line, auto numbers .. [#]
     lineFootnoteNumRef: seq[TLineInfo]  # footnote line, their reference [#]_
     currFootnoteNumRef: int             # ... their counter for `resolveSubs`
@@ -518,7 +550,7 @@ type
     findFile: FindFileHandler   # How to find files.
     filenames*: RstFileTable    # map file name <-> FileIndex (for storing
                                 # file names for warnings after 1st stage)
-    currFileIdx: FileIndex      # current index in `filesnames`
+    currFileIdx*: FileIndex     # current index in `filenames`
     hasToc*: bool
 
   PRstSharedState* = ref RstSharedState
@@ -532,6 +564,7 @@ type
                                 ## in case of error/warning reporting to
                                 ## (relative) line/column of the token.
     curAnchor*: string          # variable to track latest anchor in s.anchors
+    curAnchorName*: string      # corresponding name in human-readable format
 
   EParseError* = object of ValueError
 
@@ -590,13 +623,16 @@ proc whichRoleAux(sym: string): RstNodeKind =
 
 proc len(filenames: RstFileTable): int = filenames.idxToFilename.len
 
-proc setCurrFilename(s: PRstSharedState, file1: string) =
+proc addFilename*(s: PRstSharedState, file1: string): FileIndex =
+  ## Returns index of filename, adding it if it has not been used before
   let nextIdx = s.filenames.len.FileIndex
-  let v = getOrDefault(s.filenames.filenameToIdx, file1, default = nextIdx)
-  if v == nextIdx:
-    s.filenames.filenameToIdx[file1] = v
+  result = getOrDefault(s.filenames.filenameToIdx, file1, default = nextIdx)
+  if result == nextIdx:
+    s.filenames.filenameToIdx[file1] = result
     s.filenames.idxToFilename.add file1
-  s.currFileIdx = v
+
+proc setCurrFilename*(s: PRstSharedState, file1: string) =
+  s.currFileIdx = addFilename(s, file1)
 
 proc getFilename(filenames: RstFileTable, fid: FileIndex): string =
   doAssert(0 <= fid.int and fid.int < filenames.len,
@@ -730,6 +766,8 @@ proc initParser(p: var RstParser, sharedState: PRstSharedState) =
   p.s = sharedState
 
 proc addNodesAux(n: PRstNode, result: var string) =
+  if n == nil:
+    return
   if n.kind == rnLeaf:
     result.add(n.text)
   else:
@@ -738,6 +776,11 @@ proc addNodesAux(n: PRstNode, result: var string) =
 proc addNodes(n: PRstNode): string =
   n.addNodesAux(result)
 
+proc linkName(n: PRstNode): string =
+  ## Returns a normalized reference name, see:
+  ## https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#reference-names
+  n.addNodes.toLowerAscii
+
 proc rstnodeToRefnameAux(n: PRstNode, r: var string, b: var bool) =
   template special(s) =
     if b:
@@ -804,15 +847,26 @@ proc findSub(s: PRstSharedState, n: PRstNode): int =
       return i
   result = -1
 
+proc lineInfo(p: RstParser, iTok: int): TLineInfo =
+  result.col = int16(p.col + p.tok[iTok].col)
+  result.line = uint16(p.line + p.tok[iTok].line)
+  result.fileIndex = p.s.currFileIdx
+
+proc lineInfo(p: RstParser): TLineInfo = lineInfo(p, p.idx)
+# TODO: we need this simplification because we don't preserve exact starting
+# token of currently parsed element:
+proc prevLineInfo(p: RstParser): TLineInfo = lineInfo(p, p.idx-1)
+
 proc setSub(p: var RstParser, key: string, value: PRstNode) =
   var length = p.s.subs.len
   for i in 0 ..< length:
     if key == p.s.subs[i].key:
       p.s.subs[i].value = value
       return
-  p.s.subs.add(Substitution(key: key, value: value))
+  p.s.subs.add(Substitution(key: key, value: value, info: prevLineInfo(p)))
 
-proc setRef(p: var RstParser, key: string, value: PRstNode) =
+proc setRef(p: var RstParser, key: string, value: PRstNode,
+            refType: SubstitutionKind) =
   var length = p.s.refs.len
   for i in 0 ..< length:
     if key == p.s.refs[i].key:
@@ -820,37 +874,111 @@ proc setRef(p: var RstParser, key: string, value: PRstNode) =
         rstMessage(p, mwRedefinitionOfLabel, key)
       p.s.refs[i].value = value
       return
-  p.s.refs.add(Substitution(key: key, value: value))
+  p.s.refs.add(Substitution(kind: refType, key: key, value: value,
+                            info: prevLineInfo(p)))
 
-proc findRef(s: PRstSharedState, key: string): PRstNode =
+proc findRef(s: PRstSharedState, key: string): seq[Substitution] =
   for i in countup(0, high(s.refs)):
     if key == s.refs[i].key:
-      return 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])
+      result.add s.refs[i]
+
+# Ambiguity in links: we don't follow procedure of removing implicit targets
+# defined in https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#implicit-hyperlink-targets
+# Instead we just give explicit links a higher priority than to implicit ones
+# and report ambiguities as warnings. Hopefully it is easy to remove
+# ambiguities manually. Nim auto-generated links from ``docgen.nim``
+# have lowest priority: 1 (for procs) and below for other symbol types.
+
+proc refPriority(k: SubstitutionKind): int =
+  case k
+  of rstSubstitution: result = 8
+  of hyperlinkAlias: result = 7
+  of implicitHyperlinkAlias: result = 2
+
+proc internalRefPriority(k: RstAnchorKind): int =
+  case k
+  of manualDirectiveAnchor: result = 6
+  of manualInlineAnchor: result = 5
+  of footnoteAnchor: result = 4
+  of headlineAnchor: result = 3
+
+proc addAnchorRst(p: var RstParser, name: string, refn: string, reset: bool,
+                  anchorType: RstAnchorKind) =
+  ## Adds anchor `refn` with an alias `name` and
+  ## updates the corresponding `curAnchor` / `curAnchorName`.
+  let prio = internalRefPriority(anchorType)
+  if p.curAnchorName == "":
+    var anchRef = new string
+    anchRef[] = refn
+    p.s.anchors.mgetOrPut(name, newSeq[AnchorSubst]()).add(
+        AnchorSubst(kind: arInternalRst, mainAnchor: anchRef, priority: prio,
+                    info: prevLineInfo(p), anchorType: anchorType))
   else:
-    p.s.anchors[^1].mainAnchor = refn
-    p.s.anchors[^1].aliases.add refn
+    # override previous mainAnchor by `ref` in all aliases
+    var anchRef = p.s.anchors[p.curAnchorName][0].mainAnchor
+    anchRef[] = refn
+    p.s.anchors.mgetOrPut(name, newSeq[AnchorSubst]()).add(
+        AnchorSubst(kind: arInternalRst, mainAnchor: anchRef, priority: prio,
+                    info: prevLineInfo(p), anchorType: anchorType))
   if reset:
     p.curAnchor = ""
+    p.curAnchorName = ""
   else:
     p.curAnchor = refn
+    p.curAnchorName = name
+
+proc addAnchorNim*(s: var PRstSharedState, refn: string, tooltip: string,
+                   langSym: LangSymbol, priority: int,
+                   info: TLineInfo) =
+  ## Adds an anchor `refn` (`mainAnchor`), which follows
+  ## the rule `arNim` (i.e. a symbol in ``*.nim`` file)
+  var anchRef = new string
+  anchRef[] = refn
+  s.anchors.mgetOrPut(langSym.name, newSeq[AnchorSubst]()).add(
+      AnchorSubst(kind: arNim, mainAnchor: anchRef, langSym: langSym,
+                  tooltip: tooltip, priority: priority,
+                  info: info))
+
+proc findMainAnchorNim(s: PRstSharedState, signature: PRstNode,
+                       info: TLineInfo):
+                      seq[AnchorSubst] =
+  let langSym = toLangSymbol(signature)
+  let substitutions = s.anchors.getOrDefault(langSym.name,
+                                             newSeq[AnchorSubst]())
+  if substitutions.len == 0:
+    return
+  # map symKind (like "proc") -> found symbols/groups:
+  var found: Table[string, seq[AnchorSubst]]
+  for s in substitutions:
+    if s.kind == arNim:
+      if match(s.langSym, langSym):
+        found.mgetOrPut(s.langSym.symKind, newSeq[AnchorSubst]()).add s
+  for symKind, sList in found:
+    if sList.len == 1:
+      result.add sList[0]
+    else:  # > 1, there are overloads, potential ambiguity in this `symKind`
+      if langSym.parametersProvided:
+        # there are non-group signatures, select only them
+        for s in sList:
+          if not s.langSym.isGroup:
+            result.add s
+      else:  # when there are many overloads a link like foo_ points to all
+             # of them, so selecting the group
+        var foundGroup = true
+        for s in sList:
+          if s.langSym.isGroup:
+            result.add s
+            foundGroup = true
+            break
+        doAssert foundGroup, "docgen has not generated the group"
 
-proc findMainAnchor(s: PRstSharedState, refn: string): string =
-  for subst in 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 findMainAnchorRst(s: PRstSharedState, linkText: string, info: TLineInfo):
+                      seq[AnchorSubst] =
+  let name = linkText.toLowerAscii
+  let substitutions = s.anchors.getOrDefault(name, newSeq[AnchorSubst]())
+  for s in substitutions:
+    if s.kind == arInternalRst:
+      result.add s
 
 proc addFootnoteNumManual(p: var RstParser, num: int) =
   ## add manually-numbered footnote
@@ -860,13 +988,6 @@ proc addFootnoteNumManual(p: var RstParser, num: int) =
       return
   p.s.footnotes.add((fnManualNumber, num, -1, -1, $num))
 
-proc lineInfo(p: RstParser, iTok: int): TLineInfo =
-  result.col = int16(p.col + p.tok[iTok].col)
-  result.line = uint16(p.line + p.tok[iTok].line)
-  result.fileIndex = p.s.currFileIdx
-
-proc lineInfo(p: RstParser): TLineInfo = lineInfo(p, p.idx)
-
 proc addFootnoteNumAuto(p: var RstParser, label: string) =
   ## add auto-numbered footnote.
   ## Empty label [#] means it'll be resolved by the occurrence.
@@ -989,6 +1110,7 @@ proc newRstNodeA(p: var RstParser, kind: RstNodeKind): PRstNode =
   if p.curAnchor != "":
     result.anchor = p.curAnchor
     p.curAnchor = ""
+    p.curAnchorName = ""
 
 template newLeaf(s: string): PRstNode = newRstLeaf(s)
 
@@ -1255,7 +1377,7 @@ proc parsePostfix(p: var RstParser, n: PRstNode): PRstNode =
       else:
         newKind = rnHyperlink
         newSons = @[a, b]
-        setRef(p, rstnodeToRefname(a), b)
+        setRef(p, rstnodeToRefname(a), b, implicitHyperlinkAlias)
       result = newRstNode(newKind, newSons)
     else:  # some link that will be resolved in `resolveSubs`
       newKind = rnRef
@@ -1562,7 +1684,8 @@ proc parseInline(p: var RstParser, father: PRstNode) =
       inc p.idx
       parseUntil(p, n, "`", false)
       let refn = rstnodeToRefname(n)
-      p.s.anchors.add (refn, @[refn])
+      addAnchorRst(p, name = linkName(n), refn = refn, reset = true,
+                   anchorType=manualInlineAnchor)
       father.add(n)
     elif roSupportMarkdown in p.s.options and currentTok(p).symbol == "```":
       inc p.idx
@@ -2084,7 +2207,8 @@ proc parseHeadline(p: var RstParser): PRstNode =
     result.level = getLevel(p, c, hasOverline=false)
     checkHeadingHierarchy(p, result.level)
     p.s.hCurLevel = result.level
-  addAnchor(p, rstnodeToRefname(result), reset=true)
+  addAnchorRst(p, linkName(result), rstnodeToRefname(result), reset=true,
+               anchorType=headlineAnchor)
 
 proc parseOverline(p: var RstParser): PRstNode =
   var c = currentTok(p).symbol[0]
@@ -2106,7 +2230,8 @@ proc parseOverline(p: var RstParser): PRstNode =
   if currentTok(p).kind == tkAdornment:
     inc p.idx
     if currentTok(p).kind == tkIndent: inc p.idx
-  addAnchor(p, rstnodeToRefname(result), reset=true)
+  addAnchorRst(p, linkName(result), rstnodeToRefname(result), reset=true,
+               anchorType=headlineAnchor)
 
 type
   IntSeq = seq[int]
@@ -2837,7 +2962,7 @@ proc parseFootnote(p: var RstParser): PRstNode =
     anchor.add $p.s.lineFootnoteSym.len
   of fnCitation:
     anchor.add rstnodeToRefname(label)
-  addAnchor(p, anchor, reset=true)
+  addAnchorRst(p, anchor, anchor, reset=true, anchorType=footnoteAnchor)
   result.anchor = anchor
   if currentTok(p).kind == tkWhite: inc p.idx
   discard parseBlockContent(p, result, parseSectionWrapper)
@@ -2858,13 +2983,23 @@ proc parseDotDot(p: var RstParser): PRstNode =
   elif match(p, p.idx, " _"):
     # hyperlink target:
     inc p.idx, 2
-    var a = getReferenceName(p, ":")
+    var ending = ":"
+    if currentTok(p).symbol == "`":
+      inc p.idx
+      ending = "`"
+    var a = getReferenceName(p, ending)
+    if ending == "`":
+      if currentTok(p).symbol == ":":
+        inc p.idx
+      else:
+        rstMessage(p, meExpected, ":")
     if currentTok(p).kind == tkWhite: inc p.idx
     var b = untilEol(p)
     if len(b) == 0:  # set internal anchor
-      addAnchor(p, rstnodeToRefname(a), reset=false)
+      addAnchorRst(p, linkName(a), rstnodeToRefname(a), reset=false,
+                   anchorType=manualDirectiveAnchor)
     else:  # external hyperlink
-      setRef(p, rstnodeToRefname(a), b)
+      setRef(p, rstnodeToRefname(a), b, refType=hyperlinkAlias)
   elif match(p, p.idx, " |"):
     # substitution definitions:
     inc p.idx, 2
@@ -2892,7 +3027,7 @@ proc rstParsePass1*(fragment: string,
                     sharedState: PRstSharedState): PRstNode =
   ## Parses an RST `fragment`.
   ## The result should be further processed by
-  ## `preparePass2` and `resolveSubs` (which is pass 2).
+  ## preparePass2_ and resolveSubs_ (which is pass 2).
   var p: RstParser
   initParser(p, sharedState)
   p.line = line
@@ -2905,6 +3040,65 @@ proc preparePass2*(s: PRstSharedState, mainNode: PRstNode) =
   countTitles(s, mainNode)
   orderFootnotes(s)
 
+proc resolveLink(s: PRstSharedState, n: PRstNode) : PRstNode =
+    # Associate this link alias with its target and change node kind to
+    # rnHyperlink or rnInternalRef appropriately.
+    type LinkDef = object
+      ar: AnchorRule
+      priority: int
+      tooltip: string
+      target: PRstNode
+      info: TLineInfo
+    proc cmp(x, y: LinkDef): int =
+      result = cmp(x.priority, y.priority)
+      if result == 0:
+        result = cmp(x.target, y.target)
+    var foundLinks: seq[LinkDef]
+    let text = newRstNode(rnInner, n.sons)
+    let refn = rstnodeToRefname(n)
+    var hyperlinks = findRef(s, refn)
+    for y in hyperlinks:
+      foundLinks.add LinkDef(ar: arHyperlink, priority: refPriority(y.kind),
+                             target: y.value, info: y.info,
+                             tooltip: "(" & $y.kind & ")")
+    let substRst = findMainAnchorRst(s, text.addNodes, n.info)
+    for subst in substRst:
+      foundLinks.add LinkDef(ar: arInternalRst, priority: subst.priority,
+                             target: newLeaf(subst.mainAnchor[]),
+                             info: subst.info,
+                             tooltip: "(" & $subst.anchorType & ")")
+    if roNimFile in s.options:
+      let substNim = findMainAnchorNim(s, signature=text, n.info)
+      for subst in substNim:
+        foundLinks.add LinkDef(ar: arNim, priority: subst.priority,
+                               target: newLeaf(subst.mainAnchor[]),
+                               info: subst.info, tooltip: subst.tooltip)
+    foundLinks.sort(cmp = cmp, order = Descending)
+    let linkText = addNodes(n)
+    if foundLinks.len >= 1:
+      let kind = if foundLinks[0].ar == arHyperlink: rnHyperlink
+                 elif foundLinks[0].ar == arNim: rnNimdocRef
+                 else: rnInternalRef
+      result = newRstNode(kind)
+      result.sons = @[text, foundLinks[0].target]
+      if kind == rnNimdocRef: result.tooltip = foundLinks[0].tooltip
+      if foundLinks.len > 1:  # report ambiguous link
+        var targets = newSeq[string]()
+        for l in foundLinks:
+          var t = "    "
+          if s.filenames.len > 1:
+            t.add getFilename(s.filenames, l.info.fileIndex)
+          let n = l.info.line
+          let c = l.info.col + ColRstOffset
+          t.add "($1, $2): $3" % [$n, $c, l.tooltip]
+          targets.add t
+        rstMessage(s.filenames, s.msgHandler, n.info, mwAmbiguousLink,
+                   "`$1`\n  clash:\n$2" % [
+                     linkText, targets.join("\n")])
+    else:  # nothing found
+      result = n
+      rstMessage(s.filenames, s.msgHandler, n.info, mwBrokenLink, linkText)
+
 proc resolveSubs*(s: PRstSharedState, n: PRstNode): PRstNode =
   ## Makes pass 2 of RST parsing.
   ## Resolves substitutions and anchor aliases, groups footnotes.
@@ -2933,21 +3127,7 @@ proc resolveSubs*(s: PRstSharedState, n: PRstNode): PRstNode =
     elif s.hTitleCnt == 0:
       n.level += 1
   of rnRef:
-    let refn = rstnodeToRefname(n)
-    var y = findRef(s, refn)
-    if y != nil:
-      result = newRstNode(rnHyperlink)
-      let text = newRstNode(rnInner, n.sons)
-      result.sons = @[text, y]
-    else:
-      let anchor = findMainAnchor(s, refn)
-      if anchor != "":
-        result = newRstNode(rnInternalRef)
-        let text = newRstNode(rnInner, n.sons)
-        result.sons = @[text,             # visible text of reference
-                        newLeaf(anchor)]  # link itself
-      else:
-        rstMessage(s.filenames, s.msgHandler, n.info, mwBrokenLink, refn)
+    result = resolveLink(s, n)
   of rnFootnote:
     var (fnType, num) = getFootnoteType(n.sons[0])
     case fnType
@@ -2993,9 +3173,10 @@ proc resolveSubs*(s: PRstSharedState, n: PRstNode): PRstNode =
     of fnCitation:
       result.add n.sons[0]
       refn.add rstnodeToRefname(n)
-    let anch = findMainAnchor(s, refn)
-    if anch != "":
-      result.add newLeaf(anch)     # add link
+    # TODO: correctly report ambiguities
+    let anchorInfo = findMainAnchorRst(s, refn, n.info)
+    if anchorInfo.len != 0:
+      result.add newLeaf(anchorInfo[0].mainAnchor[])  # add link
     else:
       rstMessage(s.filenames, s.msgHandler, n.info, mwBrokenLink, refn)
       result.add newLeaf(refn)  # add link
diff --git a/lib/packages/docutils/rstast.nim b/lib/packages/docutils/rstast.nim
index e1ed7c099..bc7b9a650 100644
--- a/lib/packages/docutils/rstast.nim
+++ b/lib/packages/docutils/rstast.nim
@@ -43,6 +43,7 @@ type
     rnFootnoteGroup,          # footnote group - exists for a purely stylistic
                               # reason: to display a few footnotes as 1 block
     rnStandaloneHyperlink, rnHyperlink, rnRef, rnInternalRef, rnFootnoteRef,
+    rnNimdocRef,              # reference to automatically generated Nim symbol
     rnDirective,              # a general directive
     rnDirArg,                 # a directive argument (for some directives).
                               # here are directives that are not rnDirective:
@@ -104,6 +105,8 @@ type
         rnInterpretedText, rnField, rnInlineCode, rnCodeBlock, rnFootnoteRef:
       info*: TLineInfo        ## To have line/column info for warnings at
                               ## nodes that are post-processed after parsing
+    of rnNimdocRef:
+      tooltip*: string
     else:
       discard
     anchor*: string           ## anchor, internal link target
diff --git a/lib/packages/docutils/rstgen.nim b/lib/packages/docutils/rstgen.nim
index 8eac37307..a556aa2e3 100644
--- a/lib/packages/docutils/rstgen.nim
+++ b/lib/packages/docutils/rstgen.nim
@@ -60,8 +60,8 @@ type
   MetaEnum* = enum
     metaNone, metaTitle, metaSubtitle, metaAuthor, metaVersion
 
-  EscapeMode = enum  # in Latex text inside options [] and URLs is
-                     # escaped slightly differently than in normal text
+  EscapeMode* = enum  # in Latex text inside options [] and URLs is
+                      # escaped slightly differently than in normal text
     emText, emOption, emUrl  # emText is currently used for code also
 
   RstGenerator* = object of RootObj
@@ -201,7 +201,9 @@ proc addTexChar(dest: var string, c: char, escMode: EscapeMode) =
   ## All escapes that need to work in text and code blocks (`emText` mode)
   ## should start from \ (to be compatible with fancyvrb/fvextra).
   case c
-  of '_', '$', '&', '#', '%': add(dest, "\\" & c)
+  of '_', '&', '#', '%': add(dest, "\\" & c)
+  # commands \label and \pageref don't accept \$ by some reason but OK with $:
+  of '$': (if escMode == emUrl: add(dest, c) else: add(dest, "\\" & c))
   # \~ and \^ have a special meaning unless they are followed by {}
   of '~', '^': add(dest, "\\" & c & "{}")
   # Latex loves to substitute ` to opening quote, even in texttt mode!
@@ -1180,7 +1182,8 @@ proc renderAdmonition(d: PDoc, n: PRstNode, result: var string) =
         "$1\n\\end{rstadmonition}\n",
       result)
 
-proc renderHyperlink(d: PDoc, text, link: PRstNode, result: var string, external: bool) =
+proc renderHyperlink(d: PDoc, text, link: PRstNode, result: var string,
+                     external: bool, nimdoc = false, tooltip="") =
   var linkStr = ""
   block:
     let mode = d.escMode
@@ -1189,14 +1192,19 @@ proc renderHyperlink(d: PDoc, text, link: PRstNode, result: var string, external
     d.escMode = mode
   var textStr = ""
   renderRstToOut(d, text, textStr)
+  let nimDocStr = if nimdoc: " nimdoc" else: ""
+  var tooltipStr = ""
+  if tooltip != "":
+    tooltipStr = """ title="$1"""" % [ esc(d.target, tooltip) ]
   if external:
     dispA(d.target, result,
-      "<a class=\"reference external\" href=\"$2\">$1</a>",
-      "\\href{$2}{$1}", [textStr, linkStr])
+      "<a class=\"reference external$3\"$4 href=\"$2\">$1</a>",
+      "\\href{$2}{$1}", [textStr, linkStr, nimDocStr, tooltipStr])
   else:
     dispA(d.target, result,
-      "<a class=\"reference internal\" href=\"#$2\">$1</a>",
-      "\\hyperlink{$2}{$1} (p.~\\pageref{$2})", [textStr, linkStr])
+      "<a class=\"reference internal$3\"$4 href=\"#$2\">$1</a>",
+      "\\hyperlink{$2}{$1} (p.~\\pageref{$2})",
+      [textStr, linkStr, nimDocStr, tooltipStr])
 
 proc renderRstToOut(d: PDoc, n: PRstNode, result: var string) =
   if n == nil: return
@@ -1329,6 +1337,9 @@ proc renderRstToOut(d: PDoc, n: PRstNode, result: var string) =
     renderHyperlink(d, text=n.sons[0], link=n.sons[0], result, external=true)
   of rnInternalRef:
     renderHyperlink(d, text=n.sons[0], link=n.sons[1], result, external=false)
+  of rnNimdocRef:
+    renderHyperlink(d, text=n.sons[0], link=n.sons[1], result, external=false,
+                    nimdoc=true, tooltip=n.tooltip)
   of rnHyperlink:
     renderHyperlink(d, text=n.sons[0], link=n.sons[1], result, external=true)
   of rnFootnoteRef:
diff --git a/lib/pure/strutils.nim b/lib/pure/strutils.nim
index 873949ca6..2234a8625 100644
--- a/lib/pure/strutils.nim
+++ b/lib/pure/strutils.nim
@@ -273,6 +273,11 @@ func nimIdentNormalize*(s: string): string =
   ##
   ## That means to convert to lower case and remove any '_' on all characters
   ## except first one.
+  ##
+  ## .. Warning:: Backticks (`) are not handled: they remain *as is* and
+  ##    spaces are preserved. See `nimIdentBackticksNormalize 
+  ##    <dochelpers.html#nimIdentBackticksNormalize,string>`_ for
+  ##    an alternative approach.
   runnableExamples:
     doAssert nimIdentNormalize("Foo_bar") == "Foobar"
   result = newString(s.len)