docgen.nim 55 KB

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