# # # Nimrod'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. import strutils, os, hashes, strtabs, rstast, rst, highlite const HtmlExt = "html" IndexExt* = ".idx" type TOutputTarget* = 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 splitAfter*: int # split too long entries in the TOC tocPart*: seq[TTocEntry] 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) = g.config = config g.target = target g.tocPart = @[] g.filename = filename g.splitAfter = 20 g.theIndex = "" g.options = options g.findFile = findFile g.msgHandler = msgHandler let s = config["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) = if g.theIndex.len > 0: writeFile(outfile, g.theIndex) proc addXmlChar(dest: var string, c: Char) = 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) = 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}") else: add(dest, c) var splitter*: string = "" proc escChar*(target: TOutputTarget, dest: var string, c: Char) {.inline.} = case target of outHtml: addXmlChar(dest, c) of outLatex: addTexChar(dest, c) 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: nil inc(result) dec(result) # last valid index proc esc*(target: TOutputTarget, s: string, splitAfter = -1): string = 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 add(result, splitter) for i in countup(j, k): escChar(target, result, s[i]) inc(partLen, k - j + 1) j = k + 1 else: for i in countup(0, len(s) - 1): escChar(target, result, s[i]) proc disp(target: TOutputTarget, xml, tex: string): string = if target != outLatex: result = xml else: result = tex proc dispF(target: TOutputTarget, xml, tex: string, args: openArray[string]): string = if target != outLatex: result = xml % args else: result = tex % args proc dispA(target: TOutputTarget, dest: var string, xml, tex: string, args: openarray[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) = 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) = 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]) # ---------------- 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 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 .. $2", "$2\\label{$1}", [id, term]) type TIndexEntry {.pure, final.} = object keyword: string link: string proc cmp(a, b: TIndexEntry): int = result = cmpIgnoreStyle(a.keyword, b.keyword) proc `<-`(a: var TIndexEntry, b: TIndexEntry) = shallowCopy a.keyword, b.keyword shallowCopy a.link, b.link proc sortIndex(a: var openArray[TIndexEntry]) = # 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: TIndexEntry 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 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 = "" var i = 0 while i < L: result.addf("
$1
\n") i = j # ---------------------------------------------------------------------------- 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) 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, "$3", "\\rsth$4{$3}\\label{$2}\n", [$n.level, d.tocPart[length].refname, tmp, $chr(n.level - 1 + ord('A'))]) else: dispA(d.target, result, "$3", "\\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: for i in countup(0, len(n)-1): renderRstToOut(d, n.sons[i], d.meta[metaTitle]) elif d.meta[metaSubtitle].len == 0: for i in countup(0, len(n)-1): renderRstToOut(d, n.sons[i], d.meta[metaSubtitle]) else: var tmp = "" for i in countup(0, len(n) - 1): renderRstToOut(d, n.sons[i], tmp) dispA(d.target, result, "
$3
", "\\rstov$4{$3}\\label{$2}\n", [$n.level, rstnodeToRefname(n), tmp, $chr(n.level - 1 + ord('A'))]) proc renderTocEntry(d: PDoc, e: TTocEntry, result: var string) = dispA(d.target, result, "
  • $2
  • \n", "\\item\\label{$1_toc} $2\\ref{$1}\n", [e.refname, e.header]) proc renderTocEntries*(d: PDoc, j: var int, lvl: int, result: var string) = var tmp = "" while j <= high(d.tocPart): var a = abs(d.tocPart[j].n.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, "", "\\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)]) if options.len > 0: options = dispF(d.target, "$1", "[$1]", [options]) dispA(d.target, result, "", "\\includegraphics$2{$1}", [getArgument(n), options]) if len(n) >= 3: renderRstToOut(d, n.sons[2], result) proc renderSmiley(d: PDoc, n: PRstNode, result: var string) = dispA(d.target, result, """""", "\\includegraphics{$1}", [n.text]) proc renderCodeBlock(d: PDoc, n: PRstNode, result: var string) = 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, "
    ", "\\begin{rstpre}\n")
      if lang == langNone:
        d.msgHandler(d.filename, 1, 0, mwUnsupportedLanguage, langstr)
        result.add(m.text)
      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, "$1", "\\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, "
    ", "\n\\end{rstpre}\n") proc renderContainer(d: PDoc, n: PRstNode, result: var string) = var tmp = "" renderRstToOut(d, n.sons[2], tmp) var arg = strip(getArgument(n)) if arg == "": dispA(d.target, result, "
    $1
    ", "$1", [tmp]) else: dispA(d.target, result, "
    $2
    ", "$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) = 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: 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, "$1\n", "$1", result) 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 rnOverline: renderOverline(d, n, result) of rnTransition: renderAux(d, n, "
    \n", "\\hrule\n", result) of rnParagraph: renderAux(d, n, "

    $1

    \n", "$1\n\n", result) of rnBulletList: renderAux(d, n, "\n", "\\begin{itemize}$1\\end{itemize}\n", result) of rnBulletItem, rnEnumItem: renderAux(d, n, "
  • $1
  • \n", "\\item $1\n", result) of rnEnumList: renderAux(d, n, "
      $1
    \n", "\\begin{enumerate}$1\\end{enumerate}\n", result) of rnDefList: renderAux(d, n, "
    $1
    \n", "\\begin{description}$1\\end{description}\n", result) of rnDefItem: renderAux(d, n, result) of rnDefName: renderAux(d, n, "
    $1
    \n", "\\item[$1] ", result) of rnDefBody: renderAux(d, n, "
    $1
    \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, "" & "" & "" & "$1" & "
    ", "\\begin{description}$1\\end{description}\n", [tmp]) of rnField: renderField(d, n, result) of rnFieldName: renderAux(d, n, "$1:", "\\item[$1:]", result) of rnFieldBody: renderAux(d, n, "$1", " $1\n", result) of rnIndex: renderRstToOut(d, n.sons[2], result) of rnOptionList: renderAux(d, n, "$1
    ", "\\begin{description}\n$1\\end{description}\n", result) of rnOptionListItem: renderAux(d, n, "$1\n", "$1", result) of rnOptionGroup: renderAux(d, n, "$1", "\\item[$1]", result) of rnDescription: renderAux(d, n, "$1\n", " $1\n", result) of rnOption, rnOptionString, rnOptionArgument: doAssert false, "renderRstToOut" of rnLiteralBlock: renderAux(d, n, "
    $1
    \n", "\\begin{rstpre}\n$1\n\\end{rstpre}\n", result) of rnQuotedLiteralBlock: doAssert false, "renderRstToOut" of rnLineBlock: renderAux(d, n, "

    $1

    ", "$1\n\n", result) of rnLineBlockItem: renderAux(d, n, "$1
    ", "$1\\\\\n", result) of rnBlockQuote: renderAux(d, n, "

    $1

    \n", "\\begin{quote}$1\\end{quote}\n", result) of rnTable, rnGridTable: renderAux(d, n, "$1
    ", "\\begin{table}\\begin{rsttab}{" & texColumns(n) & "|}\n\\hline\n$1\\end{rsttab}\\end{table}", result) of rnTableRow: if len(n) >= 1: if d.target == outLatex: var tmp = "" renderRstToOut(d, n.sons[0], tmp) for i in countup(1, len(n) - 1): result.add(" & ") renderRstToOut(d, n.sons[i], result) result.add("\\\\\n\\hline\n") else: result.add("") renderAux(d, n, result) result.add("\n") of rnTableDataCell: renderAux(d, n, "$1", "$1", result) of rnTableHeaderCell: renderAux(d, n, "$1", "\\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, "$1", "$1\\ref{$2}", [tmp, rstnodeToRefname(n)]) of rnStandaloneHyperlink: renderAux(d, n, "$1", "\\href{$1}{$1}", result) of rnHyperlink: var tmp0 = "" var tmp1 = "" renderRstToOut(d, n.sons[0], tmp0) renderRstToOut(d, n.sons[1], tmp1) dispA(d.target, result, "$1", "\\href{$2}{$1}", [tmp0, tmp1]) of rnDirArg, rnRaw: renderAux(d, n, result) of rnRawHtml: if d.target != outLatex: result.add addNodes(lastSon(n)) of rnRawLatex: if d.target == outLatex: result.add addNodes(lastSon(n)) of rnImage, rnFigure: renderImage(d, n, result) of rnCodeBlock: renderCodeBlock(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 rnGeneralRole: var tmp0 = "" var tmp1 = "" renderRstToOut(d, n.sons[0], tmp0) renderRstToOut(d, n.sons[1], tmp1) dispA(d.target, result, "$1", "\\span$2{$1}", [tmp0, tmp1]) of rnSub: renderAux(d, n, "$1", "\\rstsub{$1}", result) of rnSup: renderAux(d, n, "$1", "\\rstsup{$1}", result) of rnEmphasis: renderAux(d, n, "$1", "\\emph{$1}", result) of rnStrongEmphasis: renderAux(d, n, "$1", "\\textbf{$1}", result) of rnTripleEmphasis: renderAux(d, n, "$1", "\\textbf{emph{$1}}", result) of rnInterpretedText: renderAux(d, n, "$1", "\\emph{$1}", result) of rnIdx: renderIndexTerm(d, n, result) of rnInlineLiteral: renderAux(d, n, "$1", "\\texttt{$1}", result) of rnSmiley: renderSmiley(d, n, result) of rnLeaf: result.add(esc(d.target, n.text)) of rnContents: d.hasToc = true of rnTitle: d.meta[metaTitle] = "" renderRstToOut(d, n.sons[0], d.meta[metaTitle]) # ----------------------------------------------------------------------------- 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(EInvalidValue, "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(EInvalidValue, "unknown substitution var: " & id) of '{': var id = "" inc(i) while frmt[i] != '}': if frmt[i] == '\0': raise newException(EInvalidValue, "'}' 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(EInvalidValue, "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*(): PStringTable = ## creates a default configuration for HTML generation. result = newStringTable(modeStyleInsensitive) template setConfigVar(key, val: expr) = result[key] = val setConfigVar("split.item.toc", "20") setConfigVar("doc.section", """

    $sectionTitle

    $content
    """) setConfigVar("doc.section.toc", """
  • $sectionTitle
  • """) setConfigVar("doc.item", """
    $header
    $desc
    """) setConfigVar("doc.item.toc", """
  • $name
  • """) setConfigVar("doc.toc", """ """) setConfigVar("doc.body_toc", """ $tableofcontents
    $moduledesc $content
    """) setConfigVar("doc.body_no_toc", "$moduledesc $content") setConfigVar("doc.file", "$content") # ---------- forum --------------------------------------------------------- proc rstToHtml*(s: string, options: TRstParseOptions, config: PStringTable): string = ## exported for *nimforum*. proc myFindFile(filename: string): string = # we don't find any files in online mode: result = "" const filen = "input" var d: TRstGenerator initRstGenerator(d, outHtml, config, filen, options, myFindFile, nil) var dummyHasToc = false var rst = rstParse(s, filen, 0, 1, dummyHasToc, options) result = "" renderRstToOut(d, rst, result)