rstgen.nim 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329
  1. #
  2. #
  3. # Nim's Runtime Library
  4. # (c) Copyright 2012 Andreas Rumpf
  5. #
  6. # See the file "copying.txt", included in this
  7. # distribution, for details about the copyright.
  8. #
  9. ## This module implements a generator of HTML/Latex from
  10. ## `reStructuredText`:idx: (see http://docutils.sourceforge.net/rst.html for
  11. ## information on this markup syntax) and is used by the compiler's `docgen
  12. ## tools <docgen.html>`_.
  13. ##
  14. ## You can generate HTML output through the convenience proc ``rstToHtml``,
  15. ## which provided an input string with rst markup returns a string with the
  16. ## generated HTML. The final output is meant to be embedded inside a full
  17. ## document you provide yourself, so it won't contain the usual ``<header>`` or
  18. ## ``<body>`` parts.
  19. ##
  20. ## You can also create a ``RstGenerator`` structure and populate it with the
  21. ## other lower level methods to finally build complete documents. This requires
  22. ## many options and tweaking, but you are not limited to snippets and can
  23. ## generate `LaTeX documents <https://en.wikipedia.org/wiki/LaTeX>`_ too.
  24. import strutils, os, hashes, strtabs, rstast, rst, highlite, tables, sequtils,
  25. algorithm, parseutils
  26. const
  27. HtmlExt = "html"
  28. IndexExt* = ".idx"
  29. type
  30. OutputTarget* = enum ## which document type to generate
  31. outHtml, # output is HTML
  32. outLatex # output is Latex
  33. TocEntry = object
  34. n*: PRstNode
  35. refname*, header*: string
  36. MetaEnum* = enum
  37. metaNone, metaTitle, metaSubtitle, metaAuthor, metaVersion
  38. RstGenerator* = object of RootObj
  39. target*: OutputTarget
  40. config*: StringTableRef
  41. splitAfter*: int # split too long entries in the TOC
  42. listingCounter*: int
  43. tocPart*: seq[TocEntry]
  44. hasToc*: bool
  45. theIndex: string # Contents of the index file to be dumped at the end.
  46. options*: RstParseOptions
  47. findFile*: FindFileHandler
  48. msgHandler*: MsgHandler
  49. filename*: string
  50. meta*: array[MetaEnum, string]
  51. currentSection: string ## \
  52. ## Stores the empty string or the last headline/overline found in the rst
  53. ## document, so it can be used as a prettier name for term index generation.
  54. seenIndexTerms: Table[string, int] ## \
  55. ## Keeps count of same text index terms to generate different identifiers
  56. ## for hyperlinks. See renderIndexTerm proc for details.
  57. id*: int ## A counter useful for generating IDs.
  58. onTestSnippet*: proc (d: var RstGenerator; filename, cmd: string; status: int;
  59. content: string)
  60. PDoc = var RstGenerator ## Alias to type less.
  61. CodeBlockParams = object ## Stores code block params.
  62. numberLines: bool ## True if the renderer has to show line numbers.
  63. startLine: int ## The starting line of the code block, by default 1.
  64. langStr: string ## Input string used to specify the language.
  65. lang: SourceLanguage ## Type of highlighting, by default none.
  66. filename: string
  67. testCmd: string
  68. status: int
  69. proc init(p: var CodeBlockParams) =
  70. ## Default initialisation of CodeBlockParams to sane values.
  71. p.startLine = 1
  72. p.lang = langNone
  73. p.langStr = ""
  74. proc initRstGenerator*(g: var RstGenerator, target: OutputTarget,
  75. config: StringTableRef, filename: string,
  76. options: RstParseOptions,
  77. findFile: FindFileHandler = nil,
  78. msgHandler: MsgHandler = nil) =
  79. ## Initializes a ``RstGenerator``.
  80. ##
  81. ## You need to call this before using a ``RstGenerator`` with any other
  82. ## procs in this module. Pass a non ``nil`` ``StringTableRef`` value as
  83. ## `config` with parameters used by the HTML output generator. If you don't
  84. ## know what to use, pass the results of the `defaultConfig()
  85. ## <#defaultConfig>_` proc.
  86. ##
  87. ## The `filename` parameter will be used for error reporting and creating
  88. ## index hyperlinks to the file, but you can pass an empty string here if you
  89. ## are parsing a stream in memory. If `filename` ends with the ``.nim``
  90. ## extension, the title for the document will be set by default to ``Module
  91. ## filename``. This default title can be overriden by the embedded rst, but
  92. ## it helps to prettify the generated index if no title is found.
  93. ##
  94. ## The ``RstParseOptions``, ``FindFileHandler`` and ``MsgHandler`` types
  95. ## are defined in the the `packages/docutils/rst module <rst.html>`_.
  96. ## ``options`` selects the behaviour of the rst parser.
  97. ##
  98. ## ``findFile`` is a proc used by the rst ``include`` directive among others.
  99. ## The purpose of this proc is to mangle or filter paths. It receives paths
  100. ## specified in the rst document and has to return a valid path to existing
  101. ## files or the empty string otherwise. If you pass ``nil``, a default proc
  102. ## will be used which given a path returns the input path only if the file
  103. ## exists. One use for this proc is to transform relative paths found in the
  104. ## document to absolute path, useful if the rst file and the resources it
  105. ## references are not in the same directory as the current working directory.
  106. ##
  107. ## The ``msgHandler`` is a proc used for user error reporting. It will be
  108. ## called with the filename, line, col, and type of any error found during
  109. ## parsing. If you pass ``nil``, a default message handler will be used which
  110. ## writes the messages to the standard output.
  111. ##
  112. ## Example:
  113. ##
  114. ## .. code-block:: nim
  115. ##
  116. ## import packages/docutils/rstgen
  117. ##
  118. ## var gen: RstGenerator
  119. ## gen.initRstGenerator(outHtml, defaultConfig(), "filename", {})
  120. g.config = config
  121. g.target = target
  122. g.tocPart = @[]
  123. g.filename = filename
  124. g.splitAfter = 20
  125. g.theIndex = ""
  126. g.options = options
  127. g.findFile = findFile
  128. g.currentSection = ""
  129. g.id = 0
  130. let fileParts = filename.splitFile
  131. if fileParts.ext == ".nim":
  132. g.currentSection = "Module " & fileParts.name
  133. g.seenIndexTerms = initTable[string, int]()
  134. g.msgHandler = msgHandler
  135. let s = config.getOrDefault"split.item.toc"
  136. if s != "": g.splitAfter = parseInt(s)
  137. for i in low(g.meta)..high(g.meta): g.meta[i] = ""
  138. proc writeIndexFile*(g: var RstGenerator, outfile: string) =
  139. ## Writes the current index buffer to the specified output file.
  140. ##
  141. ## You previously need to add entries to the index with the `setIndexTerm()
  142. ## <#setIndexTerm>`_ proc. If the index is empty the file won't be created.
  143. if g.theIndex.len > 0: writeFile(outfile, g.theIndex)
  144. proc addXmlChar(dest: var string, c: char) =
  145. case c
  146. of '&': add(dest, "&amp;")
  147. of '<': add(dest, "&lt;")
  148. of '>': add(dest, "&gt;")
  149. of '\"': add(dest, "&quot;")
  150. else: add(dest, c)
  151. proc addRtfChar(dest: var string, c: char) =
  152. case c
  153. of '{': add(dest, "\\{")
  154. of '}': add(dest, "\\}")
  155. of '\\': add(dest, "\\\\")
  156. else: add(dest, c)
  157. proc addTexChar(dest: var string, c: char) =
  158. case c
  159. of '_': add(dest, "\\_")
  160. of '{': add(dest, "\\symbol{123}")
  161. of '}': add(dest, "\\symbol{125}")
  162. of '[': add(dest, "\\symbol{91}")
  163. of ']': add(dest, "\\symbol{93}")
  164. of '\\': add(dest, "\\symbol{92}")
  165. of '$': add(dest, "\\$")
  166. of '&': add(dest, "\\&")
  167. of '#': add(dest, "\\#")
  168. of '%': add(dest, "\\%")
  169. of '~': add(dest, "\\symbol{126}")
  170. of '@': add(dest, "\\symbol{64}")
  171. of '^': add(dest, "\\symbol{94}")
  172. of '`': add(dest, "\\symbol{96}")
  173. else: add(dest, c)
  174. proc escChar*(target: OutputTarget, dest: var string, c: char) {.inline.} =
  175. case target
  176. of outHtml: addXmlChar(dest, c)
  177. of outLatex: addTexChar(dest, c)
  178. proc addSplitter(target: OutputTarget; dest: var string) {.inline.} =
  179. case target
  180. of outHtml: add(dest, "<wbr />")
  181. of outLatex: add(dest, "\\-")
  182. proc nextSplitPoint*(s: string, start: int): int =
  183. result = start
  184. while result < len(s) + 0:
  185. case s[result]
  186. of '_': return
  187. of 'a'..'z':
  188. if result + 1 < len(s) + 0:
  189. if s[result + 1] in {'A'..'Z'}: return
  190. else: discard
  191. inc(result)
  192. dec(result) # last valid index
  193. proc esc*(target: OutputTarget, s: string, splitAfter = -1): string =
  194. ## Escapes the HTML.
  195. result = ""
  196. if splitAfter >= 0:
  197. var partLen = 0
  198. var j = 0
  199. while j < len(s):
  200. var k = nextSplitPoint(s, j)
  201. #if (splitter != " ") or (partLen + k - j + 1 > splitAfter):
  202. partLen = 0
  203. addSplitter(target, result)
  204. for i in countup(j, k): escChar(target, result, s[i])
  205. inc(partLen, k - j + 1)
  206. j = k + 1
  207. else:
  208. for i in countup(0, len(s) - 1): escChar(target, result, s[i])
  209. proc disp(target: OutputTarget, xml, tex: string): string =
  210. if target != outLatex: result = xml
  211. else: result = tex
  212. proc dispF(target: OutputTarget, xml, tex: string,
  213. args: varargs[string]): string =
  214. if target != outLatex: result = xml % args
  215. else: result = tex % args
  216. proc dispA(target: OutputTarget, dest: var string,
  217. xml, tex: string, args: varargs[string]) =
  218. if target != outLatex: addf(dest, xml, args)
  219. else: addf(dest, tex, args)
  220. proc `or`(x, y: string): string {.inline.} =
  221. result = if x.len == 0: y else: x
  222. proc renderRstToOut*(d: var RstGenerator, n: PRstNode, result: var string)
  223. ## Writes into ``result`` the rst ast ``n`` using the ``d`` configuration.
  224. ##
  225. ## Before using this proc you need to initialise a ``RstGenerator`` with
  226. ## ``initRstGenerator`` and parse a rst file with ``rstParse`` from the
  227. ## `packages/docutils/rst module <rst.html>`_. Example:
  228. ##
  229. ## .. code-block:: nim
  230. ##
  231. ## # ...configure gen and rst vars...
  232. ## var generatedHtml = ""
  233. ## renderRstToOut(gen, rst, generatedHtml)
  234. ## echo generatedHtml
  235. proc renderAux(d: PDoc, n: PRstNode, result: var string) =
  236. for i in countup(0, len(n)-1): renderRstToOut(d, n.sons[i], result)
  237. proc renderAux(d: PDoc, n: PRstNode, frmtA, frmtB: string, result: var string) =
  238. var tmp = ""
  239. for i in countup(0, len(n)-1): renderRstToOut(d, n.sons[i], tmp)
  240. if d.target != outLatex:
  241. result.addf(frmtA, [tmp])
  242. else:
  243. result.addf(frmtB, [tmp])
  244. # ---------------- index handling --------------------------------------------
  245. proc quoteIndexColumn(text: string): string =
  246. ## Returns a safe version of `text` for serialization to the ``.idx`` file.
  247. ##
  248. ## The returned version can be put without worries in a line based tab
  249. ## separated column text file. The following character sequence replacements
  250. ## will be performed for that goal:
  251. ##
  252. ## * ``"\\"`` => ``"\\\\"``
  253. ## * ``"\n"`` => ``"\\n"``
  254. ## * ``"\t"`` => ``"\\t"``
  255. result = newStringOfCap(text.len + 3)
  256. for c in text:
  257. case c
  258. of '\\': result.add "\\"
  259. of '\L': result.add "\\n"
  260. of '\C': discard
  261. of '\t': result.add "\\t"
  262. else: result.add c
  263. proc unquoteIndexColumn(text: string): string =
  264. ## Returns the unquoted version generated by ``quoteIndexColumn``.
  265. result = text.multiReplace(("\\t", "\t"), ("\\n", "\n"), ("\\\\", "\\"))
  266. proc setIndexTerm*(d: var RstGenerator, htmlFile, id, term: string,
  267. linkTitle, linkDesc = "") =
  268. ## Adds a `term` to the index using the specified hyperlink identifier.
  269. ##
  270. ## A new entry will be added to the index using the format
  271. ## ``term<tab>file#id``. The file part will come from the `htmlFile`
  272. ## parameter.
  273. ##
  274. ## The `id` will be appended with a hash character only if its length is not
  275. ## zero, otherwise no specific anchor will be generated. In general you
  276. ## should only pass an empty `id` value for the title of standalone rst
  277. ## documents (they are special for the `mergeIndexes() <#mergeIndexes>`_
  278. ## proc, see `Index (idx) file format <docgen.html#index-idx-file-format>`_
  279. ## for more information). Unlike other index terms, title entries are
  280. ## inserted at the beginning of the accumulated buffer to maintain a logical
  281. ## order of entries.
  282. ##
  283. ## If `linkTitle` or `linkDesc` are not the empty string, two additional
  284. ## columns with their contents will be added.
  285. ##
  286. ## The index won't be written to disk unless you call `writeIndexFile()
  287. ## <#writeIndexFile>`_. The purpose of the index is documented in the `docgen
  288. ## tools guide <docgen.html#index-switch>`_.
  289. var
  290. entry = term
  291. isTitle = false
  292. entry.add('\t')
  293. entry.add(htmlFile)
  294. if id.len > 0:
  295. entry.add('#')
  296. entry.add(id)
  297. else:
  298. isTitle = true
  299. if linkTitle.len > 0 or linkDesc.len > 0:
  300. entry.add('\t' & linkTitle.quoteIndexColumn)
  301. entry.add('\t' & linkDesc.quoteIndexColumn)
  302. entry.add("\n")
  303. if isTitle: d.theIndex.insert(entry)
  304. else: d.theIndex.add(entry)
  305. proc hash(n: PRstNode): int =
  306. if n.kind == rnLeaf:
  307. result = hash(n.text)
  308. elif n.len > 0:
  309. result = hash(n.sons[0])
  310. for i in 1 ..< len(n):
  311. result = result !& hash(n.sons[i])
  312. result = !$result
  313. proc renderIndexTerm*(d: PDoc, n: PRstNode, result: var string) =
  314. ## Renders the string decorated within \`foobar\`\:idx\: markers.
  315. ##
  316. ## Additionally adds the encosed text to the index as a term. Since we are
  317. ## interested in different instances of the same term to have different
  318. ## entries, a table is used to keep track of the amount of times a term has
  319. ## previously appeared to give a different identifier value for each.
  320. let refname = n.rstnodeToRefname
  321. if d.seenIndexTerms.hasKey(refname):
  322. d.seenIndexTerms[refname] = d.seenIndexTerms.getOrDefault(refname) + 1
  323. else:
  324. d.seenIndexTerms[refname] = 1
  325. let id = refname & '_' & $d.seenIndexTerms.getOrDefault(refname)
  326. var term = ""
  327. renderAux(d, n, term)
  328. setIndexTerm(d, changeFileExt(extractFilename(d.filename), HtmlExt), id, term, d.currentSection)
  329. dispA(d.target, result, "<span id=\"$1\">$2</span>", "$2\\label{$1}",
  330. [id, term])
  331. type
  332. IndexEntry = object
  333. keyword: string
  334. link: string
  335. linkTitle: string ## contains a prettier text for the href
  336. linkDesc: string ## the title attribute of the final href
  337. IndexedDocs = Table[IndexEntry, seq[IndexEntry]] ## \
  338. ## Contains the index sequences for doc types.
  339. ##
  340. ## The key is a *fake* IndexEntry which will contain the title of the
  341. ## document in the `keyword` field and `link` will contain the html
  342. ## filename for the document. `linkTitle` and `linkDesc` will be empty.
  343. ##
  344. ## The value indexed by this IndexEntry is a sequence with the real index
  345. ## entries found in the ``.idx`` file.
  346. proc cmp(a, b: IndexEntry): int =
  347. ## Sorts two ``IndexEntry`` first by `keyword` field, then by `link`.
  348. result = cmpIgnoreStyle(a.keyword, b.keyword)
  349. if result == 0:
  350. result = cmpIgnoreStyle(a.link, b.link)
  351. proc hash(x: IndexEntry): Hash =
  352. ## Returns the hash for the combined fields of the type.
  353. ##
  354. ## The hash is computed as the chained hash of the individual string hashes.
  355. result = x.keyword.hash !& x.link.hash
  356. result = result !& x.linkTitle.hash
  357. result = result !& x.linkDesc.hash
  358. result = !$result
  359. proc `<-`(a: var IndexEntry, b: IndexEntry) =
  360. shallowCopy a.keyword, b.keyword
  361. shallowCopy a.link, b.link
  362. shallowCopy a.linkTitle, b.linkTitle
  363. shallowCopy a.linkDesc, b.linkDesc
  364. proc sortIndex(a: var openArray[IndexEntry]) =
  365. # we use shellsort here; fast and simple
  366. let n = len(a)
  367. var h = 1
  368. while true:
  369. h = 3 * h + 1
  370. if h > n: break
  371. while true:
  372. h = h div 3
  373. for i in countup(h, n - 1):
  374. var v: IndexEntry
  375. v <- a[i]
  376. var j = i
  377. while cmp(a[j-h], v) >= 0:
  378. a[j] <- a[j-h]
  379. j = j-h
  380. if j < h: break
  381. a[j] <- v
  382. if h == 1: break
  383. proc escapeLink(s: string): string =
  384. ## This proc is mostly copied from uri/encodeUrl except that
  385. ## these chars are also left unencoded: '#', '/'.
  386. result = newStringOfCap(s.len + s.len shr 2)
  387. for c in items(s):
  388. case c
  389. of 'a'..'z', 'A'..'Z', '0'..'9', '-', '.', '_', '~': # same as that in uri/encodeUrl
  390. add(result, c)
  391. of '#', '/': # example.com/foo/#bar (don't escape the '/' and '#' in such links)
  392. add(result, c)
  393. else:
  394. add(result, "%")
  395. add(result, toHex(ord(c), 2))
  396. proc generateSymbolIndex(symbols: seq[IndexEntry]): string =
  397. result = "<dl>"
  398. var i = 0
  399. while i < symbols.len:
  400. let keyword = symbols[i].keyword
  401. let cleanedKeyword = keyword.escapeLink
  402. result.addf("<dt><a name=\"$2\" href=\"#$2\"><span>$1:</span></a></dt><dd><ul class=\"simple\">\n",
  403. [keyword, cleanedKeyword])
  404. var j = i
  405. while j < symbols.len and keyword == symbols[j].keyword:
  406. let
  407. url = symbols[j].link.escapeLink
  408. text = if symbols[j].linkTitle.len > 0: symbols[j].linkTitle else: url
  409. desc = if symbols[j].linkDesc.len > 0: symbols[j].linkDesc else: ""
  410. if desc.len > 0:
  411. result.addf("""<li><a class="reference external"
  412. title="$3" data-doc-search-tag="$2" href="$1">$2</a></li>
  413. """, [url, text, desc])
  414. else:
  415. result.addf("""<li><a class="reference external"
  416. data-doc-search-tag="$2" href="$1">$2</a></li>
  417. """, [url, text])
  418. inc j
  419. result.add("</ul></dd>\n")
  420. i = j
  421. result.add("</dl>")
  422. proc isDocumentationTitle(hyperlink: string): bool =
  423. ## Returns true if the hyperlink is actually a documentation title.
  424. ##
  425. ## Documentation titles lack the hash. See `mergeIndexes() <#mergeIndexes>`_
  426. ## for a more detailed explanation.
  427. result = hyperlink.find('#') < 0
  428. proc stripTocLevel(s: string): tuple[level: int, text: string] =
  429. ## Returns the *level* of the toc along with the text without it.
  430. for c in 0 ..< s.len:
  431. result.level = c
  432. if s[c] != ' ': break
  433. result.text = s[result.level ..< s.len]
  434. proc indentToLevel(level: var int, newLevel: int): string =
  435. ## Returns the sequence of <ul>|</ul> characters to switch to `newLevel`.
  436. ##
  437. ## The amount of lists added/removed will be based on the `level` variable,
  438. ## which will be reset to `newLevel` at the end of the proc.
  439. result = ""
  440. if level == newLevel:
  441. return
  442. if newLevel > level:
  443. result = repeat("<li><ul>", newLevel - level)
  444. else:
  445. result = repeat("</ul></li>", level - newLevel)
  446. level = newLevel
  447. proc generateDocumentationToc(entries: seq[IndexEntry]): string =
  448. ## Returns the sequence of index entries in an HTML hierarchical list.
  449. result = ""
  450. # Build a list of levels and extracted titles to make processing easier.
  451. var
  452. titleRef: string
  453. titleTag: string
  454. levels: seq[tuple[level: int, text: string]]
  455. L = 0
  456. level = 1
  457. levels.newSeq(entries.len)
  458. for entry in entries:
  459. let (rawLevel, rawText) = stripTocLevel(entry.linkTitle or entry.keyword)
  460. if rawLevel < 1:
  461. # This is a normal symbol, push it *inside* one level from the last one.
  462. levels[L].level = level + 1
  463. # Also, ignore the linkTitle and use directly the keyword.
  464. levels[L].text = entry.keyword
  465. else:
  466. # The level did change, update the level indicator.
  467. level = rawLevel
  468. levels[L].level = rawLevel
  469. levels[L].text = rawText
  470. inc L
  471. # Now generate hierarchical lists based on the precalculated levels.
  472. result = "<ul>\n"
  473. level = 1
  474. L = 0
  475. while L < entries.len:
  476. let link = entries[L].link
  477. if link.isDocumentationTitle:
  478. titleRef = link
  479. titleTag = levels[L].text
  480. else:
  481. result.add(level.indentToLevel(levels[L].level))
  482. result.addf("""<li><a class="reference" data-doc-search-tag="$1: $2" href="$3">
  483. $3</a></li>
  484. """, [titleTag, levels[L].text, link, levels[L].text])
  485. inc L
  486. result.add(level.indentToLevel(1) & "</ul>\n")
  487. proc generateDocumentationIndex(docs: IndexedDocs): string =
  488. ## Returns all the documentation TOCs in an HTML hierarchical list.
  489. result = ""
  490. # Sort the titles to generate their toc in alphabetical order.
  491. var titles = toSeq(keys[IndexEntry, seq[IndexEntry]](docs))
  492. sort(titles, cmp)
  493. for title in titles:
  494. let tocList = generateDocumentationToc(docs.getOrDefault(title))
  495. result.add("<ul><li><a href=\"" &
  496. title.link & "\">" & title.keyword & "</a>\n" & tocList & "</li></ul>\n")
  497. proc generateDocumentationJumps(docs: IndexedDocs): string =
  498. ## Returns a plain list of hyperlinks to documentation TOCs in HTML.
  499. result = "Documents: "
  500. # Sort the titles to generate their toc in alphabetical order.
  501. var titles = toSeq(keys[IndexEntry, seq[IndexEntry]](docs))
  502. sort(titles, cmp)
  503. var chunks: seq[string] = @[]
  504. for title in titles:
  505. chunks.add("<a href=\"" & title.link & "\">" & title.keyword & "</a>")
  506. result.add(chunks.join(", ") & ".<br/>")
  507. proc generateModuleJumps(modules: seq[string]): string =
  508. ## Returns a plain list of hyperlinks to the list of modules.
  509. result = "Modules: "
  510. var chunks: seq[string] = @[]
  511. for name in modules:
  512. chunks.add("<a href=\"" & name & ".html\">" & name & "</a>")
  513. result.add(chunks.join(", ") & ".<br/>")
  514. proc readIndexDir(dir: string):
  515. tuple[modules: seq[string], symbols: seq[IndexEntry], docs: IndexedDocs] =
  516. ## Walks `dir` reading ``.idx`` files converting them in IndexEntry items.
  517. ##
  518. ## Returns the list of found module names, the list of free symbol entries
  519. ## and the different documentation indexes. The list of modules is sorted.
  520. ## See the documentation of ``mergeIndexes`` for details.
  521. result.modules = @[]
  522. result.docs = initTable[IndexEntry, seq[IndexEntry]](32)
  523. newSeq(result.symbols, 15_000)
  524. setLen(result.symbols, 0)
  525. var L = 0
  526. # Scan index files and build the list of symbols.
  527. for path in walkDirRec(dir):
  528. if path.endsWith(IndexExt):
  529. var
  530. fileEntries: seq[IndexEntry]
  531. title: IndexEntry
  532. F = 0
  533. newSeq(fileEntries, 500)
  534. setLen(fileEntries, 0)
  535. for line in lines(path):
  536. let s = line.find('\t')
  537. if s < 0: continue
  538. setLen(fileEntries, F+1)
  539. fileEntries[F].keyword = line.substr(0, s-1)
  540. fileEntries[F].link = line.substr(s+1)
  541. # See if we detect a title, a link without a `#foobar` trailing part.
  542. if title.keyword.len == 0 and fileEntries[F].link.isDocumentationTitle:
  543. title.keyword = fileEntries[F].keyword
  544. title.link = fileEntries[F].link
  545. if fileEntries[F].link.find('\t') > 0:
  546. let extraCols = fileEntries[F].link.split('\t')
  547. fileEntries[F].link = extraCols[0]
  548. assert extraCols.len == 3
  549. fileEntries[F].linkTitle = extraCols[1].unquoteIndexColumn
  550. fileEntries[F].linkDesc = extraCols[2].unquoteIndexColumn
  551. else:
  552. fileEntries[F].linkTitle = ""
  553. fileEntries[F].linkDesc = ""
  554. inc F
  555. # Depending on type add this to the list of symbols or table of APIs.
  556. if title.keyword.len == 0:
  557. for i in 0 ..< F:
  558. # Don't add to symbols TOC entries (they start with a whitespace).
  559. let toc = fileEntries[i].linkTitle
  560. if toc.len > 0 and toc[0] == ' ':
  561. continue
  562. # Ok, non TOC entry, add it.
  563. setLen(result.symbols, L + 1)
  564. result.symbols[L] = fileEntries[i]
  565. inc L
  566. if fileEntries.len > 0:
  567. var x = fileEntries[0].link
  568. let i = find(x, '#')
  569. if i > 0:
  570. x = x.substr(0, i-1)
  571. if i != 0:
  572. # don't add entries starting with '#'
  573. result.modules.add(x.changeFileExt(""))
  574. else:
  575. # Generate the symbolic anchor for index quickjumps.
  576. title.linkTitle = "doc_toc_" & $result.docs.len
  577. result.docs[title] = fileEntries
  578. sort(result.modules, system.cmp)
  579. proc mergeIndexes*(dir: string): string =
  580. ## Merges all index files in `dir` and returns the generated index as HTML.
  581. ##
  582. ## This proc will first scan `dir` for index files with the ``.idx``
  583. ## extension previously created by commands like ``nim doc|rst2html``
  584. ## which use the ``--index:on`` switch. These index files are the result of
  585. ## calls to `setIndexTerm() <#setIndexTerm>`_ and `writeIndexFile()
  586. ## <#writeIndexFile>`_, so they are simple tab separated files.
  587. ##
  588. ## As convention this proc will split index files into two categories:
  589. ## documentation and API. API indices will be all joined together into a
  590. ## single big sorted index, making the bulk of the final index. This is good
  591. ## for API documentation because many symbols are repated in different
  592. ## modules. On the other hand, documentation indices are essentially table of
  593. ## contents plus a few special markers. These documents will be rendered in a
  594. ## separate section which tries to maintain the order and hierarchy of the
  595. ## symbols in the index file.
  596. ##
  597. ## To differentiate between a documentation and API file a convention is
  598. ## used: indices which contain one entry without the HTML hash character (#)
  599. ## will be considered `documentation`, since this hash-less entry is the
  600. ## explicit title of the document. Indices without this explicit entry will
  601. ## be considered `generated API` extracted out of a source ``.nim`` file.
  602. ##
  603. ## Returns the merged and sorted indices into a single HTML block which can
  604. ## be further embedded into nimdoc templates.
  605. var (modules, symbols, docs) = readIndexDir(dir)
  606. result = ""
  607. # Generate a quick jump list of documents.
  608. if docs.len > 0:
  609. result.add(generateDocumentationJumps(docs))
  610. result.add("<p />")
  611. # Generate hyperlinks to all the linked modules.
  612. if modules.len > 0:
  613. result.add(generateModuleJumps(modules))
  614. result.add("<p />")
  615. when false:
  616. # Generate the HTML block with API documents.
  617. if docs.len > 0:
  618. result.add("<h2>Documentation files</h2>\n")
  619. result.add(generateDocumentationIndex(docs))
  620. # Generate the HTML block with symbols.
  621. if symbols.len > 0:
  622. sortIndex(symbols)
  623. result.add("<h2>API symbols</h2>\n")
  624. result.add(generateSymbolIndex(symbols))
  625. # ----------------------------------------------------------------------------
  626. proc stripTocHtml(s: string): string =
  627. ## Ugly quick hack to remove HTML tags from TOC titles.
  628. ##
  629. ## A TocEntry.header field already contains rendered HTML tags. Instead of
  630. ## implementing a proper version of renderRstToOut() which recursively
  631. ## renders an rst tree to plain text, we simply remove text found between
  632. ## angled brackets. Given the limited possibilities of rst inside TOC titles
  633. ## this should be enough.
  634. result = s
  635. var first = result.find('<')
  636. while first >= 0:
  637. let last = result.find('>', first)
  638. if last < 0:
  639. # Abort, since we didn't found a closing angled bracket.
  640. return
  641. result.delete(first, last)
  642. first = result.find('<', first)
  643. proc renderHeadline(d: PDoc, n: PRstNode, result: var string) =
  644. var tmp = ""
  645. for i in countup(0, len(n) - 1): renderRstToOut(d, n.sons[i], tmp)
  646. d.currentSection = tmp
  647. # Find the last higher level section for unique reference name
  648. var sectionPrefix = ""
  649. for i in countdown(d.tocPart.high, 0):
  650. let n2 = d.tocPart[i].n
  651. if n2.level < n.level:
  652. sectionPrefix = rstnodeToRefname(n2) & "-"
  653. break
  654. var refname = sectionPrefix & rstnodeToRefname(n)
  655. if d.hasToc:
  656. var length = len(d.tocPart)
  657. setLen(d.tocPart, length + 1)
  658. d.tocPart[length].refname = refname
  659. d.tocPart[length].n = n
  660. d.tocPart[length].header = tmp
  661. dispA(d.target, result, "\n<h$1><a class=\"toc-backref\" " &
  662. "id=\"$2\" href=\"#$2\">$3</a></h$1>", "\\rsth$4{$3}\\label{$2}\n",
  663. [$n.level, d.tocPart[length].refname, tmp, $chr(n.level - 1 + ord('A'))])
  664. else:
  665. dispA(d.target, result, "\n<h$1 id=\"$2\">$3</h$1>",
  666. "\\rsth$4{$3}\\label{$2}\n", [
  667. $n.level, refname, tmp,
  668. $chr(n.level - 1 + ord('A'))])
  669. # Generate index entry using spaces to indicate TOC level for the output HTML.
  670. assert n.level >= 0
  671. setIndexTerm(d, changeFileExt(extractFilename(d.filename), HtmlExt), refname, tmp.stripTocHtml,
  672. spaces(max(0, n.level)) & tmp)
  673. proc renderOverline(d: PDoc, n: PRstNode, result: var string) =
  674. if d.meta[metaTitle].len == 0:
  675. for i in countup(0, len(n)-1):
  676. renderRstToOut(d, n.sons[i], d.meta[metaTitle])
  677. d.currentSection = d.meta[metaTitle]
  678. elif d.meta[metaSubtitle].len == 0:
  679. for i in countup(0, len(n)-1):
  680. renderRstToOut(d, n.sons[i], d.meta[metaSubtitle])
  681. d.currentSection = d.meta[metaSubtitle]
  682. else:
  683. var tmp = ""
  684. for i in countup(0, len(n) - 1): renderRstToOut(d, n.sons[i], tmp)
  685. d.currentSection = tmp
  686. dispA(d.target, result, "<h$1 id=\"$2\"><center>$3</center></h$1>",
  687. "\\rstov$4{$3}\\label{$2}\n", [$n.level,
  688. rstnodeToRefname(n), tmp, $chr(n.level - 1 + ord('A'))])
  689. proc renderTocEntry(d: PDoc, e: TocEntry, result: var string) =
  690. dispA(d.target, result,
  691. "<li><a class=\"reference\" id=\"$1_toc\" href=\"#$1\">$2</a></li>\n",
  692. "\\item\\label{$1_toc} $2\\ref{$1}\n", [e.refname, e.header])
  693. proc renderTocEntries*(d: var RstGenerator, j: var int, lvl: int,
  694. result: var string) =
  695. var tmp = ""
  696. while j <= high(d.tocPart):
  697. var a = abs(d.tocPart[j].n.level)
  698. if a == lvl:
  699. renderTocEntry(d, d.tocPart[j], tmp)
  700. inc(j)
  701. elif a > lvl:
  702. renderTocEntries(d, j, a, tmp)
  703. else:
  704. break
  705. if lvl > 1:
  706. dispA(d.target, result, "<ul class=\"simple\">$1</ul>",
  707. "\\begin{enumerate}$1\\end{enumerate}", [tmp])
  708. else:
  709. result.add(tmp)
  710. proc renderImage(d: PDoc, n: PRstNode, result: var string) =
  711. let
  712. arg = getArgument(n)
  713. var
  714. options = ""
  715. var s = esc(d.target, getFieldValue(n, "scale").strip())
  716. if s.len > 0:
  717. dispA(d.target, options, " scale=\"$1\"", " scale=$1", [s])
  718. s = esc(d.target, getFieldValue(n, "height").strip())
  719. if s.len > 0:
  720. dispA(d.target, options, " height=\"$1\"", " height=$1", [s])
  721. s = esc(d.target, getFieldValue(n, "width").strip())
  722. if s.len > 0:
  723. dispA(d.target, options, " width=\"$1\"", " width=$1", [s])
  724. s = esc(d.target, getFieldValue(n, "alt").strip())
  725. if s.len > 0:
  726. dispA(d.target, options, " alt=\"$1\"", "", [s])
  727. s = esc(d.target, getFieldValue(n, "align").strip())
  728. if s.len > 0:
  729. dispA(d.target, options, " align=\"$1\"", "", [s])
  730. if options.len > 0: options = dispF(d.target, "$1", "[$1]", [options])
  731. var htmlOut = ""
  732. if arg.endsWith(".mp4") or arg.endsWith(".ogg") or
  733. arg.endsWith(".webm"):
  734. htmlOut = """
  735. <video src="$1"$2 autoPlay='true' loop='true' muted='true'>
  736. Sorry, your browser doesn't support embedded videos
  737. </video>
  738. """
  739. else:
  740. htmlOut = "<img src=\"$1\"$2/>"
  741. dispA(d.target, result, htmlOut, "\\includegraphics$2{$1}",
  742. [esc(d.target, arg), options])
  743. if len(n) >= 3: renderRstToOut(d, n.sons[2], result)
  744. proc renderSmiley(d: PDoc, n: PRstNode, result: var string) =
  745. dispA(d.target, result,
  746. """<img src="$1" width="15"
  747. height="17" hspace="2" vspace="2" class="smiley" />""",
  748. "\\includegraphics{$1}",
  749. [d.config.getOrDefault"doc.smiley_format" % n.text])
  750. proc parseCodeBlockField(d: PDoc, n: PRstNode, params: var CodeBlockParams) =
  751. ## Parses useful fields which can appear before a code block.
  752. ##
  753. ## This supports the special ``default-language`` internal string generated
  754. ## by the ``rst`` module to communicate a specific default language.
  755. case n.getArgument.toLowerAscii
  756. of "number-lines":
  757. params.numberLines = true
  758. # See if the field has a parameter specifying a different line than 1.
  759. var number: int
  760. if parseInt(n.getFieldValue, number) > 0:
  761. params.startLine = number
  762. of "file", "filename":
  763. # The ``file`` option is a Nim extension to the official spec, it acts
  764. # like it would for other directives like ``raw`` or ``cvs-table``. This
  765. # field is dealt with in ``rst.nim`` which replaces the existing block with
  766. # the referenced file, so we only need to ignore it here to avoid incorrect
  767. # warning messages.
  768. params.filename = n.getFieldValue.strip
  769. of "test":
  770. params.testCmd = n.getFieldValue.strip
  771. if params.testCmd.len == 0:
  772. params.testCmd = "nim c -r $1"
  773. else:
  774. params.testCmd = unescape(params.testCmd)
  775. of "status", "exitcode":
  776. var status: int
  777. if parseInt(n.getFieldValue, status) > 0:
  778. params.status = status
  779. of "default-language":
  780. params.langStr = n.getFieldValue.strip
  781. params.lang = params.langStr.getSourceLanguage
  782. else:
  783. d.msgHandler(d.filename, 1, 0, mwUnsupportedField, n.getArgument)
  784. proc parseCodeBlockParams(d: PDoc, n: PRstNode): CodeBlockParams =
  785. ## Iterates over all code block fields and returns processed params.
  786. ##
  787. ## Also processes the argument of the directive as the default language. This
  788. ## is done last so as to override any internal communication field variables.
  789. result.init
  790. if n.isNil:
  791. return
  792. assert n.kind == rnCodeBlock
  793. assert(not n.sons[2].isNil)
  794. # Parse the field list for rendering parameters if there are any.
  795. if not n.sons[1].isNil:
  796. for son in n.sons[1].sons: d.parseCodeBlockField(son, result)
  797. # Parse the argument and override the language.
  798. result.langStr = strip(getArgument(n))
  799. if result.langStr != "":
  800. result.lang = getSourceLanguage(result.langStr)
  801. proc buildLinesHtmlTable(d: PDoc; params: CodeBlockParams, code: string):
  802. tuple[beginTable, endTable: string] =
  803. ## Returns the necessary tags to start/end a code block in HTML.
  804. ##
  805. ## If the numberLines has not been used, the tags will default to a simple
  806. ## <pre> pair. Otherwise it will build a table and insert an initial column
  807. ## with all the line numbers, which requires you to pass the `code` to detect
  808. ## how many lines have to be generated (and starting at which point!).
  809. inc d.listingCounter
  810. let id = $d.listingCounter
  811. if not params.numberLines:
  812. result = (d.config.getOrDefault"doc.listing_start" %
  813. [id, sourceLanguageToStr[params.lang]],
  814. d.config.getOrDefault"doc.listing_end" % id)
  815. return
  816. var codeLines = code.strip.countLines
  817. assert codeLines > 0
  818. result.beginTable = """<table class="line-nums-table"><tbody><tr><td class="blob-line-nums"><pre class="line-nums">"""
  819. var line = params.startLine
  820. while codeLines > 0:
  821. result.beginTable.add($line & "\n")
  822. line.inc
  823. codeLines.dec
  824. result.beginTable.add("</pre></td><td>" & (
  825. d.config.getOrDefault"doc.listing_start" %
  826. [id, sourceLanguageToStr[params.lang]]))
  827. result.endTable = (d.config.getOrDefault"doc.listing_end" % id) &
  828. "</td></tr></tbody></table>" & (
  829. d.config.getOrDefault"doc.listing_button" % id)
  830. proc renderCodeBlock(d: PDoc, n: PRstNode, result: var string) =
  831. ## Renders a code block, appending it to `result`.
  832. ##
  833. ## If the code block uses the ``number-lines`` option, a table will be
  834. ## generated with two columns, the first being a list of numbers and the
  835. ## second the code block itself. The code block can use syntax highlighting,
  836. ## which depends on the directive argument specified by the rst input, and
  837. ## may also come from the parser through the internal ``default-language``
  838. ## option to differentiate between a plain code block and Nim's code block
  839. ## extension.
  840. assert n.kind == rnCodeBlock
  841. if n.sons[2] == nil: return
  842. var params = d.parseCodeBlockParams(n)
  843. var m = n.sons[2].sons[0]
  844. assert m.kind == rnLeaf
  845. if params.testCmd.len > 0 and d.onTestSnippet != nil:
  846. d.onTestSnippet(d, params.filename, params.testCmd, params.status, m.text)
  847. let (blockStart, blockEnd) = buildLinesHtmlTable(d, params, m.text)
  848. dispA(d.target, result, blockStart, "\\begin{rstpre}\n", [])
  849. if params.lang == langNone:
  850. if len(params.langStr) > 0:
  851. d.msgHandler(d.filename, 1, 0, mwUnsupportedLanguage, params.langStr)
  852. for letter in m.text: escChar(d.target, result, letter)
  853. else:
  854. var g: GeneralTokenizer
  855. initGeneralTokenizer(g, m.text)
  856. while true:
  857. getNextToken(g, params.lang)
  858. case g.kind
  859. of gtEof: break
  860. of gtNone, gtWhitespace:
  861. add(result, substr(m.text, g.start, g.length + g.start - 1))
  862. else:
  863. dispA(d.target, result, "<span class=\"$2\">$1</span>", "\\span$2{$1}", [
  864. esc(d.target, substr(m.text, g.start, g.length+g.start-1)),
  865. tokenClassToStr[g.kind]])
  866. deinitGeneralTokenizer(g)
  867. dispA(d.target, result, blockEnd, "\n\\end{rstpre}\n")
  868. proc renderContainer(d: PDoc, n: PRstNode, result: var string) =
  869. var tmp = ""
  870. renderRstToOut(d, n.sons[2], tmp)
  871. var arg = esc(d.target, strip(getArgument(n)))
  872. if arg == "":
  873. dispA(d.target, result, "<div>$1</div>", "$1", [tmp])
  874. else:
  875. dispA(d.target, result, "<div class=\"$1\">$2</div>", "$2", [arg, tmp])
  876. proc texColumns(n: PRstNode): string =
  877. result = ""
  878. for i in countup(1, len(n)): add(result, "|X")
  879. proc renderField(d: PDoc, n: PRstNode, result: var string) =
  880. var b = false
  881. if d.target == outLatex:
  882. var fieldname = addNodes(n.sons[0])
  883. var fieldval = esc(d.target, strip(addNodes(n.sons[1])))
  884. if cmpIgnoreStyle(fieldname, "author") == 0 or
  885. cmpIgnoreStyle(fieldname, "authors") == 0:
  886. if d.meta[metaAuthor].len == 0:
  887. d.meta[metaAuthor] = fieldval
  888. b = true
  889. elif cmpIgnoreStyle(fieldname, "version") == 0:
  890. if d.meta[metaVersion].len == 0:
  891. d.meta[metaVersion] = fieldval
  892. b = true
  893. if not b:
  894. renderAux(d, n, "<tr>$1</tr>\n", "$1", result)
  895. proc renderRstToOut(d: PDoc, n: PRstNode, result: var string) =
  896. if n == nil: return
  897. case n.kind
  898. of rnInner: renderAux(d, n, result)
  899. of rnHeadline: renderHeadline(d, n, result)
  900. of rnOverline: renderOverline(d, n, result)
  901. of rnTransition: renderAux(d, n, "<hr />\n", "\\hrule\n", result)
  902. of rnParagraph: renderAux(d, n, "<p>$1</p>\n", "$1\n\n", result)
  903. of rnBulletList:
  904. renderAux(d, n, "<ul class=\"simple\">$1</ul>\n",
  905. "\\begin{itemize}$1\\end{itemize}\n", result)
  906. of rnBulletItem, rnEnumItem:
  907. renderAux(d, n, "<li>$1</li>\n", "\\item $1\n", result)
  908. of rnEnumList:
  909. renderAux(d, n, "<ol class=\"simple\">$1</ol>\n",
  910. "\\begin{enumerate}$1\\end{enumerate}\n", result)
  911. of rnDefList:
  912. renderAux(d, n, "<dl class=\"docutils\">$1</dl>\n",
  913. "\\begin{description}$1\\end{description}\n", result)
  914. of rnDefItem: renderAux(d, n, result)
  915. of rnDefName: renderAux(d, n, "<dt>$1</dt>\n", "\\item[$1] ", result)
  916. of rnDefBody: renderAux(d, n, "<dd>$1</dd>\n", "$1\n", result)
  917. of rnFieldList:
  918. var tmp = ""
  919. for i in countup(0, len(n) - 1):
  920. renderRstToOut(d, n.sons[i], tmp)
  921. if tmp.len != 0:
  922. dispA(d.target, result,
  923. "<table class=\"docinfo\" frame=\"void\" rules=\"none\">" &
  924. "<col class=\"docinfo-name\" />" &
  925. "<col class=\"docinfo-content\" />" &
  926. "<tbody valign=\"top\">$1" &
  927. "</tbody></table>",
  928. "\\begin{description}$1\\end{description}\n",
  929. [tmp])
  930. of rnField: renderField(d, n, result)
  931. of rnFieldName:
  932. renderAux(d, n, "<th class=\"docinfo-name\">$1:</th>",
  933. "\\item[$1:]", result)
  934. of rnFieldBody:
  935. renderAux(d, n, "<td>$1</td>", " $1\n", result)
  936. of rnIndex:
  937. renderRstToOut(d, n.sons[2], result)
  938. of rnOptionList:
  939. renderAux(d, n, "<table frame=\"void\">$1</table>",
  940. "\\begin{description}\n$1\\end{description}\n", result)
  941. of rnOptionListItem:
  942. renderAux(d, n, "<tr>$1</tr>\n", "$1", result)
  943. of rnOptionGroup:
  944. renderAux(d, n, "<th align=\"left\">$1</th>", "\\item[$1]", result)
  945. of rnDescription:
  946. renderAux(d, n, "<td align=\"left\">$1</td>\n", " $1\n", result)
  947. of rnOption, rnOptionString, rnOptionArgument:
  948. doAssert false, "renderRstToOut"
  949. of rnLiteralBlock:
  950. renderAux(d, n, "<pre>$1</pre>\n",
  951. "\\begin{rstpre}\n$1\n\\end{rstpre}\n", result)
  952. of rnQuotedLiteralBlock:
  953. doAssert false, "renderRstToOut"
  954. of rnLineBlock:
  955. renderAux(d, n, "<p>$1</p>", "$1\n\n", result)
  956. of rnLineBlockItem:
  957. renderAux(d, n, "$1<br />", "$1\\\\\n", result)
  958. of rnBlockQuote:
  959. renderAux(d, n, "<blockquote><p>$1</p></blockquote>\n",
  960. "\\begin{quote}$1\\end{quote}\n", result)
  961. of rnTable, rnGridTable:
  962. renderAux(d, n,
  963. "<table border=\"1\" class=\"docutils\">$1</table>",
  964. "\\begin{table}\\begin{rsttab}{" &
  965. texColumns(n) & "|}\n\\hline\n$1\\end{rsttab}\\end{table}", result)
  966. of rnTableRow:
  967. if len(n) >= 1:
  968. if d.target == outLatex:
  969. #var tmp = ""
  970. renderRstToOut(d, n.sons[0], result)
  971. for i in countup(1, len(n) - 1):
  972. result.add(" & ")
  973. renderRstToOut(d, n.sons[i], result)
  974. result.add("\\\\\n\\hline\n")
  975. else:
  976. result.add("<tr>")
  977. renderAux(d, n, result)
  978. result.add("</tr>\n")
  979. of rnTableDataCell:
  980. renderAux(d, n, "<td>$1</td>", "$1", result)
  981. of rnTableHeaderCell:
  982. renderAux(d, n, "<th>$1</th>", "\\textbf{$1}", result)
  983. of rnLabel:
  984. doAssert false, "renderRstToOut" # used for footnotes and other
  985. of rnFootnote:
  986. doAssert false, "renderRstToOut" # a footnote
  987. of rnCitation:
  988. doAssert false, "renderRstToOut" # similar to footnote
  989. of rnRef:
  990. var tmp = ""
  991. renderAux(d, n, tmp)
  992. dispA(d.target, result,
  993. "<a class=\"reference external\" href=\"#$2\">$1</a>",
  994. "$1\\ref{$2}", [tmp, rstnodeToRefname(n)])
  995. of rnStandaloneHyperlink:
  996. renderAux(d, n,
  997. "<a class=\"reference external\" href=\"$1\">$1</a>",
  998. "\\href{$1}{$1}", result)
  999. of rnHyperlink:
  1000. var tmp0 = ""
  1001. var tmp1 = ""
  1002. renderRstToOut(d, n.sons[0], tmp0)
  1003. renderRstToOut(d, n.sons[1], tmp1)
  1004. dispA(d.target, result,
  1005. "<a class=\"reference external\" href=\"$2\">$1</a>",
  1006. "\\href{$2}{$1}", [tmp0, tmp1])
  1007. of rnDirArg, rnRaw: renderAux(d, n, result)
  1008. of rnRawHtml:
  1009. if d.target != outLatex:
  1010. result.add addNodes(lastSon(n))
  1011. of rnRawLatex:
  1012. if d.target == outLatex:
  1013. result.add addNodes(lastSon(n))
  1014. of rnImage, rnFigure: renderImage(d, n, result)
  1015. of rnCodeBlock: renderCodeBlock(d, n, result)
  1016. of rnContainer: renderContainer(d, n, result)
  1017. of rnSubstitutionReferences, rnSubstitutionDef:
  1018. renderAux(d, n, "|$1|", "|$1|", result)
  1019. of rnDirective:
  1020. renderAux(d, n, "", "", result)
  1021. of rnGeneralRole:
  1022. var tmp0 = ""
  1023. var tmp1 = ""
  1024. renderRstToOut(d, n.sons[0], tmp0)
  1025. renderRstToOut(d, n.sons[1], tmp1)
  1026. dispA(d.target, result, "<span class=\"$2\">$1</span>", "\\span$2{$1}",
  1027. [tmp0, tmp1])
  1028. of rnSub: renderAux(d, n, "<sub>$1</sub>", "\\rstsub{$1}", result)
  1029. of rnSup: renderAux(d, n, "<sup>$1</sup>", "\\rstsup{$1}", result)
  1030. of rnEmphasis: renderAux(d, n, "<em>$1</em>", "\\emph{$1}", result)
  1031. of rnStrongEmphasis:
  1032. renderAux(d, n, "<strong>$1</strong>", "\\textbf{$1}", result)
  1033. of rnTripleEmphasis:
  1034. renderAux(d, n, "<strong><em>$1</em></strong>",
  1035. "\\textbf{emph{$1}}", result)
  1036. of rnInterpretedText:
  1037. renderAux(d, n, "<cite>$1</cite>", "\\emph{$1}", result)
  1038. of rnIdx:
  1039. renderIndexTerm(d, n, result)
  1040. of rnInlineLiteral:
  1041. renderAux(d, n,
  1042. "<tt class=\"docutils literal\"><span class=\"pre\">$1</span></tt>",
  1043. "\\texttt{$1}", result)
  1044. of rnSmiley: renderSmiley(d, n, result)
  1045. of rnLeaf: result.add(esc(d.target, n.text))
  1046. of rnContents: d.hasToc = true
  1047. of rnTitle:
  1048. d.meta[metaTitle] = ""
  1049. renderRstToOut(d, n.sons[0], d.meta[metaTitle])
  1050. # -----------------------------------------------------------------------------
  1051. proc getVarIdx(varnames: openArray[string], id: string): int =
  1052. for i in countup(0, high(varnames)):
  1053. if cmpIgnoreStyle(varnames[i], id) == 0:
  1054. return i
  1055. result = -1
  1056. proc formatNamedVars*(frmt: string, varnames: openArray[string],
  1057. varvalues: openArray[string]): string =
  1058. var i = 0
  1059. var L = len(frmt)
  1060. result = ""
  1061. var num = 0
  1062. while i < L:
  1063. if frmt[i] == '$':
  1064. inc(i) # skip '$'
  1065. case frmt[i]
  1066. of '#':
  1067. add(result, varvalues[num])
  1068. inc(num)
  1069. inc(i)
  1070. of '$':
  1071. add(result, "$")
  1072. inc(i)
  1073. of '0'..'9':
  1074. var j = 0
  1075. while true:
  1076. j = (j * 10) + ord(frmt[i]) - ord('0')
  1077. inc(i)
  1078. if i > L-1 or frmt[i] notin {'0'..'9'}: break
  1079. if j > high(varvalues) + 1:
  1080. raise newException(ValueError, "invalid index: " & $j)
  1081. num = j
  1082. add(result, varvalues[j - 1])
  1083. of 'A'..'Z', 'a'..'z', '\x80'..'\xFF':
  1084. var id = ""
  1085. while true:
  1086. add(id, frmt[i])
  1087. inc(i)
  1088. if frmt[i] notin {'A'..'Z', '_', 'a'..'z', '\x80'..'\xFF'}: break
  1089. var idx = getVarIdx(varnames, id)
  1090. if idx >= 0:
  1091. add(result, varvalues[idx])
  1092. else:
  1093. raise newException(ValueError, "unknown substitution var: " & id)
  1094. of '{':
  1095. var id = ""
  1096. inc(i)
  1097. while frmt[i] != '}':
  1098. if frmt[i] == '\0':
  1099. raise newException(ValueError, "'}' expected")
  1100. add(id, frmt[i])
  1101. inc(i)
  1102. inc(i) # skip }
  1103. # search for the variable:
  1104. var idx = getVarIdx(varnames, id)
  1105. if idx >= 0: add(result, varvalues[idx])
  1106. else:
  1107. raise newException(ValueError, "unknown substitution var: " & id)
  1108. else:
  1109. raise newException(ValueError, "unknown substitution: $" & $frmt[i])
  1110. var start = i
  1111. while i < L:
  1112. if frmt[i] != '$': inc(i)
  1113. else: break
  1114. if i-1 >= start: add(result, substr(frmt, start, i - 1))
  1115. proc defaultConfig*(): StringTableRef =
  1116. ## Returns a default configuration for embedded HTML generation.
  1117. ##
  1118. ## The returned ``StringTableRef`` contains the parameters used by the HTML
  1119. ## engine to build the final output. For information on what these parameters
  1120. ## are and their purpose, please look up the file ``config/nimdoc.cfg``
  1121. ## bundled with the compiler.
  1122. ##
  1123. ## The only difference between the contents of that file and the values
  1124. ## provided by this proc is the ``doc.file`` variable. The ``doc.file``
  1125. ## variable of the configuration file contains HTML to build standalone
  1126. ## pages, while this proc returns just the content for procs like
  1127. ## ``rstToHtml`` to generate the bare minimum HTML.
  1128. result = newStringTable(modeStyleInsensitive)
  1129. template setConfigVar(key, val) =
  1130. result[key] = val
  1131. # If you need to modify these values, it might be worth updating the template
  1132. # file in config/nimdoc.cfg.
  1133. setConfigVar("split.item.toc", "20")
  1134. setConfigVar("doc.section", """
  1135. <div class="section" id="$sectionID">
  1136. <h1><a class="toc-backref" href="#$sectionTitleID">$sectionTitle</a></h1>
  1137. <dl class="item">
  1138. $content
  1139. </dl></div>
  1140. """)
  1141. setConfigVar("doc.section.toc", """
  1142. <li>
  1143. <a class="reference" href="#$sectionID" id="$sectionTitleID">$sectionTitle</a>
  1144. <ul class="simple">
  1145. $content
  1146. </ul>
  1147. </li>
  1148. """)
  1149. setConfigVar("doc.item", """
  1150. <dt id="$itemID"><a name="$itemSymOrIDEnc"></a><pre>$header</pre></dt>
  1151. <dd>
  1152. $desc
  1153. </dd>
  1154. """)
  1155. setConfigVar("doc.item.toc", """
  1156. <li><a class="reference" href="#$itemSymOrIDEnc"
  1157. title="$header_plain">$name</a></li>
  1158. """)
  1159. setConfigVar("doc.toc", """
  1160. <div class="navigation" id="navigation">
  1161. <ul class="simple">
  1162. $content
  1163. </ul>
  1164. </div>""")
  1165. setConfigVar("doc.body_toc", """
  1166. $tableofcontents
  1167. <div class="content" id="content">
  1168. $moduledesc
  1169. $content
  1170. </div>
  1171. """)
  1172. setConfigVar("doc.listing_start", "<pre class = \"listing\">")
  1173. setConfigVar("doc.listing_end", "</pre>")
  1174. setConfigVar("doc.listing_button", "</pre>")
  1175. setConfigVar("doc.body_no_toc", "$moduledesc $content")
  1176. setConfigVar("doc.file", "$content")
  1177. setConfigVar("doc.smiley_format", "/images/smilies/$1.gif")
  1178. # ---------- forum ---------------------------------------------------------
  1179. proc rstToHtml*(s: string, options: RstParseOptions,
  1180. config: StringTableRef): string =
  1181. ## Converts an input rst string into embeddable HTML.
  1182. ##
  1183. ## This convenience proc parses any input string using rst markup (it doesn't
  1184. ## have to be a full document!) and returns an embeddable piece of HTML. The
  1185. ## proc is meant to be used in *online* environments without access to a
  1186. ## meaningful filesystem, and therefore rst ``include`` like directives won't
  1187. ## work. For an explanation of the ``config`` parameter see the
  1188. ## ``initRstGenerator`` proc. Example:
  1189. ##
  1190. ## .. code-block:: nim
  1191. ## import packages/docutils/rstgen, strtabs
  1192. ##
  1193. ## echo rstToHtml("*Hello* **world**!", {},
  1194. ## newStringTable(modeStyleInsensitive))
  1195. ## # --> <em>Hello</em> <strong>world</strong>!
  1196. ##
  1197. ## If you need to allow the rst ``include`` directive or tweak the generated
  1198. ## output you have to create your own ``RstGenerator`` with
  1199. ## ``initRstGenerator`` and related procs.
  1200. proc myFindFile(filename: string): string =
  1201. # we don't find any files in online mode:
  1202. result = ""
  1203. const filen = "input"
  1204. var d: RstGenerator
  1205. initRstGenerator(d, outHtml, config, filen, options, myFindFile,
  1206. rst.defaultMsgHandler)
  1207. var dummyHasToc = false
  1208. var rst = rstParse(s, filen, 0, 1, dummyHasToc, options)
  1209. result = ""
  1210. renderRstToOut(d, rst, result)
  1211. when isMainModule:
  1212. assert rstToHtml("*Hello* **world**!", {},
  1213. newStringTable(modeStyleInsensitive)) ==
  1214. "<em>Hello</em> <strong>world</strong>!"