Adds support for the `.. code::` rst directive.

This modifies the existing ``.. code-block::`` to be treated as spec
extension with a specific default language syntax. Also implements line
numbering.
This commit is contained in:
Grzegorz Adam Hankiewicz 2014-07-19 13:56:42 +02:00
commit 0dbec5a278
2 changed files with 167 additions and 25 deletions

View file

@ -39,7 +39,8 @@ type
meInvalidDirective, meInvalidDirective,
mwRedefinitionOfLabel, mwRedefinitionOfLabel,
mwUnknownSubstitution, mwUnknownSubstitution,
mwUnsupportedLanguage mwUnsupportedLanguage,
mwUnsupportedField
TMsgHandler* = proc (filename: string, line, col: int, msgKind: TMsgKind, TMsgHandler* = proc (filename: string, line, col: int, msgKind: TMsgKind,
arg: string) {.nimcall.} ## what to do in case of an error arg: string) {.nimcall.} ## what to do in case of an error
@ -55,7 +56,8 @@ const
meInvalidDirective: "invalid directive: '$1'", meInvalidDirective: "invalid directive: '$1'",
mwRedefinitionOfLabel: "redefinition of label '$1'", mwRedefinitionOfLabel: "redefinition of label '$1'",
mwUnknownSubstitution: "unknown substitution '$1'", mwUnknownSubstitution: "unknown substitution '$1'",
mwUnsupportedLanguage: "language '$1' not supported" mwUnsupportedLanguage: "language '$1' not supported",
mwUnsupportedField: "field '$1' not supported"
] ]
proc rstnodeToRefname*(n: PRstNode): string proc rstnodeToRefname*(n: PRstNode): string
@ -850,13 +852,13 @@ proc parseComment(p: var TRstParser): PRstNode =
type type
TDirKind = enum # must be ordered alphabetically! TDirKind = enum # must be ordered alphabetically!
dkNone, dkAuthor, dkAuthors, dkCodeBlock, dkContainer, dkContents, dkNone, dkAuthor, dkAuthors, dkCode, dkCodeBlock, dkContainer, dkContents,
dkFigure, dkImage, dkInclude, dkIndex, dkRaw, dkTitle dkFigure, dkImage, dkInclude, dkIndex, dkRaw, dkTitle
const const
DirIds: array[0..11, string] = ["", "author", "authors", "code-block", DirIds: array[0..12, string] = ["", "author", "authors", "code",
"container", "contents", "figure", "image", "include", "index", "raw", "code-block", "container", "contents", "figure", "image", "include",
"title"] "index", "raw", "title"]
proc getDirKind(s: string): TDirKind = proc getDirKind(s: string): TDirKind =
let i = find(DirIds, s) let i = find(DirIds, s)
@ -876,7 +878,10 @@ proc parseUntilNewline(p: var TRstParser, father: PRstNode) =
of tkEof, tkIndent: break of tkEof, tkIndent: break
proc parseSection(p: var TRstParser, result: PRstNode) proc parseSection(p: var TRstParser, result: PRstNode)
proc parseField(p: var TRstParser): PRstNode = proc parseField(p: var TRstParser): PRstNode =
## Returns a parsed rnField node.
##
## rnField nodes have two children nodes, a rnFieldName and a rnFieldBody.
result = newRstNode(rnField) result = newRstNode(rnField)
var col = p.tok[p.idx].col var col = p.tok[p.idx].col
var fieldname = newRstNode(rnFieldName) var fieldname = newRstNode(rnFieldName)
@ -892,7 +897,11 @@ proc parseField(p: var TRstParser): PRstNode =
add(result, fieldname) add(result, fieldname)
add(result, fieldbody) add(result, fieldbody)
proc parseFields(p: var TRstParser): PRstNode = proc parseFields(p: var TRstParser): PRstNode =
## Parses fields for a section or directive block.
##
## This proc may return nil if the parsing doesn't find anything of value,
## otherwise it will return a node of rnFieldList type with children.
result = nil result = nil
var atStart = p.idx == 0 and p.tok[0].symbol == ":" var atStart = p.idx == 0 and p.tok[0].symbol == ":"
if (p.tok[p.idx].kind == tkIndent) and (p.tok[p.idx + 1].symbol == ":") or if (p.tok[p.idx].kind == tkIndent) and (p.tok[p.idx + 1].symbol == ":") or
@ -908,6 +917,18 @@ proc parseFields(p: var TRstParser): PRstNode =
else: else:
break break
proc getFieldValue*(n: PRstNode): string =
## Returns the value of a specific ``rnField`` node.
##
## This proc will assert if the node is not of the expected type. The empty
## string will be returned as a minimum. Any value in the rst will be
## stripped form leading/trailing whitespace.
assert n.kind == rnField
assert n.len == 2
assert n.sons[0].kind == rnFieldName
assert n.sons[1].kind == rnFieldBody
result = addNodes(n.sons[1]).strip
proc getFieldValue(n: PRstNode, fieldname: string): string = proc getFieldValue(n: PRstNode, fieldname: string): string =
result = "" result = ""
if n.sons[1] == nil: return if n.sons[1] == nil: return
@ -1387,7 +1408,16 @@ type
TDirFlags = set[TDirFlag] TDirFlags = set[TDirFlag]
TSectionParser = proc (p: var TRstParser): PRstNode {.nimcall.} TSectionParser = proc (p: var TRstParser): PRstNode {.nimcall.}
proc parseDirective(p: var TRstParser, flags: TDirFlags): PRstNode = proc parseDirective(p: var TRstParser, flags: TDirFlags): PRstNode =
## Parses arguments and options for a directive block.
##
## A directive block will always have three sons: the arguments for the
## directive (rnDirArg), the options (rnFieldList) and the block
## (rnLineBlock). This proc parses the two first nodes, the block is left to
## the outer `parseDirective` call.
##
## Both rnDirArg and rnFieldList children nodes might be nil, so you need to
## check them before accessing.
result = newRstNode(rnDirective) result = newRstNode(rnDirective)
var args: PRstNode = nil var args: PRstNode = nil
var options: PRstNode = nil var options: PRstNode = nil
@ -1421,6 +1451,9 @@ proc indFollows(p: TRstParser): bool =
proc parseDirective(p: var TRstParser, flags: TDirFlags, proc parseDirective(p: var TRstParser, flags: TDirFlags,
contentParser: TSectionParser): PRstNode = contentParser: TSectionParser): PRstNode =
## Returns a generic rnDirective tree.
##
## The children are rnDirArg, rnFieldList and rnLineBlock. Any might be nil.
result = parseDirective(p, flags) result = parseDirective(p, flags)
if not isNil(contentParser) and indFollows(p): if not isNil(contentParser) and indFollows(p):
pushInd(p, p.tok[p.idx].ival) pushInd(p, p.tok[p.idx].ival)
@ -1474,7 +1507,19 @@ proc dirInclude(p: var TRstParser): PRstNode =
# InternalError("Too many binary zeros in include file") # InternalError("Too many binary zeros in include file")
result = parseDoc(q) result = parseDoc(q)
proc dirCodeBlock(p: var TRstParser): PRstNode = proc dirCodeBlock(p: var TRstParser, nimrodExtension = false): PRstNode =
## Parses a code block.
##
## Code blocks are rnDirective trees with a `kind` of rnCodeBlock. See the
## description of ``parseDirective`` for further structure information.
##
## Code blocks can come in two forms, the standard `code directive
## <http://docutils.sourceforge.net/docs/ref/rst/directives.html#code>`_ and
## the nimrod extension ``.. code-block::``. If the block is an extension, we
## want the default language syntax highlighting to be Nimrod, so we create a
## fake internal field to comminicate with the generator. The field is named
## ``default-language``, which is unlikely to collide with a field specified
## by any random rst input file.
result = parseDirective(p, {hasArg, hasOptions}, parseLiteralBlock) result = parseDirective(p, {hasArg, hasOptions}, parseLiteralBlock)
var filename = strip(getFieldValue(result, "file")) var filename = strip(getFieldValue(result, "file"))
if filename != "": if filename != "":
@ -1483,6 +1528,20 @@ proc dirCodeBlock(p: var TRstParser): PRstNode =
var n = newRstNode(rnLiteralBlock) var n = newRstNode(rnLiteralBlock)
add(n, newRstNode(rnLeaf, readFile(path))) add(n, newRstNode(rnLeaf, readFile(path)))
result.sons[2] = n result.sons[2] = n
# Extend the field block if we are using our custom extension.
if nimrodExtension:
# Create a field block if the input block didn't have any.
if result.sons[1].isNil: result.sons[1] = newRstNode(rnFieldList)
assert result.sons[1].kind == rnFieldList
# Hook the extra field and specify the Nimrod language as value.
var extraNode = newRstNode(rnField)
extraNode.add(newRstNode(rnFieldName))
extraNode.add(newRstNode(rnFieldBody))
extraNode.sons[0].add(newRstNode(rnLeaf, "default-language"))
extraNode.sons[1].add(newRstNode(rnLeaf, "Nimrod"))
result.sons[1].add(extraNode)
result.kind = rnCodeBlock result.kind = rnCodeBlock
proc dirContainer(p: var TRstParser): PRstNode = proc dirContainer(p: var TRstParser): PRstNode =
@ -1566,7 +1625,8 @@ proc parseDotDot(p: var TRstParser): PRstNode =
result = dirRaw(p) result = dirRaw(p)
else: else:
rstMessage(p, meInvalidDirective, d) rstMessage(p, meInvalidDirective, d)
of dkCodeBlock: result = dirCodeBlock(p) of dkCode: result = dirCodeBlock(p)
of dkCodeBlock: result = dirCodeBlock(p, nimrodExtension = true)
of dkIndex: result = dirIndex(p) of dkIndex: result = dirIndex(p)
else: rstMessage(p, meInvalidDirective, d) else: rstMessage(p, meInvalidDirective, d)
popInd(p) popInd(p)

