diff options
Diffstat (limited to 'lib/packages/docutils/rstgen.nim')
-rw-r--r-- | lib/packages/docutils/rstgen.nim | 1667 |
1 files changed, 1269 insertions, 398 deletions
diff --git a/lib/packages/docutils/rstgen.nim b/lib/packages/docutils/rstgen.nim index 53bd8188e..7fc0ac03a 100644 --- a/lib/packages/docutils/rstgen.nim +++ b/lib/packages/docutils/rstgen.nim @@ -1,216 +1,427 @@ # # -# Nimrod's Runtime Library +# Nim's Runtime Library # (c) Copyright 2012 Andreas Rumpf # # See the file "copying.txt", included in this # distribution, for details about the copyright. # -## This module implements a generator of HTML/Latex from `reStructuredText`:idx. +## This module implements a generator of HTML/Latex from +## `reStructuredText`:idx: (see http://docutils.sourceforge.net/rst.html for +## information on this markup syntax) and is used by the compiler's `docgen +## tools <docgen.html>`_. +## +## You can generate HTML output through the convenience proc ``rstToHtml``, +## which provided an input string with rst markup returns a string with the +## generated HTML. The final output is meant to be embedded inside a full +## document you provide yourself, so it won't contain the usual ``<header>`` or +## ``<body>`` parts. +## +## You can also create a ``RstGenerator`` structure and populate it with the +## other lower level methods to finally build complete documents. This requires +## many options and tweaking, but you are not limited to snippets and can +## generate `LaTeX documents <https://en.wikipedia.org/wiki/LaTeX>`_ too. +## +## `Docutils configuration files`_ are not supported. Instead HTML generation +## can be tweaked by editing file ``config/nimdoc.cfg``. +## +## .. _Docutils configuration files: https://docutils.sourceforge.io/docs/user/config.htm +## +## There are stylistic difference between how this module renders some elements +## and how original Python Docutils does: +## +## * Backreferences to TOC in section headings are not generated. +## In HTML each section is also a link that points to the section itself: +## this is done for user to be able to copy the link into clipboard. +## +## * The same goes for footnotes/citations links: they point to themselves. +## No backreferences are generated since finding all references of a footnote +## can be done by simply searching for ``[footnoteName]``. -import strutils, os, hashes, strtabs, rstast, rst, highlite +import std/[strutils, os, hashes, strtabs, tables, sequtils, + algorithm, parseutils, strbasics] + +import rstast, rst, rstidx, highlite + +when defined(nimPreviewSlimSystem): + import std/[assertions, syncio, formatfloat] + + +import ../../std/private/since const HtmlExt = "html" IndexExt* = ".idx" type - TOutputTarget* = enum ## which document type to generate + OutputTarget* = enum ## which document type to generate outHtml, # output is HTML outLatex # output is Latex - - TTocEntry{.final.} = object - n*: PRstNode - refname*, header*: string - - TMetaEnum* = enum - metaNone, metaTitle, metaSubtitle, metaAuthor, metaVersion - - TRstGenerator* = object of TObject - target*: TOutputTarget - config*: PStringTable + + MetaEnum* = enum + metaNone, metaTitleRaw, metaTitle, metaSubtitle, metaAuthor, metaVersion + + EscapeMode* = enum # in Latex text inside options [] and URLs is + # escaped slightly differently than in normal text + emText, emOption, emUrl # emText is currently used for code also + + RstGenerator* = object of RootObj + target*: OutputTarget + config*: StringTableRef splitAfter*: int # split too long entries in the TOC - tocPart*: seq[TTocEntry] + listingCounter*: int + tocPart*: seq[PRstNode] # headings for Table of Contents hasToc*: bool - theIndex: string - options*: TRstParseOptions - findFile*: TFindFileHandler - msgHandler*: TMsgHandler - filename*: string - meta*: array[TMetaEnum, string] - - PDoc = var TRstGenerator - -proc initRstGenerator*(g: var TRstGenerator, target: TOutputTarget, - config: PStringTable, filename: string, - options: TRstParseOptions, - findFile: TFindFileHandler, - msgHandler: TMsgHandler) = + theIndex: string # Contents of the index file to be dumped at the end. + findFile*: FindFileHandler + msgHandler*: MsgHandler + outDir*: string ## output directory, initialized by docgen.nim + destFile*: string ## output (HTML) file, initialized by docgen.nim + filenames*: RstFileTable + filename*: string ## source Nim or Rst file + meta*: array[MetaEnum, string] + currentSection: string ## \ + ## Stores the empty string or the last headline/overline found in the rst + ## document, so it can be used as a prettier name for term index generation. + seenIndexTerms: Table[string, int] ## \ + ## Keeps count of same text index terms to generate different identifiers + ## for hyperlinks. See renderIndexTerm proc for details. + id*: int ## A counter useful for generating IDs. + onTestSnippet*: proc (d: var RstGenerator; filename, cmd: string; status: int; + content: string) {.gcsafe.} + escMode*: EscapeMode + curQuotationDepth: int + + PDoc = var RstGenerator ## Alias to type less. + + CodeBlockParams = object ## Stores code block params. + numberLines: bool ## True if the renderer has to show line numbers. + startLine: int ## The starting line of the code block, by default 1. + langStr: string ## Input string used to specify the language. + lang: SourceLanguage ## Type of highlighting, by default none. + filename: string + testCmd: string + status: int + +proc prettyLink*(file: string): string = + changeFileExt(file, "").replace("_._", "..") + +proc init(p: var CodeBlockParams) = + ## Default initialisation of CodeBlockParams to sane values. + p.startLine = 1 + p.lang = langNone + p.langStr = "" + +proc initRstGenerator*(g: var RstGenerator, target: OutputTarget, + config: StringTableRef, filename: string, + findFile: FindFileHandler = nil, + msgHandler: MsgHandler = nil, + filenames = default(RstFileTable), + hasToc = false) = + ## Initializes a ``RstGenerator``. + ## + ## You need to call this before using a ``RstGenerator`` with any other + ## procs in this module. Pass a non ``nil`` ``StringTableRef`` value as + ## `config` with parameters used by the HTML output generator. If you don't + ## know what to use, pass the results of the `defaultConfig() + ## <#defaultConfig>_` proc. + ## + ## The `filename` parameter will be used for error reporting and creating + ## index hyperlinks to the file, but you can pass an empty string here if you + ## are parsing a stream in memory. If `filename` ends with the ``.nim`` + ## extension, the title for the document will be set by default to ``Module + ## filename``. This default title can be overridden by the embedded rst, but + ## it helps to prettify the generated index if no title is found. + ## + ## The ``RstParseOptions``, ``FindFileHandler`` and ``MsgHandler`` types + ## are defined in the `packages/docutils/rst module <rst.html>`_. + ## ``options`` selects the behaviour of the rst parser. + ## + ## ``findFile`` is a proc used by the rst ``include`` directive among others. + ## The purpose of this proc is to mangle or filter paths. It receives paths + ## specified in the rst document and has to return a valid path to existing + ## files or the empty string otherwise. If you pass ``nil``, a default proc + ## will be used which given a path returns the input path only if the file + ## exists. One use for this proc is to transform relative paths found in the + ## document to absolute path, useful if the rst file and the resources it + ## references are not in the same directory as the current working directory. + ## + ## The ``msgHandler`` is a proc used for user error reporting. It will be + ## called with the filename, line, col, and type of any error found during + ## parsing. If you pass ``nil``, a default message handler will be used which + ## writes the messages to the standard output. + ## + ## Example: + ## + ## ```nim + ## import packages/docutils/rstgen + ## + ## var gen: RstGenerator + ## gen.initRstGenerator(outHtml, defaultConfig(), "filename", {}) + ## ``` g.config = config g.target = target g.tocPart = @[] + g.hasToc = hasToc g.filename = filename + g.filenames = filenames g.splitAfter = 20 g.theIndex = "" - g.options = options g.findFile = findFile + g.currentSection = "" + g.id = 0 + g.escMode = emText + g.curQuotationDepth = 0 + let fileParts = filename.splitFile + if fileParts.ext == ".nim": + g.currentSection = "Module " & fileParts.name + g.seenIndexTerms = initTable[string, int]() g.msgHandler = msgHandler - - let s = config["split.item.toc"] + + let s = config.getOrDefault"split.item.toc" if s != "": g.splitAfter = parseInt(s) for i in low(g.meta)..high(g.meta): g.meta[i] = "" -proc writeIndexFile*(g: var TRstGenerator, outfile: string) = +proc writeIndexFile*(g: var RstGenerator, outfile: string) = + ## Writes the current index buffer to the specified output file. + ## + ## You previously need to add entries to the index with the `setIndexTerm() + ## <#setIndexTerm,RstGenerator,string,string,string,string,string>`_ proc. + ## If the index is empty the file won't be created. if g.theIndex.len > 0: writeFile(outfile, g.theIndex) - -proc addXmlChar(dest: var string, c: Char) = + +proc addHtmlChar(dest: var string, c: char) = + # Escapes HTML characters. Note that single quote ' is not escaped as + # ' -- unlike XML (for standards pre HTML5 it was even forbidden). case c of '&': add(dest, "&") of '<': add(dest, "<") of '>': add(dest, ">") of '\"': add(dest, """) else: add(dest, c) - -proc addRtfChar(dest: var string, c: Char) = - case c - of '{': add(dest, "\\{") - of '}': add(dest, "\\}") - of '\\': add(dest, "\\\\") - else: add(dest, c) - -proc addTexChar(dest: var string, c: Char) = + +proc addTexChar(dest: var string, c: char, escMode: EscapeMode) = + ## Escapes 10 special Latex characters and sometimes ` and [, ]. + ## TODO: @ is always a normal symbol (besides the header), am I wrong? + ## All escapes that need to work in text and code blocks (`emText` mode) + ## should start from \ (to be compatible with fancyvrb/fvextra). case c - of '_': add(dest, "\\_") - of '{': add(dest, "\\symbol{123}") - of '}': add(dest, "\\symbol{125}") - of '[': add(dest, "\\symbol{91}") - of ']': add(dest, "\\symbol{93}") - of '\\': add(dest, "\\symbol{92}") - of '$': add(dest, "\\$") - of '&': add(dest, "\\&") - of '#': add(dest, "\\#") - of '%': add(dest, "\\%") - of '~': add(dest, "\\symbol{126}") - of '@': add(dest, "\\symbol{64}") - of '^': add(dest, "\\symbol{94}") - of '`': add(dest, "\\symbol{96}") + of '_', '&', '#', '%': add(dest, "\\" & c) + # commands \label and \pageref don't accept \$ by some reason but OK with $: + of '$': (if escMode == emUrl: add(dest, c) else: add(dest, "\\" & c)) + # \~ and \^ have a special meaning unless they are followed by {} + of '~', '^': add(dest, "\\" & c & "{}") + # Latex loves to substitute ` to opening quote, even in texttt mode! + of '`': add(dest, "\\textasciigrave{}") + # add {} to avoid gobbling up space by \textbackslash + of '\\': add(dest, "\\textbackslash{}") + # Using { and } in URL in Latex: https://tex.stackexchange.com/a/469175 + of '{': + add(dest, if escMode == emUrl: "\\%7B" else: "\\{") + of '}': + add(dest, if escMode == emUrl: "\\%7D" else: "\\}") + of ']': + # escape ] inside an optional argument in e.g. \section[static[T]]{.. + add(dest, if escMode == emOption: "\\text{]}" else: "]") else: add(dest, c) -var splitter*: string = "<wbr />" +proc escChar*(target: OutputTarget, dest: var string, + c: char, escMode: EscapeMode) {.inline.} = + case target + of outHtml: addHtmlChar(dest, c) + of outLatex: addTexChar(dest, c, escMode) -proc escChar*(target: TOutputTarget, dest: var string, c: Char) {.inline.} = +proc addSplitter(target: OutputTarget; dest: var string) {.inline.} = case target - of outHtml: addXmlChar(dest, c) - of outLatex: addTexChar(dest, c) - -proc nextSplitPoint*(s: string, start: int): int = + of outHtml: add(dest, "<wbr />") + of outLatex: add(dest, "\\-") + +proc nextSplitPoint*(s: string, start: int): int = result = start - while result < len(s) + 0: + while result < len(s) + 0: case s[result] - of '_': return - of 'a'..'z': - if result + 1 < len(s) + 0: - if s[result + 1] in {'A'..'Z'}: return - else: nil + of '_': return + of 'a'..'z': + if result + 1 < len(s) + 0: + if s[result + 1] in {'A'..'Z'}: return + else: discard inc(result) dec(result) # last valid index - -proc esc*(target: TOutputTarget, s: string, splitAfter = -1): string = + +proc esc*(target: OutputTarget, s: string, splitAfter = -1, escMode = emText): string = + ## Escapes the HTML. result = "" - if splitAfter >= 0: + if splitAfter >= 0: var partLen = 0 var j = 0 - while j < len(s): + while j < len(s): var k = nextSplitPoint(s, j) - if (splitter != " ") or (partLen + k - j + 1 > splitAfter): - partLen = 0 - add(result, splitter) - for i in countup(j, k): escChar(target, result, s[i]) + #if (splitter != " ") or (partLen + k - j + 1 > splitAfter): + partLen = 0 + addSplitter(target, result) + for i in countup(j, k): escChar(target, result, s[i], escMode) inc(partLen, k - j + 1) j = k + 1 - else: - for i in countup(0, len(s) - 1): escChar(target, result, s[i]) + else: + for i in countup(0, len(s) - 1): escChar(target, result, s[i], escMode) -proc disp(target: TOutputTarget, xml, tex: string): string = - if target != outLatex: result = xml +proc disp(target: OutputTarget, xml, tex: string): string = + if target != outLatex: result = xml else: result = tex - -proc dispF(target: TOutputTarget, xml, tex: string, - args: varargs[string]): string = - if target != outLatex: result = xml % args + +proc dispF(target: OutputTarget, xml, tex: string, + args: varargs[string]): string = + if target != outLatex: result = xml % args else: result = tex % args - -proc dispA(target: TOutputTarget, dest: var string, + +proc dispA(target: OutputTarget, dest: var string, xml, tex: string, args: varargs[string]) = if target != outLatex: addf(dest, xml, args) else: addf(dest, tex, args) - -proc renderRstToOut*(d: PDoc, n: PRstNode, result: var string) -proc renderAux(d: PDoc, n: PRstNode, result: var string) = +proc `or`(x, y: string): string {.inline.} = + result = if x.len == 0: y else: x + +proc renderRstToOut*(d: var RstGenerator, n: PRstNode, result: var string) {.gcsafe.} + ## Writes into ``result`` the rst ast ``n`` using the ``d`` configuration. + ## + ## Before using this proc you need to initialise a ``RstGenerator`` with + ## ``initRstGenerator`` and parse a rst file with ``rstParse`` from the + ## `packages/docutils/rst module <rst.html>`_. Example: + ## ```nim + ## # ...configure gen and rst vars... + ## var generatedHtml = "" + ## renderRstToOut(gen, rst, generatedHtml) + ## echo generatedHtml + ## ``` + +proc renderAux(d: PDoc, n: PRstNode, result: var string) = for i in countup(0, len(n)-1): renderRstToOut(d, n.sons[i], result) -proc renderAux(d: PDoc, n: PRstNode, frmtA, frmtB: string, result: var string) = +template idS(txt: string): string = + if txt == "": "" + else: + case d.target + of outHtml: + " id=\"" & txt & "\"" + of outLatex: + "\\label{" & txt & "}\\hypertarget{" & txt & "}{}" + # we add \label for page number references via \pageref, while + # \hypertarget is for clickable links via \hyperlink. + +proc renderAux(d: PDoc, n: PRstNode, html, tex: string, result: var string) = + # formats sons of `n` as substitution variable $1 inside strings `html` and + # `tex`, internal target (anchor) is provided as substitute $2. var tmp = "" for i in countup(0, len(n)-1): renderRstToOut(d, n.sons[i], tmp) - if d.target != outLatex: - result.addf(frmtA, [tmp]) - else: - result.addf(frmtB, [tmp]) + case d.target + of outHtml: result.addf(html, [tmp, n.anchor.idS]) + of outLatex: result.addf(tex, [tmp, n.anchor.idS]) # ---------------- index handling -------------------------------------------- -proc setIndexTerm*(d: PDoc, id, term: string) = - d.theIndex.add(term) - d.theIndex.add('\t') - let htmlFile = changeFileExt(extractFilename(d.filename), HtmlExt) - d.theIndex.add(htmlFile) - d.theIndex.add('#') - d.theIndex.add(id) - d.theIndex.add("\n") +proc setIndexTerm*(d: var RstGenerator; k: IndexEntryKind, htmlFile, id, term: string, + linkTitle, linkDesc = "", line = 0) = + ## Adds a `term` to the index using the specified hyperlink identifier. + ## + ## A new entry will be added to the index using the format + ## ``term<tab>file#id``. The file part will come from the `htmlFile` + ## parameter. + ## + ## The `id` will be appended with a hash character only if its length is not + ## zero, otherwise no specific anchor will be generated. In general you + ## should only pass an empty `id` value for the title of standalone rst + ## documents (they are special for the `mergeIndexes() <#mergeIndexes,string>`_ + ## proc, see `Index (idx) file format <docgen.html#index-idx-file-format>`_ + ## for more information). Unlike other index terms, title entries are + ## inserted at the beginning of the accumulated buffer to maintain a logical + ## order of entries. + ## + ## If `linkTitle` or `linkDesc` are not the empty string, two additional + ## columns with their contents will be added. + ## + ## The index won't be written to disk unless you call `writeIndexFile() + ## <#writeIndexFile,RstGenerator,string>`_. The purpose of the index is + ## documented in the `docgen tools guide + ## <docgen.html#related-options-index-switch>`_. + let (entry, isTitle) = formatIndexEntry(k, htmlFile, id, term, + linkTitle, linkDesc, line) + if isTitle: d.theIndex.insert(entry) + else: d.theIndex.add(entry) proc hash(n: PRstNode): int = if n.kind == rnLeaf: result = hash(n.text) elif n.len > 0: result = hash(n.sons[0]) - for i in 1 .. <len(n): + for i in 1 ..< len(n): result = result !& hash(n.sons[i]) result = !$result -proc renderIndexTerm(d: PDoc, n: PRstNode, result: var string) = - let id = rstnodeToRefname(n) & '_' & $abs(hash(n)) +proc htmlFileRelPath(d: PDoc): string = + if d.outDir.len == 0: + # /foo/bar/zoo.nim -> zoo.html + changeFileExt(extractFilename(d.filename), HtmlExt) + else: # d is initialized in docgen.nim + # outDir = /foo -\ + # destFile = /foo/bar/zoo.html -|-> bar/zoo.html + d.destFile.relativePath(d.outDir, '/') + +proc renderIndexTerm*(d: PDoc, n: PRstNode, result: var string) = + ## Renders the string decorated within \`foobar\`\:idx\: markers. + ## + ## Additionally adds the enclosed text to the index as a term. Since we are + ## interested in different instances of the same term to have different + ## entries, a table is used to keep track of the amount of times a term has + ## previously appeared to give a different identifier value for each. + let refname = n.rstnodeToRefname + if d.seenIndexTerms.hasKey(refname): + d.seenIndexTerms[refname] = d.seenIndexTerms.getOrDefault(refname) + 1 + else: + d.seenIndexTerms[refname] = 1 + let id = refname & '_' & $d.seenIndexTerms.getOrDefault(refname) + var term = "" renderAux(d, n, term) - setIndexTerm(d, id, term) - dispA(d.target, result, "<span id=\"$1\">$2</span>", "$2\\label{$1}", + setIndexTerm(d, ieIdxRole, + htmlFileRelPath(d), id, term, d.currentSection) + dispA(d.target, result, "<span id=\"$1\">$2</span>", "\\nimindexterm{$1}{$2}", [id, term]) type - TIndexEntry {.pure, final.} = object - keyword: string - link: string - -proc cmp(a, b: TIndexEntry): int = - result = cmpIgnoreStyle(a.keyword, b.keyword) + IndexedDocs* = Table[IndexEntry, seq[IndexEntry]] ## \ + ## Contains the index sequences for doc types. + ## + ## The key is a *fake* IndexEntry which will contain the title of the + ## document in the `keyword` field and `link` will contain the html + ## filename for the document. `linkTitle` and `linkDesc` will be empty. + ## + ## The value indexed by this IndexEntry is a sequence with the real index + ## entries found in the ``.idx`` file. -proc `<-`(a: var TIndexEntry, b: TIndexEntry) = - shallowCopy a.keyword, b.keyword - shallowCopy a.link, b.link +when defined(gcDestructors): + template `<-`(a, b: var IndexEntry) = a = move(b) +else: + proc `<-`(a: var IndexEntry, b: IndexEntry) = + shallowCopy a.keyword, b.keyword + shallowCopy a.link, b.link + shallowCopy a.linkTitle, b.linkTitle + shallowCopy a.linkDesc, b.linkDesc + shallowCopy a.module, b.module -proc sortIndex(a: var openArray[TIndexEntry]) = +proc sortIndex(a: var openArray[IndexEntry]) = # we use shellsort here; fast and simple - let N = len(a) + let n = len(a) var h = 1 while true: h = 3 * h + 1 - if h > N: break + if h > n: break while true: h = h div 3 - for i in countup(h, N - 1): - var v: TIndexEntry + for i in countup(h, n - 1): + var v: IndexEntry v <- a[i] var j = i while cmp(a[j-h], v) >= 0: @@ -220,85 +431,309 @@ proc sortIndex(a: var openArray[TIndexEntry]) = a[j] <- v if h == 1: break -proc mergeIndexes*(dir: string): string = - ## merges all index files in `dir` and returns the generated index as HTML. - ## The result is no full HTML for flexibility. - var a: seq[TIndexEntry] - newSeq(a, 15_000) - setLen(a, 0) - var L = 0 - for kind, path in walkDir(dir): - if kind == pcFile and path.endsWith(IndexExt): - for line in lines(path): - let s = line.find('\t') - if s < 0: continue - setLen(a, L+1) - a[L].keyword = line.substr(0, s-1) - a[L].link = line.substr(s+1) - inc L - sortIndex(a) - result = "" +proc escapeLink(s: string): string = + ## This proc is mostly copied from uri/encodeUrl except that + ## these chars are also left unencoded: '#', '/'. + result = newStringOfCap(s.len + s.len shr 2) + for c in items(s): + case c + of 'a'..'z', 'A'..'Z', '0'..'9', '-', '.', '_', '~': # same as that in uri/encodeUrl + add(result, c) + of '#', '/': # example.com/foo/#bar (don't escape the '/' and '#' in such links) + add(result, c) + else: + add(result, "%") + add(result, toHex(ord(c), 2)) + +proc generateSymbolIndex(symbols: seq[IndexEntry]): string = + result = "<dl>" var i = 0 - while i < L: - result.addf("<dt><span>$1</span></dt><ul class=\"simple\"><dd>\n", - [a[i].keyword]) + while i < symbols.len: + let keyword = esc(outHtml, symbols[i].keyword) + let cleanedKeyword = keyword.escapeLink + result.addf("<dt><a name=\"$2\" href=\"#$2\"><span>$1:</span></a></dt><dd><ul class=\"simple\">\n", + [keyword, cleanedKeyword]) var j = i - while j < L and a[i].keyword == a[j].keyword: - result.addf( - "<li><a class=\"reference external\" href=\"$1\">$1</a></li>\n", - [a[j].link]) + while j < symbols.len and symbols[i].keyword == symbols[j].keyword: + let + url = symbols[j].link.escapeLink + module = symbols[j].module + text = + if symbols[j].linkTitle.len > 0: + esc(outHtml, module & ": " & symbols[j].linkTitle) + else: url + desc = symbols[j].linkDesc + if desc.len > 0: + result.addf("""<li><a class="reference external" + title="$3" data-doc-search-tag="$2" href="$1">$2</a></li> + """, [url, text, desc]) + else: + result.addf("""<li><a class="reference external" + data-doc-search-tag="$2" href="$1">$2</a></li> + """, [url, text]) inc j result.add("</ul></dd>\n") i = j - -# ---------------------------------------------------------------------------- - -proc renderHeadline(d: PDoc, n: PRstNode, result: var string) = + result.add("</dl>") + +proc stripTocLevel(s: string): tuple[level: int, text: string] = + ## Returns the *level* of the toc along with the text without it. + for c in 0 ..< s.len: + result.level = c + if s[c] != ' ': break + result.text = s[result.level ..< s.len] + +proc indentToLevel(level: var int, newLevel: int): string = + ## Returns the sequence of <ul>|</ul> characters to switch to `newLevel`. + ## + ## The amount of lists added/removed will be based on the `level` variable, + ## which will be reset to `newLevel` at the end of the proc. + result = "" + if level == newLevel: + return + if newLevel > level: + result = repeat("<li><ul>", newLevel - level) + else: + result = repeat("</ul></li>", level - newLevel) + level = newLevel + +proc generateDocumentationToc(entries: seq[IndexEntry]): string = + ## Returns the sequence of index entries in an HTML hierarchical list. + result = "" + # Build a list of levels and extracted titles to make processing easier. + var + titleRef: string + titleTag: string + levels: seq[tuple[level: int, text: string]] + L = 0 + level = 1 + levels.newSeq(entries.len) + for entry in entries: + let (rawLevel, rawText) = stripTocLevel(entry.linkTitle) + if rawLevel < 1: + # This is a normal symbol, push it *inside* one level from the last one. + levels[L].level = level + 1 + else: + # The level did change, update the level indicator. + level = rawLevel + levels[L].level = rawLevel + levels[L].text = rawText + inc L + + # Now generate hierarchical lists based on the precalculated levels. + result = "<ul>\n" + level = 1 + L = 0 + while L < entries.len: + let link = entries[L].link + if link.isDocumentationTitle: + titleRef = link + titleTag = levels[L].text + else: + result.add(level.indentToLevel(levels[L].level)) + result.addf("""<li><a class="reference" data-doc-search-tag="$1: $2" href="$3"> + $3</a></li> + """, [titleTag, levels[L].text, link, levels[L].text]) + inc L + result.add(level.indentToLevel(1) & "</ul>\n") + +proc generateDocumentationIndex(docs: IndexedDocs): string = + ## Returns all the documentation TOCs in an HTML hierarchical list. + result = "" + + # Sort the titles to generate their toc in alphabetical order. + var titles = toSeq(keys[IndexEntry, seq[IndexEntry]](docs)) + sort(titles, cmp) + + for title in titles: + let tocList = generateDocumentationToc(docs.getOrDefault(title)) + result.add("<ul><li><a href=\"" & + title.link & "\">" & title.linkTitle & "</a>\n" & tocList & "</li></ul>\n") + +proc generateDocumentationJumps(docs: IndexedDocs): string = + ## Returns a plain list of hyperlinks to documentation TOCs in HTML. + result = "Documents: " + + # Sort the titles to generate their toc in alphabetical order. + var titles = toSeq(keys[IndexEntry, seq[IndexEntry]](docs)) + sort(titles, cmp) + + var chunks: seq[string] = @[] + for title in titles: + chunks.add("<a href=\"" & title.link & "\">" & title.linkTitle & "</a>") + + result.add(chunks.join(", ") & ".<br/>") + +proc generateModuleJumps(modules: seq[string]): string = + ## Returns a plain list of hyperlinks to the list of modules. + result = "Modules: " + + var chunks: seq[string] = @[] + for name in modules: + chunks.add("<a href=\"$1.html\">$2</a>" % [name, name.prettyLink]) + + result.add(chunks.join(", ") & ".<br/>") + +proc readIndexDir*(dir: string): + tuple[modules: seq[string], symbols: seq[IndexEntry], docs: IndexedDocs] = + ## Walks `dir` reading ``.idx`` files converting them in IndexEntry items. + ## + ## Returns the list of found module names, the list of free symbol entries + ## and the different documentation indexes. The list of modules is sorted. + ## See the documentation of ``mergeIndexes`` for details. + result.modules = @[] + result.docs = initTable[IndexEntry, seq[IndexEntry]](32) + newSeq(result.symbols, 15_000) + setLen(result.symbols, 0) + var L = 0 + # Scan index files and build the list of symbols. + for path in walkDirRec(dir): + if path.endsWith(IndexExt): + var (fileEntries, title) = parseIdxFile(path) + # Depending on type add this to the list of symbols or table of APIs. + + if title.kind == ieNimTitle: + for i in 0 ..< fileEntries.len: + if fileEntries[i].kind != ieNim: + continue + # Ok, non TOC entry, add it. + setLen(result.symbols, L + 1) + result.symbols[L] = fileEntries[i] + inc L + if fileEntries.len > 0: + var x = fileEntries[0].link + let i = find(x, '#') + if i > 0: + x.setLen(i) + if i != 0: + # don't add entries starting with '#' + result.modules.add(x.changeFileExt("")) + else: + # Generate the symbolic anchor for index quickjumps. + title.aux = "doc_toc_" & $result.docs.len + result.docs[title] = fileEntries + + for i in 0 ..< fileEntries.len: + if fileEntries[i].kind != ieIdxRole: + continue + + setLen(result.symbols, L + 1) + result.symbols[L] = fileEntries[i] + inc L + +proc mergeIndexes*(dir: string): string = + ## Merges all index files in `dir` and returns the generated index as HTML. + ## + ## This proc will first scan `dir` for index files with the ``.idx`` + ## extension previously created by commands like ``nim doc|rst2html`` + ## which use the ``--index:on`` switch. These index files are the result of + ## calls to `setIndexTerm() + ## <#setIndexTerm,RstGenerator,string,string,string,string,string>`_ + ## and `writeIndexFile() <#writeIndexFile,RstGenerator,string>`_, so they are + ## simple tab separated files. + ## + ## As convention this proc will split index files into two categories: + ## documentation and API. API indices will be all joined together into a + ## single big sorted index, making the bulk of the final index. This is good + ## for API documentation because many symbols are repeated in different + ## modules. On the other hand, documentation indices are essentially table of + ## contents plus a few special markers. These documents will be rendered in a + ## separate section which tries to maintain the order and hierarchy of the + ## symbols in the index file. + ## + ## To differentiate between a documentation and API file a convention is + ## used: indices which contain one entry without the HTML hash character (#) + ## will be considered `documentation`, since this hash-less entry is the + ## explicit title of the document. Indices without this explicit entry will + ## be considered `generated API` extracted out of a source ``.nim`` file. + ## + ## Returns the merged and sorted indices into a single HTML block which can + ## be further embedded into nimdoc templates. + var (modules, symbols, docs) = readIndexDir(dir) + sort(modules, system.cmp) + + result = "" + # Generate a quick jump list of documents. + if docs.len > 0: + result.add(generateDocumentationJumps(docs)) + result.add("<p />") + + # Generate hyperlinks to all the linked modules. + if modules.len > 0: + result.add(generateModuleJumps(modules)) + result.add("<p />") + + when false: + # Generate the HTML block with API documents. + if docs.len > 0: + result.add("<h2>Documentation files</h2>\n") + result.add(generateDocumentationIndex(docs)) + + # Generate the HTML block with symbols. + if symbols.len > 0: + sortIndex(symbols) + result.add("<h2>API symbols</h2>\n") + result.add(generateSymbolIndex(symbols)) + + +# ---------------------------------------------------------------------------- + +proc renderHeadline(d: PDoc, n: PRstNode, result: var string) = var tmp = "" for i in countup(0, len(n) - 1): renderRstToOut(d, n.sons[i], tmp) - var refname = rstnodeToRefname(n) + d.currentSection = tmp + var tocName = esc(d.target, renderRstToText(n), escMode = emOption) + # for Latex: simple text without commands that may break TOC/hyperref if d.hasToc: - var length = len(d.tocPart) - setlen(d.tocPart, length + 1) - d.tocPart[length].refname = refname - d.tocPart[length].n = n - d.tocPart[length].header = tmp - - dispA(d.target, result, - "<h$1><a class=\"toc-backref\" id=\"$2\" href=\"#$2_toc\">$3</a></h$1>", - "\\rsth$4{$3}\\label{$2}\n", [$n.level, - d.tocPart[length].refname, tmp, - $chr(n.level - 1 + ord('A'))]) + d.tocPart.add n + dispA(d.target, result, "\n<h$1><a class=\"toc-backref\"" & + "$2 href=\"#$5\">$3</a></h$1>", "\\rsth$4[$6]{$3}$2\n", + [$n.level, n.anchor.idS, tmp, + $chr(n.level - 1 + ord('A')), n.anchor, tocName]) else: - dispA(d.target, result, "<h$1 id=\"$2\">$3</h$1>", - "\\rsth$4{$3}\\label{$2}\n", [ - $n.level, refname, tmp, - $chr(n.level - 1 + ord('A'))]) - -proc renderOverline(d: PDoc, n: PRstNode, result: var string) = - if d.meta[metaTitle].len == 0: + dispA(d.target, result, "\n<h$1$2>$3</h$1>", + "\\rsth$4[$5]{$3}$2\n", [ + $n.level, n.anchor.idS, tmp, + $chr(n.level - 1 + ord('A')), tocName]) + + # Generate index entry using spaces to indicate TOC level for the output HTML. + assert n.level >= 0 + setIndexTerm(d, ieHeading, htmlFile = d.htmlFileRelPath, id = n.anchor, + term = n.addNodes, linkTitle = spaces(max(0, n.level)) & tmp) + +proc renderOverline(d: PDoc, n: PRstNode, result: var string) = + if n.level == 0 and d.meta[metaTitle].len == 0: + d.meta[metaTitleRaw] = n.addNodes for i in countup(0, len(n)-1): renderRstToOut(d, n.sons[i], d.meta[metaTitle]) - elif d.meta[metaSubtitle].len == 0: + d.currentSection = d.meta[metaTitle] + elif n.level == 0 and d.meta[metaSubtitle].len == 0: for i in countup(0, len(n)-1): renderRstToOut(d, n.sons[i], d.meta[metaSubtitle]) + d.currentSection = d.meta[metaSubtitle] else: var tmp = "" for i in countup(0, len(n) - 1): renderRstToOut(d, n.sons[i], tmp) - dispA(d.target, result, "<h$1 id=\"$2\"><center>$3</center></h$1>", - "\\rstov$4{$3}\\label{$2}\n", [$n.level, - rstnodeToRefname(n), tmp, $chr(n.level - 1 + ord('A'))]) - + d.currentSection = tmp + var tocName = esc(d.target, renderRstToText(n), escMode=emOption) + dispA(d.target, result, "<h$1$2><center>$3</center></h$1>", + "\\rstov$4[$5]{$3}$2\n", [$n.level, + n.anchor.idS, tmp, $chr(n.level - 1 + ord('A')), tocName]) + setIndexTerm(d, ieHeading, htmlFile = d.htmlFileRelPath, id = n.anchor, + term = n.addNodes, linkTitle = spaces(max(0, n.level)) & tmp) -proc renderTocEntry(d: PDoc, e: TTocEntry, result: var string) = +proc renderTocEntry(d: PDoc, n: PRstNode, result: var string) = + var header = "" + for i in countup(0, len(n) - 1): renderRstToOut(d, n.sons[i], header) dispA(d.target, result, - "<li><a class=\"reference\" id=\"$1_toc\" href=\"#$1\">$2</a></li>\n", - "\\item\\label{$1_toc} $2\\ref{$1}\n", [e.refname, e.header]) + "<li><a class=\"reference\" id=\"$1_toc\" href=\"#$1\">$2</a></li>\n", + "\\item\\label{$1_toc} $2\\ref{$1}\n", [n.anchor, header]) -proc renderTocEntries*(d: PDoc, j: var int, lvl: int, result: var string) = +proc renderTocEntries*(d: var RstGenerator, j: var int, lvl: int, + result: var string) = var tmp = "" - while j <= high(d.tocPart): - var a = abs(d.tocPart[j].n.level) + while j <= high(d.tocPart): + var a = abs(d.tocPart[j].level) if a == lvl: renderTocEntry(d, d.tocPart[j], tmp) inc(j) @@ -307,332 +742,726 @@ proc renderTocEntries*(d: PDoc, j: var int, lvl: int, result: var string) = else: break if lvl > 1: - dispA(d.target, result, "<ul class=\"simple\">$1</ul>", + dispA(d.target, result, "<ul class=\"simple\">$1</ul>", "\\begin{enumerate}$1\\end{enumerate}", [tmp]) else: result.add(tmp) - -proc renderImage(d: PDoc, n: PRstNode, result: var string) = - var options = "" - var s = getFieldValue(n, "scale") - if s != "": dispA(d.target, options, " scale=\"$1\"", " scale=$1", [strip(s)]) - - s = getFieldValue(n, "height") - if s != "": dispA(d.target, options, " height=\"$1\"", " height=$1", [strip(s)]) - - s = getFieldValue(n, "width") - if s != "": dispA(d.target, options, " width=\"$1\"", " width=$1", [strip(s)]) - - s = getFieldValue(n, "alt") - if s != "": dispA(d.target, options, " alt=\"$1\"", "", [strip(s)]) - - s = getFieldValue(n, "align") - if s != "": dispA(d.target, options, " align=\"$1\"", "", [strip(s)]) - + +proc renderImage(d: PDoc, n: PRstNode, result: var string) = + let + arg = getArgument(n) + var + options = "" + + var s = esc(d.target, getFieldValue(n, "scale").strip()) + if s.len > 0: + dispA(d.target, options, " scale=\"$1\"", " scale=$1", [s]) + + s = esc(d.target, getFieldValue(n, "height").strip()) + if s.len > 0: + dispA(d.target, options, " height=\"$1\"", " height=$1", [s]) + + s = esc(d.target, getFieldValue(n, "width").strip()) + if s.len > 0: + dispA(d.target, options, " width=\"$1\"", " width=$1", [s]) + + s = esc(d.target, getFieldValue(n, "alt").strip()) + if s.len > 0: + dispA(d.target, options, " alt=\"$1\"", "", [s]) + + s = esc(d.target, getFieldValue(n, "align").strip()) + if s.len > 0: + dispA(d.target, options, " align=\"$1\"", "", [s]) + if options.len > 0: options = dispF(d.target, "$1", "[$1]", [options]) - - dispA(d.target, result, "<img src=\"$1\"$2 />", "\\includegraphics$2{$1}", - [getArgument(n), options]) + + var htmlOut = "" + if arg.endsWith(".mp4") or arg.endsWith(".ogg") or + arg.endsWith(".webm"): + htmlOut = """ + <video$3 src="$1"$2 autoPlay='true' loop='true' muted='true'> + Sorry, your browser doesn't support embedded videos + </video> + """ + else: + htmlOut = "<img$3 src=\"$1\"$2/>" + + # support for `:target:` links for images: + var target = esc(d.target, getFieldValue(n, "target").strip(), escMode=emUrl) + discard safeProtocol(target) + + if target.len > 0: + # `htmlOut` needs to be of the following format for link to work for images: + # <a class="reference external" href="target"><img src=\"$1\"$2/></a> + var htmlOutWithLink = "" + dispA(d.target, htmlOutWithLink, + "<a class=\"reference external\" href=\"$2\">$1</a>", + "\\href{$2}{$1}", [htmlOut, target]) + htmlOut = htmlOutWithLink + + dispA(d.target, result, htmlOut, "$3\\includegraphics$2{$1}", + [esc(d.target, arg), options, n.anchor.idS]) if len(n) >= 3: renderRstToOut(d, n.sons[2], result) - + proc renderSmiley(d: PDoc, n: PRstNode, result: var string) = dispA(d.target, result, - """<img src="/images/smilies/$1.gif" width="15" - height="17" hspace="2" vspace="2" />""", - "\\includegraphics{$1}", [n.text]) - -proc renderCodeBlock(d: PDoc, n: PRstNode, result: var string) = + """<img src="$1" width="15" + height="17" hspace="2" vspace="2" class="smiley" />""", + "\\includegraphics{$1}", + [d.config.getOrDefault"doc.smiley_format" % n.text]) + +proc getField1Int(d: PDoc, n: PRstNode, fieldName: string): int = + template err(msg: string) = + rstMessage(d.filenames, d.msgHandler, n.info, meInvalidField, msg) + let value = n.getFieldValue + var number: int + let nChars = parseInt(value, number) + if nChars == 0: + if value.len == 0: + # use a good default value: + result = 1 + else: + err("field $1 requires an integer, but '$2' was given" % + [fieldName, value]) + elif nChars < value.len: + err("extra arguments were given to $1: '$2'" % + [fieldName, value[nChars..^1]]) + else: + result = number + +proc parseCodeBlockField(d: PDoc, n: PRstNode, params: var CodeBlockParams) = + ## Parses useful fields which can appear before a code block. + ## + ## This supports the special ``default-language`` internal string generated + ## by the ``rst`` module to communicate a specific default language. + case n.getArgument.toLowerAscii + of "number-lines": + params.numberLines = true + # See if the field has a parameter specifying a different line than 1. + params.startLine = getField1Int(d, n, "number-lines") + of "file", "filename": + # The ``file`` option is a Nim extension to the official spec, it acts + # like it would for other directives like ``raw`` or ``cvs-table``. This + # field is dealt with in ``rst.nim`` which replaces the existing block with + # the referenced file, so we only need to ignore it here to avoid incorrect + # warning messages. + params.filename = n.getFieldValue.strip + of "test": + params.testCmd = n.getFieldValue.strip + if params.testCmd.len == 0: + # factor with D20210224T221756. Note that `$docCmd` should appear before `$file` + # but after all other options, but currently `$options` merges both options and `$file` so it's tricky. + params.testCmd = "$nim r --backend:$backend --lib:$libpath $docCmd $options" + else: + # consider whether `$docCmd` should be appended here too + params.testCmd = unescape(params.testCmd) + of "status", "exitcode": + params.status = getField1Int(d, n, n.getArgument) + of "default-language": + params.langStr = n.getFieldValue.strip + params.lang = params.langStr.getSourceLanguage + else: + rstMessage(d.filenames, d.msgHandler, n.info, mwUnsupportedField, + n.getArgument) + +proc parseCodeBlockParams(d: PDoc, n: PRstNode): CodeBlockParams = + ## Iterates over all code block fields and returns processed params. + ## + ## Also processes the argument of the directive as the default language. This + ## is done last so as to override any internal communication field variables. + result.init + if n.isNil: + return + assert n.kind in {rnCodeBlock, rnInlineCode} + + # Parse the field list for rendering parameters if there are any. + if not n.sons[1].isNil: + for son in n.sons[1].sons: d.parseCodeBlockField(son, result) + + # Parse the argument and override the language. + result.langStr = strip(getArgument(n)) + if result.langStr != "": + result.lang = getSourceLanguage(result.langStr) + +proc buildLinesHtmlTable(d: PDoc; params: CodeBlockParams, code: string, + idStr: string): + tuple[beginTable, endTable: string] = + ## Returns the necessary tags to start/end a code block in HTML. + ## + ## If the numberLines has not been used, the tags will default to a simple + ## <pre> pair. Otherwise it will build a table and insert an initial column + ## with all the line numbers, which requires you to pass the `code` to detect + ## how many lines have to be generated (and starting at which point!). + inc d.listingCounter + let id = $d.listingCounter + if not params.numberLines: + result = (d.config.getOrDefault"doc.listing_start" % + [id, sourceLanguageToStr[params.lang], idStr], + d.config.getOrDefault"doc.listing_end" % id) + return + + var codeLines = code.strip.countLines + assert codeLines > 0 + result.beginTable = """<table$1 class="line-nums-table">""" % [idStr] & + """<tbody><tr><td class="blob-line-nums"><pre class="line-nums">""" + var line = params.startLine + while codeLines > 0: + result.beginTable.add($line & "\n") + line.inc + codeLines.dec + result.beginTable.add("</pre></td><td>" & ( + d.config.getOrDefault"doc.listing_start" % + [id, sourceLanguageToStr[params.lang], idStr])) + result.endTable = (d.config.getOrDefault"doc.listing_end" % id) & + "</td></tr></tbody></table>" & ( + d.config.getOrDefault"doc.listing_button" % id) + +proc renderCodeLang*(result: var string, lang: SourceLanguage, code: string, + target: OutputTarget) = + var g: GeneralTokenizer + initGeneralTokenizer(g, code) + while true: + getNextToken(g, lang) + case g.kind + of gtEof: break + of gtNone, gtWhitespace: + add(result, substr(code, g.start, g.length + g.start - 1)) + else: + dispA(target, result, "<span class=\"$2\">$1</span>", "\\span$2{$1}", [ + esc(target, substr(code, g.start, g.length+g.start-1)), + tokenClassToStr[g.kind]]) + deinitGeneralTokenizer(g) + +proc renderNimCode*(result: var string, code: string, target: OutputTarget) = + renderCodeLang(result, langNim, code, target) + +proc renderCode(d: PDoc, n: PRstNode, result: var string) {.gcsafe.} = + ## Renders a code (code block or inline code), appending it to `result`. + ## + ## If the code block uses the ``number-lines`` option, a table will be + ## generated with two columns, the first being a list of numbers and the + ## second the code block itself. The code block can use syntax highlighting, + ## which depends on the directive argument specified by the rst input, and + ## may also come from the parser through the internal ``default-language`` + ## option to differentiate between a plain code block and Nim's code block + ## extension. + assert n.kind in {rnCodeBlock, rnInlineCode} + var params = d.parseCodeBlockParams(n) if n.sons[2] == nil: return var m = n.sons[2].sons[0] assert m.kind == rnLeaf - var langstr = strip(getArgument(n)) - var lang: TSourceLanguage - if langstr == "": - lang = langNimrod # default language - else: - lang = getSourceLanguage(langstr) - - dispA(d.target, result, "<pre>", "\\begin{rstpre}\n", []) - if lang == langNone: - d.msgHandler(d.filename, 1, 0, mwUnsupportedLanguage, langstr) - result.add(m.text) + + if params.testCmd.len > 0 and d.onTestSnippet != nil: + d.onTestSnippet(d, params.filename, params.testCmd, params.status, m.text) + + var blockStart, blockEnd: string + case d.target + of outHtml: + if n.kind == rnCodeBlock: + (blockStart, blockEnd) = buildLinesHtmlTable(d, params, m.text, + n.anchor.idS) + else: # rnInlineCode + blockStart = "<tt class=\"docutils literal\"><span class=\"pre\">" + blockEnd = "</span></tt>" + of outLatex: + if n.kind == rnCodeBlock: + blockStart = "\n\n" & n.anchor.idS & "\\begin{rstpre}\n" + blockEnd = "\n\\end{rstpre}\n\n" + else: # rnInlineCode + blockStart = "\\rstcode{" + blockEnd = "}" + dispA(d.target, result, blockStart, blockStart, []) + if params.lang == langNone: + if len(params.langStr) > 0 and params.langStr.toLowerAscii != "none": + rstMessage(d.filenames, d.msgHandler, n.info, mwUnsupportedLanguage, + params.langStr) + for letter in m.text: escChar(d.target, result, letter, emText) else: - var g: TGeneralTokenizer - initGeneralTokenizer(g, m.text) - while true: - getNextToken(g, lang) - case g.kind - of gtEof: break - of gtNone, gtWhitespace: - add(result, substr(m.text, g.start, g.length + g.start - 1)) - else: - dispA(d.target, result, "<span class=\"$2\">$1</span>", "\\span$2{$1}", [ - esc(d.target, substr(m.text, g.start, g.length+g.start-1)), - tokenClassToStr[g.kind]]) - deinitGeneralTokenizer(g) - dispA(d.target, result, "</pre>", "\n\\end{rstpre}\n") - -proc renderContainer(d: PDoc, n: PRstNode, result: var string) = + renderCodeLang(result, params.lang, m.text, d.target) + dispA(d.target, result, blockEnd, blockEnd) + +proc renderContainer(d: PDoc, n: PRstNode, result: var string) = var tmp = "" renderRstToOut(d, n.sons[2], tmp) - var arg = strip(getArgument(n)) - if arg == "": + var arg = esc(d.target, strip(getArgument(n))) + if arg == "": dispA(d.target, result, "<div>$1</div>", "$1", [tmp]) else: dispA(d.target, result, "<div class=\"$1\">$2</div>", "$2", [arg, tmp]) - -proc texColumns(n: PRstNode): string = - result = "" - for i in countup(1, len(n)): add(result, "|X") - -proc renderField(d: PDoc, n: PRstNode, result: var string) = + +proc renderField(d: PDoc, n: PRstNode, result: var string) = var b = false - if d.target == outLatex: + if d.target == outLatex: var fieldname = addNodes(n.sons[0]) var fieldval = esc(d.target, strip(addNodes(n.sons[1]))) - if cmpIgnoreStyle(fieldname, "author") == 0 or + if cmpIgnoreStyle(fieldname, "author") == 0 or cmpIgnoreStyle(fieldname, "authors") == 0: if d.meta[metaAuthor].len == 0: d.meta[metaAuthor] = fieldval b = true - elif cmpIgnoreStyle(fieldName, "version") == 0: + elif cmpIgnoreStyle(fieldname, "version") == 0: if d.meta[metaVersion].len == 0: d.meta[metaVersion] = fieldval b = true if not b: renderAux(d, n, "<tr>$1</tr>\n", "$1", result) - + +proc renderEnumList(d: PDoc, n: PRstNode, result: var string) = + var + specifier = "" + specStart = "" + i1 = 0 + pre = "" + i2 = n.labelFmt.len - 1 + post = "" + if n.labelFmt[0] == '(': + i1 = 1 + pre = "(" + if n.labelFmt[^1] == ')' or n.labelFmt[^1] == '.': + i2 = n.labelFmt.len - 2 + post = $n.labelFmt[^1] + let enumR = i1 .. i2 # enumerator range without surrounding (, ), . + if d.target == outLatex: + result.add ("\n%" & n.labelFmt & "\n") + # use enumerate parameters from package enumitem + if n.labelFmt[i1].isDigit: + var labelDef = "" + if pre != "" or post != "": + labelDef = "label=" & pre & "\\arabic*" & post & "," + if n.labelFmt[enumR] != "1": + specStart = "start=$1" % [n.labelFmt[enumR]] + if labelDef != "" or specStart != "": + specifier = "[$1$2]" % [labelDef, specStart] + else: + let (first, labelDef) = + if n.labelFmt[i1].isUpperAscii: ('A', "label=" & pre & "\\Alph*" & post) + else: ('a', "label=" & pre & "\\alph*" & post) + if n.labelFmt[i1] != first: + specStart = ",start=" & $(ord(n.labelFmt[i1]) - ord(first) + 1) + specifier = "[$1$2]" % [labelDef, specStart] + else: # HTML + # TODO: implement enumerator formatting using pre and post ( and ) for HTML + if n.labelFmt[i1].isDigit: + if n.labelFmt[enumR] != "1": + specStart = " start=\"$1\"" % [n.labelFmt[enumR]] + specifier = "class=\"simple\"" & specStart + else: + let (first, labelDef) = + if n.labelFmt[i1].isUpperAscii: ('A', "class=\"upperalpha simple\"") + else: ('a', "class=\"loweralpha simple\"") + if n.labelFmt[i1] != first: + specStart = " start=\"$1\"" % [ $(ord(n.labelFmt[i1]) - ord(first) + 1) ] + specifier = labelDef & specStart + renderAux(d, n, "<ol$2 " & specifier & ">$1</ol>\n", + "\\begin{enumerate}" & specifier & "$2$1\\end{enumerate}\n", + result) + +proc renderAdmonition(d: PDoc, n: PRstNode, result: var string) = + var + htmlCls = "admonition_warning" + texSz = "\\large" + texColor = "orange" + case n.adType + of "hint", "note", "tip": + htmlCls = "admonition-info"; texSz = "\\normalsize"; texColor = "green" + of "attention", "admonition", "important", "warning", "caution": + htmlCls = "admonition-warning"; texSz = "\\large"; texColor = "orange" + of "danger", "error": + htmlCls = "admonition-error"; texSz = "\\Large"; texColor = "red" + else: discard + let txt = n.adType.capitalizeAscii() + let htmlHead = "<div class=\"admonition " & htmlCls & "\">" + renderAux(d, n, + htmlHead & "<span$2 class=\"" & htmlCls & "-text\"><b>" & txt & + ":</b></span>\n" & "$1</div>\n", + "\n\n\\begin{rstadmonition}[borderline west={0.2em}{0pt}{" & + texColor & "}]$2\n" & + "{" & texSz & "\\color{" & texColor & "}{\\textbf{" & txt & ":}}} " & + "$1\n\\end{rstadmonition}\n", + result) + +proc renderHyperlink(d: PDoc, text, link: PRstNode, result: var string, + external: bool, nimdoc = false, tooltip="") = + var linkStr = "" + block: + let mode = d.escMode + d.escMode = emUrl + renderRstToOut(d, link, linkStr) + d.escMode = mode + discard safeProtocol(linkStr) + var textStr = "" + renderRstToOut(d, text, textStr) + let nimDocStr = if nimdoc: " nimdoc" else: "" + var tooltipStr = "" + if tooltip != "": + tooltipStr = """ title="$1"""" % [ esc(d.target, tooltip) ] + if external: + dispA(d.target, result, + "<a class=\"reference external$3\"$4 href=\"$2\">$1</a>", + "\\href{$2}{$1}", [textStr, linkStr, nimDocStr, tooltipStr]) + else: + dispA(d.target, result, + "<a class=\"reference internal$3\"$4 href=\"#$2\">$1</a>", + "\\hyperlink{$2}{$1} (p.~\\pageref{$2})", + [textStr, linkStr, nimDocStr, tooltipStr]) + +proc traverseForIndex*(d: PDoc, n: PRstNode) = + ## A version of [renderRstToOut] that only fills entries for ``.idx`` files. + var discarded: string + if n == nil: return + case n.kind + of rnIdx: renderIndexTerm(d, n, discarded) + of rnHeadline, rnMarkdownHeadline: renderHeadline(d, n, discarded) + of rnOverline: renderOverline(d, n, discarded) + else: + for i in 0 ..< len(n): + traverseForIndex(d, n.sons[i]) + proc renderRstToOut(d: PDoc, n: PRstNode, result: var string) = if n == nil: return case n.kind of rnInner: renderAux(d, n, result) - of rnHeadline: renderHeadline(d, n, result) + of rnHeadline, rnMarkdownHeadline: renderHeadline(d, n, result) of rnOverline: renderOverline(d, n, result) - of rnTransition: renderAux(d, n, "<hr />\n", "\\hrule\n", result) - of rnParagraph: renderAux(d, n, "<p>$1</p>\n", "$1\n\n", result) + of rnTransition: renderAux(d, n, "<hr$2 />\n", "\n\n\\vspace{0.6em}\\hrule$2\n", result) + of rnParagraph: renderAux(d, n, "<p$2>$1</p>\n", "\n\n$2\n$1\n\n", result) of rnBulletList: - renderAux(d, n, "<ul class=\"simple\">$1</ul>\n", - "\\begin{itemize}$1\\end{itemize}\n", result) + renderAux(d, n, "<ul$2 class=\"simple\">$1</ul>\n", + "\\begin{itemize}\n$2\n$1\\end{itemize}\n", result) of rnBulletItem, rnEnumItem: - renderAux(d, n, "<li>$1</li>\n", "\\item $1\n", result) - of rnEnumList: - renderAux(d, n, "<ol class=\"simple\">$1</ol>\n", - "\\begin{enumerate}$1\\end{enumerate}\n", result) - of rnDefList: - renderAux(d, n, "<dl class=\"docutils\">$1</dl>\n", - "\\begin{description}$1\\end{description}\n", result) + renderAux(d, n, "<li$2>$1</li>\n", "\\item $2$1\n", result) + of rnEnumList: renderEnumList(d, n, result) + of rnDefList, rnMdDefList: + renderAux(d, n, "<dl$2 class=\"docutils\">$1</dl>\n", + "\\begin{description}\n$2\n$1\\end{description}\n", result) of rnDefItem: renderAux(d, n, result) - of rnDefName: renderAux(d, n, "<dt>$1</dt>\n", "\\item[$1] ", result) - of rnDefBody: renderAux(d, n, "<dd>$1</dd>\n", "$1\n", result) + of rnDefName: renderAux(d, n, "<dt$2>$1</dt>\n", "$2\\item[$1]\\ ", result) + of rnDefBody: renderAux(d, n, "<dd$2>$1</dd>\n", "$2\n$1\n", result) of rnFieldList: var tmp = "" - for i in countup(0, len(n) - 1): + for i in countup(0, len(n) - 1): renderRstToOut(d, n.sons[i], tmp) - if tmp.len != 0: + if tmp.len != 0: dispA(d.target, result, - "<table class=\"docinfo\" frame=\"void\" rules=\"none\">" & + "<table$2 class=\"docinfo\" frame=\"void\" rules=\"none\">" & "<col class=\"docinfo-name\" />" & - "<col class=\"docinfo-content\" />" & + "<col class=\"docinfo-content\" />" & "<tbody valign=\"top\">$1" & - "</tbody></table>", - "\\begin{description}$1\\end{description}\n", - [tmp]) + "</tbody></table>", + "\\begin{description}\n$2\n$1\\end{description}\n", + [tmp, n.anchor.idS]) of rnField: renderField(d, n, result) - of rnFieldName: - renderAux(d, n, "<th class=\"docinfo-name\">$1:</th>", "\\item[$1:]", result) - of rnFieldBody: + of rnFieldName: + renderAux(d, n, "<th class=\"docinfo-name\">$1:</th>", + "\\item[$1:]", result) + of rnFieldBody: renderAux(d, n, "<td>$1</td>", " $1\n", result) - of rnIndex: + of rnIndex: renderRstToOut(d, n.sons[2], result) - of rnOptionList: - renderAux(d, n, "<table frame=\"void\">$1</table>", - "\\begin{description}\n$1\\end{description}\n", result) - of rnOptionListItem: - renderAux(d, n, "<tr>$1</tr>\n", "$1", result) - of rnOptionGroup: - renderAux(d, n, "<th align=\"left\">$1</th>", "\\item[$1]", result) - of rnDescription: - renderAux(d, n, "<td align=\"left\">$1</td>\n", " $1\n", result) - of rnOption, rnOptionString, rnOptionArgument: - doAssert false, "renderRstToOut" + of rnOptionList: + renderAux(d, n, "<div$2 class=\"option-list\">$1</div>", + "\\begin{rstoptlist}$2\n$1\\end{rstoptlist}", result) + of rnOptionListItem: + var addclass = if n.order mod 2 == 1: " odd" else: "" + renderAux(d, n, + "<div class=\"option-list-item" & addclass & "\">$1</div>\n", + "$1", result) + of rnOptionGroup: + renderAux(d, n, + "<div class=\"option-list-label\"><tt><span class=\"option\">" & + "$1</span></tt></div>", + "\\item[\\rstcodeitem{\\spanoption{$1}}]", result) + of rnDescription: + renderAux(d, n, "<div class=\"option-list-description\">$1</div>", + " $1\n", result) + of rnOption, rnOptionString, rnOptionArgument: + raiseAssert "renderRstToOut" of rnLiteralBlock: - renderAux(d, n, "<pre>$1</pre>\n", - "\\begin{rstpre}\n$1\n\\end{rstpre}\n", result) - of rnQuotedLiteralBlock: - doAssert false, "renderRstToOut" - of rnLineBlock: - renderAux(d, n, "<p>$1</p>", "$1\n\n", result) - of rnLineBlockItem: - renderAux(d, n, "$1<br />", "$1\\\\\n", result) - of rnBlockQuote: - renderAux(d, n, "<blockquote><p>$1</p></blockquote>\n", - "\\begin{quote}$1\\end{quote}\n", result) - of rnTable, rnGridTable: - renderAux(d, n, - "<table border=\"1\" class=\"docutils\">$1</table>", - "\\begin{table}\\begin{rsttab}{" & - texColumns(n) & "|}\n\\hline\n$1\\end{rsttab}\\end{table}", result) - of rnTableRow: + renderAux(d, n, "<pre$2>$1</pre>\n", + "\n\n$2\\begin{rstpre}\n$1\n\\end{rstpre}\n\n", result) + of rnMarkdownBlockQuote: + d.curQuotationDepth = 1 + var tmp = "" + renderAux(d, n, "$1", "$1", tmp) + let itemEnding = + if d.target == outHtml: "</blockquote>" else: "\\end{rstquote}" + tmp.add itemEnding.repeat(d.curQuotationDepth - 1) + dispA(d.target, result, + "<blockquote$2 class=\"markdown-quote\">$1</blockquote>\n", + "\n\\begin{rstquote}\n$2\n$1\\end{rstquote}\n", [tmp, n.anchor.idS]) + of rnMarkdownBlockQuoteItem: + let addQuotationDepth = n.quotationDepth - d.curQuotationDepth + var itemPrefix: string # start or ending (quotation grey bar on the left) + if addQuotationDepth >= 0: + let s = + if d.target == outHtml: "<blockquote class=\"markdown-quote\">" + else: "\\begin{rstquote}" + itemPrefix = s.repeat(addQuotationDepth) + else: + let s = + if d.target == outHtml: "</blockquote>" + else: "\\end{rstquote}" + itemPrefix = s.repeat(-addQuotationDepth) + renderAux(d, n, itemPrefix & "<p>$1</p>", itemPrefix & "\n$1", result) + d.curQuotationDepth = n.quotationDepth + of rnLineBlock: + if n.sons.len == 1 and n.sons[0].lineIndent == "\n": + # whole line block is one empty line, no need to add extra spacing + renderAux(d, n, "<p$2>$1</p> ", "\n\n$2\n$1", result) + else: # add extra spacing around the line block for Latex + renderAux(d, n, "<p$2>$1</p>", + "\n\\vspace{0.5em}$2\n$1\\vspace{0.5em}\n", result) + of rnLineBlockItem: + if n.lineIndent.len == 0: # normal case - no additional indentation + renderAux(d, n, "$1<br/>", "\\noindent $1\n\n", result) + elif n.lineIndent == "\n": # add one empty line + renderAux(d, n, "<br/>", "\\vspace{1em}\n", result) + else: # additional indentation w.r.t. '| ' + let indent = $(0.5 * (n.lineIndent.len - 1).toFloat) & "em" + renderAux(d, n, + "<span style=\"margin-left: " & indent & "\">$1</span><br/>", + "\\noindent\\hspace{" & indent & "}$1\n\n", result) + of rnBlockQuote: + renderAux(d, n, "<blockquote$2><p>$1</p></blockquote>\n", + "\\begin{quote}\n$2\n$1\\end{quote}\n", result) + of rnAdmonition: renderAdmonition(d, n, result) + of rnTable, rnGridTable, rnMarkdownTable: + renderAux(d, n, + "<table$2 border=\"1\" class=\"docutils\">$1</table>", + "\n$2\n\\begin{rsttab}{" & + "L".repeat(n.colCount) & "}\n\\toprule\n$1" & + "\\addlinespace[0.1em]\\bottomrule\n\\end{rsttab}", result) + of rnTableRow: if len(n) >= 1: - if d.target == outLatex: - #var tmp = "" - renderRstToOut(d, n.sons[0], result) - for i in countup(1, len(n) - 1): - result.add(" & ") - renderRstToOut(d, n.sons[i], result) - result.add("\\\\\n\\hline\n") - else: + case d.target + of outHtml: result.add("<tr>") renderAux(d, n, result) result.add("</tr>\n") - of rnTableDataCell: - renderAux(d, n, "<td>$1</td>", "$1", result) - of rnTableHeaderCell: - renderAux(d, n, "<th>$1</th>", "\\textbf{$1}", result) - of rnLabel: - doAssert false, "renderRstToOut" # used for footnotes and other - of rnFootnote: - doAssert false, "renderRstToOut" # a footnote - of rnCitation: - doAssert false, "renderRstToOut" # similar to footnote - of rnRef: - var tmp = "" - renderAux(d, n, tmp) - dispA(d.target, result, "<a class=\"reference external\" href=\"#$2\">$1</a>", - "$1\\ref{$2}", [tmp, rstnodeToRefname(n)]) - of rnStandaloneHyperlink: - renderAux(d, n, - "<a class=\"reference external\" href=\"$1\">$1</a>", - "\\href{$1}{$1}", result) + of outLatex: + if n.sons[0].kind == rnTableHeaderCell: + result.add "\\rowcolor{gray!15} " + var spanLines: seq[(int, int)] + var nCell = 0 + for uCell in 0 .. n.len - 1: + renderRstToOut(d, n.sons[uCell], result) + if n.sons[uCell].span > 0: + spanLines.add (nCell + 1, nCell + n.sons[uCell].span) + nCell += n.sons[uCell].span + else: + nCell += 1 + if uCell != n.len - 1: + result.add(" & ") + result.add("\\\\") + if n.endsHeader: result.add("\\midrule\n") + for (start, stop) in spanLines: + result.add("\\cmidrule(lr){$1-$2}" % [$start, $stop]) + result.add("\n") + of rnTableHeaderCell, rnTableDataCell: + case d.target + of outHtml: + let tag = if n.kind == rnTableHeaderCell: "th" else: "td" + var spanSpec: string + if n.span <= 1: spanSpec = "" + else: + spanSpec = " colspan=\"" & $n.span & "\" style=\"text-align: center\"" + renderAux(d, n, "<$1$2>$$1</$1>" % [tag, spanSpec], "", result) + of outLatex: + let text = if n.kind == rnTableHeaderCell: "\\textbf{$1}" else: "$1" + var latexStr: string + if n.span <= 1: latexStr = text + else: latexStr = "\\multicolumn{" & $n.span & "}{c}{" & text & "}" + renderAux(d, n, "", latexStr, result) + of rnFootnoteGroup: + renderAux(d, n, + "<hr class=\"footnote\">" & + "<div class=\"footnote-group\">\n$1</div>\n", + "\n\n\\noindent\\rule{0.25\\linewidth}{.4pt}\n" & + "\\begin{rstfootnote}\n$1\\end{rstfootnote}\n\n", + result) + of rnFootnote, rnCitation: + var mark = "" + renderAux(d, n.sons[0], mark) + var body = "" + renderRstToOut(d, n.sons[1], body) + dispA(d.target, result, + "<div$2><div class=\"footnote-label\">" & + "<sup><strong><a href=\"#$4\">[$3]</a></strong></sup>" & + "</div>   $1\n</div>\n", + "\\item[\\textsuperscript{[$3]}]$2 $1\n", + [body, n.anchor.idS, mark, n.anchor]) + of rnPandocRef: + renderHyperlink(d, text=n.sons[0], link=n.sons[1], result, external=false) + of rnRstRef: + renderHyperlink(d, text=n.sons[0], link=n.sons[0], result, external=false) + of rnStandaloneHyperlink: + renderHyperlink(d, text=n.sons[0], link=n.sons[0], result, external=true) + of rnInternalRef: + renderHyperlink(d, text=n.sons[0], link=n.sons[1], result, external=false) + of rnNimdocRef: + renderHyperlink(d, text=n.sons[0], link=n.sons[1], result, external=false, + nimdoc=true, tooltip=n.tooltip) of rnHyperlink: - var tmp0 = "" - var tmp1 = "" - renderRstToOut(d, n.sons[0], tmp0) - renderRstToOut(d, n.sons[1], tmp1) - dispA(d.target, result, "<a class=\"reference external\" href=\"$2\">$1</a>", - "\\href{$2}{$1}", - [tmp0, tmp1]) + renderHyperlink(d, text=n.sons[0], link=n.sons[1], result, external=true) + of rnFootnoteRef: + var tmp = "[" + renderAux(d, n.sons[0], tmp) + tmp.add "]" + dispA(d.target, result, + "<sup><strong><a class=\"reference internal\" href=\"#$2\">" & + "$1</a></strong></sup>", + "\\textsuperscript{\\hyperlink{$2}{\\textbf{$1}}}", + [tmp, n.sons[1].text]) of rnDirArg, rnRaw: renderAux(d, n, result) of rnRawHtml: - if d.target != outLatex: + if d.target != outLatex and not lastSon(n).isNil: result.add addNodes(lastSon(n)) of rnRawLatex: - if d.target == outLatex: + if d.target == outLatex and not lastSon(n).isNil: result.add addNodes(lastSon(n)) - + of rnImage, rnFigure: renderImage(d, n, result) - of rnCodeBlock: renderCodeBlock(d, n, result) + of rnCodeBlock, rnInlineCode: renderCode(d, n, result) of rnContainer: renderContainer(d, n, result) - of rnSubstitutionReferences, rnSubstitutionDef: + of rnSubstitutionReferences, rnSubstitutionDef: renderAux(d, n, "|$1|", "|$1|", result) of rnDirective: renderAux(d, n, "", "", result) - of rnGeneralRole: + of rnUnknownRole, rnCodeFragment: var tmp0 = "" var tmp1 = "" renderRstToOut(d, n.sons[0], tmp0) renderRstToOut(d, n.sons[1], tmp1) - dispA(d.target, result, "<span class=\"$2\">$1</span>", "\\span$2{$1}", - [tmp0, tmp1]) + var class = tmp1 + # don't allow missing role break latex compilation: + if d.target == outLatex and n.kind == rnUnknownRole: class = "Other" + if n.kind == rnCodeFragment: + dispA(d.target, result, + "<tt class=\"docutils literal\"><span class=\"pre $2\">" & + "$1</span></tt>", + "\\rstcode{\\span$2{$1}}", [tmp0, class]) + else: # rnUnknownRole, not necessarily code/monospace font + dispA(d.target, result, "<span class=\"$2\">$1</span>", "\\span$2{$1}", + [tmp0, class]) of rnSub: renderAux(d, n, "<sub>$1</sub>", "\\rstsub{$1}", result) of rnSup: renderAux(d, n, "<sup>$1</sup>", "\\rstsup{$1}", result) of rnEmphasis: renderAux(d, n, "<em>$1</em>", "\\emph{$1}", result) of rnStrongEmphasis: renderAux(d, n, "<strong>$1</strong>", "\\textbf{$1}", result) of rnTripleEmphasis: - renderAux(d, n, "<strong><em>$1</em></strong>", + renderAux(d, n, "<strong><em>$1</em></strong>", "\\textbf{emph{$1}}", result) - of rnInterpretedText: - renderAux(d, n, "<cite>$1</cite>", "\\emph{$1}", result) of rnIdx: renderIndexTerm(d, n, result) - of rnInlineLiteral: - renderAux(d, n, - "<tt class=\"docutils literal\"><span class=\"pre\">$1</span></tt>", - "\\texttt{$1}", result) + of rnInlineLiteral, rnInterpretedText: + renderAux(d, n, + "<tt class=\"docutils literal\"><span class=\"pre\">$1</span></tt>", + "\\rstcode{$1}", result) + of rnInlineTarget: + var tmp = "" + renderAux(d, n, tmp) + dispA(d.target, result, + "<span class=\"target\" id=\"$2\">$1</span>", + "\\label{$2}\\hypertarget{$2}{$1}", + [tmp, rstnodeToRefname(n)]) of rnSmiley: renderSmiley(d, n, result) - of rnLeaf: result.add(esc(d.target, n.text)) + of rnLeaf: result.add(esc(d.target, n.text, escMode=d.escMode)) of rnContents: d.hasToc = true + of rnDefaultRole: discard of rnTitle: d.meta[metaTitle] = "" renderRstToOut(d, n.sons[0], d.meta[metaTitle]) + d.meta[metaTitleRaw] = n.sons[0].addNodes # ----------------------------------------------------------------------------- -proc getVarIdx(varnames: openarray[string], id: string): int = - for i in countup(0, high(varnames)): - if cmpIgnoreStyle(varnames[i], id) == 0: +proc getVarIdx(varnames: openArray[string], id: string): int = + for i in countup(0, high(varnames)): + if cmpIgnoreStyle(varnames[i], id) == 0: return i result = -1 -proc formatNamedVars*(frmt: string, varnames: openarray[string], - varvalues: openarray[string]): string = +proc formatNamedVars*(frmt: string, varnames: openArray[string], + varvalues: openArray[string]): string = var i = 0 var L = len(frmt) result = "" var num = 0 - while i < L: - if frmt[i] == '$': + while i < L: + if frmt[i] == '$': inc(i) # skip '$' case frmt[i] - of '#': + of '#': add(result, varvalues[num]) inc(num) inc(i) - of '$': + of '$': add(result, "$") inc(i) - of '0'..'9': + of '0'..'9': var j = 0 - while true: - j = (j * 10) + Ord(frmt[i]) - ord('0') + while true: + j = (j * 10) + ord(frmt[i]) - ord('0') inc(i) - if i > L-1 or frmt[i] notin {'0'..'9'}: break + if i > L-1 or frmt[i] notin {'0'..'9'}: break if j > high(varvalues) + 1: - raise newException(EInvalidValue, "invalid index: " & $j) + raise newException(ValueError, "invalid index: " & $j) num = j add(result, varvalues[j - 1]) - of 'A'..'Z', 'a'..'z', '\x80'..'\xFF': + of 'A'..'Z', 'a'..'z', '\x80'..'\xFF': var id = "" - while true: + while true: add(id, frmt[i]) inc(i) - if frmt[i] notin {'A'..'Z', '_', 'a'..'z', '\x80'..'\xFF'}: break + if frmt[i] notin {'A'..'Z', '_', 'a'..'z', '\x80'..'\xFF'}: break var idx = getVarIdx(varnames, id) - if idx >= 0: + if idx >= 0: add(result, varvalues[idx]) else: - raise newException(EInvalidValue, "unknown substitution var: " & id) - of '{': + raise newException(ValueError, "unknown substitution var: " & id) + of '{': var id = "" inc(i) - while frmt[i] != '}': - if frmt[i] == '\0': - raise newException(EInvalidValue, "'}' expected") + while frmt[i] != '}': + if frmt[i] == '\0': + raise newException(ValueError, "'}' expected") add(id, frmt[i]) inc(i) inc(i) # skip } # search for the variable: var idx = getVarIdx(varnames, id) if idx >= 0: add(result, varvalues[idx]) - else: - raise newException(EInvalidValue, "unknown substitution var: " & id) + else: + raise newException(ValueError, "unknown substitution var: " & id) else: - raise newException(EInvalidValue, "unknown substitution: $" & $frmt[i]) + raise newException(ValueError, "unknown substitution: $" & $frmt[i]) var start = i - while i < L: + while i < L: if frmt[i] != '$': inc(i) else: break if i-1 >= start: add(result, substr(frmt, start, i - 1)) -proc defaultConfig*(): PStringTable = - ## creates a default configuration for HTML generation. +proc defaultConfig*(): StringTableRef = + ## Returns a default configuration for embedded HTML generation. + ## + ## The returned ``StringTableRef`` contains the parameters used by the HTML + ## engine to build the final output. For information on what these parameters + ## are and their purpose, please look up the file ``config/nimdoc.cfg`` + ## bundled with the compiler. + ## + ## The only difference between the contents of that file and the values + ## provided by this proc is the ``doc.file`` variable. The ``doc.file`` + ## variable of the configuration file contains HTML to build standalone + ## pages, while this proc returns just the content for procs like + ## ``rstToHtml`` to generate the bare minimum HTML. result = newStringTable(modeStyleInsensitive) - - template setConfigVar(key, val: expr) = + + template setConfigVar(key, val) = result[key] = val - + + # If you need to modify these values, it might be worth updating the template + # file in config/nimdoc.cfg. setConfigVar("split.item.toc", "20") setConfigVar("doc.section", """ <div class="section" id="$sectionID"> @@ -650,13 +1479,14 @@ $content </li> """) setConfigVar("doc.item", """ -<dt id="$itemID"><pre>$header</pre></dt> +<dt id="$itemID"><a name="$itemSymOrIDEnc"></a><pre>$header</pre></dt> <dd> $desc </dd> """) setConfigVar("doc.item.toc", """ - <li><a class="reference" href="#$itemID">$name</a></li> + <li><a class="reference" href="#$itemSymOrIDEnc" + title="$header_plain">$name</a></li> """) setConfigVar("doc.toc", """ <div class="navigation" id="navigation"> @@ -671,25 +1501,66 @@ $moduledesc $content </div> """) + setConfigVar("doc.listing_start", "<pre$3 class = \"listing\">") + setConfigVar("doc.listing_end", "</pre>") + setConfigVar("doc.listing_button", "</pre>") setConfigVar("doc.body_no_toc", "$moduledesc $content") setConfigVar("doc.file", "$content") + setConfigVar("doc.smiley_format", "/images/smilies/$1.gif") # ---------- forum --------------------------------------------------------- -proc rstToHtml*(s: string, options: TRstParseOptions, - config: PStringTable): string = - ## exported for *nimforum*. - - proc myFindFile(filename: string): string = +proc rstToHtml*(s: string, options: RstParseOptions, + config: StringTableRef, + msgHandler: MsgHandler = rst.defaultMsgHandler): string {.gcsafe.} = + ## Converts an input rst string into embeddable HTML. + ## + ## This convenience proc parses any input string using rst markup (it doesn't + ## have to be a full document!) and returns an embeddable piece of HTML. The + ## proc is meant to be used in *online* environments without access to a + ## meaningful filesystem, and therefore rst ``include`` like directives won't + ## work. For an explanation of the ``config`` parameter see the + ## ``initRstGenerator`` proc. Example: + ## + ## ```nim + ## import packages/docutils/rstgen, strtabs + ## + ## echo rstToHtml("*Hello* **world**!", {}, + ## newStringTable(modeStyleInsensitive)) + ## # --> <em>Hello</em> <strong>world</strong>! + ## ``` + ## + ## If you need to allow the rst ``include`` directive or tweak the generated + ## output you have to create your own ``RstGenerator`` with + ## ``initRstGenerator`` and related procs. + + proc myFindFile(filename: string): string = # we don't find any files in online mode: result = "" + proc myFindRefFile(filename: string): (string, string) = + result = ("", "") const filen = "input" - var d: TRstGenerator - initRstGenerator(d, outHtml, config, filen, options, myFindFile, - rst.defaultMsgHandler) - var dummyHasToc = false - var rst = rstParse(s, filen, 0, 1, dummyHasToc, options) + let (rst, filenames, t) = rstParse(s, filen, + line=LineRstInit, column=ColRstInit, + options, myFindFile, myFindRefFile, msgHandler) + var d: RstGenerator + initRstGenerator(d, outHtml, config, filen, myFindFile, msgHandler, + filenames, hasToc = t) result = "" renderRstToOut(d, rst, result) - \ No newline at end of file + strbasics.strip(result) + + +proc rstToLatex*(rstSource: string; options: RstParseOptions): string {.inline, since: (1, 3).} = + ## Convenience proc for `renderRstToOut` and `initRstGenerator`. + runnableExamples: doAssert rstToLatex("*Hello* **world**", {}) == """\emph{Hello} \textbf{world}""" + if rstSource.len == 0: return + let (rst, filenames, t) = rstParse(rstSource, "", + line=LineRstInit, column=ColRstInit, + options) + var rstGenera: RstGenerator + rstGenera.initRstGenerator(outLatex, defaultConfig(), "input", + filenames=filenames, hasToc = t) + rstGenera.renderRstToOut(rst, result) + strbasics.strip(result) |