docgen.nim 62 KB

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