View file

@ -24,7 +24,7 @@
## generate `LaTeX documents <https://en.wikipedia.org/wiki/LaTeX>`_ too. ## generate `LaTeX documents <https://en.wikipedia.org/wiki/LaTeX>`_ too.
import strutils, os, hashes, strtabs, rstast, rst, highlite, tables, sequtils, import strutils, os, hashes, strtabs, rstast, rst, highlite, tables, sequtils,
algorithm algorithm, parseutils
const const
HtmlExt = "html" HtmlExt = "html"
@ -63,6 +63,19 @@ type
PDoc = var TRstGenerator ## Alias to type less. PDoc = var TRstGenerator ## 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: TSourceLanguage ## Type of highlighting, by default none.
proc init(p: var CodeBlockParams) =
## Default initialisation of CodeBlockParams to sane values.
p.startLine = 1
p.lang = langNone
p.langStr = ""
proc initRstGenerator*(g: var TRstGenerator, target: TOutputTarget, proc initRstGenerator*(g: var TRstGenerator, target: TOutputTarget,
config: PStringTable, filename: string, config: PStringTable, filename: string,
options: TRstParseOptions, options: TRstParseOptions,
@ -761,26 +774,95 @@ proc renderSmiley(d: PDoc, n: PRstNode, result: var string) =
height="17" hspace="2" vspace="2" />""", height="17" hspace="2" vspace="2" />""",
"\\includegraphics{$1}", [n.text]) "\\includegraphics{$1}", [n.text])
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.toLower
of "number-lines":
params.numberLines = true
# See if the field has a parameter specifying a different line than 1.
var number: int
if parseInt(n.getFieldValue, number) > 0:
params.startLine = number
of "default-language":
params.langStr = n.getFieldValue.strip
params.lang = params.langStr.getSourceLanguage
else:
d.msgHandler(d.filename, 1, 0, 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 == rnCodeBlock
assert(not n.sons[2].isNil)
# 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(params: CodeBlockParams, code: 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!).
if not params.numberLines:
result = ("<pre>", "</pre>")
return
var codeLines = code.strip(leading = false).countLines
assert codeLines > 0
result.beginTable = """<table><tbody><tr><td class="blob-line-nums"><pre>"""
var line = params.startLine
while codeLines > 0:
result.beginTable.add($line & "\n")
line.inc
codeLines.dec
result.beginTable.add("</pre></td><td><pre>")
result.endTable = "</pre></td></tr></tbody></table>"
proc renderCodeBlock(d: PDoc, n: PRstNode, result: var string) = proc renderCodeBlock(d: PDoc, n: PRstNode, result: var string) =
## Renders a code block, 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 nimrod's code block
## extension.
assert n.kind == rnCodeBlock
if n.sons[2] == nil: return if n.sons[2] == nil: return
var params = d.parseCodeBlockParams(n)
var m = n.sons[2].sons[0] var m = n.sons[2].sons[0]
assert m.kind == rnLeaf assert m.kind == rnLeaf
var langstr = strip(getArgument(n))
var lang: TSourceLanguage let (blockStart, blockEnd) = params.buildLinesHTMLTable(m.text)
if langstr == "":
lang = langNimrod # default language dispA(d.target, result, blockStart, "\\begin{rstpre}\n", [])
else: if params.lang == langNone:
lang = getSourceLanguage(langstr) if len(params.langStr) > 0:
d.msgHandler(d.filename, 1, 0, mwUnsupportedLanguage, params.langStr)
dispA(d.target, result, "<pre>", "\\begin{rstpre}\n", [])
if lang == langNone:
d.msgHandler(d.filename, 1, 0, mwUnsupportedLanguage, langstr)
result.add(m.text) result.add(m.text)
else: else:
var g: TGeneralTokenizer var g: TGeneralTokenizer
initGeneralTokenizer(g, m.text) initGeneralTokenizer(g, m.text)
while true: while true:
getNextToken(g, lang) getNextToken(g, params.lang)
case g.kind case g.kind
of gtEof: break of gtEof: break
of gtNone, gtWhitespace: of gtNone, gtWhitespace:
@ -790,8 +872,8 @@ proc renderCodeBlock(d: PDoc, n: PRstNode, result: var string) =
esc(d.target, substr(m.text, g.start, g.length+g.start-1)), esc(d.target, substr(m.text, g.start, g.length+g.start-1)),
tokenClassToStr[g.kind]]) tokenClassToStr[g.kind]])
deinitGeneralTokenizer(g) deinitGeneralTokenizer(g)
dispA(d.target, result, "</pre>", "\n\\end{rstpre}\n") dispA(d.target, result, blockEnd, "\n\\end{rstpre}\n")
proc renderContainer(d: PDoc, n: PRstNode, result: var string) = proc renderContainer(d: PDoc, n: PRstNode, result: var string) =
var tmp = "" var tmp = ""
renderRstToOut(d, n.sons[2], tmp) renderRstToOut(d, n.sons[2], tmp)