diff options
Diffstat (limited to 'compiler/docgen.nim')
-rw-r--r-- | compiler/docgen.nim | 1954 |
1 files changed, 1954 insertions, 0 deletions
diff --git a/compiler/docgen.nim b/compiler/docgen.nim new file mode 100644 index 000000000..8e5f5e4e7 --- /dev/null +++ b/compiler/docgen.nim @@ -0,0 +1,1954 @@ +# +# +# The Nim Compiler +# (c) Copyright 2012 Andreas Rumpf +# +# See the file "copying.txt", included in this +# distribution, for details about the copyright. +# + +## This is the Nim documentation generator. Cross-references are generated +## by knowing how the anchors are going to be named. +## +## .. importdoc:: ../docgen.md +## +## For corresponding users' documentation see [Nim DocGen Tools Guide]. + +import + ast, options, msgs, idents, + wordrecg, syntaxes, renderer, lexer, + packages/docutils/[rst, rstidx, rstgen, dochelpers], + trees, types, + typesrenderer, astalgo, lineinfos, + pathutils, nimpaths, renderverbatim, packages +import packages/docutils/rstast except FileIndex, TLineInfo + +import std/[os, strutils, strtabs, algorithm, json, osproc, tables, intsets, xmltree, sequtils] +from std/uri import encodeUrl +from nodejs import findNodeJs + +when defined(nimPreviewSlimSystem): + import std/[assertions, syncio] + + +const + exportSection = skField + docCmdSkip = "skip" + DocColOffset = "## ".len # assuming that a space was added after ## + +type + ItemFragment = object ## A fragment from each item will be eventually + ## constructed by converting `rst` fields to strings. + case isRst: bool + of true: + rst: PRstNode + of false: ## contains ready markup e.g. from runnableExamples + str: string + ItemPre = seq[ItemFragment] ## A pre-processed item. + Item = object ## Any item in documentation, e.g. symbol + ## entry. Configuration variable ``doc.item`` + ## is used for its HTML rendering. + descRst: ItemPre ## Description of the item (may contain + ## runnableExamples). + substitutions: seq[string] ## Variable names in `doc.item`... + sortName: string ## The string used for sorting in output + info: rstast.TLineInfo ## place where symbol was defined (for messages) + anchor: string ## e.g. HTML anchor + name: string ## short name of the symbol, not unique + ## (includes backticks ` if present) + detailedName: string ## longer name like `proc search(x: int): int` + ModSection = object ## Section like Procs, Types, etc. + secItems: Table[string, seq[Item]] + ## Map basic name -> pre-processed items. + finalMarkup: string ## The items, after RST pass 2 and rendering. + ModSections = array[TSymKind, ModSection] + TocItem = object ## HTML TOC item + content: string + sortName: string + TocSectionsFinal = array[TSymKind, string] + ExampleGroup = ref object + ## a group of runnableExamples with same rdoccmd + rdoccmd: string ## from 1st arg in `runnableExamples(rdoccmd): body` + docCmd: string ## from user config, e.g. --doccmd:-d:foo + code: string ## contains imports; each import contains `body` + index: int ## group index + JsonItem = object # pre-processed item: `rst` should be finalized + json: JsonNode + rst: PRstNode + rstField: string + TDocumentor = object of rstgen.RstGenerator + modDescPre: ItemPre # module description, not finalized + modDescFinal: string # module description, after RST pass 2 and rendering + module: PSym + modDeprecationMsg: string + section: ModSections # entries of ``.nim`` file (for `proc`s, etc) + tocSimple: array[TSymKind, seq[TocItem]] + # TOC entries for non-overloadable symbols (e.g. types, constants)... + tocTable: array[TSymKind, Table[string, seq[TocItem]]] + # ...otherwise (e.g. procs) + toc2: TocSectionsFinal # TOC `content`, which is probably wrapped + # in `doc.section.toc2` + toc: TocSectionsFinal # final TOC (wrapped in `doc.section.toc`) + indexValFilename: string + analytics: string # Google Analytics javascript, "" if doesn't exist + seenSymbols: StringTableRef # avoids duplicate symbol generation for HTML. + jEntriesPre: seq[JsonItem] # pre-processed RST + JSON content + jEntriesFinal: JsonNode # final JSON after RST pass 2 and rendering + types: TStrTable + sharedState: PRstSharedState + standaloneDoc: bool # is markup (.rst/.md) document? + conf*: ConfigRef + cache*: IdentCache + exampleCounter: int + emitted: IntSet # we need to track which symbols have been emitted + # already. See bug #3655 + thisDir*: AbsoluteDir + exampleGroups: OrderedTable[string, ExampleGroup] + wroteSupportFiles*: bool + nimToRstFid: Table[lineinfos.FileIndex, rstast.FileIndex] + ## map Nim FileIndex -> RST one, it's needed because we keep them separate + + PDoc* = ref TDocumentor ## Alias to type less. + +proc add(dest: var ItemPre, rst: PRstNode) = dest.add ItemFragment(isRst: true, rst: rst) +proc add(dest: var ItemPre, str: string) = dest.add ItemFragment(isRst: false, str: str) + +proc addRstFileIndex(d: PDoc, fileIndex: lineinfos.FileIndex): rstast.FileIndex = + let invalid = rstast.FileIndex(-1) + result = d.nimToRstFid.getOrDefault(fileIndex, default = invalid) + if result == invalid: + let fname = toFullPath(d.conf, fileIndex) + result = addFilename(d.sharedState, fname) + d.nimToRstFid[fileIndex] = result + +proc addRstFileIndex(d: PDoc, info: lineinfos.TLineInfo): rstast.FileIndex = + addRstFileIndex(d, info.fileIndex) + +proc cmpDecimalsIgnoreCase(a, b: string): int = + ## For sorting with correct handling of cases like 'uint8' and 'uint16'. + ## Also handles leading zeros well (however note that leading zeros are + ## significant when lengths of numbers mismatch, e.g. 'bar08' > 'bar8' !). + runnableExamples: + doAssert cmpDecimalsIgnoreCase("uint8", "uint16") < 0 + doAssert cmpDecimalsIgnoreCase("val00032", "val16suffix") > 0 + doAssert cmpDecimalsIgnoreCase("val16suffix", "val16") > 0 + doAssert cmpDecimalsIgnoreCase("val_08_32", "val_08_8") > 0 + doAssert cmpDecimalsIgnoreCase("val_07_32", "val_08_8") < 0 + doAssert cmpDecimalsIgnoreCase("ab8", "ab08") < 0 + doAssert cmpDecimalsIgnoreCase("ab8de", "ab08c") < 0 # sanity check + let aLen = a.len + let bLen = b.len + var + iA = 0 + iB = 0 + while iA < aLen and iB < bLen: + if isDigit(a[iA]) and isDigit(b[iB]): + var + limitA = iA # index after the last (least significant) digit + limitB = iB + while limitA < aLen and isDigit(a[limitA]): inc limitA + while limitB < bLen and isDigit(b[limitB]): inc limitB + var pos = max(limitA-iA, limitB-iA) + while pos > 0: + if limitA-pos < iA: # digit in `a` is 0 effectively + result = ord('0') - ord(b[limitB-pos]) + elif limitB-pos < iB: # digit in `b` is 0 effectively + result = ord(a[limitA-pos]) - ord('0') + else: + result = ord(a[limitA-pos]) - ord(b[limitB-pos]) + if result != 0: return + dec pos + result = (limitA - iA) - (limitB - iB) # consider 'bar08' > 'bar8' + if result != 0: return + iA = limitA + iB = limitB + else: + result = ord(toLowerAscii(a[iA])) - ord(toLowerAscii(b[iB])) + if result != 0: return + inc iA + inc iB + result = (aLen - iA) - (bLen - iB) + +proc prettyString(a: object): string = + # xxx pending std/prettyprint refs https://github.com/nim-lang/RFCs/issues/203#issuecomment-602534906 + result = "" + for k, v in fieldPairs(a): + result.add k & ": " & $v & "\n" + +proc presentationPath*(conf: ConfigRef, file: AbsoluteFile): RelativeFile = + ## returns a relative file that will be appended to outDir + let file2 = $file + template bail() = + result = relativeTo(file, conf.projectPath) + proc nimbleDir(): AbsoluteDir = + getNimbleFile(conf, file2).parentDir.AbsoluteDir + case conf.docRoot: + of docRootDefault: + result = getRelativePathFromConfigPath(conf, file) + let dir = nimbleDir() + if not dir.isEmpty: + let result2 = relativeTo(file, dir) + if not result2.isEmpty and (result.isEmpty or result2.string.len < result.string.len): + result = result2 + if result.isEmpty: bail() + of "@pkg": + let dir = nimbleDir() + if dir.isEmpty: bail() + else: result = relativeTo(file, dir) + of "@path": + result = getRelativePathFromConfigPath(conf, file) + if result.isEmpty: bail() + elif conf.docRoot.len > 0: + # we're (currently) requiring `isAbsolute` to avoid confusion when passing + # a relative path (would it be relative with regard to $PWD or to projectfile) + conf.globalAssert conf.docRoot.isAbsolute, arg=conf.docRoot + conf.globalAssert conf.docRoot.dirExists, arg=conf.docRoot + # needed because `canonicalizePath` called on `file` + result = file.relativeTo conf.docRoot.expandFilename.AbsoluteDir + else: + bail() + if isAbsolute(result.string): + result = file.string.splitPath()[1].RelativeFile + result = result.string.replace("..", dotdotMangle).RelativeFile + doAssert not result.isEmpty + doAssert not isAbsolute(result.string) + +proc whichType(d: PDoc; n: PNode): PSym = + if n.kind == nkSym: + if d.types.strTableContains(n.sym): + result = n.sym + else: + result = nil + else: + result = nil + for i in 0..<n.safeLen: + let x = whichType(d, n[i]) + if x != nil: return x + +proc attachToType(d: PDoc; p: PSym): PSym = + result = nil + let params = p.ast[paramsPos] + template check(i) = + result = whichType(d, params[i]) + if result != nil: return result + + # first check the first parameter, then the return type, + # then the other parameter: + if params.len > 1: check(1) + if params.len > 0: check(0) + for i in 2..<params.len: check(i) + +template declareClosures(currentFilename: AbsoluteFile, destFile: string) = + proc compilerMsgHandler(filename: string, line, col: int, + msgKind: rst.MsgKind, arg: string) {.gcsafe.} = + # translate msg kind: + var k: TMsgKind + case msgKind + of meCannotOpenFile: k = errCannotOpenFile + of meExpected: k = errXExpected + of meMissingClosing: k = errRstMissingClosing + of meGridTableNotImplemented: k = errRstGridTableNotImplemented + of meMarkdownIllformedTable: k = errRstMarkdownIllformedTable + of meIllformedTable: k = errRstIllformedTable + of meNewSectionExpected: k = errRstNewSectionExpected + of meGeneralParseError: k = errRstGeneralParseError + of meInvalidDirective: k = errRstInvalidDirectiveX + of meInvalidField: k = errRstInvalidField + of meFootnoteMismatch: k = errRstFootnoteMismatch + of meSandboxedDirective: k = errRstSandboxedDirective + of mwRedefinitionOfLabel: k = warnRstRedefinitionOfLabel + of mwUnknownSubstitution: k = warnRstUnknownSubstitutionX + of mwAmbiguousLink: k = warnRstAmbiguousLink + of mwBrokenLink: k = warnRstBrokenLink + of mwUnsupportedLanguage: k = warnRstLanguageXNotSupported + of mwUnsupportedField: k = warnRstFieldXNotSupported + of mwUnusedImportdoc: k = warnRstUnusedImportdoc + of mwRstStyle: k = warnRstStyle + {.gcsafe.}: + let errorsAsWarnings = (roPreferMarkdown in d.sharedState.options) and + not d.standaloneDoc # not tolerate errors in .rst/.md files + if whichMsgClass(msgKind) == mcError and errorsAsWarnings: + liMessage(conf, newLineInfo(conf, AbsoluteFile filename, line, col), + k, arg, doNothing, instLoc(), ignoreError=true) + # when our Markdown parser fails, we currently can only terminate the + # parsing (and then we will return monospaced text instead of markup): + raiseRecoverableError("") + else: + globalError(conf, newLineInfo(conf, AbsoluteFile filename, line, col), k, arg) + + proc docgenFindFile(s: string): string {.gcsafe.} = + result = options.findFile(conf, s).string + if result.len == 0: + result = getCurrentDir() / s + if not fileExists(result): result = "" + + proc docgenFindRefFile(targetRelPath: string): + tuple[targetPath: string, linkRelPath: string] {.gcsafe.} = + let fromDir = splitFile(destFile).dir # dir where we reference from + let basedir = os.splitFile(currentFilename.string).dir + let outDirPath: RelativeFile = + presentationPath(conf, AbsoluteFile(basedir / targetRelPath)) + # use presentationPath because `..` path can be be mangled to `_._` + result = (string(conf.outDir / outDirPath), "") + if not fileExists(result.targetPath): + # this can happen if targetRelPath goes to parent directory `OUTDIR/..`. + # Trying it, this may cause ambiguities, but allows us to insert + # "packages" into each other, which is actually used in Nim repo itself. + let destPath = fromDir / targetRelPath + if destPath != result.targetPath and fileExists(destPath): + result.targetPath = destPath + + result.linkRelPath = relativePath(result.targetPath.splitFile.dir, + fromDir).replace('\\', '/') + + +proc parseRst(text: string, + line, column: int, + conf: ConfigRef, sharedState: PRstSharedState): PRstNode = + result = rstParsePass1(text, line, column, sharedState) + +proc getOutFile2(conf: ConfigRef; filename: RelativeFile, + ext: string, guessTarget: bool): AbsoluteFile = + if optWholeProject in conf.globalOptions or guessTarget: + let d = conf.outDir + createDir(d) + result = d / changeFileExt(filename, ext) + elif not conf.outFile.isEmpty: + result = absOutFile(conf) + else: + result = getOutFile(conf, filename, ext) + +proc isLatexCmd(conf: ConfigRef): bool = + conf.cmd in {cmdRst2tex, cmdMd2tex, cmdDoc2tex} + +proc newDocumentor*(filename: AbsoluteFile; cache: IdentCache; conf: ConfigRef, + outExt: string = HtmlExt, module: PSym = nil, + standaloneDoc = false, preferMarkdown = true, + hasToc = true): PDoc = + let destFile = getOutFile2(conf, presentationPath(conf, filename), outExt, false).string + new(result) + let d = result # pass `d` to `declareClosures`: + declareClosures(currentFilename = filename, destFile = destFile) + result.module = module + result.conf = conf + result.cache = cache + result.outDir = conf.outDir.string + result.standaloneDoc = standaloneDoc + var options= {roSupportRawDirective, roSupportMarkdown, roSandboxDisabled} + if preferMarkdown: + options.incl roPreferMarkdown + if not standaloneDoc: options.incl roNimFile + # (options can be changed dynamically in `setDoctype` by `{.doctype.}`) + result.hasToc = hasToc + result.sharedState = newRstSharedState( + options, filename.string, + docgenFindFile, docgenFindRefFile, compilerMsgHandler, hasToc) + initRstGenerator(result[], (if conf.isLatexCmd: outLatex else: outHtml), + conf.configVars, filename.string, + docgenFindFile, compilerMsgHandler) + + if conf.configVars.hasKey("doc.googleAnalytics") and + conf.configVars.hasKey("doc.plausibleAnalytics"): + raiseAssert "Either use googleAnalytics or plausibleAnalytics" + + if conf.configVars.hasKey("doc.googleAnalytics"): + result.analytics = """ +<script> + (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ + (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), + m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) + })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); + + ga('create', '$1', 'auto'); + ga('send', 'pageview'); + +</script> + """ % [conf.configVars.getOrDefault"doc.googleAnalytics"] + elif conf.configVars.hasKey("doc.plausibleAnalytics"): + result.analytics = """ + <script defer data-domain="$1" src="https://plausible.io/js/plausible.js"></script> + """ % [conf.configVars.getOrDefault"doc.plausibleAnalytics"] + else: + result.analytics = "" + + result.seenSymbols = newStringTable(modeCaseInsensitive) + result.id = 100 + result.jEntriesFinal = newJArray() + result.types = initStrTable() + result.onTestSnippet = + proc (gen: var RstGenerator; filename, cmd: string; status: int; content: string) {.gcsafe.} = + if conf.docCmd == docCmdSkip: return + inc(gen.id) + var d = (ptr TDocumentor)(addr gen) + var outp: AbsoluteFile + if filename.len == 0: + let nameOnly = splitFile(d.filename).name + # "snippets" needed, refs bug #17183 + outp = getNimcacheDir(conf) / "snippets".RelativeDir / RelativeDir(nameOnly) / + RelativeFile(nameOnly & "_snippet_" & $d.id & ".nim") + elif isAbsolute(filename): + outp = AbsoluteFile(filename) + else: + # Nim's convention: every path is relative to the file it was written in: + let nameOnly = splitFile(d.filename).name + outp = AbsoluteDir(nameOnly) / RelativeFile(filename) + # Make sure the destination directory exists + createDir(outp.splitFile.dir) + # Include the current file if we're parsing a nim file + let importStmt = if d.standaloneDoc: "" else: "import \"$1\"\n" % [d.filename.replace("\\", "/")] + writeFile(outp, importStmt & content) + + proc interpSnippetCmd(cmd: string): string = + # backward compatibility hacks; interpolation commands should explicitly use `$` + if cmd.startsWith "nim ": result = "$nim " & cmd[4..^1] + else: result = cmd + # factor with D20210224T221756 + result = result.replace("$1", "$options") % [ + "nim", os.getAppFilename().quoteShell, + "libpath", quoteShell(d.conf.libpath), + "docCmd", d.conf.docCmd, + "backend", $d.conf.backend, + "options", outp.quoteShell, + # xxx `quoteShell` seems buggy if user passes options = "-d:foo somefile.nim" + ] + let cmd = cmd.interpSnippetCmd + rawMessage(conf, hintExecuting, cmd) + let (output, gotten) = execCmdEx(cmd) + if gotten != status: + rawMessage(conf, errGenerated, "snippet failed: cmd: '$1' status: $2 expected: $3 output: $4" % [cmd, $gotten, $status, output]) + result.emitted = initIntSet() + result.destFile = destFile + result.thisDir = result.destFile.AbsoluteFile.splitFile.dir + +template dispA(conf: ConfigRef; dest: var string, xml, tex: string, + args: openArray[string]) = + if not conf.isLatexCmd: dest.addf(xml, args) + else: dest.addf(tex, args) + +proc getVarIdx(varnames: openArray[string], id: string): int = + for i in 0..high(varnames): + if cmpIgnoreStyle(varnames[i], id) == 0: + return i + result = -1 + +proc genComment(d: PDoc, n: PNode): PRstNode = + if n.comment.len > 0: + d.sharedState.currFileIdx = addRstFileIndex(d, n.info) + try: + result = parseRst(n.comment, + toLinenumber(n.info), + toColumn(n.info) + DocColOffset, + d.conf, d.sharedState) + except ERecoverableError: + result = newRstNode(rnLiteralBlock, @[newRstLeaf(n.comment)]) + else: + result = nil + +proc genRecCommentAux(d: PDoc, n: PNode): PRstNode = + if n == nil: return nil + result = genComment(d, n) + if result == nil: + if n.kind in {nkStmtList, nkStmtListExpr, nkTypeDef, nkConstDef, nkTypeClassTy, + nkObjectTy, nkRefTy, nkPtrTy, nkAsgn, nkFastAsgn, nkSinkAsgn, nkHiddenStdConv}: + # notin {nkEmpty..nkNilLit, nkEnumTy, nkTupleTy}: + for i in 0..<n.len: + result = genRecCommentAux(d, n[i]) + if result != nil: return + else: + n.comment = "" + +proc genRecComment(d: PDoc, n: PNode): PRstNode = + if n == nil: return nil + result = genComment(d, n) + if result == nil: + if n.kind in {nkProcDef, nkFuncDef, nkMethodDef, nkIteratorDef, + nkMacroDef, nkTemplateDef, nkConverterDef}: + result = genRecCommentAux(d, n[bodyPos]) + else: + result = genRecCommentAux(d, n) + +proc getPlainDocstring(n: PNode): string = + ## Gets the plain text docstring of a node non destructively. + ## + ## You need to call this before genRecComment, whose side effects are removal + ## of comments from the tree. The proc will recursively scan and return all + ## the concatenated ``##`` comments of the node. + if n == nil: result = "" + elif startsWith(n.comment, "##"): + result = n.comment + else: + result = "" + for i in 0..<n.safeLen: + result = getPlainDocstring(n[i]) + if result.len > 0: return + +proc externalDep(d: PDoc; module: PSym): string = + if optWholeProject in d.conf.globalOptions or d.conf.docRoot.len > 0: + let full = AbsoluteFile toFullPath(d.conf, FileIndex module.position) + let tmp = getOutFile2(d.conf, presentationPath(d.conf, full), HtmlExt, sfMainModule notin module.flags) + result = relativeTo(tmp, d.thisDir, '/').string + else: + result = extractFilename toFullPath(d.conf, FileIndex module.position) + +proc nodeToHighlightedHtml(d: PDoc; n: PNode; result: var string; + renderFlags: TRenderFlags = {}; + procLink: string) = + var r: TSrcGen = initTokRender(n, renderFlags) + var literal = "" + var kind = tkEof + var tokenPos = 0 + var procTokenPos = 0 + template escLit(): untyped = esc(d.target, literal) + while true: + getNextTok(r, kind, literal) + inc tokenPos + case kind + of tkEof: + break + of tkComment: + dispA(d.conf, result, "<span class=\"Comment\">$1</span>", "\\spanComment{$1}", + [escLit]) + of tokKeywordLow..tokKeywordHigh: + if kind in {tkProc, tkMethod, tkIterator, tkMacro, tkTemplate, tkFunc, tkConverter}: + procTokenPos = tokenPos + dispA(d.conf, result, "<span class=\"Keyword\">$1</span>", "\\spanKeyword{$1}", + [literal]) + of tkOpr: + dispA(d.conf, result, "<span class=\"Operator\">$1</span>", "\\spanOperator{$1}", + [escLit]) + of tkStrLit..tkTripleStrLit, tkCustomLit: + dispA(d.conf, result, "<span class=\"StringLit\">$1</span>", + "\\spanStringLit{$1}", [escLit]) + of tkCharLit: + dispA(d.conf, result, "<span class=\"CharLit\">$1</span>", "\\spanCharLit{$1}", + [escLit]) + of tkIntLit..tkUInt64Lit: + dispA(d.conf, result, "<span class=\"DecNumber\">$1</span>", + "\\spanDecNumber{$1}", [escLit]) + of tkFloatLit..tkFloat128Lit: + dispA(d.conf, result, "<span class=\"FloatNumber\">$1</span>", + "\\spanFloatNumber{$1}", [escLit]) + of tkSymbol: + let s = getTokSym(r) + # -2 because of the whitespace in between: + if procTokenPos == tokenPos-2 and procLink != "": + dispA(d.conf, result, "<a href=\"#$2\"><span class=\"Identifier\">$1</span></a>", + "\\spanIdentifier{$1}", [escLit, procLink]) + elif s != nil and s.kind in {skType, skVar, skLet, skConst} and + sfExported in s.flags and s.owner != nil and + belongsToProjectPackage(d.conf, s.owner) and d.target == outHtml: + let external = externalDep(d, s.owner) + result.addf "<a href=\"$1#$2\"><span class=\"Identifier\">$3</span></a>", + [changeFileExt(external, "html"), literal, + escLit] + else: + dispA(d.conf, result, "<span class=\"Identifier\">$1</span>", + "\\spanIdentifier{$1}", [escLit]) + of tkSpaces, tkInvalid: + result.add(literal) + of tkHideableStart: + template fun(s) = dispA(d.conf, result, s, "\\spanOther{$1}", [escLit]) + if renderRunnableExamples in renderFlags: fun "$1" + else: + # 1st span is required for the JS to work properly + fun """ +<span> +<span class="Other pragmadots">...</span> +</span> +<span class="pragmawrap">""".replace("\n", "") # Must remove newlines because wrapped in a <pre> + of tkHideableEnd: + template fun(s) = dispA(d.conf, result, s, "\\spanOther{$1}", [escLit]) + if renderRunnableExamples in renderFlags: fun "$1" + else: fun "</span>" + of tkCurlyDotLe: dispA(d.conf, result, "$1", "\\spanOther{$1}", [escLit]) + of tkCurlyDotRi: dispA(d.conf, result, "$1", "\\spanOther{$1}", [escLit]) + of tkParLe, tkParRi, tkBracketLe, tkBracketRi, tkCurlyLe, tkCurlyRi, + tkBracketDotLe, tkBracketDotRi, tkParDotLe, + tkParDotRi, tkComma, tkSemiColon, tkColon, tkEquals, tkDot, tkDotDot, + tkAccent, tkColonColon, + tkGStrLit, tkGTripleStrLit, tkInfixOpr, tkPrefixOpr, tkPostfixOpr, + tkBracketLeColon: + dispA(d.conf, result, "<span class=\"Other\">$1</span>", "\\spanOther{$1}", + [escLit]) + +proc exampleOutputDir(d: PDoc): AbsoluteDir = d.conf.getNimcacheDir / RelativeDir"runnableExamples" + +proc runAllExamples(d: PDoc) = + # This used to be: `let backend = if isDefined(d.conf, "js"): "js"` (etc), however + # using `-d:js` (etc) cannot work properly, e.g. would fail with `importjs` + # since semantics are affected by `config.backend`, not by isDefined(d.conf, "js") + let outputDir = d.exampleOutputDir + for _, group in d.exampleGroups: + if group.docCmd == docCmdSkip: continue + let outp = outputDir / RelativeFile("$1_group$2_examples.nim" % [d.filename.splitFile.name, $group.index]) + group.code = "# autogenerated by docgen\n# source: $1\n# rdoccmd: $2\n$3" % [d.filename, group.rdoccmd, group.code] + writeFile(outp, group.code) + # most useful semantics is that `docCmd` comes after `rdoccmd`, so that we can (temporarily) override + # via command line + # D20210224T221756:here + var pathArgs = "--path:$path" % [ "path", quoteShell(d.conf.projectPath) ] + for p in d.conf.searchPaths: + pathArgs = "$args --path:$path" % [ "args", pathArgs, "path", quoteShell(p) ] + let cmd = "$nim $backend -r --lib:$libpath --warning:UnusedImport:off $pathArgs --nimcache:$nimcache $rdoccmd $docCmd $file" % [ + "nim", quoteShell(os.getAppFilename()), + "backend", $d.conf.backend, + "pathArgs", pathArgs, + "libpath", quoteShell(d.conf.libpath), + "nimcache", quoteShell(outputDir), + "file", quoteShell(outp), + "rdoccmd", group.rdoccmd, + "docCmd", group.docCmd, + ] + if d.conf.backend == backendJs and findNodeJs() == "": + discard "ignore JS runnableExample" + elif os.execShellCmd(cmd) != 0: + d.conf.quitOrRaise "[runnableExamples] failed: generated file: '$1' group: '$2' cmd: $3" % [outp.string, group[].prettyString, cmd] + else: + # keep generated source file `outp` to allow inspection. + rawMessage(d.conf, hintSuccess, ["runnableExamples: " & outp.string]) + # removeFile(outp.changeFileExt(ExeExt)) # it's in nimcache, no need to remove + +proc quoted(a: string): string = + result = "" + result.addQuoted(a) + +proc toInstantiationInfo(conf: ConfigRef, info: TLineInfo): (string, int, int) = + # xxx expose in compiler/lineinfos.nim + (conf.toMsgFilename(info), info.line.int, info.col.int + ColOffset) + +proc prepareExample(d: PDoc; n: PNode, topLevel: bool): tuple[rdoccmd: string, code: string] = + ## returns `rdoccmd` and source code for this runnableExamples + var rdoccmd = "" + if n.len < 2 or n.len > 3: globalError(d.conf, n.info, "runnableExamples invalid") + if n.len == 3: + let n1 = n[1] + # xxx this should be evaluated during sempass + if n1.kind notin nkStrKinds: globalError(d.conf, n1.info, "string litteral expected") + rdoccmd = n1.strVal + + let useRenderModule = false + let loc = d.conf.toFileLineCol(n.info) + let code = extractRunnableExamplesSource(d.conf, n) + + if d.conf.errorCounter > 0: + return (rdoccmd, code) + + let comment = "autogenerated by docgen\nloc: $1\nrdoccmd: $2" % [loc, rdoccmd] + let outputDir = d.exampleOutputDir + createDir(outputDir) + inc d.exampleCounter + let outp = outputDir / RelativeFile("$#_examples_$#.nim" % [d.filename.extractFilename.changeFileExt"", $d.exampleCounter]) + + if useRenderModule: + var docComment = newTree(nkCommentStmt) + docComment.comment = comment + var runnableExamples = newTree(nkStmtList, + docComment, + newTree(nkImportStmt, newStrNode(nkStrLit, "std/assertions")), + newTree(nkImportStmt, newStrNode(nkStrLit, d.filename))) + runnableExamples.info = n.info + for a in n.lastSon: runnableExamples.add a + + # buggy, refs bug #17292 + # still worth fixing as it can affect other code relying on `renderModule`, + # so we keep this code path here for now, which could still be useful in some + # other situations. + renderModule(runnableExamples, outp.string, conf = d.conf) + + else: + var code2 = code + if code.len > 0 and "codeReordering" notin code: + let codeIndent = extractRunnableExamplesSource(d.conf, n, indent = 2) + # hacky but simplest solution, until we devise a way to make `{.line.}` + # work without introducing a scope + code2 = """ +{.line: $#.}: +$# +""" % [$toInstantiationInfo(d.conf, n.info), codeIndent] + code2 = """ +#[ +$# +]# +import std/assertions +import $# +$# +""" % [comment, d.filename.quoted, code2] + writeFile(outp.string, code2) + + if rdoccmd notin d.exampleGroups: + d.exampleGroups[rdoccmd] = ExampleGroup(rdoccmd: rdoccmd, docCmd: d.conf.docCmd, index: d.exampleGroups.len) + d.exampleGroups[rdoccmd].code.add "import $1\n" % outp.string.quoted + + var codeShown: string + if topLevel: # refs https://github.com/nim-lang/RFCs/issues/352 + let title = canonicalImport(d.conf, AbsoluteFile d.filename) + codeShown = "import $#\n$#" % [title, code] + else: + codeShown = code + result = (rdoccmd, codeShown) + when false: + proc extractImports(n: PNode; result: PNode) = + if n.kind in {nkImportStmt, nkImportExceptStmt, nkFromStmt}: + result.add copyTree(n) + n.kind = nkEmpty + return + for i in 0..<n.safeLen: extractImports(n[i], result) + let imports = newTree(nkStmtList) + var savedLastSon = copyTree n.lastSon + extractImports(savedLastSon, imports) + for imp in imports: runnableExamples.add imp + runnableExamples.add newTree(nkBlockStmt, newNode(nkEmpty), copyTree savedLastSon) + +type RunnableState = enum + rsStart + rsComment + rsRunnable + rsDone + +proc getAllRunnableExamplesImpl(d: PDoc; n: PNode, dest: var ItemPre, + state: RunnableState, topLevel: bool): + RunnableState = + ##[ + Simple state machine to tell whether we render runnableExamples and doc comments. + This is to ensure that we can interleave runnableExamples and doc comments freely; + the logic is easy to change but currently a doc comment following another doc comment + will not render, to avoid rendering in following case: + + proc fn* = + runnableExamples: discard + ## d1 + runnableExamples: discard + ## d2 + + ## internal explanation # <- this one should be out; it's part of rest of function body and would likey not make sense in doc comment + discard # some code + ]## + + case n.kind + of nkCommentStmt: + if state in {rsStart, rsRunnable}: + dest.add genRecComment(d, n) + return rsComment + of nkCallKinds: + if isRunnableExamples(n[0]) and + n.len >= 2 and n.lastSon.kind == nkStmtList: + if state in {rsStart, rsComment, rsRunnable}: + let (rdoccmd, code) = prepareExample(d, n, topLevel) + var msg = "Example:" + if rdoccmd.len > 0: msg.add " cmd: " & rdoccmd + var s: string = "" + dispA(d.conf, s, "\n<p><strong class=\"examples_text\">$1</strong></p>\n", + "\n\n\\textbf{$1}\n", [msg]) + dest.add s + inc d.listingCounter + let id = $d.listingCounter + dest.add(d.config.getOrDefault"doc.listing_start" % [id, "langNim", ""]) + var dest2 = "" + renderNimCode(dest2, code, d.target) + dest.add dest2 + dest.add(d.config.getOrDefault"doc.listing_end" % id) + return rsRunnable + else: + localError(d.conf, n.info, errUser, "runnableExamples must appear before the first non-comment statement") + else: discard + return rsDone + # change this to `rsStart` if you want to keep generating doc comments + # and runnableExamples that occur after some code in routine + +proc getRoutineBody(n: PNode): PNode = + ##[ + nim transforms these quite differently: + + proc someType*(): int = + ## foo + result = 3 +=> + result = + ## foo + 3; + + proc someType*(): int = + ## foo + 3 +=> + ## foo + result = 3; + + so we normalize the results to get to the statement list containing the + (0 or more) doc comments and runnableExamples. + ]## + result = n[bodyPos] + + # This won't be transformed: result.id = 10. Namely result[0].kind != nkSym. + if result.kind == nkAsgn and result[0].kind == nkSym and + n.len > bodyPos+1 and n[bodyPos+1].kind == nkSym: + doAssert result.len == 2 + result = result[1] + +proc getAllRunnableExamples(d: PDoc, n: PNode, dest: var ItemPre) = + var n = n + var state = rsStart + template fn(n2, topLevel) = + state = getAllRunnableExamplesImpl(d, n2, dest, state, topLevel) + dest.add genComment(d, n) + case n.kind + of routineDefs: + n = n.getRoutineBody + case n.kind + of nkCommentStmt, nkCallKinds: fn(n, topLevel = false) + else: + for i in 0..<n.safeLen: + fn(n[i], topLevel = false) + if state == rsDone: discard # check all sons + else: fn(n, topLevel = true) + +proc isVisible(d: PDoc; n: PNode): bool = + result = false + if n.kind == nkPostfix: + if n.len == 2 and n[0].kind == nkIdent: + var v = n[0].ident + result = v.id == ord(wStar) or v.id == ord(wMinus) + elif n.kind == nkSym: + # we cannot generate code for forwarded symbols here as we have no + # exception tracking information here. Instead we copy over the comment + # from the proc header. + if optDocInternal in d.conf.globalOptions: + result = {sfFromGeneric, sfForward}*n.sym.flags == {} + else: + result = {sfExported, sfFromGeneric, sfForward}*n.sym.flags == {sfExported} + if result and containsOrIncl(d.emitted, n.sym.id): + result = false + elif n.kind == nkPragmaExpr: + result = isVisible(d, n[0]) + +proc getName(n: PNode): string = + case n.kind + of nkPostfix: result = getName(n[1]) + of nkPragmaExpr: result = getName(n[0]) + of nkSym: result = n.sym.renderDefinitionName + of nkIdent: result = n.ident.s + of nkAccQuoted: + result = "`" + for i in 0..<n.len: result.add(getName(n[i])) + result = "`" + of nkOpenSymChoice, nkClosedSymChoice, nkOpenSym: + result = getName(n[0]) + else: + result = "" + +proc getNameEsc(d: PDoc, n: PNode): string = + esc(d.target, getName(n)) + +proc getNameIdent(cache: IdentCache; n: PNode): PIdent = + case n.kind + of nkPostfix: result = getNameIdent(cache, n[1]) + of nkPragmaExpr: result = getNameIdent(cache, n[0]) + of nkSym: result = n.sym.name + of nkIdent: result = n.ident + of nkAccQuoted: + var r = "" + for i in 0..<n.len: r.add(getNameIdent(cache, n[i]).s) + result = getIdent(cache, r) + of nkOpenSymChoice, nkClosedSymChoice, nkOpenSym: + result = getNameIdent(cache, n[0]) + else: + result = nil + +proc getRstName(n: PNode): PRstNode = + case n.kind + of nkPostfix: result = getRstName(n[1]) + of nkPragmaExpr: result = getRstName(n[0]) + of nkSym: result = newRstLeaf(n.sym.renderDefinitionName) + of nkIdent: result = newRstLeaf(n.ident.s) + of nkAccQuoted: + result = getRstName(n[0]) + for i in 1..<n.len: result.text.add(getRstName(n[i]).text) + of nkOpenSymChoice, nkClosedSymChoice, nkOpenSym: + result = getRstName(n[0]) + else: + result = nil + +proc newUniquePlainSymbol(d: PDoc, original: string): string = + ## Returns a new unique plain symbol made up from the original. + ## + ## When a collision is found in the seenSymbols table, new numerical variants + ## with underscore + number will be generated. + if not d.seenSymbols.hasKey(original): + result = original + d.seenSymbols[original] = "" + return + # Iterate over possible numeric variants of the original name. + var count = 2 + while true: + result = original & "_" & $count + if not d.seenSymbols.hasKey(result): + d.seenSymbols[result] = "" + break + count += 1 + +proc complexName(k: TSymKind, n: PNode, baseName: string): string = + ## Builds a complex unique href name for the node. + ## + ## Pass as ``baseName`` the plain symbol obtained from the nodeName. The + ## format of the returned symbol will be ``baseName(.callable type)?,(param + ## type)?(,param type)*``. The callable type part will be added only if the + ## node is not a proc, as those are the common ones. The suffix will be a dot + ## and a single letter representing the type of the callable. The parameter + ## types will be added with a preceding dash. Return types won't be added. + ## + ## If you modify the output of this proc, please update the anchor generation + ## section of ``doc/docgen.rst``. + result = baseName + case k + of skProc, skFunc: discard + of skMacro: result.add(".m") + of skMethod: result.add(".e") + of skIterator: result.add(".i") + of skTemplate: result.add(".t") + of skConverter: result.add(".c") + else: discard + if n.safeLen > paramsPos and n[paramsPos].kind == nkFormalParams: + let params = renderParamTypes(n[paramsPos]) + if params.len > 0: + result.add(defaultParamSeparator) + result.add(params) + +proc docstringSummary(rstText: string): string = + ## Returns just the first line or a brief chunk of text from a rst string. + ## + ## Most docstrings will contain a one liner summary, so stripping at the + ## first newline is usually fine. If after that the content is still too big, + ## it is stripped at the first comma, colon or dot, usual English sentence + ## separators. + ## + ## No guarantees are made on the size of the output, but it should be small. + ## Also, we hope to not break the rst, but maybe we do. If there is any + ## trimming done, an ellipsis unicode char is added. + const maxDocstringChars = 100 + assert(rstText.len < 2 or (rstText[0] == '#' and rstText[1] == '#')) + result = rstText.substr(2).strip + var pos = result.find('\L') + if pos > 0: + result.setLen(pos - 1) + result.add("…") + if pos < maxDocstringChars: + return + # Try to keep trimming at other natural boundaries. + pos = result.find({'.', ',', ':'}) + let last = result.len - 1 + if pos > 0 and pos < last: + result.setLen(pos - 1) + result.add("…") + +proc genDeprecationMsg(d: PDoc, n: PNode): string = + ## Given a nkPragma wDeprecated node output a well-formatted section + if n == nil: return + + case n.safeLen: + of 0: # Deprecated w/o any message + result = getConfigVar(d.conf, "doc.deprecationmsg") % [ + "label" , "Deprecated", "message", ""] + of 2: # Deprecated w/ a message + if n[1].kind in {nkStrLit..nkTripleStrLit}: + result = getConfigVar(d.conf, "doc.deprecationmsg") % [ + "label", "Deprecated:", "message", xmltree.escape(n[1].strVal)] + else: + result = "" + else: + raiseAssert "unreachable" + +type DocFlags = enum + kDefault + kForceExport + +proc genSeeSrc(d: PDoc, path: string, line: int): string = + result = "" + let docItemSeeSrc = getConfigVar(d.conf, "doc.item.seesrc") + if docItemSeeSrc.len > 0: + let path = relativeTo(AbsoluteFile path, AbsoluteDir getCurrentDir(), '/') + when false: + let cwd = canonicalizePath(d.conf, getCurrentDir()) + var path = path + if path.startsWith(cwd): + path = path[cwd.len+1..^1].replace('\\', '/') + let gitUrl = getConfigVar(d.conf, "git.url") + if gitUrl.len > 0: + let defaultBranch = + if NimPatch mod 2 == 1: "devel" + else: "version-$1-$2" % [$NimMajor, $NimMinor] + let commit = getConfigVar(d.conf, "git.commit", defaultBranch) + let develBranch = getConfigVar(d.conf, "git.devel", "devel") + dispA(d.conf, result, "$1", "", [docItemSeeSrc % [ + "path", path.string, "line", $line, "url", gitUrl, + "commit", commit, "devel", develBranch]]) + +proc symbolPriority(k: TSymKind): int = + result = case k + of skMacro: -3 + of skTemplate: -2 + of skIterator: -1 + else: 0 # including skProc which have higher priority + # documentation itself has even higher priority 1 + +proc getTypeKind(n: PNode): string = + case n[2].kind + of nkEnumTy: "enum" + of nkObjectTy: "object" + of nkTupleTy: "tuple" + else: "" + +proc toLangSymbol(k: TSymKind, n: PNode, baseName: string): LangSymbol = + ## Converts symbol info (names/types/parameters) in `n` into format + ## `LangSymbol` convenient for ``rst.nim``/``dochelpers.nim``. + result = LangSymbol(name: baseName.nimIdentNormalize, + symKind: k.toHumanStr + ) + if k in routineKinds: + var + paramTypes: seq[string] = @[] + renderParamTypes(paramTypes, n[paramsPos], toNormalize=true) + let paramNames = renderParamNames(n[paramsPos], toNormalize=true) + # In some rare cases (system.typeof) parameter type is not set for default: + doAssert paramTypes.len <= paramNames.len + for i in 0 ..< paramNames.len: + if i < paramTypes.len: + result.parameters.add (paramNames[i], paramTypes[i]) + else: + result.parameters.add (paramNames[i], "") + result.parametersProvided = true + + result.outType = renderOutType(n[paramsPos], toNormalize=true) + + if k in {skProc, skFunc, skType, skIterator}: + # Obtain `result.generics` + # Use `n[miscPos]` since n[genericParamsPos] does not contain constraints + var genNode: PNode = nil + if k == skType: + genNode = n[1] # FIXME: what is index 1? + else: + if n[miscPos].kind != nkEmpty: + genNode = n[miscPos][1] # FIXME: what is index 1? + if genNode != nil: + var literal = "" + var r: TSrcGen = initTokRender(genNode, {renderNoBody, renderNoComments, + renderNoPragmas, renderNoProcDefs, renderExpandUsing, renderNoPostfix}) + var kind = tkEof + while true: + getNextTok(r, kind, literal) + if kind == tkEof: + break + if kind != tkSpaces: + result.generics.add(literal.nimIdentNormalize) + + if k == skType: result.symTypeKind = getTypeKind(n) + +proc genItem(d: PDoc, n, nameNode: PNode, k: TSymKind, docFlags: DocFlags, nonExports: bool = false) = + if (docFlags != kForceExport) and not isVisible(d, nameNode): return + let + name = getName(nameNode) + nameEsc = esc(d.target, name) + var plainDocstring = getPlainDocstring(n) # call here before genRecComment! + var result = "" + var literal, plainName = "" + var kind = tkEof + var comm: ItemPre = default(ItemPre) + if n.kind in routineDefs: + getAllRunnableExamples(d, n, comm) + else: + comm.add genRecComment(d, n) + + # Obtain the plain rendered string for hyperlink titles. + var r: TSrcGen = initTokRender(n, {renderNoBody, renderNoComments, renderDocComments, + renderNoPragmas, renderNoProcDefs, renderExpandUsing, renderNoPostfix}) + while true: + getNextTok(r, kind, literal) + if kind == tkEof: + break + plainName.add(literal) + + var pragmaNode = getDeclPragma(n) + if pragmaNode != nil: pragmaNode = findPragma(pragmaNode, wDeprecated) + + inc(d.id) + let + plainNameEsc = esc(d.target, plainName.strip) + typeDescr = + if k == skType and getTypeKind(n) != "": getTypeKind(n) + else: k.toHumanStr + detailedName = typeDescr & " " & ( + if k in routineKinds: plainName else: name) + uniqueName = if k in routineKinds: plainNameEsc else: nameEsc + sortName = if k in routineKinds: plainName.strip else: name + cleanPlainSymbol = renderPlainSymbolName(nameNode) + complexSymbol = complexName(k, n, cleanPlainSymbol) + plainSymbolEnc = encodeUrl(cleanPlainSymbol, usePlus = false) + symbolOrId = d.newUniquePlainSymbol(complexSymbol) + symbolOrIdEnc = encodeUrl(symbolOrId, usePlus = false) + deprecationMsg = genDeprecationMsg(d, pragmaNode) + rstLangSymbol = toLangSymbol(k, n, cleanPlainSymbol) + symNameNode = + if nameNode.kind == nkPostfix: nameNode[1] + else: nameNode + + # we generate anchors automatically for subsequent use in doc comments + let lineinfo = rstast.TLineInfo( + line: nameNode.info.line, col: nameNode.info.col, + fileIndex: addRstFileIndex(d, nameNode.info)) + addAnchorNim(d.sharedState, external = false, refn = symbolOrId, + tooltip = detailedName, langSym = rstLangSymbol, + priority = symbolPriority(k), info = lineinfo, + module = addRstFileIndex(d, FileIndex d.module.position)) + + var renderFlags = {renderNoBody, renderNoComments, renderDocComments, + renderSyms, renderExpandUsing, renderNoPostfix} + if nonExports: + renderFlags.incl renderNonExportedFields + nodeToHighlightedHtml(d, n, result, renderFlags, symbolOrIdEnc) + + let seeSrc = genSeeSrc(d, toFullPath(d.conf, n.info), n.info.line.int) + + d.section[k].secItems.mgetOrPut(cleanPlainSymbol, newSeq[Item]()).add Item( + descRst: comm, + sortName: sortName, + info: lineinfo, + anchor: symbolOrId, + detailedName: detailedName, + name: name, + substitutions: @[ + "uniqueName", uniqueName, + "header", result, "itemID", $d.id, + "header_plain", plainNameEsc, "itemSym", cleanPlainSymbol, + "itemSymEnc", plainSymbolEnc, + "itemSymOrIDEnc", symbolOrIdEnc, "seeSrc", seeSrc, + "deprecationMsg", deprecationMsg]) + + let external = d.destFile.AbsoluteFile.relativeTo(d.conf.outDir, '/').changeFileExt(HtmlExt).string + + var attype = "" + if k in routineKinds and symNameNode.kind == nkSym: + let att = attachToType(d, nameNode.sym) + if att != nil: + attype = esc(d.target, att.name.s) + elif k == skType and symNameNode.kind == nkSym and + symNameNode.sym.typ.kind in {tyEnum, tyBool}: + let etyp = symNameNode.sym.typ + for e in etyp.n: + if e.sym.kind != skEnumField: continue + let plain = renderPlainSymbolName(e) + let symbolOrId = d.newUniquePlainSymbol(plain) + setIndexTerm(d[], ieNim, htmlFile = external, id = symbolOrId, + term = plain, linkTitle = symNameNode.sym.name.s & '.' & plain, + linkDesc = xmltree.escape(getPlainDocstring(e).docstringSummary), + line = n.info.line.int) + + d.tocSimple[k].add TocItem( + sortName: sortName, + content: getConfigVar(d.conf, "doc.item.toc") % [ + "name", name, "header_plain", plainNameEsc, + "itemSymOrIDEnc", symbolOrIdEnc]) + + d.tocTable[k].mgetOrPut(cleanPlainSymbol, newSeq[TocItem]()).add TocItem( + sortName: sortName, + content: getConfigVar(d.conf, "doc.item.tocTable") % [ + "name", name, "header_plain", plainNameEsc, + "itemSymOrID", symbolOrId.replace(",", ",<wbr>"), + "itemSymOrIDEnc", symbolOrIdEnc]) + + setIndexTerm(d[], ieNim, htmlFile = external, id = symbolOrId, term = name, + linkTitle = detailedName, + linkDesc = xmltree.escape(plainDocstring.docstringSummary), + line = n.info.line.int) + if k == skType and symNameNode.kind == nkSym: + d.types.strTableAdd symNameNode.sym + +proc genJsonItem(d: PDoc, n, nameNode: PNode, k: TSymKind, nonExports = false): JsonItem = + if not isVisible(d, nameNode): return + var + name = getNameEsc(d, nameNode) + comm = genRecComment(d, n) + r: TSrcGen + renderFlags = {renderNoBody, renderNoComments, renderDocComments, + renderExpandUsing, renderNoPostfix} + if nonExports: + renderFlags.incl renderNonExportedFields + r = initTokRender(n, renderFlags) + result = JsonItem(json: %{ "name": %name, "type": %($k), "line": %n.info.line.int, + "col": %n.info.col} + ) + if comm != nil: + result.rst = comm + result.rstField = "description" + if r.buf.len > 0: + result.json["code"] = %r.buf + if k in routineKinds: + result.json["signature"] = newJObject() + if n[paramsPos][0].kind != nkEmpty: + result.json["signature"]["return"] = %($n[paramsPos][0]) + if n[paramsPos].len > 1: + result.json["signature"]["arguments"] = newJArray() + for paramIdx in 1 ..< n[paramsPos].len: + for identIdx in 0 ..< n[paramsPos][paramIdx].len - 2: + let + paramName = $n[paramsPos][paramIdx][identIdx] + paramType = $n[paramsPos][paramIdx][^2] + if n[paramsPos][paramIdx][^1].kind != nkEmpty: + let paramDefault = $n[paramsPos][paramIdx][^1] + result.json["signature"]["arguments"].add %{"name": %paramName, "type": %paramType, "default": %paramDefault} + else: + result.json["signature"]["arguments"].add %{"name": %paramName, "type": %paramType} + if n[pragmasPos].kind != nkEmpty: + result.json["signature"]["pragmas"] = newJArray() + for pragma in n[pragmasPos]: + result.json["signature"]["pragmas"].add %($pragma) + if n[genericParamsPos].kind != nkEmpty: + result.json["signature"]["genericParams"] = newJArray() + for genericParam in n[genericParamsPos]: + var param = %{"name": %($genericParam)} + if genericParam.sym.typ.len > 0: + param["types"] = newJArray() + param["types"] = %($genericParam.sym.typ.elementType) + result.json["signature"]["genericParams"].add param + if optGenIndex in d.conf.globalOptions: + genItem(d, n, nameNode, k, kForceExport) + +proc setDoctype(d: PDoc, n: PNode) = + ## Processes `{.doctype.}` pragma changing Markdown/RST parsing options. + if n == nil: + return + if n.len != 2: + localError(d.conf, n.info, errUser, + "doctype pragma takes exactly 1 argument" + ) + return + var dt = "" + case n[1].kind + of nkStrLit: + dt = toLowerAscii(n[1].strVal) + of nkIdent: + dt = toLowerAscii(n[1].ident.s) + else: + localError(d.conf, n.info, errUser, + "unknown argument type $1 provided to doctype" % [$n[1].kind] + ) + return + case dt + of "markdown": + d.sharedState.options.incl roSupportMarkdown + d.sharedState.options.incl roPreferMarkdown + of "rstmarkdown": + d.sharedState.options.incl roSupportMarkdown + d.sharedState.options.excl roPreferMarkdown + of "rst": + d.sharedState.options.excl roSupportMarkdown + d.sharedState.options.excl roPreferMarkdown + else: + localError(d.conf, n.info, errUser, + ( + "unknown doctype value \"$1\", should be from " & + "\"RST\", \"Markdown\", \"RstMarkdown\"" + ) % [dt] + ) + +proc checkForFalse(n: PNode): bool = + result = n.kind == nkIdent and cmpIgnoreStyle(n.ident.s, "false") == 0 + +proc traceDeps(d: PDoc, it: PNode) = + const k = skModule + if it.kind == nkInfix and it.len == 3 and it[2].kind == nkBracket: + let sep = it[0] + let dir = it[1] + let a = newNodeI(nkInfix, it.info) + a.add sep + a.add dir + a.add sep # dummy entry, replaced in the loop + for x in it[2]: + a[2] = x + traceDeps(d, a) + elif it.kind == nkSym and belongsToProjectPackage(d.conf, it.sym): + let external = externalDep(d, it.sym) + if d.section[k].finalMarkup != "": d.section[k].finalMarkup.add(", ") + dispA(d.conf, d.section[k].finalMarkup, + "<a class=\"reference external\" href=\"$2\">$1</a>", + "$1", [esc(d.target, external.prettyLink), + changeFileExt(external, "html")]) + +proc exportSym(d: PDoc; s: PSym) = + const k = exportSection + if s.kind == skModule and belongsToProjectPackage(d.conf, s): + let external = externalDep(d, s) + if d.section[k].finalMarkup != "": d.section[k].finalMarkup.add(", ") + dispA(d.conf, d.section[k].finalMarkup, + "<a class=\"reference external\" href=\"$2\">$1</a>", + "$1", [esc(d.target, external.prettyLink), + changeFileExt(external, "html")]) + elif s.kind != skModule and s.owner != nil: + let module = originatingModule(s) + if belongsToProjectPackage(d.conf, module): + let + complexSymbol = complexName(s.kind, s.ast, s.name.s) + symbolOrId = d.newUniquePlainSymbol(complexSymbol) + external = externalDep(d, module) + if d.section[k].finalMarkup != "": d.section[k].finalMarkup.add(", ") + # XXX proper anchor generation here + dispA(d.conf, d.section[k].finalMarkup, + "<a href=\"$2#$3\"><span class=\"Identifier\">$1</span></a>", + "$1", [esc(d.target, s.name.s), + changeFileExt(external, "html"), + symbolOrId]) + +proc documentNewEffect(cache: IdentCache; n: PNode): PNode = + let s = n[namePos].sym + if tfReturnsNew in s.typ.flags: + result = newIdentNode(getIdent(cache, "new"), n.info) + else: + result = nil + +proc documentEffect(cache: IdentCache; n, x: PNode, effectType: TSpecialWord, idx: int): PNode = + let spec = effectSpec(x, effectType) + if isNil(spec): + let s = n[namePos].sym + + let actual = s.typ.n[0] + if actual.len != effectListLen: return + let real = actual[idx] + if real == nil: return + let realLen = real.len + # warning: hack ahead: + var effects = newNodeI(nkBracket, n.info, realLen) + for i in 0..<realLen: + var t = typeToString(real[i].typ) + if t.startsWith("ref "): t = substr(t, 4) + effects[i] = newIdentNode(getIdent(cache, t), n.info) + # set the type so that the following analysis doesn't screw up: + effects[i].typ = real[i].typ + + result = newTreeI(nkExprColonExpr, n.info, + newIdentNode(getIdent(cache, $effectType), n.info), effects) + else: + result = nil + +proc documentWriteEffect(cache: IdentCache; n: PNode; flag: TSymFlag; pragmaName: string): PNode = + let s = n[namePos].sym + let params = s.typ.n + + var effects = newNodeI(nkBracket, n.info) + for i in 1..<params.len: + if params[i].kind == nkSym and flag in params[i].sym.flags: + effects.add params[i] + + if effects.len > 0: + result = newTreeI(nkExprColonExpr, n.info, + newIdentNode(getIdent(cache, pragmaName), n.info), effects) + else: + result = nil + +proc documentRaises*(cache: IdentCache; n: PNode) = + if n[namePos].kind != nkSym: return + let pragmas = n[pragmasPos] + let p1 = documentEffect(cache, n, pragmas, wRaises, exceptionEffects) + let p2 = documentEffect(cache, n, pragmas, wTags, tagEffects) + let p3 = documentWriteEffect(cache, n, sfWrittenTo, "writes") + let p4 = documentNewEffect(cache, n) + let p5 = documentWriteEffect(cache, n, sfEscapes, "escapes") + let p6 = documentEffect(cache, n, pragmas, wForbids, forbiddenEffects) + + if p1 != nil or p2 != nil or p3 != nil or p4 != nil or p5 != nil or p6 != nil: + if pragmas.kind == nkEmpty: + n[pragmasPos] = newNodeI(nkPragma, n.info) + if p1 != nil: n[pragmasPos].add p1 + if p2 != nil: n[pragmasPos].add p2 + if p3 != nil: n[pragmasPos].add p3 + if p4 != nil: n[pragmasPos].add p4 + if p5 != nil: n[pragmasPos].add p5 + if p6 != nil: n[pragmasPos].add p6 + +proc generateDoc*(d: PDoc, n, orig: PNode, config: ConfigRef, docFlags: DocFlags = kDefault) = + ## Goes through nim nodes recursively and collects doc comments. + ## Main function for `doc`:option: command, + ## which is implemented in ``docgen2.nim``. + template genItemAux(skind) = + genItem(d, n, n[namePos], skind, docFlags) + let showNonExports = optShowNonExportedFields in config.globalOptions + case n.kind + of nkPragma: + let pragmaNode = findPragma(n, wDeprecated) + d.modDeprecationMsg.add(genDeprecationMsg(d, pragmaNode)) + let doctypeNode = findPragma(n, wDoctype) + setDoctype(d, doctypeNode) + of nkCommentStmt: d.modDescPre.add(genComment(d, n)) + of nkProcDef, nkFuncDef: + when useEffectSystem: documentRaises(d.cache, n) + genItemAux(skProc) + of nkMethodDef: + when useEffectSystem: documentRaises(d.cache, n) + genItemAux(skMethod) + of nkIteratorDef: + when useEffectSystem: documentRaises(d.cache, n) + genItemAux(skIterator) + of nkMacroDef: genItemAux(skMacro) + of nkTemplateDef: genItemAux(skTemplate) + of nkConverterDef: + when useEffectSystem: documentRaises(d.cache, n) + genItemAux(skConverter) + of nkTypeSection, nkVarSection, nkLetSection, nkConstSection: + for i in 0..<n.len: + if n[i].kind != nkCommentStmt: + # order is always 'type var let const': + genItem(d, n[i], n[i][0], + succ(skType, ord(n.kind)-ord(nkTypeSection)), docFlags, showNonExports) + of nkStmtList: + for i in 0..<n.len: generateDoc(d, n[i], orig, config) + of nkWhenStmt: + # generate documentation for the first branch only: + if not checkForFalse(n[0][0]): + generateDoc(d, lastSon(n[0]), orig, config) + of nkImportStmt: + for it in n: traceDeps(d, it) + of nkExportStmt: + for it in n: + # bug #23051; don't generate documentation for exported symbols again + if it.kind == nkSym and sfExported notin it.sym.flags: + if d.module != nil and d.module == it.sym.owner: + generateDoc(d, it.sym.ast, orig, config, kForceExport) + elif it.sym.ast != nil: + exportSym(d, it.sym) + of nkExportExceptStmt: discard "transformed into nkExportStmt by semExportExcept" + of nkFromStmt, nkImportExceptStmt: traceDeps(d, n[0]) + of nkCallKinds: + var comm: ItemPre = default(ItemPre) + getAllRunnableExamples(d, n, comm) + if comm.len != 0: d.modDescPre.add(comm) + else: discard + +proc overloadGroupName(s: string, k: TSymKind): string = + ## Turns a name like `f` into anchor `f-procs-all` + s & "-" & k.toHumanStr & "s-all" + +proc setIndexTitle(d: PDoc, useMetaTitle: bool) = + let titleKind = if d.standaloneDoc: ieMarkupTitle else: ieNimTitle + let external = AbsoluteFile(d.destFile) + .relativeTo(d.conf.outDir, '/') + .changeFileExt(HtmlExt) + .string + var term, linkTitle: string + if useMetaTitle and d.meta[metaTitle].len != 0: + term = d.meta[metaTitleRaw] + linkTitle = d.meta[metaTitleRaw] + else: + let filename = extractFilename(d.filename) + term = + if d.standaloneDoc: filename # keep .rst/.md extension + else: changeFileExt(filename, "") # rm .nim extension + linkTitle = + if d.standaloneDoc: term # keep .rst/.md extension + else: canonicalImport(d.conf, AbsoluteFile d.filename) + if not d.standaloneDoc: + linkTitle = "module " & linkTitle + setIndexTerm(d[], titleKind, htmlFile = external, id = "", + term = term, linkTitle = linkTitle) + +proc finishGenerateDoc*(d: var PDoc) = + ## Perform 2nd RST pass for resolution of links/footnotes/headings... + # copy file map `filenames` to ``rstgen.nim`` for its warnings + d.filenames = d.sharedState.filenames + + # Main title/subtitle are allowed only in the first RST fragment of document + var firstRst = PRstNode(nil) + for fragment in d.modDescPre: + if fragment.isRst: + firstRst = fragment.rst + break + d.hasToc = d.hasToc or d.sharedState.hasToc + # in --index:only mode we do NOT want to load other .idx, only write ours: + let importdoc = optGenIndexOnly notin d.conf.globalOptions and + optNoImportdoc notin d.conf.globalOptions + preparePass2(d.sharedState, firstRst, importdoc) + + if optGenIndexOnly in d.conf.globalOptions: + # Top-level doc.comments may contain titles and :idx: statements: + for fragment in d.modDescPre: + if fragment.isRst: + traverseForIndex(d[], fragment.rst) + setIndexTitle(d, useMetaTitle = d.standaloneDoc) + # Symbol-associated doc.comments may contain :idx: statements: + for k in TSymKind: + for _, overloadChoices in d.section[k].secItems: + for item in overloadChoices: + for fragment in item.descRst: + if fragment.isRst: + traverseForIndex(d[], fragment.rst) + + # add anchors to overload groups before RST resolution + for k in TSymKind: + if k in routineKinds: + for plainName, overloadChoices in d.section[k].secItems: + if overloadChoices.len > 1: + let refn = overloadGroupName(plainName, k) + let tooltip = "$1 ($2 overloads)" % [ + k.toHumanStr & " " & plainName, $overloadChoices.len] + let name = nimIdentBackticksNormalize(plainName) + # save overload group to ``.idx`` + let external = d.destFile.AbsoluteFile.relativeTo(d.conf.outDir, '/'). + changeFileExt(HtmlExt).string + setIndexTerm(d[], ieNimGroup, htmlFile = external, id = refn, + term = name, linkTitle = k.toHumanStr, + linkDesc = "", line = overloadChoices[0].info.line.int) + if optGenIndexOnly in d.conf.globalOptions: continue + addAnchorNim(d.sharedState, external=false, refn, tooltip, + LangSymbol(symKind: k.toHumanStr, + name: name, + isGroup: true), + priority = symbolPriority(k), + # select index `0` just to have any meaningful warning: + info = overloadChoices[0].info, + module = addRstFileIndex(d, FileIndex d.module.position)) + + if optGenIndexOnly in d.conf.globalOptions: + return + + # Finalize fragments of ``.nim`` or ``.rst`` file + proc renderItemPre(d: PDoc, fragments: ItemPre, result: var string) = + for f in fragments: + case f.isRst: + of true: + var resolved = resolveSubs(d.sharedState, f.rst) + renderRstToOut(d[], resolved, result) + of false: result &= f.str + proc cmp(x, y: Item): int = cmpDecimalsIgnoreCase(x.sortName, y.sortName) + for k in TSymKind: + # add symbols to section for each `k`, while optionally wrapping + # overloadable items with the same basic name by ``doc.item2`` + let overloadableNames = toSeq(keys(d.section[k].secItems)) + for plainName in overloadableNames.sorted(cmpDecimalsIgnoreCase): + var overloadChoices = d.section[k].secItems[plainName] + overloadChoices.sort(cmp) + var nameContent = "" + for item in overloadChoices: + var itemDesc: string = "" + renderItemPre(d, item.descRst, itemDesc) + nameContent.add( + getConfigVar(d.conf, "doc.item") % ( + item.substitutions & @[ + "desc", itemDesc, + "name", item.name, + "itemSymOrID", item.anchor])) + if k in routineKinds: + let plainNameEsc1 = esc(d.target, plainName.strip) + let plainNameEsc2 = esc(d.target, plainName.strip, escMode=emUrl) + d.section[k].finalMarkup.add( + getConfigVar(d.conf, "doc.item2") % ( + @["header_plain", plainNameEsc1, + "overloadGroupName", overloadGroupName(plainNameEsc2, k), + "content", nameContent])) + else: + d.section[k].finalMarkup.add(nameContent) + d.section[k].secItems.clear + renderItemPre(d, d.modDescPre, d.modDescFinal) + d.modDescPre.setLen 0 + + # Finalize fragments of ``.json`` file + for i, entry in d.jEntriesPre: + if entry.rst != nil: + let resolved = resolveSubs(d.sharedState, entry.rst) + var str: string = "" + renderRstToOut(d[], resolved, str) + entry.json[entry.rstField] = %str + d.jEntriesPre[i].rst = nil + + d.jEntriesFinal.add entry.json # generates docs + + setIndexTitle(d, useMetaTitle = d.standaloneDoc) + completePass2(d.sharedState) + +proc add(d: PDoc; j: JsonItem) = + if j.json != nil or j.rst != nil: d.jEntriesPre.add j + +proc generateJson*(d: PDoc, n: PNode, config: ConfigRef, includeComments: bool = true) = + case n.kind + of nkPragma: + let doctypeNode = findPragma(n, wDoctype) + setDoctype(d, doctypeNode) + of nkCommentStmt: + if includeComments: + d.add JsonItem(rst: genComment(d, n), rstField: "comment", + json: %Table[string, string]()) + else: + d.modDescPre.add(genComment(d, n)) + of nkProcDef, nkFuncDef: + when useEffectSystem: documentRaises(d.cache, n) + d.add genJsonItem(d, n, n[namePos], skProc) + of nkMethodDef: + when useEffectSystem: documentRaises(d.cache, n) + d.add genJsonItem(d, n, n[namePos], skMethod) + of nkIteratorDef: + when useEffectSystem: documentRaises(d.cache, n) + d.add genJsonItem(d, n, n[namePos], skIterator) + of nkMacroDef: + d.add genJsonItem(d, n, n[namePos], skMacro) + of nkTemplateDef: + d.add genJsonItem(d, n, n[namePos], skTemplate) + of nkConverterDef: + when useEffectSystem: documentRaises(d.cache, n) + d.add genJsonItem(d, n, n[namePos], skConverter) + of nkTypeSection, nkVarSection, nkLetSection, nkConstSection: + for i in 0..<n.len: + if n[i].kind != nkCommentStmt: + # order is always 'type var let const': + d.add genJsonItem(d, n[i], n[i][0], + succ(skType, ord(n.kind)-ord(nkTypeSection)), optShowNonExportedFields in config.globalOptions) + of nkStmtList: + for i in 0..<n.len: + generateJson(d, n[i], config, includeComments) + of nkWhenStmt: + # generate documentation for the first branch only: + if not checkForFalse(n[0][0]): + generateJson(d, lastSon(n[0]), config, includeComments) + else: discard + +proc genTagsItem(d: PDoc, n, nameNode: PNode, k: TSymKind): string = + result = getNameEsc(d, nameNode) & "\n" + +proc generateTags*(d: PDoc, n: PNode, r: var string) = + case n.kind + of nkCommentStmt: + if startsWith(n.comment, "##"): + let stripped = n.comment.substr(2).strip + r.add stripped + of nkProcDef: + when useEffectSystem: documentRaises(d.cache, n) + r.add genTagsItem(d, n, n[namePos], skProc) + of nkFuncDef: + when useEffectSystem: documentRaises(d.cache, n) + r.add genTagsItem(d, n, n[namePos], skFunc) + of nkMethodDef: + when useEffectSystem: documentRaises(d.cache, n) + r.add genTagsItem(d, n, n[namePos], skMethod) + of nkIteratorDef: + when useEffectSystem: documentRaises(d.cache, n) + r.add genTagsItem(d, n, n[namePos], skIterator) + of nkMacroDef: + r.add genTagsItem(d, n, n[namePos], skMacro) + of nkTemplateDef: + r.add genTagsItem(d, n, n[namePos], skTemplate) + of nkConverterDef: + when useEffectSystem: documentRaises(d.cache, n) + r.add genTagsItem(d, n, n[namePos], skConverter) + of nkTypeSection, nkVarSection, nkLetSection, nkConstSection: + for i in 0..<n.len: + if n[i].kind != nkCommentStmt: + # order is always 'type var let const': + r.add genTagsItem(d, n[i], n[i][0], + succ(skType, ord(n.kind)-ord(nkTypeSection))) + of nkStmtList: + for i in 0..<n.len: + generateTags(d, n[i], r) + of nkWhenStmt: + # generate documentation for the first branch only: + if not checkForFalse(n[0][0]): + generateTags(d, lastSon(n[0]), r) + else: discard + +proc genSection(d: PDoc, kind: TSymKind, groupedToc = false) = + const sectionNames: array[skModule..skField, string] = [ + "Imports", "Types", "Vars", "Lets", "Consts", "Vars", "Procs", "Funcs", + "Methods", "Iterators", "Converters", "Macros", "Templates", "Exports" + ] + if d.section[kind].finalMarkup == "": return + var title = sectionNames[kind] + d.section[kind].finalMarkup = getConfigVar(d.conf, "doc.section") % [ + "sectionid", $ord(kind), "sectionTitle", title, + "sectionTitleID", $(ord(kind) + 50), "content", d.section[kind].finalMarkup] + + proc cmp(x, y: TocItem): int = cmpDecimalsIgnoreCase(x.sortName, y.sortName) + if groupedToc: + let overloadableNames = toSeq(keys(d.tocTable[kind])) + for plainName in overloadableNames.sorted(cmpDecimalsIgnoreCase): + var overloadChoices = d.tocTable[kind][plainName] + overloadChoices.sort(cmp) + var content: string = "" + for item in overloadChoices: + content.add item.content + d.toc2[kind].add getConfigVar(d.conf, "doc.section.toc2") % [ + "sectionid", $ord(kind), "sectionTitle", title, + "sectionTitleID", $(ord(kind) + 50), + "content", content, "plainName", plainName] + else: + for item in d.tocSimple[kind].sorted(cmp): + d.toc2[kind].add item.content + + let sectionValues = @[ + "sectionID", $ord(kind), "sectionTitleID", $(ord(kind) + 50), + "sectionTitle", title + ] + + # Check if the toc has any children + if d.toc2[kind] != "": + # Use the dropdown version instead and store the children in the dropdown + d.toc[kind] = getConfigVar(d.conf, "doc.section.toc") % (sectionValues & @[ + "content", d.toc2[kind] + ]) + else: + # Just have the link + d.toc[kind] = getConfigVar(d.conf, "doc.section.toc_item") % sectionValues + +proc relLink(outDir: AbsoluteDir, destFile: AbsoluteFile, linkto: RelativeFile): string = + $relativeTo(outDir / linkto, destFile.splitFile().dir, '/') + +proc genOutFile(d: PDoc, groupedToc = false): string = + var + code, content: string = "" + title = "" + var j = 0 + var toc = "" + renderTocEntries(d[], j, 1, toc) + for i in TSymKind: + var shouldSort = i in routineKinds and groupedToc + genSection(d, i, shouldSort) + toc.add(d.toc[i]) + if toc != "" or d.target == outLatex: + # for Latex $doc.toc will automatically generate TOC if `d.hasToc` is set + toc = getConfigVar(d.conf, "doc.toc") % ["content", toc] + for i in TSymKind: code.add(d.section[i].finalMarkup) + + # Extract the title. Non API modules generate an entry in the index table. + if d.meta[metaTitle].len != 0: + title = d.meta[metaTitle] + else: + title = canonicalImport(d.conf, AbsoluteFile d.filename) + title = esc(d.target, title) + var subtitle = "" + if d.meta[metaSubtitle] != "": + dispA(d.conf, subtitle, "<h2 class=\"subtitle\">$1</h2>", + "\\\\\\vspace{0.5em}\\large $1", [esc(d.target, d.meta[metaSubtitle])]) + + var groupsection = getConfigVar(d.conf, "doc.body_toc_groupsection") + let bodyname = if d.hasToc and not d.standaloneDoc and not d.conf.isLatexCmd: + groupsection.setLen 0 + "doc.body_toc_group" + elif d.hasToc: "doc.body_toc" + else: "doc.body_no_toc" + let seeSrc = genSeeSrc(d, d.filename, 1) + content = getConfigVar(d.conf, bodyname) % [ + "title", title, "subtitle", subtitle, + "tableofcontents", toc, "moduledesc", d.modDescFinal, "date", getDateStr(), + "time", getClockStr(), "content", code, + "deprecationMsg", d.modDeprecationMsg, + "theindexhref", relLink(d.conf.outDir, d.destFile.AbsoluteFile, + theindexFname.RelativeFile), + "body_toc_groupsection", groupsection, "seeSrc", seeSrc] + if optCompileOnly notin d.conf.globalOptions: + # XXX what is this hack doing here? 'optCompileOnly' means raw output!? + code = getConfigVar(d.conf, "doc.file") % [ + "nimdoccss", relLink(d.conf.outDir, d.destFile.AbsoluteFile, + nimdocOutCss.RelativeFile), + "dochackjs", relLink(d.conf.outDir, d.destFile.AbsoluteFile, + docHackJsFname.RelativeFile), + "title", title, "subtitle", subtitle, "tableofcontents", toc, + "moduledesc", d.modDescFinal, "date", getDateStr(), "time", getClockStr(), + "content", content, "author", d.meta[metaAuthor], + "version", esc(d.target, d.meta[metaVersion]), "analytics", d.analytics, + "deprecationMsg", d.modDeprecationMsg, "nimVersion", $NimMajor & "." & $NimMinor & "." & $NimPatch] + else: + code = content + result = code + +proc indexFile(d: PDoc): AbsoluteFile = + let dir = d.conf.outDir + result = dir / changeFileExt(presentationPath(d.conf, + AbsoluteFile d.filename), + IndexExt) + let (finalDir, _, _) = result.string.splitFile + createDir(finalDir) + +proc generateIndex*(d: PDoc) = + if optGenIndex in d.conf.globalOptions: + let dest = indexFile(d) + writeIndexFile(d[], dest.string) + +proc updateOutfile(d: PDoc, outfile: AbsoluteFile) = + if d.module == nil or sfMainModule in d.module.flags: # nil for e.g. for commandRst2Html + if d.conf.outFile.isEmpty: + d.conf.outFile = outfile.relativeTo(d.conf.outDir) + if isAbsolute(d.conf.outFile.string): + d.conf.outFile = splitPath(d.conf.outFile.string)[1].RelativeFile + +proc writeOutput*(d: PDoc, useWarning = false, groupedToc = false) = + if optGenIndexOnly in d.conf.globalOptions: + d.conf.outFile = indexFile(d).relativeTo(d.conf.outDir) # just for display + return + runAllExamples(d) + var content = genOutFile(d, groupedToc) + if optStdout in d.conf.globalOptions: + write(stdout, content) + else: + template outfile: untyped = d.destFile.AbsoluteFile + #let outfile = getOutFile2(d.conf, shortenDir(d.conf, filename), outExt) + let dir = outfile.splitFile.dir + createDir(dir) + updateOutfile(d, outfile) + try: + writeFile(outfile, content) + except IOError: + rawMessage(d.conf, if useWarning: warnCannotOpenFile else: errCannotOpenFile, + outfile.string) + if not d.wroteSupportFiles: # nimdoc.css + dochack.js + let nimr = $d.conf.getPrefixDir() + case d.target + of outHtml: + copyFile(docCss.interp(nimr = nimr), $d.conf.outDir / nimdocOutCss) + of outLatex: + copyFile(docCls.interp(nimr = nimr), $d.conf.outDir / nimdocOutCls) + if optGenIndex in d.conf.globalOptions: + let docHackJs2 = getDocHacksJs(nimr, nim = getAppFilename()) + copyFile(docHackJs2, $d.conf.outDir / docHackJs2.lastPathPart) + d.wroteSupportFiles = true + +proc writeOutputJson*(d: PDoc, useWarning = false) = + runAllExamples(d) + var modDesc: string = "" + for desc in d.modDescFinal: + modDesc &= desc + let content = %*{"orig": d.filename, + "nimble": getPackageName(d.conf, d.filename), + "moduleDescription": modDesc, + "entries": d.jEntriesFinal} + if optStdout in d.conf.globalOptions: + writeLine(stdout, $content) + else: + let dir = d.destFile.splitFile.dir + createDir(dir) + var f: File = default(File) + if open(f, d.destFile, fmWrite): + write(f, $content) + close(f) + updateOutfile(d, d.destFile.AbsoluteFile) + else: + localError(d.conf, newLineInfo(d.conf, AbsoluteFile d.filename, -1, -1), + warnUser, "unable to open file \"" & d.destFile & + "\" for writing") + +proc handleDocOutputOptions*(conf: ConfigRef) = + if optWholeProject in conf.globalOptions: + # Backward compatibility with previous versions + # xxx this is buggy when user provides `nim doc --project -o:sub/bar.html main`, + # it'd write to `sub/bar.html/main.html` + conf.outDir = AbsoluteDir(conf.outDir / conf.outFile) + +proc commandDoc*(cache: IdentCache, conf: ConfigRef) = + ## implementation of deprecated ``doc0`` command (without semantic checking) + handleDocOutputOptions conf + var ast = parseFile(conf.projectMainIdx, cache, conf) + if ast == nil: return + var d = newDocumentor(conf.projectFull, cache, conf, hasToc = true) + generateDoc(d, ast, ast, conf) + finishGenerateDoc(d) + writeOutput(d) + generateIndex(d) + +proc commandRstAux(cache: IdentCache, conf: ConfigRef; + filename: AbsoluteFile, outExt: string, + preferMarkdown: bool) = + var filen = addFileExt(filename, "txt") + var d = newDocumentor(filen, cache, conf, outExt, standaloneDoc = true, + preferMarkdown = preferMarkdown, hasToc = false) + try: + let rst = parseRst(readFile(filen.string), + line=LineRstInit, column=ColRstInit, + conf, d.sharedState) + d.modDescPre = @[ItemFragment(isRst: true, rst: rst)] + finishGenerateDoc(d) + writeOutput(d) + generateIndex(d) + except ERecoverableError: + discard "already reported the error" + +proc commandRst2Html*(cache: IdentCache, conf: ConfigRef, + preferMarkdown=false) = + commandRstAux(cache, conf, conf.projectFull, HtmlExt, preferMarkdown) + +proc commandRst2TeX*(cache: IdentCache, conf: ConfigRef, + preferMarkdown=false) = + commandRstAux(cache, conf, conf.projectFull, TexExt, preferMarkdown) + +proc commandJson*(cache: IdentCache, conf: ConfigRef) = + ## implementation of a deprecated jsondoc0 command + var ast = parseFile(conf.projectMainIdx, cache, conf) + if ast == nil: return + var d = newDocumentor(conf.projectFull, cache, conf, hasToc = true) + d.onTestSnippet = proc (d: var RstGenerator; filename, cmd: string; + status: int; content: string) {.gcsafe.} = + localError(conf, newLineInfo(conf, AbsoluteFile d.filename, -1, -1), + warnUser, "the ':test:' attribute is not supported by this backend") + generateJson(d, ast, conf) + finishGenerateDoc(d) + let json = d.jEntriesFinal + let content = pretty(json) + + if optStdout in d.conf.globalOptions: + write(stdout, content) + else: + #echo getOutFile(gProjectFull, JsonExt) + let filename = getOutFile(conf, RelativeFile conf.projectName, JsonExt) + try: + writeFile(filename, content) + except IOError: + rawMessage(conf, errCannotOpenFile, filename.string) + +proc commandTags*(cache: IdentCache, conf: ConfigRef) = + var ast = parseFile(conf.projectMainIdx, cache, conf) + if ast == nil: return + var d = newDocumentor(conf.projectFull, cache, conf, hasToc = true) + d.onTestSnippet = proc (d: var RstGenerator; filename, cmd: string; + status: int; content: string) {.gcsafe.} = + localError(conf, newLineInfo(conf, AbsoluteFile d.filename, -1, -1), + warnUser, "the ':test:' attribute is not supported by this backend") + var + content = "" + generateTags(d, ast, content) + + if optStdout in d.conf.globalOptions: + write(stdout, content) + else: + #echo getOutFile(gProjectFull, TagsExt) + let filename = getOutFile(conf, RelativeFile conf.projectName, TagsExt) + try: + writeFile(filename, content) + except IOError: + rawMessage(conf, errCannotOpenFile, filename.string) + +proc commandBuildIndex*(conf: ConfigRef, dir: string, outFile = RelativeFile"") = + if optGenIndexOnly in conf.globalOptions: + return + var content = mergeIndexes(dir) + + var outFile = outFile + if outFile.isEmpty: outFile = theindexFname.RelativeFile.changeFileExt("") + let filename = getOutFile(conf, outFile, HtmlExt) + + let code = getConfigVar(conf, "doc.file") % [ + "nimdoccss", relLink(conf.outDir, filename, nimdocOutCss.RelativeFile), + "dochackjs", relLink(conf.outDir, filename, docHackJsFname.RelativeFile), + "title", "Index", + "subtitle", "", "tableofcontents", "", "moduledesc", "", + "date", getDateStr(), "time", getClockStr(), + "content", content, "author", "", "version", "", "analytics", "", "nimVersion", $NimMajor & "." & $NimMinor & "." & $NimPatch] + # no analytics because context is not available + + try: + writeFile(filename, code) + except IOError: + rawMessage(conf, errCannotOpenFile, filename.string) + +proc commandBuildIndexJson*(conf: ConfigRef, dir: string, outFile = RelativeFile"") = + var (modules, symbols, docs) = readIndexDir(dir) + let documents = toSeq(keys(Table[IndexEntry, seq[IndexEntry]](docs))) + let body = %*({"documents": documents, "modules": modules, "symbols": symbols}) + + var outFile = outFile + if outFile.isEmpty: outFile = theindexFname.RelativeFile.changeFileExt("") + let filename = getOutFile(conf, outFile, JsonExt) + + try: + writeFile(filename, $body) + except IOError: + rawMessage(conf, errCannotOpenFile, filename.string) |