1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567 |
- #
- #
- # 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: (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 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
- OutputTarget* = enum ## which document type to generate
- outHtml, # output is HTML
- outLatex # output is Latex
- 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
- listingCounter*: int
- tocPart*: seq[PRstNode] # headings for Table of Contents
- hasToc*: bool
- 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.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.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 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 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 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, "\\" & 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)
- 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 addSplitter(target: OutputTarget; dest: var string) {.inline.} =
- case target
- of outHtml: add(dest, "<wbr />")
- of outLatex: add(dest, "\\-")
- proc nextSplitPoint*(s: string, start: int): int =
- result = start
- 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: discard
- inc(result)
- dec(result) # last valid index
- proc esc*(target: OutputTarget, s: string, splitAfter = -1, escMode = emText): string =
- ## Escapes the HTML.
- result = ""
- if splitAfter >= 0:
- var partLen = 0
- var j = 0
- while j < len(s):
- var k = nextSplitPoint(s, j)
- #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], escMode)
- proc disp(target: OutputTarget, xml, tex: string): string =
- if target != outLatex: result = xml
- else: result = tex
- proc dispF(target: OutputTarget, xml, tex: string,
- args: varargs[string]): string =
- if target != outLatex: result = xml % args
- else: result = tex % args
- 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 `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)
- 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)
- 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: 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):
- result = result !& hash(n.sons[i])
- result = !$result
- 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, ieIdxRole,
- htmlFileRelPath(d), id, term, d.currentSection)
- dispA(d.target, result, "<span id=\"$1\">$2</span>", "\\nimindexterm{$1}{$2}",
- [id, term])
- type
- 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.
- 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[IndexEntry]) =
- # we use shellsort here; fast and simple
- let n = len(a)
- var h = 1
- while true:
- h = 3 * h + 1
- if h > n: break
- while true:
- h = h div 3
- for i in countup(h, n - 1):
- var v: IndexEntry
- v <- a[i]
- var j = i
- while cmp(a[j-h], v) >= 0:
- a[j] <- a[j-h]
- j = j-h
- if j < h: break
- a[j] <- v
- if h == 1: break
- 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 < 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 < 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
- 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)
- 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:
- 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, "\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])
- 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)
- 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, 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", [n.anchor, header])
- 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].level)
- if a == lvl:
- renderTocEntry(d, d.tocPart[j], tmp)
- inc(j)
- elif a > lvl:
- renderTocEntries(d, j, a, tmp)
- else:
- break
- if lvl > 1:
- 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) =
- 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])
- 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="$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
- 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:
- 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 = 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 renderField(d: PDoc, n: PRstNode, result: var string) =
- var b = false
- 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
- cmpIgnoreStyle(fieldname, "authors") == 0:
- if d.meta[metaAuthor].len == 0:
- d.meta[metaAuthor] = fieldval
- b = true
- 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, rnMarkdownHeadline: renderHeadline(d, n, result)
- of rnOverline: renderOverline(d, 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$2 class=\"simple\">$1</ul>\n",
- "\\begin{itemize}\n$2\n$1\\end{itemize}\n", result)
- of rnBulletItem, rnEnumItem:
- 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$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):
- renderRstToOut(d, n.sons[i], tmp)
- if tmp.len != 0:
- dispA(d.target, result,
- "<table$2 class=\"docinfo\" frame=\"void\" rules=\"none\">" &
- "<col class=\"docinfo-name\" />" &
- "<col class=\"docinfo-content\" />" &
- "<tbody valign=\"top\">$1" &
- "</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:
- renderAux(d, n, "<td>$1</td>", " $1\n", result)
- of rnIndex:
- renderRstToOut(d, n.sons[2], result)
- 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$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:
- case d.target
- of outHtml:
- result.add("<tr>")
- renderAux(d, n, result)
- result.add("</tr>\n")
- 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:
- 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 and not lastSon(n).isNil:
- result.add addNodes(lastSon(n))
- of rnRawLatex:
- if d.target == outLatex and not lastSon(n).isNil:
- result.add addNodes(lastSon(n))
- of rnImage, rnFigure: renderImage(d, n, result)
- of rnCodeBlock, rnInlineCode: renderCode(d, n, result)
- of rnContainer: renderContainer(d, n, result)
- of rnSubstitutionReferences, rnSubstitutionDef:
- renderAux(d, n, "|$1|", "|$1|", result)
- of rnDirective:
- renderAux(d, n, "", "", result)
- of rnUnknownRole, rnCodeFragment:
- var tmp0 = ""
- var tmp1 = ""
- renderRstToOut(d, n.sons[0], tmp0)
- renderRstToOut(d, n.sons[1], 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>",
- "\\textbf{emph{$1}}", result)
- of rnIdx:
- renderIndexTerm(d, n, 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, 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:
- return i
- result = -1
- 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] == '$':
- inc(i) # skip '$'
- case frmt[i]
- of '#':
- add(result, varvalues[num])
- inc(num)
- inc(i)
- of '$':
- add(result, "$")
- inc(i)
- of '0'..'9':
- var j = 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 j > high(varvalues) + 1:
- raise newException(ValueError, "invalid index: " & $j)
- num = j
- add(result, varvalues[j - 1])
- of 'A'..'Z', 'a'..'z', '\x80'..'\xFF':
- var id = ""
- while true:
- add(id, frmt[i])
- inc(i)
- if frmt[i] notin {'A'..'Z', '_', 'a'..'z', '\x80'..'\xFF'}: break
- var idx = getVarIdx(varnames, id)
- if idx >= 0:
- add(result, varvalues[idx])
- else:
- raise newException(ValueError, "unknown substitution var: " & id)
- of '{':
- var id = ""
- inc(i)
- 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(ValueError, "unknown substitution var: " & id)
- else:
- raise newException(ValueError, "unknown substitution: $" & $frmt[i])
- var start = i
- while i < L:
- if frmt[i] != '$': inc(i)
- else: break
- if i-1 >= start: add(result, substr(frmt, start, i - 1))
- 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) =
- 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">
- <h1><a class="toc-backref" href="#$sectionTitleID">$sectionTitle</a></h1>
- <dl class="item">
- $content
- </dl></div>
- """)
- setConfigVar("doc.section.toc", """
- <li>
- <a class="reference" href="#$sectionID" id="$sectionTitleID">$sectionTitle</a>
- <ul class="simple">
- $content
- </ul>
- </li>
- """)
- setConfigVar("doc.item", """
- <dt id="$itemID"><a name="$itemSymOrIDEnc"></a><pre>$header</pre></dt>
- <dd>
- $desc
- </dd>
- """)
- setConfigVar("doc.item.toc", """
- <li><a class="reference" href="#$itemSymOrIDEnc"
- title="$header_plain">$name</a></li>
- """)
- setConfigVar("doc.toc", """
- <div class="navigation" id="navigation">
- <ul class="simple">
- $content
- </ul>
- </div>""")
- setConfigVar("doc.body_toc", """
- $tableofcontents
- <div class="content" id="content">
- $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: 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"
- 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)
- 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)
|