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:
parent
81d287f758
commit
0dbec5a278
2 changed files with 167 additions and 25 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue