docgen.nim 55 KB

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