docgen.nim 70 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809
  1. #
  2. #
  3. # The Nim Compiler
  4. # (c) Copyright 2012 Andreas Rumpf
  5. #
  6. # See the file "copying.txt", included in this
  7. # distribution, for details about the copyright.
  8. #
  9. # This is the documentation generator. Cross-references are generated
  10. # by knowing how the anchors are going to be named.
  11. import
  12. ast, strutils, strtabs, algorithm, sequtils, options, msgs, os, idents,
  13. wordrecg, syntaxes, renderer, lexer,
  14. packages/docutils/[rst, rstgen, dochelpers],
  15. json, xmltree, trees, types,
  16. typesrenderer, astalgo, lineinfos, intsets,
  17. pathutils, tables, nimpaths, renderverbatim, osproc, packages
  18. import packages/docutils/rstast except FileIndex, TLineInfo
  19. from uri import encodeUrl
  20. from std/private/globs import nativeToUnixPath
  21. from nodejs import findNodeJs
  22. when defined(nimPreviewSlimSystem):
  23. import std/[assertions, syncio]
  24. const
  25. exportSection = skField
  26. docCmdSkip = "skip"
  27. DocColOffset = "## ".len # assuming that a space was added after ##
  28. type
  29. ItemFragment = object ## A fragment from each item will be eventually
  30. ## constructed by converting `rst` fields to strings.
  31. case isRst: bool
  32. of true:
  33. rst: PRstNode
  34. of false: ## contains ready markup e.g. from runnableExamples
  35. str: string
  36. ItemPre = seq[ItemFragment] ## A pre-processed item.
  37. Item = object ## Any item in documentation, e.g. symbol
  38. ## entry. Configuration variable ``doc.item``
  39. ## is used for its HTML rendering.
  40. descRst: ItemPre ## Description of the item (may contain
  41. ## runnableExamples).
  42. substitutions: seq[string] ## Variable names in `doc.item`...
  43. sortName: string ## The string used for sorting in output
  44. info: rstast.TLineInfo ## place where symbol was defined (for messages)
  45. anchor: string ## e.g. HTML anchor
  46. name: string ## short name of the symbol, not unique
  47. ## (includes backticks ` if present)
  48. detailedName: string ## longer name like `proc search(x: int): int`
  49. ModSection = object ## Section like Procs, Types, etc.
  50. secItems: Table[string, seq[Item]]
  51. ## Map basic name -> pre-processed items.
  52. finalMarkup: string ## The items, after RST pass 2 and rendering.
  53. ModSections = array[TSymKind, ModSection]
  54. TocItem = object ## HTML TOC item
  55. content: string
  56. sortName: string
  57. TocSectionsFinal = array[TSymKind, string]
  58. ExampleGroup = ref object
  59. ## a group of runnableExamples with same rdoccmd
  60. rdoccmd: string ## from 1st arg in `runnableExamples(rdoccmd): body`
  61. docCmd: string ## from user config, e.g. --doccmd:-d:foo
  62. code: string ## contains imports; each import contains `body`
  63. index: int ## group index
  64. JsonItem = object # pre-processed item: `rst` should be finalized
  65. json: JsonNode
  66. rst: PRstNode
  67. rstField: string
  68. TDocumentor = object of rstgen.RstGenerator
  69. modDescPre: ItemPre # module description, not finalized
  70. modDescFinal: string # module description, after RST pass 2 and rendering
  71. module: PSym
  72. modDeprecationMsg: string
  73. section: ModSections # entries of ``.nim`` file (for `proc`s, etc)
  74. tocSimple: array[TSymKind, seq[TocItem]]
  75. # TOC entries for non-overloadable symbols (e.g. types, constants)...
  76. tocTable: array[TSymKind, Table[string, seq[TocItem]]]
  77. # ...otherwise (e.g. procs)
  78. toc2: TocSectionsFinal # TOC `content`, which is probably wrapped
  79. # in `doc.section.toc2`
  80. toc: TocSectionsFinal # final TOC (wrapped in `doc.section.toc`)
  81. indexValFilename: string
  82. analytics: string # Google Analytics javascript, "" if doesn't exist
  83. seenSymbols: StringTableRef # avoids duplicate symbol generation for HTML.
  84. jEntriesPre: seq[JsonItem] # pre-processed RST + JSON content
  85. jEntriesFinal: JsonNode # final JSON after RST pass 2 and rendering
  86. types: TStrTable
  87. sharedState: PRstSharedState
  88. standaloneDoc: bool
  89. conf*: ConfigRef
  90. cache*: IdentCache
  91. exampleCounter: int
  92. emitted: IntSet # we need to track which symbols have been emitted
  93. # already. See bug #3655
  94. thisDir*: AbsoluteDir
  95. exampleGroups: OrderedTable[string, ExampleGroup]
  96. wroteSupportFiles*: bool
  97. nimToRstFid: Table[lineinfos.FileIndex, rstast.FileIndex]
  98. ## map Nim FileIndex -> RST one, it's needed because we keep them separate
  99. PDoc* = ref TDocumentor ## Alias to type less.
  100. proc add(dest: var ItemPre, rst: PRstNode) = dest.add ItemFragment(isRst: true, rst: rst)
  101. proc add(dest: var ItemPre, str: string) = dest.add ItemFragment(isRst: false, str: str)
  102. proc addRstFileIndex(d: PDoc, info: lineinfos.TLineInfo): rstast.FileIndex =
  103. let invalid = rstast.FileIndex(-1)
  104. result = d.nimToRstFid.getOrDefault(info.fileIndex, default = invalid)
  105. if result == invalid:
  106. let fname = toFullPath(d.conf, info)
  107. result = addFilename(d.sharedState, fname)
  108. d.nimToRstFid[info.fileIndex] = result
  109. proc cmpDecimalsIgnoreCase(a, b: string): int =
  110. ## For sorting with correct handling of cases like 'uint8' and 'uint16'.
  111. ## Also handles leading zeros well (however note that leading zeros are
  112. ## significant when lengths of numbers mismatch, e.g. 'bar08' > 'bar8' !).
  113. runnableExamples:
  114. doAssert cmpDecimalsIgnoreCase("uint8", "uint16") < 0
  115. doAssert cmpDecimalsIgnoreCase("val00032", "val16suffix") > 0
  116. doAssert cmpDecimalsIgnoreCase("val16suffix", "val16") > 0
  117. doAssert cmpDecimalsIgnoreCase("val_08_32", "val_08_8") > 0
  118. doAssert cmpDecimalsIgnoreCase("val_07_32", "val_08_8") < 0
  119. doAssert cmpDecimalsIgnoreCase("ab8", "ab08") < 0
  120. doAssert cmpDecimalsIgnoreCase("ab8de", "ab08c") < 0 # sanity check
  121. let aLen = a.len
  122. let bLen = b.len
  123. var
  124. iA = 0
  125. iB = 0
  126. while iA < aLen and iB < bLen:
  127. if isDigit(a[iA]) and isDigit(b[iB]):
  128. var
  129. limitA = iA # index after the last (least significant) digit
  130. limitB = iB
  131. while limitA < aLen and isDigit(a[limitA]): inc limitA
  132. while limitB < bLen and isDigit(b[limitB]): inc limitB
  133. var pos = max(limitA-iA, limitB-iA)
  134. while pos > 0:
  135. if limitA-pos < iA: # digit in `a` is 0 effectively
  136. result = ord('0') - ord(b[limitB-pos])
  137. elif limitB-pos < iB: # digit in `b` is 0 effectively
  138. result = ord(a[limitA-pos]) - ord('0')
  139. else:
  140. result = ord(a[limitA-pos]) - ord(b[limitB-pos])
  141. if result != 0: return
  142. dec pos
  143. result = (limitA - iA) - (limitB - iB) # consider 'bar08' > 'bar8'
  144. if result != 0: return
  145. iA = limitA
  146. iB = limitB
  147. else:
  148. result = ord(toLowerAscii(a[iA])) - ord(toLowerAscii(b[iB]))
  149. if result != 0: return
  150. inc iA
  151. inc iB
  152. result = (aLen - iA) - (bLen - iB)
  153. proc prettyString(a: object): string =
  154. # xxx pending std/prettyprint refs https://github.com/nim-lang/RFCs/issues/203#issuecomment-602534906
  155. for k, v in fieldPairs(a):
  156. result.add k & ": " & $v & "\n"
  157. proc presentationPath*(conf: ConfigRef, file: AbsoluteFile): RelativeFile =
  158. ## returns a relative file that will be appended to outDir
  159. let file2 = $file
  160. template bail() =
  161. result = relativeTo(file, conf.projectPath)
  162. proc nimbleDir(): AbsoluteDir =
  163. getNimbleFile(conf, file2).parentDir.AbsoluteDir
  164. case conf.docRoot:
  165. of docRootDefault:
  166. result = getRelativePathFromConfigPath(conf, file)
  167. let dir = nimbleDir()
  168. if not dir.isEmpty:
  169. let result2 = relativeTo(file, dir)
  170. if not result2.isEmpty and (result.isEmpty or result2.string.len < result.string.len):
  171. result = result2
  172. if result.isEmpty: bail()
  173. of "@pkg":
  174. let dir = nimbleDir()
  175. if dir.isEmpty: bail()
  176. else: result = relativeTo(file, dir)
  177. of "@path":
  178. result = getRelativePathFromConfigPath(conf, file)
  179. if result.isEmpty: bail()
  180. elif conf.docRoot.len > 0:
  181. # we're (currently) requiring `isAbsolute` to avoid confusion when passing
  182. # a relative path (would it be relative with regard to $PWD or to projectfile)
  183. conf.globalAssert conf.docRoot.isAbsolute, arg=conf.docRoot
  184. conf.globalAssert conf.docRoot.dirExists, arg=conf.docRoot
  185. # needed because `canonicalizePath` called on `file`
  186. result = file.relativeTo conf.docRoot.expandFilename.AbsoluteDir
  187. else:
  188. bail()
  189. if isAbsolute(result.string):
  190. result = file.string.splitPath()[1].RelativeFile
  191. result = result.string.replace("..", dotdotMangle).RelativeFile
  192. doAssert not result.isEmpty
  193. doAssert not isAbsolute(result.string)
  194. proc whichType(d: PDoc; n: PNode): PSym =
  195. if n.kind == nkSym:
  196. if d.types.strTableContains(n.sym):
  197. result = n.sym
  198. else:
  199. for i in 0..<n.safeLen:
  200. let x = whichType(d, n[i])
  201. if x != nil: return x
  202. proc attachToType(d: PDoc; p: PSym): PSym =
  203. let params = p.ast[paramsPos]
  204. template check(i) =
  205. result = whichType(d, params[i])
  206. if result != nil: return result
  207. # first check the first parameter, then the return type,
  208. # then the other parameter:
  209. if params.len > 1: check(1)
  210. if params.len > 0: check(0)
  211. for i in 2..<params.len: check(i)
  212. template declareClosures =
  213. proc compilerMsgHandler(filename: string, line, col: int,
  214. msgKind: rst.MsgKind, arg: string) {.gcsafe.} =
  215. # translate msg kind:
  216. var k: TMsgKind
  217. case msgKind
  218. of meCannotOpenFile: k = errCannotOpenFile
  219. of meExpected: k = errXExpected
  220. of meMissingClosing: k = errRstMissingClosing
  221. of meGridTableNotImplemented: k = errRstGridTableNotImplemented
  222. of meMarkdownIllformedTable: k = errRstMarkdownIllformedTable
  223. of meIllformedTable: k = errRstIllformedTable
  224. of meNewSectionExpected: k = errRstNewSectionExpected
  225. of meGeneralParseError: k = errRstGeneralParseError
  226. of meInvalidDirective: k = errRstInvalidDirectiveX
  227. of meInvalidField: k = errRstInvalidField
  228. of meFootnoteMismatch: k = errRstFootnoteMismatch
  229. of meSandboxedDirective: k = errRstSandboxedDirective
  230. of mwRedefinitionOfLabel: k = warnRstRedefinitionOfLabel
  231. of mwUnknownSubstitution: k = warnRstUnknownSubstitutionX
  232. of mwAmbiguousLink: k = warnRstAmbiguousLink
  233. of mwBrokenLink: k = warnRstBrokenLink
  234. of mwUnsupportedLanguage: k = warnRstLanguageXNotSupported
  235. of mwUnsupportedField: k = warnRstFieldXNotSupported
  236. of mwRstStyle: k = warnRstStyle
  237. {.gcsafe.}:
  238. globalError(conf, newLineInfo(conf, AbsoluteFile filename, line, col), k, arg)
  239. proc docgenFindFile(s: string): string {.gcsafe.} =
  240. result = options.findFile(conf, s).string
  241. if result.len == 0:
  242. result = getCurrentDir() / s
  243. if not fileExists(result): result = ""
  244. proc parseRst(text: string,
  245. line, column: int,
  246. conf: ConfigRef, sharedState: PRstSharedState): PRstNode =
  247. declareClosures()
  248. result = rstParsePass1(text, line, column, sharedState)
  249. proc getOutFile2(conf: ConfigRef; filename: RelativeFile,
  250. ext: string, guessTarget: bool): AbsoluteFile =
  251. if optWholeProject in conf.globalOptions or guessTarget:
  252. let d = conf.outDir
  253. createDir(d)
  254. result = d / changeFileExt(filename, ext)
  255. elif not conf.outFile.isEmpty:
  256. result = absOutFile(conf)
  257. else:
  258. result = getOutFile(conf, filename, ext)
  259. proc isLatexCmd(conf: ConfigRef): bool =
  260. conf.cmd in {cmdRst2tex, cmdMd2tex, cmdDoc2tex}
  261. proc newDocumentor*(filename: AbsoluteFile; cache: IdentCache; conf: ConfigRef,
  262. outExt: string = HtmlExt, module: PSym = nil,
  263. standaloneDoc = false, preferMarkdown = true,
  264. hasToc = true): PDoc =
  265. declareClosures()
  266. new(result)
  267. result.module = module
  268. result.conf = conf
  269. result.cache = cache
  270. result.outDir = conf.outDir.string
  271. result.standaloneDoc = standaloneDoc
  272. var options= {roSupportRawDirective, roSupportMarkdown, roSandboxDisabled}
  273. if preferMarkdown:
  274. options.incl roPreferMarkdown
  275. if not standaloneDoc: options.incl roNimFile
  276. # (options can be changed dynamically in `setDoctype` by `{.doctype.}`)
  277. result.hasToc = hasToc
  278. result.sharedState = newRstSharedState(
  279. options, filename.string,
  280. docgenFindFile, compilerMsgHandler, hasToc)
  281. initRstGenerator(result[], (if conf.isLatexCmd: outLatex else: outHtml),
  282. conf.configVars, filename.string,
  283. docgenFindFile, compilerMsgHandler)
  284. if conf.configVars.hasKey("doc.googleAnalytics") and
  285. conf.configVars.hasKey("doc.plausibleAnalytics"):
  286. doAssert false, "Either use googleAnalytics or plausibleAnalytics"
  287. if conf.configVars.hasKey("doc.googleAnalytics"):
  288. result.analytics = """
  289. <script>
  290. (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
  291. (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
  292. m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
  293. })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
  294. ga('create', '$1', 'auto');
  295. ga('send', 'pageview');
  296. </script>
  297. """ % [conf.configVars.getOrDefault"doc.googleAnalytics"]
  298. elif conf.configVars.hasKey("doc.plausibleAnalytics"):
  299. result.analytics = """
  300. <script defer data-domain="$1" src="https://plausible.io/js/plausible.js"></script>
  301. """ % [conf.configVars.getOrDefault"doc.plausibleAnalytics"]
  302. else:
  303. result.analytics = ""
  304. result.seenSymbols = newStringTable(modeCaseInsensitive)
  305. result.id = 100
  306. result.jEntriesFinal = newJArray()
  307. initStrTable result.types
  308. result.onTestSnippet =
  309. proc (gen: var RstGenerator; filename, cmd: string; status: int; content: string) =
  310. if conf.docCmd == docCmdSkip: return
  311. inc(gen.id)
  312. var d = (ptr TDocumentor)(addr gen)
  313. var outp: AbsoluteFile
  314. if filename.len == 0:
  315. let nameOnly = splitFile(d.filename).name
  316. # "snippets" needed, refs bug #17183
  317. outp = getNimcacheDir(conf) / "snippets".RelativeDir / RelativeDir(nameOnly) /
  318. RelativeFile(nameOnly & "_snippet_" & $d.id & ".nim")
  319. elif isAbsolute(filename):
  320. outp = AbsoluteFile(filename)
  321. else:
  322. # Nim's convention: every path is relative to the file it was written in:
  323. let nameOnly = splitFile(d.filename).name
  324. outp = AbsoluteDir(nameOnly) / RelativeFile(filename)
  325. # Make sure the destination directory exists
  326. createDir(outp.splitFile.dir)
  327. # Include the current file if we're parsing a nim file
  328. let importStmt = if d.standaloneDoc: "" else: "import \"$1\"\n" % [d.filename.replace("\\", "/")]
  329. writeFile(outp, importStmt & content)
  330. proc interpSnippetCmd(cmd: string): string =
  331. # backward compatibility hacks; interpolation commands should explicitly use `$`
  332. if cmd.startsWith "nim ": result = "$nim " & cmd[4..^1]
  333. else: result = cmd
  334. # factor with D20210224T221756
  335. result = result.replace("$1", "$options") % [
  336. "nim", os.getAppFilename().quoteShell,
  337. "libpath", quoteShell(d.conf.libpath),
  338. "docCmd", d.conf.docCmd,
  339. "backend", $d.conf.backend,
  340. "options", outp.quoteShell,
  341. # xxx `quoteShell` seems buggy if user passes options = "-d:foo somefile.nim"
  342. ]
  343. let cmd = cmd.interpSnippetCmd
  344. rawMessage(conf, hintExecuting, cmd)
  345. let (output, gotten) = execCmdEx(cmd)
  346. if gotten != status:
  347. rawMessage(conf, errGenerated, "snippet failed: cmd: '$1' status: $2 expected: $3 output: $4" % [cmd, $gotten, $status, output])
  348. result.emitted = initIntSet()
  349. result.destFile = getOutFile2(conf, presentationPath(conf, filename), outExt, false).string
  350. result.thisDir = result.destFile.AbsoluteFile.splitFile.dir
  351. template dispA(conf: ConfigRef; dest: var string, xml, tex: string,
  352. args: openArray[string]) =
  353. if not conf.isLatexCmd: dest.addf(xml, args)
  354. else: dest.addf(tex, args)
  355. proc getVarIdx(varnames: openArray[string], id: string): int =
  356. for i in 0..high(varnames):
  357. if cmpIgnoreStyle(varnames[i], id) == 0:
  358. return i
  359. result = -1
  360. proc genComment(d: PDoc, n: PNode): PRstNode =
  361. if n.comment.len > 0:
  362. d.sharedState.currFileIdx = addRstFileIndex(d, n.info)
  363. result = parseRst(n.comment,
  364. toLinenumber(n.info),
  365. toColumn(n.info) + DocColOffset,
  366. d.conf, d.sharedState)
  367. proc genRecCommentAux(d: PDoc, n: PNode): PRstNode =
  368. if n == nil: return nil
  369. result = genComment(d, n)
  370. if result == nil:
  371. if n.kind in {nkStmtList, nkStmtListExpr, nkTypeDef, nkConstDef,
  372. nkObjectTy, nkRefTy, nkPtrTy, nkAsgn, nkFastAsgn, nkHiddenStdConv}:
  373. # notin {nkEmpty..nkNilLit, nkEnumTy, nkTupleTy}:
  374. for i in 0..<n.len:
  375. result = genRecCommentAux(d, n[i])
  376. if result != nil: return
  377. else:
  378. n.comment = ""
  379. proc genRecComment(d: PDoc, n: PNode): PRstNode =
  380. if n == nil: return nil
  381. result = genComment(d, n)
  382. if result == nil:
  383. if n.kind in {nkProcDef, nkFuncDef, nkMethodDef, nkIteratorDef,
  384. nkMacroDef, nkTemplateDef, nkConverterDef}:
  385. result = genRecCommentAux(d, n[bodyPos])
  386. else:
  387. result = genRecCommentAux(d, n)
  388. proc getPlainDocstring(n: PNode): string =
  389. ## Gets the plain text docstring of a node non destructively.
  390. ##
  391. ## You need to call this before genRecComment, whose side effects are removal
  392. ## of comments from the tree. The proc will recursively scan and return all
  393. ## the concatenated ``##`` comments of the node.
  394. if n == nil: result = ""
  395. elif startsWith(n.comment, "##"):
  396. result = n.comment
  397. else:
  398. for i in 0..<n.safeLen:
  399. result = getPlainDocstring(n[i])
  400. if result.len > 0: return
  401. proc externalDep(d: PDoc; module: PSym): string =
  402. if optWholeProject in d.conf.globalOptions or d.conf.docRoot.len > 0:
  403. let full = AbsoluteFile toFullPath(d.conf, FileIndex module.position)
  404. let tmp = getOutFile2(d.conf, presentationPath(d.conf, full), HtmlExt, sfMainModule notin module.flags)
  405. result = relativeTo(tmp, d.thisDir, '/').string
  406. else:
  407. result = extractFilename toFullPath(d.conf, FileIndex module.position)
  408. proc nodeToHighlightedHtml(d: PDoc; n: PNode; result: var string;
  409. renderFlags: TRenderFlags = {};
  410. procLink: string) =
  411. var r: TSrcGen
  412. var literal = ""
  413. initTokRender(r, n, renderFlags)
  414. var kind = tkEof
  415. var tokenPos = 0
  416. var procTokenPos = 0
  417. template escLit(): untyped = esc(d.target, literal)
  418. while true:
  419. getNextTok(r, kind, literal)
  420. inc tokenPos
  421. case kind
  422. of tkEof:
  423. break
  424. of tkComment:
  425. dispA(d.conf, result, "<span class=\"Comment\">$1</span>", "\\spanComment{$1}",
  426. [escLit])
  427. of tokKeywordLow..tokKeywordHigh:
  428. if kind in {tkProc, tkMethod, tkIterator, tkMacro, tkTemplate, tkFunc, tkConverter}:
  429. procTokenPos = tokenPos
  430. dispA(d.conf, result, "<span class=\"Keyword\">$1</span>", "\\spanKeyword{$1}",
  431. [literal])
  432. of tkOpr:
  433. dispA(d.conf, result, "<span class=\"Operator\">$1</span>", "\\spanOperator{$1}",
  434. [escLit])
  435. of tkStrLit..tkTripleStrLit, tkCustomLit:
  436. dispA(d.conf, result, "<span class=\"StringLit\">$1</span>",
  437. "\\spanStringLit{$1}", [escLit])
  438. of tkCharLit:
  439. dispA(d.conf, result, "<span class=\"CharLit\">$1</span>", "\\spanCharLit{$1}",
  440. [escLit])
  441. of tkIntLit..tkUInt64Lit:
  442. dispA(d.conf, result, "<span class=\"DecNumber\">$1</span>",
  443. "\\spanDecNumber{$1}", [escLit])
  444. of tkFloatLit..tkFloat128Lit:
  445. dispA(d.conf, result, "<span class=\"FloatNumber\">$1</span>",
  446. "\\spanFloatNumber{$1}", [escLit])
  447. of tkSymbol:
  448. let s = getTokSym(r)
  449. # -2 because of the whitespace in between:
  450. if procTokenPos == tokenPos-2 and procLink != "":
  451. dispA(d.conf, result, "<a href=\"#$2\"><span class=\"Identifier\">$1</span></a>",
  452. "\\spanIdentifier{$1}", [escLit, procLink])
  453. elif s != nil and s.kind in {skType, skVar, skLet, skConst} and
  454. sfExported in s.flags and s.owner != nil and
  455. belongsToProjectPackage(d.conf, s.owner) and d.target == outHtml:
  456. let external = externalDep(d, s.owner)
  457. result.addf "<a href=\"$1#$2\"><span class=\"Identifier\">$3</span></a>",
  458. [changeFileExt(external, "html"), literal,
  459. escLit]
  460. else:
  461. dispA(d.conf, result, "<span class=\"Identifier\">$1</span>",
  462. "\\spanIdentifier{$1}", [escLit])
  463. of tkSpaces, tkInvalid:
  464. result.add(literal)
  465. of tkHideableStart:
  466. template fun(s) = dispA(d.conf, result, s, "\\spanOther{$1}", [escLit])
  467. if renderRunnableExamples in renderFlags: fun "$1"
  468. else:
  469. # 1st span is required for the JS to work properly
  470. fun """
  471. <span>
  472. <span class="Other pragmadots">...</span>
  473. </span>
  474. <span class="pragmawrap">""".replace("\n", "") # Must remove newlines because wrapped in a <pre>
  475. of tkHideableEnd:
  476. template fun(s) = dispA(d.conf, result, s, "\\spanOther{$1}", [escLit])
  477. if renderRunnableExamples in renderFlags: fun "$1"
  478. else: fun "</span>"
  479. of tkCurlyDotLe: dispA(d.conf, result, "$1", "\\spanOther{$1}", [escLit])
  480. of tkCurlyDotRi: dispA(d.conf, result, "$1", "\\spanOther{$1}", [escLit])
  481. of tkParLe, tkParRi, tkBracketLe, tkBracketRi, tkCurlyLe, tkCurlyRi,
  482. tkBracketDotLe, tkBracketDotRi, tkParDotLe,
  483. tkParDotRi, tkComma, tkSemiColon, tkColon, tkEquals, tkDot, tkDotDot,
  484. tkAccent, tkColonColon,
  485. tkGStrLit, tkGTripleStrLit, tkInfixOpr, tkPrefixOpr, tkPostfixOpr,
  486. tkBracketLeColon:
  487. dispA(d.conf, result, "<span class=\"Other\">$1</span>", "\\spanOther{$1}",
  488. [escLit])
  489. proc exampleOutputDir(d: PDoc): AbsoluteDir = d.conf.getNimcacheDir / RelativeDir"runnableExamples"
  490. proc runAllExamples(d: PDoc) =
  491. # This used to be: `let backend = if isDefined(d.conf, "js"): "js"` (etc), however
  492. # using `-d:js` (etc) cannot work properly, e.g. would fail with `importjs`
  493. # since semantics are affected by `config.backend`, not by isDefined(d.conf, "js")
  494. let outputDir = d.exampleOutputDir
  495. for _, group in d.exampleGroups:
  496. if group.docCmd == docCmdSkip: continue
  497. let outp = outputDir / RelativeFile("$1_group$2_examples.nim" % [d.filename.splitFile.name, $group.index])
  498. group.code = "# autogenerated by docgen\n# source: $1\n# rdoccmd: $2\n$3" % [d.filename, group.rdoccmd, group.code]
  499. writeFile(outp, group.code)
  500. # most useful semantics is that `docCmd` comes after `rdoccmd`, so that we can (temporarily) override
  501. # via command line
  502. # D20210224T221756:here
  503. let cmd = "$nim $backend -r --lib:$libpath --warning:UnusedImport:off --path:$path --nimcache:$nimcache $rdoccmd $docCmd $file" % [
  504. "nim", quoteShell(os.getAppFilename()),
  505. "backend", $d.conf.backend,
  506. "path", quoteShell(d.conf.projectPath),
  507. "libpath", quoteShell(d.conf.libpath),
  508. "nimcache", quoteShell(outputDir),
  509. "file", quoteShell(outp),
  510. "rdoccmd", group.rdoccmd,
  511. "docCmd", group.docCmd,
  512. ]
  513. if d.conf.backend == backendJs and findNodeJs() == "":
  514. discard "ignore JS runnableExample"
  515. elif os.execShellCmd(cmd) != 0:
  516. d.conf.quitOrRaise "[runnableExamples] failed: generated file: '$1' group: '$2' cmd: $3" % [outp.string, group[].prettyString, cmd]
  517. else:
  518. # keep generated source file `outp` to allow inspection.
  519. rawMessage(d.conf, hintSuccess, ["runnableExamples: " & outp.string])
  520. # removeFile(outp.changeFileExt(ExeExt)) # it's in nimcache, no need to remove
  521. proc quoted(a: string): string = result.addQuoted(a)
  522. proc toInstantiationInfo(conf: ConfigRef, info: TLineInfo): auto =
  523. # xxx expose in compiler/lineinfos.nim
  524. (conf.toMsgFilename(info), info.line.int, info.col.int + ColOffset)
  525. proc prepareExample(d: PDoc; n: PNode, topLevel: bool): tuple[rdoccmd: string, code: string] =
  526. ## returns `rdoccmd` and source code for this runnableExamples
  527. var rdoccmd = ""
  528. if n.len < 2 or n.len > 3: globalError(d.conf, n.info, "runnableExamples invalid")
  529. if n.len == 3:
  530. let n1 = n[1]
  531. # xxx this should be evaluated during sempass
  532. if n1.kind notin nkStrKinds: globalError(d.conf, n1.info, "string litteral expected")
  533. rdoccmd = n1.strVal
  534. let useRenderModule = false
  535. let loc = d.conf.toFileLineCol(n.info)
  536. let code = extractRunnableExamplesSource(d.conf, n)
  537. let codeIndent = extractRunnableExamplesSource(d.conf, n, indent = 2)
  538. if d.conf.errorCounter > 0:
  539. return (rdoccmd, code)
  540. let comment = "autogenerated by docgen\nloc: $1\nrdoccmd: $2" % [loc, rdoccmd]
  541. let outputDir = d.exampleOutputDir
  542. createDir(outputDir)
  543. inc d.exampleCounter
  544. let outp = outputDir / RelativeFile("$#_examples_$#.nim" % [d.filename.extractFilename.changeFileExt"", $d.exampleCounter])
  545. if useRenderModule:
  546. var docComment = newTree(nkCommentStmt)
  547. docComment.comment = comment
  548. var runnableExamples = newTree(nkStmtList,
  549. docComment,
  550. newTree(nkImportStmt, newStrNode(nkStrLit, d.filename)))
  551. runnableExamples.info = n.info
  552. for a in n.lastSon: runnableExamples.add a
  553. # buggy, refs bug #17292
  554. # still worth fixing as it can affect other code relying on `renderModule`,
  555. # so we keep this code path here for now, which could still be useful in some
  556. # other situations.
  557. renderModule(runnableExamples, outp.string, conf = d.conf)
  558. else:
  559. var code2 = code
  560. if code.len > 0 and "codeReordering" notin code:
  561. # hacky but simplest solution, until we devise a way to make `{.line.}`
  562. # work without introducing a scope
  563. code2 = """
  564. {.line: $#.}:
  565. $#
  566. """ % [$toInstantiationInfo(d.conf, n.info), codeIndent]
  567. code2 = """
  568. #[
  569. $#
  570. ]#
  571. import $#
  572. $#
  573. """ % [comment, d.filename.quoted, code2]
  574. writeFile(outp.string, code2)
  575. if rdoccmd notin d.exampleGroups:
  576. d.exampleGroups[rdoccmd] = ExampleGroup(rdoccmd: rdoccmd, docCmd: d.conf.docCmd, index: d.exampleGroups.len)
  577. d.exampleGroups[rdoccmd].code.add "import $1\n" % outp.string.quoted
  578. var codeShown: string
  579. if topLevel: # refs https://github.com/nim-lang/RFCs/issues/352
  580. let title = canonicalImport(d.conf, AbsoluteFile d.filename)
  581. codeShown = "import $#\n$#" % [title, code]
  582. else:
  583. codeShown = code
  584. result = (rdoccmd, codeShown)
  585. when false:
  586. proc extractImports(n: PNode; result: PNode) =
  587. if n.kind in {nkImportStmt, nkImportExceptStmt, nkFromStmt}:
  588. result.add copyTree(n)
  589. n.kind = nkEmpty
  590. return
  591. for i in 0..<n.safeLen: extractImports(n[i], result)
  592. let imports = newTree(nkStmtList)
  593. var savedLastSon = copyTree n.lastSon
  594. extractImports(savedLastSon, imports)
  595. for imp in imports: runnableExamples.add imp
  596. runnableExamples.add newTree(nkBlockStmt, newNode(nkEmpty), copyTree savedLastSon)
  597. type RunnableState = enum
  598. rsStart
  599. rsComment
  600. rsRunnable
  601. rsDone
  602. proc getAllRunnableExamplesImpl(d: PDoc; n: PNode, dest: var ItemPre,
  603. state: RunnableState, topLevel: bool):
  604. RunnableState =
  605. ##[
  606. Simple state machine to tell whether we render runnableExamples and doc comments.
  607. This is to ensure that we can interleave runnableExamples and doc comments freely;
  608. the logic is easy to change but currently a doc comment following another doc comment
  609. will not render, to avoid rendering in following case:
  610. proc fn* =
  611. runnableExamples: discard
  612. ## d1
  613. runnableExamples: discard
  614. ## d2
  615. ## internal explanation # <- this one should be out; it's part of rest of function body and would likey not make sense in doc comment
  616. discard # some code
  617. ]##
  618. case n.kind
  619. of nkCommentStmt:
  620. if state in {rsStart, rsRunnable}:
  621. dest.add genRecComment(d, n)
  622. return rsComment
  623. of nkCallKinds:
  624. if isRunnableExamples(n[0]) and
  625. n.len >= 2 and n.lastSon.kind == nkStmtList:
  626. if state in {rsStart, rsComment, rsRunnable}:
  627. let (rdoccmd, code) = prepareExample(d, n, topLevel)
  628. var msg = "Example:"
  629. if rdoccmd.len > 0: msg.add " cmd: " & rdoccmd
  630. var s: string
  631. dispA(d.conf, s, "\n<p><strong class=\"examples_text\">$1</strong></p>\n",
  632. "\n\n\\textbf{$1}\n", [msg])
  633. dest.add s
  634. inc d.listingCounter
  635. let id = $d.listingCounter
  636. dest.add(d.config.getOrDefault"doc.listing_start" % [id, "langNim", ""])
  637. var dest2 = ""
  638. renderNimCode(dest2, code, d.target)
  639. dest.add dest2
  640. dest.add(d.config.getOrDefault"doc.listing_end" % id)
  641. return rsRunnable
  642. else:
  643. localError(d.conf, n.info, errUser, "runnableExamples must appear before the first non-comment statement")
  644. else: discard
  645. return rsDone
  646. # change this to `rsStart` if you want to keep generating doc comments
  647. # and runnableExamples that occur after some code in routine
  648. proc getRoutineBody(n: PNode): PNode =
  649. ##[
  650. nim transforms these quite differently:
  651. proc someType*(): int =
  652. ## foo
  653. result = 3
  654. =>
  655. result =
  656. ## foo
  657. 3;
  658. proc someType*(): int =
  659. ## foo
  660. 3
  661. =>
  662. ## foo
  663. result = 3;
  664. so we normalize the results to get to the statement list containing the
  665. (0 or more) doc comments and runnableExamples.
  666. ]##
  667. result = n[bodyPos]
  668. # This won't be transformed: result.id = 10. Namely result[0].kind != nkSym.
  669. if result.kind == nkAsgn and result[0].kind == nkSym and
  670. n.len > bodyPos+1 and n[bodyPos+1].kind == nkSym:
  671. doAssert result.len == 2
  672. result = result[1]
  673. proc getAllRunnableExamples(d: PDoc, n: PNode, dest: var ItemPre) =
  674. var n = n
  675. var state = rsStart
  676. template fn(n2, topLevel) =
  677. state = getAllRunnableExamplesImpl(d, n2, dest, state, topLevel)
  678. dest.add genComment(d, n)
  679. case n.kind
  680. of routineDefs:
  681. n = n.getRoutineBody
  682. case n.kind
  683. of nkCommentStmt, nkCallKinds: fn(n, topLevel = false)
  684. else:
  685. for i in 0..<n.safeLen:
  686. fn(n[i], topLevel = false)
  687. if state == rsDone: discard # check all sons
  688. else: fn(n, topLevel = true)
  689. proc isVisible(d: PDoc; n: PNode): bool =
  690. result = false
  691. if n.kind == nkPostfix:
  692. if n.len == 2 and n[0].kind == nkIdent:
  693. var v = n[0].ident
  694. result = v.id == ord(wStar) or v.id == ord(wMinus)
  695. elif n.kind == nkSym:
  696. # we cannot generate code for forwarded symbols here as we have no
  697. # exception tracking information here. Instead we copy over the comment
  698. # from the proc header.
  699. if optDocInternal in d.conf.globalOptions:
  700. result = {sfFromGeneric, sfForward}*n.sym.flags == {}
  701. else:
  702. result = {sfExported, sfFromGeneric, sfForward}*n.sym.flags == {sfExported}
  703. if result and containsOrIncl(d.emitted, n.sym.id):
  704. result = false
  705. elif n.kind == nkPragmaExpr:
  706. result = isVisible(d, n[0])
  707. proc getName(d: PDoc, n: PNode, splitAfter = -1): string =
  708. case n.kind
  709. of nkPostfix: result = getName(d, n[1], splitAfter)
  710. of nkPragmaExpr: result = getName(d, n[0], splitAfter)
  711. of nkSym: result = esc(d.target, n.sym.renderDefinitionName, splitAfter)
  712. of nkIdent: result = esc(d.target, n.ident.s, splitAfter)
  713. of nkAccQuoted:
  714. result = esc(d.target, "`")
  715. for i in 0..<n.len: result.add(getName(d, n[i], splitAfter))
  716. result.add esc(d.target, "`")
  717. of nkOpenSymChoice, nkClosedSymChoice:
  718. result = getName(d, n[0], splitAfter)
  719. else:
  720. result = ""
  721. proc getNameIdent(cache: IdentCache; n: PNode): PIdent =
  722. case n.kind
  723. of nkPostfix: result = getNameIdent(cache, n[1])
  724. of nkPragmaExpr: result = getNameIdent(cache, n[0])
  725. of nkSym: result = n.sym.name
  726. of nkIdent: result = n.ident
  727. of nkAccQuoted:
  728. var r = ""
  729. for i in 0..<n.len: r.add(getNameIdent(cache, n[i]).s)
  730. result = getIdent(cache, r)
  731. of nkOpenSymChoice, nkClosedSymChoice:
  732. result = getNameIdent(cache, n[0])
  733. else:
  734. result = nil
  735. proc getRstName(n: PNode): PRstNode =
  736. case n.kind
  737. of nkPostfix: result = getRstName(n[1])
  738. of nkPragmaExpr: result = getRstName(n[0])
  739. of nkSym: result = newRstLeaf(n.sym.renderDefinitionName)
  740. of nkIdent: result = newRstLeaf(n.ident.s)
  741. of nkAccQuoted:
  742. result = getRstName(n[0])
  743. for i in 1..<n.len: result.text.add(getRstName(n[i]).text)
  744. of nkOpenSymChoice, nkClosedSymChoice:
  745. result = getRstName(n[0])
  746. else:
  747. result = nil
  748. proc newUniquePlainSymbol(d: PDoc, original: string): string =
  749. ## Returns a new unique plain symbol made up from the original.
  750. ##
  751. ## When a collision is found in the seenSymbols table, new numerical variants
  752. ## with underscore + number will be generated.
  753. if not d.seenSymbols.hasKey(original):
  754. result = original
  755. d.seenSymbols[original] = ""
  756. return
  757. # Iterate over possible numeric variants of the original name.
  758. var count = 2
  759. while true:
  760. result = original & "_" & $count
  761. if not d.seenSymbols.hasKey(result):
  762. d.seenSymbols[result] = ""
  763. break
  764. count += 1
  765. proc complexName(k: TSymKind, n: PNode, baseName: string): string =
  766. ## Builds a complex unique href name for the node.
  767. ##
  768. ## Pass as ``baseName`` the plain symbol obtained from the nodeName. The
  769. ## format of the returned symbol will be ``baseName(.callable type)?,(param
  770. ## type)?(,param type)*``. The callable type part will be added only if the
  771. ## node is not a proc, as those are the common ones. The suffix will be a dot
  772. ## and a single letter representing the type of the callable. The parameter
  773. ## types will be added with a preceding dash. Return types won't be added.
  774. ##
  775. ## If you modify the output of this proc, please update the anchor generation
  776. ## section of ``doc/docgen.rst``.
  777. result = baseName
  778. case k
  779. of skProc, skFunc: discard
  780. of skMacro: result.add(".m")
  781. of skMethod: result.add(".e")
  782. of skIterator: result.add(".i")
  783. of skTemplate: result.add(".t")
  784. of skConverter: result.add(".c")
  785. else: discard
  786. if n.safeLen > paramsPos and n[paramsPos].kind == nkFormalParams:
  787. let params = renderParamTypes(n[paramsPos])
  788. if params.len > 0:
  789. result.add(defaultParamSeparator)
  790. result.add(params)
  791. proc docstringSummary(rstText: string): string =
  792. ## Returns just the first line or a brief chunk of text from a rst string.
  793. ##
  794. ## Most docstrings will contain a one liner summary, so stripping at the
  795. ## first newline is usually fine. If after that the content is still too big,
  796. ## it is stripped at the first comma, colon or dot, usual English sentence
  797. ## separators.
  798. ##
  799. ## No guarantees are made on the size of the output, but it should be small.
  800. ## Also, we hope to not break the rst, but maybe we do. If there is any
  801. ## trimming done, an ellipsis unicode char is added.
  802. const maxDocstringChars = 100
  803. assert(rstText.len < 2 or (rstText[0] == '#' and rstText[1] == '#'))
  804. result = rstText.substr(2).strip
  805. var pos = result.find('\L')
  806. if pos > 0:
  807. result.setLen(pos - 1)
  808. result.add("…")
  809. if pos < maxDocstringChars:
  810. return
  811. # Try to keep trimming at other natural boundaries.
  812. pos = result.find({'.', ',', ':'})
  813. let last = result.len - 1
  814. if pos > 0 and pos < last:
  815. result.setLen(pos - 1)
  816. result.add("…")
  817. proc genDeprecationMsg(d: PDoc, n: PNode): string =
  818. ## Given a nkPragma wDeprecated node output a well-formatted section
  819. if n == nil: return
  820. case n.safeLen:
  821. of 0: # Deprecated w/o any message
  822. result = getConfigVar(d.conf, "doc.deprecationmsg") % [
  823. "label" , "Deprecated", "message", ""]
  824. of 2: # Deprecated w/ a message
  825. if n[1].kind in {nkStrLit..nkTripleStrLit}:
  826. result = getConfigVar(d.conf, "doc.deprecationmsg") % [
  827. "label", "Deprecated:", "message", xmltree.escape(n[1].strVal)]
  828. else:
  829. doAssert false
  830. type DocFlags = enum
  831. kDefault
  832. kForceExport
  833. proc genSeeSrc(d: PDoc, path: string, line: int): string =
  834. let docItemSeeSrc = getConfigVar(d.conf, "doc.item.seesrc")
  835. if docItemSeeSrc.len > 0:
  836. let path = relativeTo(AbsoluteFile path, AbsoluteDir getCurrentDir(), '/')
  837. when false:
  838. let cwd = canonicalizePath(d.conf, getCurrentDir())
  839. var path = path
  840. if path.startsWith(cwd):
  841. path = path[cwd.len+1..^1].replace('\\', '/')
  842. let gitUrl = getConfigVar(d.conf, "git.url")
  843. if gitUrl.len > 0:
  844. let defaultBranch =
  845. if NimPatch mod 2 == 1: "devel"
  846. else: "version-$1-$2" % [$NimMajor, $NimMinor]
  847. let commit = getConfigVar(d.conf, "git.commit", defaultBranch)
  848. let develBranch = getConfigVar(d.conf, "git.devel", "devel")
  849. dispA(d.conf, result, "$1", "", [docItemSeeSrc % [
  850. "path", path.string, "line", $line, "url", gitUrl,
  851. "commit", commit, "devel", develBranch]])
  852. proc symbolPriority(k: TSymKind): int =
  853. result = case k
  854. of skMacro: -3
  855. of skTemplate: -2
  856. of skIterator: -1
  857. else: 0 # including skProc which have higher priority
  858. # documentation itself has even higher priority 1
  859. proc toLangSymbol(k: TSymKind, n: PNode, baseName: string): LangSymbol =
  860. ## Converts symbol info (names/types/parameters) in `n` into format
  861. ## `LangSymbol` convenient for ``rst.nim``/``dochelpers.nim``.
  862. result.name = baseName.nimIdentNormalize
  863. result.symKind = k.toHumanStr
  864. if k in routineKinds:
  865. var
  866. paramTypes: seq[string]
  867. renderParamTypes(paramTypes, n[paramsPos], toNormalize=true)
  868. let paramNames = renderParamNames(n[paramsPos], toNormalize=true)
  869. # In some rare cases (system.typeof) parameter type is not set for default:
  870. doAssert paramTypes.len <= paramNames.len
  871. for i in 0 ..< paramNames.len:
  872. if i < paramTypes.len:
  873. result.parameters.add (paramNames[i], paramTypes[i])
  874. else:
  875. result.parameters.add (paramNames[i], "")
  876. result.parametersProvided = true
  877. result.outType = renderOutType(n[paramsPos], toNormalize=true)
  878. if k in {skProc, skFunc, skType, skIterator}:
  879. # Obtain `result.generics`
  880. # Use `n[miscPos]` since n[genericParamsPos] does not contain constraints
  881. var genNode: PNode = nil
  882. if k == skType:
  883. genNode = n[1] # FIXME: what is index 1?
  884. else:
  885. if n[miscPos].kind != nkEmpty:
  886. genNode = n[miscPos][1] # FIXME: what is index 1?
  887. if genNode != nil:
  888. var literal = ""
  889. var r: TSrcGen
  890. initTokRender(r, genNode, {renderNoBody, renderNoComments,
  891. renderNoPragmas, renderNoProcDefs})
  892. var kind = tkEof
  893. while true:
  894. getNextTok(r, kind, literal)
  895. if kind == tkEof:
  896. break
  897. if kind != tkSpaces:
  898. result.generics.add(literal.nimIdentNormalize)
  899. if k == skType:
  900. case n[2].kind
  901. of nkEnumTy: result.symTypeKind = "enum"
  902. of nkObjectTy: result.symTypeKind = "object"
  903. of nkTupleTy: result.symTypeKind = "tuple"
  904. else: discard
  905. proc genItem(d: PDoc, n, nameNode: PNode, k: TSymKind, docFlags: DocFlags) =
  906. if (docFlags != kForceExport) and not isVisible(d, nameNode): return
  907. let
  908. name = getName(d, nameNode)
  909. var plainDocstring = getPlainDocstring(n) # call here before genRecComment!
  910. var result = ""
  911. var literal, plainName = ""
  912. var kind = tkEof
  913. var comm: ItemPre
  914. if n.kind in routineDefs:
  915. getAllRunnableExamples(d, n, comm)
  916. else:
  917. comm.add genRecComment(d, n)
  918. var r: TSrcGen
  919. # Obtain the plain rendered string for hyperlink titles.
  920. initTokRender(r, n, {renderNoBody, renderNoComments, renderDocComments,
  921. renderNoPragmas, renderNoProcDefs})
  922. while true:
  923. getNextTok(r, kind, literal)
  924. if kind == tkEof:
  925. break
  926. plainName.add(literal)
  927. var pragmaNode = getDeclPragma(n)
  928. if pragmaNode != nil: pragmaNode = findPragma(pragmaNode, wDeprecated)
  929. inc(d.id)
  930. let
  931. plainNameEsc = esc(d.target, plainName.strip)
  932. detailedName = k.toHumanStr & " " & (
  933. if k in routineKinds: plainName else: name)
  934. uniqueName = if k in routineKinds: plainNameEsc else: name
  935. sortName = if k in routineKinds: plainName.strip else: name
  936. cleanPlainSymbol = renderPlainSymbolName(nameNode)
  937. complexSymbol = complexName(k, n, cleanPlainSymbol)
  938. plainSymbolEnc = encodeUrl(cleanPlainSymbol, usePlus = false)
  939. symbolOrId = d.newUniquePlainSymbol(complexSymbol)
  940. symbolOrIdEnc = encodeUrl(symbolOrId, usePlus = false)
  941. deprecationMsg = genDeprecationMsg(d, pragmaNode)
  942. rstLangSymbol = toLangSymbol(k, n, cleanPlainSymbol)
  943. # we generate anchors automatically for subsequent use in doc comments
  944. let lineinfo = rstast.TLineInfo(
  945. line: nameNode.info.line, col: nameNode.info.col,
  946. fileIndex: addRstFileIndex(d, nameNode.info))
  947. addAnchorNim(d.sharedState, refn = symbolOrId, tooltip = detailedName,
  948. rstLangSymbol, priority = symbolPriority(k), info = lineinfo)
  949. nodeToHighlightedHtml(d, n, result, {renderNoBody, renderNoComments,
  950. renderDocComments, renderSyms}, symbolOrIdEnc)
  951. let seeSrc = genSeeSrc(d, toFullPath(d.conf, n.info), n.info.line.int)
  952. d.section[k].secItems.mgetOrPut(cleanPlainSymbol, newSeq[Item]()).add Item(
  953. descRst: comm,
  954. sortName: sortName,
  955. info: lineinfo,
  956. anchor: symbolOrId,
  957. detailedName: detailedName,
  958. name: name,
  959. substitutions: @[
  960. "uniqueName", uniqueName,
  961. "header", result, "itemID", $d.id,
  962. "header_plain", plainNameEsc, "itemSym", cleanPlainSymbol,
  963. "itemSymEnc", plainSymbolEnc,
  964. "itemSymOrIDEnc", symbolOrIdEnc, "seeSrc", seeSrc,
  965. "deprecationMsg", deprecationMsg])
  966. let external = d.destFile.AbsoluteFile.relativeTo(d.conf.outDir, '/').changeFileExt(HtmlExt).string
  967. var attype = ""
  968. if k in routineKinds and nameNode.kind == nkSym:
  969. let att = attachToType(d, nameNode.sym)
  970. if att != nil:
  971. attype = esc(d.target, att.name.s)
  972. elif k == skType and nameNode.kind == nkSym and nameNode.sym.typ.kind in {tyEnum, tyBool}:
  973. let etyp = nameNode.sym.typ
  974. for e in etyp.n:
  975. if e.sym.kind != skEnumField: continue
  976. let plain = renderPlainSymbolName(e)
  977. let symbolOrId = d.newUniquePlainSymbol(plain)
  978. setIndexTerm(d[], external, symbolOrId, plain, nameNode.sym.name.s & '.' & plain,
  979. xmltree.escape(getPlainDocstring(e).docstringSummary))
  980. d.tocSimple[k].add TocItem(
  981. sortName: sortName,
  982. content: getConfigVar(d.conf, "doc.item.toc") % [
  983. "name", name, "header_plain", plainNameEsc,
  984. "itemSymOrIDEnc", symbolOrIdEnc])
  985. d.tocTable[k].mgetOrPut(cleanPlainSymbol, newSeq[TocItem]()).add TocItem(
  986. sortName: sortName,
  987. content: getConfigVar(d.conf, "doc.item.tocTable") % [
  988. "name", name, "header_plain", plainNameEsc,
  989. "itemSymOrID", symbolOrId.replace(",", ",<wbr>"),
  990. "itemSymOrIDEnc", symbolOrIdEnc])
  991. # Ironically for types the complexSymbol is *cleaner* than the plainName
  992. # because it doesn't include object fields or documentation comments. So we
  993. # use the plain one for callable elements, and the complex for the rest.
  994. var linkTitle = changeFileExt(extractFilename(d.filename), "") & ": "
  995. if n.kind in routineDefs: linkTitle.add(xmltree.escape(plainName.strip))
  996. else: linkTitle.add(xmltree.escape(complexSymbol.strip))
  997. setIndexTerm(d[], external, symbolOrId, name, linkTitle,
  998. xmltree.escape(plainDocstring.docstringSummary))
  999. if k == skType and nameNode.kind == nkSym:
  1000. d.types.strTableAdd nameNode.sym
  1001. proc genJsonItem(d: PDoc, n, nameNode: PNode, k: TSymKind): JsonItem =
  1002. if not isVisible(d, nameNode): return
  1003. var
  1004. name = getName(d, nameNode)
  1005. comm = genRecComment(d, n)
  1006. r: TSrcGen
  1007. initTokRender(r, n, {renderNoBody, renderNoComments, renderDocComments})
  1008. result.json = %{ "name": %name, "type": %($k), "line": %n.info.line.int,
  1009. "col": %n.info.col}
  1010. if comm != nil:
  1011. result.rst = comm
  1012. result.rstField = "description"
  1013. if r.buf.len > 0:
  1014. result.json["code"] = %r.buf
  1015. if k in routineKinds:
  1016. result.json["signature"] = newJObject()
  1017. if n[paramsPos][0].kind != nkEmpty:
  1018. result.json["signature"]["return"] = %($n[paramsPos][0])
  1019. if n[paramsPos].len > 1:
  1020. result.json["signature"]["arguments"] = newJArray()
  1021. for paramIdx in 1 ..< n[paramsPos].len:
  1022. for identIdx in 0 ..< n[paramsPos][paramIdx].len - 2:
  1023. let
  1024. paramName = $n[paramsPos][paramIdx][identIdx]
  1025. paramType = $n[paramsPos][paramIdx][^2]
  1026. if n[paramsPos][paramIdx][^1].kind != nkEmpty:
  1027. let paramDefault = $n[paramsPos][paramIdx][^1]
  1028. result.json["signature"]["arguments"].add %{"name": %paramName, "type": %paramType, "default": %paramDefault}
  1029. else:
  1030. result.json["signature"]["arguments"].add %{"name": %paramName, "type": %paramType}
  1031. if n[pragmasPos].kind != nkEmpty:
  1032. result.json["signature"]["pragmas"] = newJArray()
  1033. for pragma in n[pragmasPos]:
  1034. result.json["signature"]["pragmas"].add %($pragma)
  1035. if n[genericParamsPos].kind != nkEmpty:
  1036. result.json["signature"]["genericParams"] = newJArray()
  1037. for genericParam in n[genericParamsPos]:
  1038. var param = %{"name": %($genericParam)}
  1039. if genericParam.sym.typ.sons.len > 0:
  1040. param["types"] = newJArray()
  1041. for kind in genericParam.sym.typ.sons:
  1042. param["types"].add %($kind)
  1043. result.json["signature"]["genericParams"].add param
  1044. if optGenIndex in d.conf.globalOptions:
  1045. genItem(d, n, nameNode, k, kForceExport)
  1046. proc setDoctype(d: PDoc, n: PNode) =
  1047. ## Processes `{.doctype.}` pragma changing Markdown/RST parsing options.
  1048. if n == nil:
  1049. return
  1050. if n.len != 2:
  1051. localError(d.conf, n.info, errUser,
  1052. "doctype pragma takes exactly 1 argument"
  1053. )
  1054. return
  1055. var dt = ""
  1056. case n[1].kind
  1057. of nkStrLit:
  1058. dt = toLowerAscii(n[1].strVal)
  1059. of nkIdent:
  1060. dt = toLowerAscii(n[1].ident.s)
  1061. else:
  1062. localError(d.conf, n.info, errUser,
  1063. "unknown argument type $1 provided to doctype" % [$n[1].kind]
  1064. )
  1065. return
  1066. case dt
  1067. of "markdown":
  1068. d.sharedState.options.incl roSupportMarkdown
  1069. d.sharedState.options.incl roPreferMarkdown
  1070. of "rstmarkdown":
  1071. d.sharedState.options.incl roSupportMarkdown
  1072. d.sharedState.options.excl roPreferMarkdown
  1073. of "rst":
  1074. d.sharedState.options.excl roSupportMarkdown
  1075. d.sharedState.options.excl roPreferMarkdown
  1076. else:
  1077. localError(d.conf, n.info, errUser,
  1078. (
  1079. "unknown doctype value \"$1\", should be from " &
  1080. "\"RST\", \"Markdown\", \"RstMarkdown\""
  1081. ) % [dt]
  1082. )
  1083. proc checkForFalse(n: PNode): bool =
  1084. result = n.kind == nkIdent and cmpIgnoreStyle(n.ident.s, "false") == 0
  1085. proc traceDeps(d: PDoc, it: PNode) =
  1086. const k = skModule
  1087. if it.kind == nkInfix and it.len == 3 and it[2].kind == nkBracket:
  1088. let sep = it[0]
  1089. let dir = it[1]
  1090. let a = newNodeI(nkInfix, it.info)
  1091. a.add sep
  1092. a.add dir
  1093. a.add sep # dummy entry, replaced in the loop
  1094. for x in it[2]:
  1095. a[2] = x
  1096. traceDeps(d, a)
  1097. elif it.kind == nkSym and belongsToProjectPackage(d.conf, it.sym):
  1098. let external = externalDep(d, it.sym)
  1099. if d.section[k].finalMarkup != "": d.section[k].finalMarkup.add(", ")
  1100. dispA(d.conf, d.section[k].finalMarkup,
  1101. "<a class=\"reference external\" href=\"$2\">$1</a>",
  1102. "$1", [esc(d.target, external.prettyLink),
  1103. changeFileExt(external, "html")])
  1104. proc exportSym(d: PDoc; s: PSym) =
  1105. const k = exportSection
  1106. if s.kind == skModule and belongsToProjectPackage(d.conf, s):
  1107. let external = externalDep(d, s)
  1108. if d.section[k].finalMarkup != "": d.section[k].finalMarkup.add(", ")
  1109. dispA(d.conf, d.section[k].finalMarkup,
  1110. "<a class=\"reference external\" href=\"$2\">$1</a>",
  1111. "$1", [esc(d.target, external.prettyLink),
  1112. changeFileExt(external, "html")])
  1113. elif s.kind != skModule and s.owner != nil:
  1114. let module = originatingModule(s)
  1115. if belongsToProjectPackage(d.conf, module):
  1116. let
  1117. complexSymbol = complexName(s.kind, s.ast, s.name.s)
  1118. symbolOrId = d.newUniquePlainSymbol(complexSymbol)
  1119. external = externalDep(d, module)
  1120. if d.section[k].finalMarkup != "": d.section[k].finalMarkup.add(", ")
  1121. # XXX proper anchor generation here
  1122. dispA(d.conf, d.section[k].finalMarkup,
  1123. "<a href=\"$2#$3\"><span class=\"Identifier\">$1</span></a>",
  1124. "$1", [esc(d.target, s.name.s),
  1125. changeFileExt(external, "html"),
  1126. symbolOrId])
  1127. proc documentNewEffect(cache: IdentCache; n: PNode): PNode =
  1128. let s = n[namePos].sym
  1129. if tfReturnsNew in s.typ.flags:
  1130. result = newIdentNode(getIdent(cache, "new"), n.info)
  1131. proc documentEffect(cache: IdentCache; n, x: PNode, effectType: TSpecialWord, idx: int): PNode =
  1132. let spec = effectSpec(x, effectType)
  1133. if isNil(spec):
  1134. let s = n[namePos].sym
  1135. let actual = s.typ.n[0]
  1136. if actual.len != effectListLen: return
  1137. let real = actual[idx]
  1138. if real == nil: return
  1139. let realLen = real.len
  1140. # warning: hack ahead:
  1141. var effects = newNodeI(nkBracket, n.info, realLen)
  1142. for i in 0..<realLen:
  1143. var t = typeToString(real[i].typ)
  1144. if t.startsWith("ref "): t = substr(t, 4)
  1145. effects[i] = newIdentNode(getIdent(cache, t), n.info)
  1146. # set the type so that the following analysis doesn't screw up:
  1147. effects[i].typ = real[i].typ
  1148. result = newTreeI(nkExprColonExpr, n.info,
  1149. newIdentNode(getIdent(cache, $effectType), n.info), effects)
  1150. proc documentWriteEffect(cache: IdentCache; n: PNode; flag: TSymFlag; pragmaName: string): PNode =
  1151. let s = n[namePos].sym
  1152. let params = s.typ.n
  1153. var effects = newNodeI(nkBracket, n.info)
  1154. for i in 1..<params.len:
  1155. if params[i].kind == nkSym and flag in params[i].sym.flags:
  1156. effects.add params[i]
  1157. if effects.len > 0:
  1158. result = newTreeI(nkExprColonExpr, n.info,
  1159. newIdentNode(getIdent(cache, pragmaName), n.info), effects)
  1160. proc documentRaises*(cache: IdentCache; n: PNode) =
  1161. if n[namePos].kind != nkSym: return
  1162. let pragmas = n[pragmasPos]
  1163. let p1 = documentEffect(cache, n, pragmas, wRaises, exceptionEffects)
  1164. let p2 = documentEffect(cache, n, pragmas, wTags, tagEffects)
  1165. let p3 = documentWriteEffect(cache, n, sfWrittenTo, "writes")
  1166. let p4 = documentNewEffect(cache, n)
  1167. let p5 = documentWriteEffect(cache, n, sfEscapes, "escapes")
  1168. let p6 = documentEffect(cache, n, pragmas, wForbids, forbiddenEffects)
  1169. if p1 != nil or p2 != nil or p3 != nil or p4 != nil or p5 != nil or p6 != nil:
  1170. if pragmas.kind == nkEmpty:
  1171. n[pragmasPos] = newNodeI(nkPragma, n.info)
  1172. if p1 != nil: n[pragmasPos].add p1
  1173. if p2 != nil: n[pragmasPos].add p2
  1174. if p3 != nil: n[pragmasPos].add p3
  1175. if p4 != nil: n[pragmasPos].add p4
  1176. if p5 != nil: n[pragmasPos].add p5
  1177. if p6 != nil: n[pragmasPos].add p6
  1178. proc generateDoc*(d: PDoc, n, orig: PNode, docFlags: DocFlags = kDefault) =
  1179. ## Goes through nim nodes recursively and collects doc comments.
  1180. ## Main function for `doc`:option: command,
  1181. ## which is implemented in ``docgen2.nim``.
  1182. template genItemAux(skind) =
  1183. genItem(d, n, n[namePos], skind, docFlags)
  1184. case n.kind
  1185. of nkPragma:
  1186. let pragmaNode = findPragma(n, wDeprecated)
  1187. d.modDeprecationMsg.add(genDeprecationMsg(d, pragmaNode))
  1188. let doctypeNode = findPragma(n, wDoctype)
  1189. setDoctype(d, doctypeNode)
  1190. of nkCommentStmt: d.modDescPre.add(genComment(d, n))
  1191. of nkProcDef, nkFuncDef:
  1192. when useEffectSystem: documentRaises(d.cache, n)
  1193. genItemAux(skProc)
  1194. of nkMethodDef:
  1195. when useEffectSystem: documentRaises(d.cache, n)
  1196. genItemAux(skMethod)
  1197. of nkIteratorDef:
  1198. when useEffectSystem: documentRaises(d.cache, n)
  1199. genItemAux(skIterator)
  1200. of nkMacroDef: genItemAux(skMacro)
  1201. of nkTemplateDef: genItemAux(skTemplate)
  1202. of nkConverterDef:
  1203. when useEffectSystem: documentRaises(d.cache, n)
  1204. genItemAux(skConverter)
  1205. of nkTypeSection, nkVarSection, nkLetSection, nkConstSection:
  1206. for i in 0..<n.len:
  1207. if n[i].kind != nkCommentStmt:
  1208. # order is always 'type var let const':
  1209. genItem(d, n[i], n[i][0],
  1210. succ(skType, ord(n.kind)-ord(nkTypeSection)), docFlags)
  1211. of nkStmtList:
  1212. for i in 0..<n.len: generateDoc(d, n[i], orig)
  1213. of nkWhenStmt:
  1214. # generate documentation for the first branch only:
  1215. if not checkForFalse(n[0][0]):
  1216. generateDoc(d, lastSon(n[0]), orig)
  1217. of nkImportStmt:
  1218. for it in n: traceDeps(d, it)
  1219. of nkExportStmt:
  1220. for it in n:
  1221. if it.kind == nkSym:
  1222. if d.module != nil and d.module == it.sym.owner:
  1223. generateDoc(d, it.sym.ast, orig, kForceExport)
  1224. elif it.sym.ast != nil:
  1225. exportSym(d, it.sym)
  1226. of nkExportExceptStmt: discard "transformed into nkExportStmt by semExportExcept"
  1227. of nkFromStmt, nkImportExceptStmt: traceDeps(d, n[0])
  1228. of nkCallKinds:
  1229. var comm: ItemPre
  1230. getAllRunnableExamples(d, n, comm)
  1231. if comm.len != 0: d.modDescPre.add(comm)
  1232. else: discard
  1233. proc overloadGroupName(s: string, k: TSymKind): string =
  1234. ## Turns a name like `f` into anchor `f-procs-all`
  1235. #s & " " & k.toHumanStr & "s all"
  1236. s & "-" & k.toHumanStr & "s-all"
  1237. proc finishGenerateDoc*(d: var PDoc) =
  1238. ## Perform 2nd RST pass for resolution of links/footnotes/headings...
  1239. # copy file map `filenames` to ``rstgen.nim`` for its warnings
  1240. d.filenames = d.sharedState.filenames
  1241. # Main title/subtitle are allowed only in the first RST fragment of document
  1242. var firstRst = PRstNode(nil)
  1243. for fragment in d.modDescPre:
  1244. if fragment.isRst:
  1245. firstRst = fragment.rst
  1246. break
  1247. d.hasToc = d.hasToc or d.sharedState.hasToc
  1248. preparePass2(d.sharedState, firstRst)
  1249. # add anchors to overload groups before RST resolution
  1250. for k in TSymKind:
  1251. if k in routineKinds:
  1252. for plainName, overloadChoices in d.section[k].secItems:
  1253. if overloadChoices.len > 1:
  1254. let refn = overloadGroupName(plainName, k)
  1255. let tooltip = "$1 ($2 overloads)" % [
  1256. k.toHumanStr & " " & plainName, $overloadChoices.len]
  1257. addAnchorNim(d.sharedState, refn, tooltip,
  1258. LangSymbol(symKind: k.toHumanStr,
  1259. name: nimIdentBackticksNormalize(plainName),
  1260. isGroup: true),
  1261. priority = symbolPriority(k),
  1262. # select index `0` just to have any meaningful warning:
  1263. info = overloadChoices[0].info)
  1264. # Finalize fragments of ``.nim`` or ``.rst`` file
  1265. proc renderItemPre(d: PDoc, fragments: ItemPre, result: var string) =
  1266. for f in fragments:
  1267. case f.isRst:
  1268. of true:
  1269. var resolved = resolveSubs(d.sharedState, f.rst)
  1270. renderRstToOut(d[], resolved, result)
  1271. of false: result &= f.str
  1272. proc cmp(x, y: Item): int = cmpDecimalsIgnoreCase(x.sortName, y.sortName)
  1273. for k in TSymKind:
  1274. # add symbols to section for each `k`, while optionally wrapping
  1275. # overloadable items with the same basic name by ``doc.item2``
  1276. let overloadableNames = toSeq(keys(d.section[k].secItems))
  1277. for plainName in overloadableNames.sorted(cmpDecimalsIgnoreCase):
  1278. var overloadChoices = d.section[k].secItems[plainName]
  1279. overloadChoices.sort(cmp)
  1280. var nameContent = ""
  1281. for item in overloadChoices:
  1282. var itemDesc: string
  1283. renderItemPre(d, item.descRst, itemDesc)
  1284. nameContent.add(
  1285. getConfigVar(d.conf, "doc.item") % (
  1286. item.substitutions & @[
  1287. "desc", itemDesc,
  1288. "name", item.name,
  1289. "itemSymOrID", item.anchor]))
  1290. if k in routineKinds:
  1291. let plainNameEsc1 = esc(d.target, plainName.strip)
  1292. let plainNameEsc2 = esc(d.target, plainName.strip, escMode=emUrl)
  1293. d.section[k].finalMarkup.add(
  1294. getConfigVar(d.conf, "doc.item2") % (
  1295. @["header_plain", plainNameEsc1,
  1296. "overloadGroupName", overloadGroupName(plainNameEsc2, k),
  1297. "content", nameContent]))
  1298. else:
  1299. d.section[k].finalMarkup.add(nameContent)
  1300. d.section[k].secItems.clear
  1301. renderItemPre(d, d.modDescPre, d.modDescFinal)
  1302. d.modDescPre.setLen 0
  1303. # Finalize fragments of ``.json`` file
  1304. for i, entry in d.jEntriesPre:
  1305. if entry.rst != nil:
  1306. let resolved = resolveSubs(d.sharedState, entry.rst)
  1307. var str: string
  1308. renderRstToOut(d[], resolved, str)
  1309. entry.json[entry.rstField] = %str
  1310. d.jEntriesPre[i].rst = nil
  1311. d.jEntriesFinal.add entry.json # generates docs
  1312. proc add(d: PDoc; j: JsonItem) =
  1313. if j.json != nil or j.rst != nil: d.jEntriesPre.add j
  1314. proc generateJson*(d: PDoc, n: PNode, includeComments: bool = true) =
  1315. case n.kind
  1316. of nkPragma:
  1317. let doctypeNode = findPragma(n, wDoctype)
  1318. setDoctype(d, doctypeNode)
  1319. of nkCommentStmt:
  1320. if includeComments:
  1321. d.add JsonItem(rst: genComment(d, n), rstField: "comment",
  1322. json: %Table[string, string]())
  1323. else:
  1324. d.modDescPre.add(genComment(d, n))
  1325. of nkProcDef, nkFuncDef:
  1326. when useEffectSystem: documentRaises(d.cache, n)
  1327. d.add genJsonItem(d, n, n[namePos], skProc)
  1328. of nkMethodDef:
  1329. when useEffectSystem: documentRaises(d.cache, n)
  1330. d.add genJsonItem(d, n, n[namePos], skMethod)
  1331. of nkIteratorDef:
  1332. when useEffectSystem: documentRaises(d.cache, n)
  1333. d.add genJsonItem(d, n, n[namePos], skIterator)
  1334. of nkMacroDef:
  1335. d.add genJsonItem(d, n, n[namePos], skMacro)
  1336. of nkTemplateDef:
  1337. d.add genJsonItem(d, n, n[namePos], skTemplate)
  1338. of nkConverterDef:
  1339. when useEffectSystem: documentRaises(d.cache, n)
  1340. d.add genJsonItem(d, n, n[namePos], skConverter)
  1341. of nkTypeSection, nkVarSection, nkLetSection, nkConstSection:
  1342. for i in 0..<n.len:
  1343. if n[i].kind != nkCommentStmt:
  1344. # order is always 'type var let const':
  1345. d.add genJsonItem(d, n[i], n[i][0],
  1346. succ(skType, ord(n.kind)-ord(nkTypeSection)))
  1347. of nkStmtList:
  1348. for i in 0..<n.len:
  1349. generateJson(d, n[i], includeComments)
  1350. of nkWhenStmt:
  1351. # generate documentation for the first branch only:
  1352. if not checkForFalse(n[0][0]):
  1353. generateJson(d, lastSon(n[0]), includeComments)
  1354. else: discard
  1355. proc genTagsItem(d: PDoc, n, nameNode: PNode, k: TSymKind): string =
  1356. result = getName(d, nameNode) & "\n"
  1357. proc generateTags*(d: PDoc, n: PNode, r: var string) =
  1358. case n.kind
  1359. of nkCommentStmt:
  1360. if startsWith(n.comment, "##"):
  1361. let stripped = n.comment.substr(2).strip
  1362. r.add stripped
  1363. of nkProcDef:
  1364. when useEffectSystem: documentRaises(d.cache, n)
  1365. r.add genTagsItem(d, n, n[namePos], skProc)
  1366. of nkFuncDef:
  1367. when useEffectSystem: documentRaises(d.cache, n)
  1368. r.add genTagsItem(d, n, n[namePos], skFunc)
  1369. of nkMethodDef:
  1370. when useEffectSystem: documentRaises(d.cache, n)
  1371. r.add genTagsItem(d, n, n[namePos], skMethod)
  1372. of nkIteratorDef:
  1373. when useEffectSystem: documentRaises(d.cache, n)
  1374. r.add genTagsItem(d, n, n[namePos], skIterator)
  1375. of nkMacroDef:
  1376. r.add genTagsItem(d, n, n[namePos], skMacro)
  1377. of nkTemplateDef:
  1378. r.add genTagsItem(d, n, n[namePos], skTemplate)
  1379. of nkConverterDef:
  1380. when useEffectSystem: documentRaises(d.cache, n)
  1381. r.add genTagsItem(d, n, n[namePos], skConverter)
  1382. of nkTypeSection, nkVarSection, nkLetSection, nkConstSection:
  1383. for i in 0..<n.len:
  1384. if n[i].kind != nkCommentStmt:
  1385. # order is always 'type var let const':
  1386. r.add genTagsItem(d, n[i], n[i][0],
  1387. succ(skType, ord(n.kind)-ord(nkTypeSection)))
  1388. of nkStmtList:
  1389. for i in 0..<n.len:
  1390. generateTags(d, n[i], r)
  1391. of nkWhenStmt:
  1392. # generate documentation for the first branch only:
  1393. if not checkForFalse(n[0][0]):
  1394. generateTags(d, lastSon(n[0]), r)
  1395. else: discard
  1396. proc genSection(d: PDoc, kind: TSymKind, groupedToc = false) =
  1397. const sectionNames: array[skModule..skField, string] = [
  1398. "Imports", "Types", "Vars", "Lets", "Consts", "Vars", "Procs", "Funcs",
  1399. "Methods", "Iterators", "Converters", "Macros", "Templates", "Exports"
  1400. ]
  1401. if d.section[kind].finalMarkup == "": return
  1402. var title = sectionNames[kind]
  1403. d.section[kind].finalMarkup = getConfigVar(d.conf, "doc.section") % [
  1404. "sectionid", $ord(kind), "sectionTitle", title,
  1405. "sectionTitleID", $(ord(kind) + 50), "content", d.section[kind].finalMarkup]
  1406. proc cmp(x, y: TocItem): int = cmpDecimalsIgnoreCase(x.sortName, y.sortName)
  1407. if groupedToc:
  1408. let overloadableNames = toSeq(keys(d.tocTable[kind]))
  1409. for plainName in overloadableNames.sorted(cmpDecimalsIgnoreCase):
  1410. var overloadChoices = d.tocTable[kind][plainName]
  1411. overloadChoices.sort(cmp)
  1412. var content: string
  1413. for item in overloadChoices:
  1414. content.add item.content
  1415. d.toc2[kind].add getConfigVar(d.conf, "doc.section.toc2") % [
  1416. "sectionid", $ord(kind), "sectionTitle", title,
  1417. "sectionTitleID", $(ord(kind) + 50),
  1418. "content", content, "plainName", plainName]
  1419. else:
  1420. for item in d.tocSimple[kind].sorted(cmp):
  1421. d.toc2[kind].add item.content
  1422. let sectionValues = @[
  1423. "sectionID", $ord(kind), "sectionTitleID", $(ord(kind) + 50),
  1424. "sectionTitle", title
  1425. ]
  1426. # Check if the toc has any children
  1427. if d.toc2[kind] != "":
  1428. # Use the dropdown version instead and store the children in the dropdown
  1429. d.toc[kind] = getConfigVar(d.conf, "doc.section.toc") % (sectionValues & @[
  1430. "content", d.toc2[kind]
  1431. ])
  1432. else:
  1433. # Just have the link
  1434. d.toc[kind] = getConfigVar(d.conf, "doc.section.toc_item") % sectionValues
  1435. proc relLink(outDir: AbsoluteDir, destFile: AbsoluteFile, linkto: RelativeFile): string =
  1436. $relativeTo(outDir / linkto, destFile.splitFile().dir, '/')
  1437. proc genOutFile(d: PDoc, groupedToc = false): string =
  1438. var
  1439. code, content: string
  1440. title = ""
  1441. var j = 0
  1442. var toc = ""
  1443. renderTocEntries(d[], j, 1, toc)
  1444. for i in TSymKind:
  1445. var shouldSort = i in routineKinds and groupedToc
  1446. genSection(d, i, shouldSort)
  1447. toc.add(d.toc[i])
  1448. if toc != "" or d.target == outLatex:
  1449. # for Latex $doc.toc will automatically generate TOC if `d.hasToc` is set
  1450. toc = getConfigVar(d.conf, "doc.toc") % ["content", toc]
  1451. for i in TSymKind: code.add(d.section[i].finalMarkup)
  1452. # Extract the title. Non API modules generate an entry in the index table.
  1453. if d.meta[metaTitle].len != 0:
  1454. title = d.meta[metaTitle]
  1455. let external = AbsoluteFile(d.destFile)
  1456. .relativeTo(d.conf.outDir, '/')
  1457. .changeFileExt(HtmlExt)
  1458. .string
  1459. setIndexTerm(d[], external, "", title)
  1460. else:
  1461. # Modules get an automatic title for the HTML, but no entry in the index.
  1462. title = canonicalImport(d.conf, AbsoluteFile d.filename)
  1463. title = esc(d.target, title)
  1464. var subtitle = ""
  1465. if d.meta[metaSubtitle] != "":
  1466. dispA(d.conf, subtitle, "<h2 class=\"subtitle\">$1</h2>",
  1467. "\\\\\\vspace{0.5em}\\large $1", [esc(d.target, d.meta[metaSubtitle])])
  1468. var groupsection = getConfigVar(d.conf, "doc.body_toc_groupsection")
  1469. let bodyname = if d.hasToc and not d.standaloneDoc and not d.conf.isLatexCmd:
  1470. groupsection.setLen 0
  1471. "doc.body_toc_group"
  1472. elif d.hasToc: "doc.body_toc"
  1473. else: "doc.body_no_toc"
  1474. let seeSrc = genSeeSrc(d, d.filename, 1)
  1475. content = getConfigVar(d.conf, bodyname) % [
  1476. "title", title, "subtitle", subtitle,
  1477. "tableofcontents", toc, "moduledesc", d.modDescFinal, "date", getDateStr(),
  1478. "time", getClockStr(), "content", code,
  1479. "deprecationMsg", d.modDeprecationMsg,
  1480. "theindexhref", relLink(d.conf.outDir, d.destFile.AbsoluteFile,
  1481. theindexFname.RelativeFile),
  1482. "body_toc_groupsection", groupsection, "seeSrc", seeSrc]
  1483. if optCompileOnly notin d.conf.globalOptions:
  1484. # XXX what is this hack doing here? 'optCompileOnly' means raw output!?
  1485. code = getConfigVar(d.conf, "doc.file") % [
  1486. "nimdoccss", relLink(d.conf.outDir, d.destFile.AbsoluteFile,
  1487. nimdocOutCss.RelativeFile),
  1488. "dochackjs", relLink(d.conf.outDir, d.destFile.AbsoluteFile,
  1489. docHackJsFname.RelativeFile),
  1490. "title", title, "subtitle", subtitle, "tableofcontents", toc,
  1491. "moduledesc", d.modDescFinal, "date", getDateStr(), "time", getClockStr(),
  1492. "content", content, "author", d.meta[metaAuthor],
  1493. "version", esc(d.target, d.meta[metaVersion]), "analytics", d.analytics,
  1494. "deprecationMsg", d.modDeprecationMsg]
  1495. else:
  1496. code = content
  1497. result = code
  1498. proc generateIndex*(d: PDoc) =
  1499. if optGenIndex in d.conf.globalOptions:
  1500. let dir = d.conf.outDir
  1501. createDir(dir)
  1502. let dest = dir / changeFileExt(presentationPath(d.conf, AbsoluteFile d.filename), IndexExt)
  1503. writeIndexFile(d[], dest.string)
  1504. proc updateOutfile(d: PDoc, outfile: AbsoluteFile) =
  1505. if d.module == nil or sfMainModule in d.module.flags: # nil for e.g. for commandRst2Html
  1506. if d.conf.outFile.isEmpty:
  1507. d.conf.outFile = outfile.relativeTo(d.conf.outDir)
  1508. if isAbsolute(d.conf.outFile.string):
  1509. d.conf.outFile = splitPath(d.conf.outFile.string)[1].RelativeFile
  1510. proc writeOutput*(d: PDoc, useWarning = false, groupedToc = false) =
  1511. runAllExamples(d)
  1512. var content = genOutFile(d, groupedToc)
  1513. if optStdout in d.conf.globalOptions:
  1514. write(stdout, content)
  1515. else:
  1516. template outfile: untyped = d.destFile.AbsoluteFile
  1517. #let outfile = getOutFile2(d.conf, shortenDir(d.conf, filename), outExt)
  1518. let dir = outfile.splitFile.dir
  1519. createDir(dir)
  1520. updateOutfile(d, outfile)
  1521. try:
  1522. writeFile(outfile, content)
  1523. except IOError:
  1524. rawMessage(d.conf, if useWarning: warnCannotOpenFile else: errCannotOpenFile,
  1525. outfile.string)
  1526. if not d.wroteSupportFiles: # nimdoc.css + dochack.js
  1527. let nimr = $d.conf.getPrefixDir()
  1528. case d.target
  1529. of outHtml:
  1530. copyFile(docCss.interp(nimr = nimr), $d.conf.outDir / nimdocOutCss)
  1531. of outLatex:
  1532. copyFile(docCls.interp(nimr = nimr), $d.conf.outDir / nimdocOutCls)
  1533. if optGenIndex in d.conf.globalOptions:
  1534. let docHackJs2 = getDocHacksJs(nimr, nim = getAppFilename())
  1535. copyFile(docHackJs2, $d.conf.outDir / docHackJs2.lastPathPart)
  1536. d.wroteSupportFiles = true
  1537. proc writeOutputJson*(d: PDoc, useWarning = false) =
  1538. runAllExamples(d)
  1539. var modDesc: string
  1540. for desc in d.modDescFinal:
  1541. modDesc &= desc
  1542. let content = %*{"orig": d.filename,
  1543. "nimble": getPackageName(d.conf, d.filename),
  1544. "moduleDescription": modDesc,
  1545. "entries": d.jEntriesFinal}
  1546. if optStdout in d.conf.globalOptions:
  1547. write(stdout, $content)
  1548. else:
  1549. let dir = d.destFile.splitFile.dir
  1550. createDir(dir)
  1551. var f: File
  1552. if open(f, d.destFile, fmWrite):
  1553. write(f, $content)
  1554. close(f)
  1555. updateOutfile(d, d.destFile.AbsoluteFile)
  1556. else:
  1557. localError(d.conf, newLineInfo(d.conf, AbsoluteFile d.filename, -1, -1),
  1558. warnUser, "unable to open file \"" & d.destFile &
  1559. "\" for writing")
  1560. proc handleDocOutputOptions*(conf: ConfigRef) =
  1561. if optWholeProject in conf.globalOptions:
  1562. # Backward compatibility with previous versions
  1563. # xxx this is buggy when user provides `nim doc --project -o:sub/bar.html main`,
  1564. # it'd write to `sub/bar.html/main.html`
  1565. conf.outDir = AbsoluteDir(conf.outDir / conf.outFile)
  1566. proc commandDoc*(cache: IdentCache, conf: ConfigRef) =
  1567. ## implementation of deprecated ``doc0`` command (without semantic checking)
  1568. handleDocOutputOptions conf
  1569. var ast = parseFile(conf.projectMainIdx, cache, conf)
  1570. if ast == nil: return
  1571. var d = newDocumentor(conf.projectFull, cache, conf, hasToc = true)
  1572. generateDoc(d, ast, ast)
  1573. finishGenerateDoc(d)
  1574. writeOutput(d)
  1575. generateIndex(d)
  1576. proc commandRstAux(cache: IdentCache, conf: ConfigRef;
  1577. filename: AbsoluteFile, outExt: string,
  1578. preferMarkdown: bool) =
  1579. var filen = addFileExt(filename, "txt")
  1580. var d = newDocumentor(filen, cache, conf, outExt, standaloneDoc = true,
  1581. preferMarkdown = preferMarkdown, hasToc = false)
  1582. let rst = parseRst(readFile(filen.string),
  1583. line=LineRstInit, column=ColRstInit,
  1584. conf, d.sharedState)
  1585. d.modDescPre = @[ItemFragment(isRst: true, rst: rst)]
  1586. finishGenerateDoc(d)
  1587. writeOutput(d)
  1588. generateIndex(d)
  1589. proc commandRst2Html*(cache: IdentCache, conf: ConfigRef,
  1590. preferMarkdown=false) =
  1591. commandRstAux(cache, conf, conf.projectFull, HtmlExt, preferMarkdown)
  1592. proc commandRst2TeX*(cache: IdentCache, conf: ConfigRef,
  1593. preferMarkdown=false) =
  1594. commandRstAux(cache, conf, conf.projectFull, TexExt, preferMarkdown)
  1595. proc commandJson*(cache: IdentCache, conf: ConfigRef) =
  1596. ## implementation of a deprecated jsondoc0 command
  1597. var ast = parseFile(conf.projectMainIdx, cache, conf)
  1598. if ast == nil: return
  1599. var d = newDocumentor(conf.projectFull, cache, conf, hasToc = true)
  1600. d.onTestSnippet = proc (d: var RstGenerator; filename, cmd: string;
  1601. status: int; content: string) =
  1602. localError(conf, newLineInfo(conf, AbsoluteFile d.filename, -1, -1),
  1603. warnUser, "the ':test:' attribute is not supported by this backend")
  1604. generateJson(d, ast)
  1605. finishGenerateDoc(d)
  1606. let json = d.jEntriesFinal
  1607. let content = pretty(json)
  1608. if optStdout in d.conf.globalOptions:
  1609. write(stdout, content)
  1610. else:
  1611. #echo getOutFile(gProjectFull, JsonExt)
  1612. let filename = getOutFile(conf, RelativeFile conf.projectName, JsonExt)
  1613. try:
  1614. writeFile(filename, content)
  1615. except:
  1616. rawMessage(conf, errCannotOpenFile, filename.string)
  1617. proc commandTags*(cache: IdentCache, conf: ConfigRef) =
  1618. var ast = parseFile(conf.projectMainIdx, cache, conf)
  1619. if ast == nil: return
  1620. var d = newDocumentor(conf.projectFull, cache, conf, hasToc = true)
  1621. d.onTestSnippet = proc (d: var RstGenerator; filename, cmd: string;
  1622. status: int; content: string) =
  1623. localError(conf, newLineInfo(conf, AbsoluteFile d.filename, -1, -1),
  1624. warnUser, "the ':test:' attribute is not supported by this backend")
  1625. var
  1626. content = ""
  1627. generateTags(d, ast, content)
  1628. if optStdout in d.conf.globalOptions:
  1629. write(stdout, content)
  1630. else:
  1631. #echo getOutFile(gProjectFull, TagsExt)
  1632. let filename = getOutFile(conf, RelativeFile conf.projectName, TagsExt)
  1633. try:
  1634. writeFile(filename, content)
  1635. except:
  1636. rawMessage(conf, errCannotOpenFile, filename.string)
  1637. proc commandBuildIndex*(conf: ConfigRef, dir: string, outFile = RelativeFile"") =
  1638. var content = mergeIndexes(dir)
  1639. var outFile = outFile
  1640. if outFile.isEmpty: outFile = theindexFname.RelativeFile.changeFileExt("")
  1641. let filename = getOutFile(conf, outFile, HtmlExt)
  1642. let code = getConfigVar(conf, "doc.file") % [
  1643. "nimdoccss", relLink(conf.outDir, filename, nimdocOutCss.RelativeFile),
  1644. "dochackjs", relLink(conf.outDir, filename, docHackJsFname.RelativeFile),
  1645. "title", "Index",
  1646. "subtitle", "", "tableofcontents", "", "moduledesc", "",
  1647. "date", getDateStr(), "time", getClockStr(),
  1648. "content", content, "author", "", "version", "", "analytics", ""]
  1649. # no analytics because context is not available
  1650. try:
  1651. writeFile(filename, code)
  1652. except:
  1653. rawMessage(conf, errCannotOpenFile, filename.string)
  1654. proc commandBuildIndexJson*(conf: ConfigRef, dir: string, outFile = RelativeFile"") =
  1655. var (modules, symbols, docs) = readIndexDir(dir)
  1656. let documents = toSeq(keys(Table[IndexEntry, seq[IndexEntry]](docs)))
  1657. let body = %*({"documents": documents, "modules": modules, "symbols": symbols})
  1658. var outFile = outFile
  1659. if outFile.isEmpty: outFile = theindexFname.RelativeFile.changeFileExt("")
  1660. let filename = getOutFile(conf, outFile, JsonExt)
  1661. try:
  1662. writeFile(filename, $body)
  1663. except:
  1664. rawMessage(conf, errCannotOpenFile, filename.string)