summary refs log tree commit diff stats
path: root/lib/packages/docutils/dochelpers.nim
blob: a11f2bbbb10a9975a71bc28e4c0edd924eb693ae (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
#
#
#            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

when defined(nimPreviewSlimSystem):
  import std/[assertions, syncio]


type
  LangSymbol* = object       ## symbol signature in Nim
    symKind*: string           ## "proc", "const", "type", etc
    symTypeKind*: string       ## ""|enum|object|tuple -
                               ## valid only when `symKind == "type"`
    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",
                   "enum", "object", "tuple"]
  template resolveSymKind(x: string) =
    if x in ["enum", "object", "tuple"]:
      result.symKind = "type"
      result.symTypeKind = x
    else:
      result.symKind = x
  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:  resolveSymKind 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`_
        resolveSymKind 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 result and docLink.symKind == "type" and docLink.symTypeKind != "":
        result = generated.symTypeKind == docLink.symTypeKind
    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