implement RST & Markdown quote blocks (#19147)
* implement RST & Markdown quote blocks * compile with nim 1.0 * Fix indentation
This commit is contained in:
parent
7772ca303c
commit
040d23e799
7 changed files with 617 additions and 12 deletions
|
|
@ -139,9 +139,17 @@ doc.file = """
|
||||||
\usepackage[most]{tcolorbox} % boxes around admonitions, code blocks, doc.item
|
\usepackage[most]{tcolorbox} % boxes around admonitions, code blocks, doc.item
|
||||||
|
|
||||||
\newtcolorbox{rstadmonition}[1][]{blanker, breakable,
|
\newtcolorbox{rstadmonition}[1][]{blanker, breakable,
|
||||||
left=3mm, right=3mm, top=1mm, bottom=1mm,
|
left=3mm, right=0mm, top=1mm, bottom=1mm,
|
||||||
before upper=\indent, parbox=false, #1}
|
before upper=\indent, parbox=false, #1}
|
||||||
|
|
||||||
|
\newtcolorbox{rstquote}[1][]{blanker, breakable,
|
||||||
|
left=3mm, right=3mm, top=1mm, bottom=1mm,
|
||||||
|
parbox=false,
|
||||||
|
borderline west={0.3em}{0pt}{lightgray},
|
||||||
|
borderline north={0.05em}{0pt}{lightgray},
|
||||||
|
borderline east={0.05em}{0pt}{lightgray},
|
||||||
|
borderline south={0.05em}{0pt}{lightgray}}
|
||||||
|
|
||||||
\definecolor{rstframecolor}{rgb}{0.85, 0.8, 0.6}
|
\definecolor{rstframecolor}{rgb}{0.85, 0.8, 0.6}
|
||||||
|
|
||||||
\newtcolorbox{rstprebox}[1][]{blanker, breakable,
|
\newtcolorbox{rstprebox}[1][]{blanker, breakable,
|
||||||
|
|
|
||||||
|
|
@ -567,6 +567,11 @@ blockquote {
|
||||||
border-left: 5px solid #bbc;
|
border-left: 5px solid #bbc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
blockquote.markdown-quote {
|
||||||
|
font-size: 0.9rem; /* use rem to avoid recursion */
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
.pre, span.tok {
|
.pre, span.tok {
|
||||||
font-family: "Source Code Pro", Monaco, Menlo, Consolas, "Courier New", monospace;
|
font-family: "Source Code Pro", Monaco, Menlo, Consolas, "Courier New", monospace;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,8 @@
|
||||||
## + field lists
|
## + field lists
|
||||||
## + option lists
|
## + option lists
|
||||||
## + indented literal blocks
|
## + indented literal blocks
|
||||||
|
## + quoted literal blocks
|
||||||
|
## + line blocks
|
||||||
## + simple tables
|
## + simple tables
|
||||||
## + directives (see official documentation in `RST directives list`_):
|
## + directives (see official documentation in `RST directives list`_):
|
||||||
## - ``image``, ``figure`` for including images and videos
|
## - ``image``, ``figure`` for including images and videos
|
||||||
|
|
@ -121,6 +123,7 @@
|
||||||
## * Markdown code blocks
|
## * Markdown code blocks
|
||||||
## * Markdown links
|
## * Markdown links
|
||||||
## * Markdown headlines
|
## * Markdown headlines
|
||||||
|
## * Markdown block quotes
|
||||||
## * using ``1`` as auto-enumerator in enumerated lists like RST ``#``
|
## * using ``1`` as auto-enumerator in enumerated lists like RST ``#``
|
||||||
## (auto-enumerator ``1`` can not be used with ``#`` in the same list)
|
## (auto-enumerator ``1`` can not be used with ``#`` in the same list)
|
||||||
##
|
##
|
||||||
|
|
@ -156,13 +159,35 @@
|
||||||
## ``\`` -- GOOD
|
## ``\`` -- GOOD
|
||||||
## So single backticks can always be input: `\`` will turn to ` code
|
## So single backticks can always be input: `\`` will turn to ` code
|
||||||
##
|
##
|
||||||
|
## .. Attention::
|
||||||
|
## We don't support some obviously poor design choices of Markdown (or RST).
|
||||||
|
##
|
||||||
|
## - no support for the rule of 2 spaces causing a line break in Markdown
|
||||||
|
## (use RST "line blocks" syntax for making line breaks)
|
||||||
|
##
|
||||||
|
## - interpretation of Markdown block quotes is also slightly different,
|
||||||
|
## e.g. case
|
||||||
|
##
|
||||||
|
## ::
|
||||||
|
##
|
||||||
|
## >>> foo
|
||||||
|
## > bar
|
||||||
|
## >>baz
|
||||||
|
##
|
||||||
|
## is a single 3rd-level quote `foo bar baz` in original Markdown, while
|
||||||
|
## in Nim we naturally see it as 3rd-level quote `foo` + 1st level `bar` +
|
||||||
|
## 2nd level `baz`:
|
||||||
|
##
|
||||||
|
## >>> foo
|
||||||
|
## > bar
|
||||||
|
## >>baz
|
||||||
|
##
|
||||||
## Limitations
|
## Limitations
|
||||||
## -----------
|
## -----------
|
||||||
##
|
##
|
||||||
## * no Unicode support in character width calculations
|
## * no Unicode support in character width calculations
|
||||||
## * body elements
|
## * body elements
|
||||||
## - no roman numerals in enumerated lists
|
## - no roman numerals in enumerated lists
|
||||||
## - no quoted literal blocks
|
|
||||||
## - no doctest blocks
|
## - no doctest blocks
|
||||||
## - no grid tables
|
## - no grid tables
|
||||||
## - some directives are missing (check official `RST directives list`_):
|
## - some directives are missing (check official `RST directives list`_):
|
||||||
|
|
@ -472,6 +497,10 @@ type
|
||||||
line: int # the last line of this style occurrence
|
line: int # the last line of this style occurrence
|
||||||
# (for error message)
|
# (for error message)
|
||||||
hasPeers: bool # has headings on the same level of hierarchy?
|
hasPeers: bool # has headings on the same level of hierarchy?
|
||||||
|
LiteralBlockKind = enum # RST-style literal blocks after `::`
|
||||||
|
lbNone,
|
||||||
|
lbIndentedLiteralBlock,
|
||||||
|
lbQuotedLiteralBlock
|
||||||
LevelMap = seq[LevelInfo] # Saves for each possible title adornment
|
LevelMap = seq[LevelInfo] # Saves for each possible title adornment
|
||||||
# style its level in the current document.
|
# style its level in the current document.
|
||||||
SubstitutionKind = enum
|
SubstitutionKind = enum
|
||||||
|
|
@ -1953,6 +1982,44 @@ proc parseLiteralBlock(p: var RstParser): PRstNode =
|
||||||
inc p.idx
|
inc p.idx
|
||||||
result.add(n)
|
result.add(n)
|
||||||
|
|
||||||
|
proc parseQuotedLiteralBlock(p: var RstParser): PRstNode =
|
||||||
|
result = newRstNodeA(p, rnLiteralBlock)
|
||||||
|
var n = newLeaf("")
|
||||||
|
if currentTok(p).kind == tkIndent:
|
||||||
|
var indent = currInd(p)
|
||||||
|
while currentTok(p).kind == tkIndent: inc p.idx # skip blank lines
|
||||||
|
var quoteSym = currentTok(p).symbol[0]
|
||||||
|
while true:
|
||||||
|
case currentTok(p).kind
|
||||||
|
of tkEof:
|
||||||
|
break
|
||||||
|
of tkIndent:
|
||||||
|
if currentTok(p).ival < indent:
|
||||||
|
break
|
||||||
|
elif currentTok(p).ival == indent:
|
||||||
|
if nextTok(p).kind == tkPunct and nextTok(p).symbol[0] == quoteSym:
|
||||||
|
n.text.add("\n")
|
||||||
|
inc p.idx
|
||||||
|
elif nextTok(p).kind == tkIndent:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
rstMessage(p, mwRstStyle, "no newline after quoted literal block")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
rstMessage(p, mwRstStyle,
|
||||||
|
"unexpected indentation in quoted literal block")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
n.text.add(currentTok(p).symbol)
|
||||||
|
inc p.idx
|
||||||
|
result.add(n)
|
||||||
|
|
||||||
|
proc parseRstLiteralBlock(p: var RstParser, kind: LiteralBlockKind): PRstNode =
|
||||||
|
if kind == lbIndentedLiteralBlock:
|
||||||
|
result = parseLiteralBlock(p)
|
||||||
|
else:
|
||||||
|
result = parseQuotedLiteralBlock(p)
|
||||||
|
|
||||||
proc getLevel(p: var RstParser, c: char, hasOverline: bool): int =
|
proc getLevel(p: var RstParser, c: char, hasOverline: bool): int =
|
||||||
## Returns (preliminary) heading level corresponding to `c` and
|
## Returns (preliminary) heading level corresponding to `c` and
|
||||||
## `hasOverline`. If level does not exist, add it first.
|
## `hasOverline`. If level does not exist, add it first.
|
||||||
|
|
@ -2023,6 +2090,33 @@ proc isLineBlock(p: RstParser): bool =
|
||||||
p.tok[j].col > currentTok(p).col or
|
p.tok[j].col > currentTok(p).col or
|
||||||
p.tok[j].symbol == "\n"
|
p.tok[j].symbol == "\n"
|
||||||
|
|
||||||
|
proc isMarkdownBlockQuote(p: RstParser): bool =
|
||||||
|
result = currentTok(p).symbol[0] == '>'
|
||||||
|
|
||||||
|
proc whichRstLiteralBlock(p: RstParser): LiteralBlockKind =
|
||||||
|
## Checks that the following tokens are either Indented Literal Block or
|
||||||
|
## Quoted Literal Block (which is not quite the same as Markdown quote block).
|
||||||
|
## https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#quoted-literal-blocks
|
||||||
|
if currentTok(p).symbol == "::" and nextTok(p).kind == tkIndent:
|
||||||
|
if currInd(p) > nextTok(p).ival:
|
||||||
|
result = lbNone
|
||||||
|
if currInd(p) < nextTok(p).ival:
|
||||||
|
result = lbIndentedLiteralBlock
|
||||||
|
elif currInd(p) == nextTok(p).ival:
|
||||||
|
var i = p.idx + 1
|
||||||
|
while p.tok[i].kind == tkIndent: inc i
|
||||||
|
const validQuotingCharacters = {
|
||||||
|
'!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-',
|
||||||
|
'.', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^',
|
||||||
|
'_', '`', '{', '|', '}', '~'}
|
||||||
|
if p.tok[i].kind in {tkPunct, tkAdornment} and
|
||||||
|
p.tok[i].symbol[0] in validQuotingCharacters:
|
||||||
|
result = lbQuotedLiteralBlock
|
||||||
|
else:
|
||||||
|
result = lbNone
|
||||||
|
else:
|
||||||
|
result = lbNone
|
||||||
|
|
||||||
proc predNL(p: RstParser): bool =
|
proc predNL(p: RstParser): bool =
|
||||||
result = true
|
result = true
|
||||||
if p.idx > 0:
|
if p.idx > 0:
|
||||||
|
|
@ -2078,6 +2172,8 @@ proc whichSection(p: RstParser): RstNodeKind =
|
||||||
elif match(p, p.idx + 1, " a"): result = rnTable
|
elif match(p, p.idx + 1, " a"): result = rnTable
|
||||||
elif currentTok(p).symbol == "|" and isLineBlock(p):
|
elif currentTok(p).symbol == "|" and isLineBlock(p):
|
||||||
result = rnLineBlock
|
result = rnLineBlock
|
||||||
|
elif roSupportMarkdown in p.s.options and isMarkdownBlockQuote(p):
|
||||||
|
result = rnMarkdownBlockQuote
|
||||||
elif match(p, p.idx + 1, "i") and isAdornmentHeadline(p, p.idx):
|
elif match(p, p.idx + 1, "i") and isAdornmentHeadline(p, p.idx):
|
||||||
result = rnOverline
|
result = rnOverline
|
||||||
else:
|
else:
|
||||||
|
|
@ -2090,6 +2186,8 @@ proc whichSection(p: RstParser): RstNodeKind =
|
||||||
result = rnMarkdownTable
|
result = rnMarkdownTable
|
||||||
elif currentTok(p).symbol == "|" and isLineBlock(p):
|
elif currentTok(p).symbol == "|" and isLineBlock(p):
|
||||||
result = rnLineBlock
|
result = rnLineBlock
|
||||||
|
elif roSupportMarkdown in p.s.options and isMarkdownBlockQuote(p):
|
||||||
|
result = rnMarkdownBlockQuote
|
||||||
elif match(p, tokenAfterNewline(p), "aI") and
|
elif match(p, tokenAfterNewline(p), "aI") and
|
||||||
isAdornmentHeadline(p, tokenAfterNewline(p)):
|
isAdornmentHeadline(p, tokenAfterNewline(p)):
|
||||||
result = rnHeadline
|
result = rnHeadline
|
||||||
|
|
@ -2143,6 +2241,102 @@ proc parseLineBlock(p: var RstParser): PRstNode =
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
proc parseDoc(p: var RstParser): PRstNode {.gcsafe.}
|
||||||
|
|
||||||
|
proc getQuoteSymbol(p: RstParser, idx: int): tuple[sym: string, depth: int, tokens: int] =
|
||||||
|
result = ("", 0, 0)
|
||||||
|
var i = idx
|
||||||
|
result.sym &= p.tok[i].symbol
|
||||||
|
result.depth += p.tok[i].symbol.len
|
||||||
|
inc result.tokens
|
||||||
|
inc i
|
||||||
|
while p.tok[i].kind == tkWhite and i+1 < p.tok.len and
|
||||||
|
p.tok[i+1].kind == tkPunct and p.tok[i+1].symbol[0] == '>':
|
||||||
|
result.sym &= p.tok[i].symbol
|
||||||
|
result.sym &= p.tok[i+1].symbol
|
||||||
|
result.depth += p.tok[i+1].symbol.len
|
||||||
|
inc result.tokens, 2
|
||||||
|
inc i, 2
|
||||||
|
|
||||||
|
proc parseMarkdownQuoteSegment(p: var RstParser, curSym: string, col: int):
|
||||||
|
PRstNode =
|
||||||
|
## We define *segment* as a group of lines that starts with exactly the
|
||||||
|
## same quote symbol. If the following lines don't contain any `>` (*lazy*
|
||||||
|
## continuation) they considered as continuation of the current segment.
|
||||||
|
var q: RstParser # to delete `>` at a start of line and then parse normally
|
||||||
|
initParser(q, p.s)
|
||||||
|
q.col = p.col
|
||||||
|
q.line = p.line
|
||||||
|
var minCol = int.high # minimum colum num in the segment
|
||||||
|
while true: # move tokens of segment from `p` to `q` skipping `curSym`
|
||||||
|
case currentTok(p).kind
|
||||||
|
of tkEof:
|
||||||
|
break
|
||||||
|
of tkIndent:
|
||||||
|
if nextTok(p).kind in {tkIndent, tkEof}:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if nextTok(p).symbol[0] == '>':
|
||||||
|
var (quoteSym, _, quoteTokens) = getQuoteSymbol(p, p.idx + 1)
|
||||||
|
if quoteSym == curSym: # the segment continues
|
||||||
|
var iTok = tokenAfterNewline(p, p.idx+1)
|
||||||
|
if p.tok[iTok].kind notin {tkEof, tkIndent} and
|
||||||
|
p.tok[iTok].symbol[0] != '>':
|
||||||
|
rstMessage(p, mwRstStyle,
|
||||||
|
"two or more quoted lines are followed by unquoted line " &
|
||||||
|
$(curLine(p) + 1))
|
||||||
|
break
|
||||||
|
q.tok.add currentTok(p)
|
||||||
|
var ival = currentTok(p).ival + quoteSym.len
|
||||||
|
inc p.idx, (1 + quoteTokens) # skip newline and > > >
|
||||||
|
if currentTok(p).kind == tkWhite:
|
||||||
|
ival += currentTok(p).symbol.len
|
||||||
|
inc p.idx
|
||||||
|
# fix up previous `tkIndent`s to ival (as if >>> were not there)
|
||||||
|
var j = q.tok.len - 1
|
||||||
|
while j >= 0 and q.tok[j].kind == tkIndent:
|
||||||
|
q.tok[j].ival = ival
|
||||||
|
dec j
|
||||||
|
else: # next segment started
|
||||||
|
break
|
||||||
|
elif currentTok(p).ival < col:
|
||||||
|
break
|
||||||
|
else: # the segment continues, a case like:
|
||||||
|
# > beginning
|
||||||
|
# continuation
|
||||||
|
q.tok.add currentTok(p)
|
||||||
|
inc p.idx
|
||||||
|
else:
|
||||||
|
if currentTok(p).col < minCol: minCol = currentTok(p).col
|
||||||
|
q.tok.add currentTok(p)
|
||||||
|
inc p.idx
|
||||||
|
q.indentStack = @[minCol]
|
||||||
|
# if initial indentation `minCol` is > 0 then final newlines
|
||||||
|
# should be omitted so that parseDoc could advance to the end of tokens:
|
||||||
|
var j = q.tok.len - 1
|
||||||
|
while q.tok[j].kind == tkIndent: dec j
|
||||||
|
q.tok.setLen (j+1)
|
||||||
|
q.tok.add Token(kind: tkEof, line: currentTok(p).line)
|
||||||
|
result = parseDoc(q)
|
||||||
|
|
||||||
|
proc parseMarkdownBlockQuote(p: var RstParser): PRstNode =
|
||||||
|
var (curSym, quotationDepth, quoteTokens) = getQuoteSymbol(p, p.idx)
|
||||||
|
let col = currentTok(p).col
|
||||||
|
result = newRstNodeA(p, rnMarkdownBlockQuote)
|
||||||
|
inc p.idx, quoteTokens # skip first >
|
||||||
|
while true:
|
||||||
|
var item = newRstNode(rnMarkdownBlockQuoteItem)
|
||||||
|
item.quotationDepth = quotationDepth
|
||||||
|
if currentTok(p).kind == tkWhite: inc p.idx
|
||||||
|
item.add parseMarkdownQuoteSegment(p, curSym, col)
|
||||||
|
result.add(item)
|
||||||
|
if currentTok(p).kind == tkIndent and currentTok(p).ival == col and
|
||||||
|
nextTok(p).kind != tkEof and nextTok(p).symbol[0] == '>':
|
||||||
|
(curSym, quotationDepth, quoteTokens) = getQuoteSymbol(p, p.idx + 1)
|
||||||
|
inc p.idx, (1 + quoteTokens) # skip newline and > > >
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
proc parseParagraph(p: var RstParser, result: PRstNode) =
|
proc parseParagraph(p: var RstParser, result: PRstNode) =
|
||||||
while true:
|
while true:
|
||||||
case currentTok(p).kind
|
case currentTok(p).kind
|
||||||
|
|
@ -2158,16 +2352,17 @@ proc parseParagraph(p: var RstParser, result: PRstNode) =
|
||||||
result.add newLeaf(" ")
|
result.add newLeaf(" ")
|
||||||
of rnLineBlock:
|
of rnLineBlock:
|
||||||
result.addIfNotNil(parseLineBlock(p))
|
result.addIfNotNil(parseLineBlock(p))
|
||||||
|
of rnMarkdownBlockQuote:
|
||||||
|
result.addIfNotNil(parseMarkdownBlockQuote(p))
|
||||||
else: break
|
else: break
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
of tkPunct:
|
of tkPunct:
|
||||||
if currentTok(p).symbol == "::" and
|
if (let literalBlockKind = whichRstLiteralBlock(p);
|
||||||
nextTok(p).kind == tkIndent and
|
literalBlockKind != lbNone):
|
||||||
currInd(p) < nextTok(p).ival:
|
|
||||||
result.add newLeaf(":")
|
result.add newLeaf(":")
|
||||||
inc p.idx # skip '::'
|
inc p.idx # skip '::'
|
||||||
result.add(parseLiteralBlock(p))
|
result.add(parseRstLiteralBlock(p, literalBlockKind))
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
parseInline(p, result)
|
parseInline(p, result)
|
||||||
|
|
@ -2257,8 +2452,6 @@ proc getColumns(p: var RstParser, cols: var IntSeq) =
|
||||||
# last column has no limit:
|
# last column has no limit:
|
||||||
cols[L - 1] = 32000
|
cols[L - 1] = 32000
|
||||||
|
|
||||||
proc parseDoc(p: var RstParser): PRstNode {.gcsafe.}
|
|
||||||
|
|
||||||
proc parseSimpleTable(p: var RstParser): PRstNode =
|
proc parseSimpleTable(p: var RstParser): PRstNode =
|
||||||
var
|
var
|
||||||
cols: IntSeq
|
cols: IntSeq
|
||||||
|
|
@ -2585,6 +2778,7 @@ proc parseSection(p: var RstParser, result: PRstNode) =
|
||||||
a = parseLiteralBlock(p)
|
a = parseLiteralBlock(p)
|
||||||
of rnBulletList: a = parseBulletList(p)
|
of rnBulletList: a = parseBulletList(p)
|
||||||
of rnLineBlock: a = parseLineBlock(p)
|
of rnLineBlock: a = parseLineBlock(p)
|
||||||
|
of rnMarkdownBlockQuote: a = parseMarkdownBlockQuote(p)
|
||||||
of rnDirective: a = parseDotDot(p)
|
of rnDirective: a = parseDotDot(p)
|
||||||
of rnEnumList: a = parseEnumList(p)
|
of rnEnumList: a = parseEnumList(p)
|
||||||
of rnLeaf: rstMessage(p, meNewSectionExpected, "(syntax error)")
|
of rnLeaf: rstMessage(p, meNewSectionExpected, "(syntax error)")
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,10 @@ type
|
||||||
rnFieldName, # consisting of a field name ...
|
rnFieldName, # consisting of a field name ...
|
||||||
rnFieldBody, # ... and a field body
|
rnFieldBody, # ... and a field body
|
||||||
rnOptionList, rnOptionListItem, rnOptionGroup, rnOption, rnOptionString,
|
rnOptionList, rnOptionListItem, rnOptionGroup, rnOption, rnOptionString,
|
||||||
rnOptionArgument, rnDescription, rnLiteralBlock, rnQuotedLiteralBlock,
|
rnOptionArgument, rnDescription, rnLiteralBlock,
|
||||||
|
rnMarkdownBlockQuote, # a quote starting from punctuation like >>>
|
||||||
|
rnMarkdownBlockQuoteItem, # a quotation block, quote lines starting with
|
||||||
|
# the same number of chars
|
||||||
rnLineBlock, # the | thingie
|
rnLineBlock, # the | thingie
|
||||||
rnLineBlockItem, # a son of rnLineBlock - one line inside it.
|
rnLineBlockItem, # a son of rnLineBlock - one line inside it.
|
||||||
# When `RstNode` lineIndent="\n" the line's empty
|
# When `RstNode` lineIndent="\n" the line's empty
|
||||||
|
|
@ -101,6 +104,8 @@ type
|
||||||
of rnFootnote, rnCitation, rnOptionListItem:
|
of rnFootnote, rnCitation, rnOptionListItem:
|
||||||
order*: int ## footnote order (for auto-symbol footnotes and
|
order*: int ## footnote order (for auto-symbol footnotes and
|
||||||
## auto-numbered ones without a label)
|
## auto-numbered ones without a label)
|
||||||
|
of rnMarkdownBlockQuoteItem:
|
||||||
|
quotationDepth*: int ## number of characters in line prefix
|
||||||
of rnRef, rnSubstitutionReferences,
|
of rnRef, rnSubstitutionReferences,
|
||||||
rnInterpretedText, rnField, rnInlineCode, rnCodeBlock, rnFootnoteRef:
|
rnInterpretedText, rnField, rnInlineCode, rnCodeBlock, rnFootnoteRef:
|
||||||
info*: TLineInfo ## To have line/column info for warnings at
|
info*: TLineInfo ## To have line/column info for warnings at
|
||||||
|
|
@ -409,6 +414,8 @@ proc treeRepr*(node: PRstNode, indent=0): string =
|
||||||
result.add " level=" & $node.level
|
result.add " level=" & $node.level
|
||||||
of rnFootnote, rnCitation, rnOptionListItem:
|
of rnFootnote, rnCitation, rnOptionListItem:
|
||||||
result.add (if node.order == 0: "" else: " order=" & $node.order)
|
result.add (if node.order == 0: "" else: " order=" & $node.order)
|
||||||
|
of rnMarkdownBlockQuoteItem:
|
||||||
|
result.add " quotationDepth=" & $node.quotationDepth
|
||||||
else:
|
else:
|
||||||
discard
|
discard
|
||||||
result.add (if node.anchor == "": "" else: " anchor='" & node.anchor & "'")
|
result.add (if node.anchor == "": "" else: " anchor='" & node.anchor & "'")
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ type
|
||||||
onTestSnippet*: proc (d: var RstGenerator; filename, cmd: string; status: int;
|
onTestSnippet*: proc (d: var RstGenerator; filename, cmd: string; status: int;
|
||||||
content: string)
|
content: string)
|
||||||
escMode*: EscapeMode
|
escMode*: EscapeMode
|
||||||
|
curQuotationDepth: int
|
||||||
|
|
||||||
PDoc = var RstGenerator ## Alias to type less.
|
PDoc = var RstGenerator ## Alias to type less.
|
||||||
|
|
||||||
|
|
@ -167,6 +168,7 @@ proc initRstGenerator*(g: var RstGenerator, target: OutputTarget,
|
||||||
g.currentSection = ""
|
g.currentSection = ""
|
||||||
g.id = 0
|
g.id = 0
|
||||||
g.escMode = emText
|
g.escMode = emText
|
||||||
|
g.curQuotationDepth = 0
|
||||||
let fileParts = filename.splitFile
|
let fileParts = filename.splitFile
|
||||||
if fileParts.ext == ".nim":
|
if fileParts.ext == ".nim":
|
||||||
g.currentSection = "Module " & fileParts.name
|
g.currentSection = "Module " & fileParts.name
|
||||||
|
|
@ -1268,8 +1270,31 @@ proc renderRstToOut(d: PDoc, n: PRstNode, result: var string) =
|
||||||
of rnLiteralBlock:
|
of rnLiteralBlock:
|
||||||
renderAux(d, n, "<pre$2>$1</pre>\n",
|
renderAux(d, n, "<pre$2>$1</pre>\n",
|
||||||
"\n\n$2\\begin{rstpre}\n$1\n\\end{rstpre}\n\n", result)
|
"\n\n$2\\begin{rstpre}\n$1\n\\end{rstpre}\n\n", result)
|
||||||
of rnQuotedLiteralBlock:
|
of rnMarkdownBlockQuote:
|
||||||
doAssert false, "renderRstToOut"
|
d.curQuotationDepth = 1
|
||||||
|
var tmp = ""
|
||||||
|
renderAux(d, n, "$1", "$1", tmp)
|
||||||
|
let itemEnding =
|
||||||
|
if d.target == outHtml: "</blockquote>" else: "\\end{rstquote}"
|
||||||
|
tmp.add itemEnding.repeat(d.curQuotationDepth - 1)
|
||||||
|
dispA(d.target, result,
|
||||||
|
"<blockquote$2 class=\"markdown-quote\">$1</blockquote>\n",
|
||||||
|
"\n\\begin{rstquote}\n$2\n$1\\end{rstquote}\n", [tmp, n.anchor.idS])
|
||||||
|
of rnMarkdownBlockQuoteItem:
|
||||||
|
let addQuotationDepth = n.quotationDepth - d.curQuotationDepth
|
||||||
|
var itemPrefix: string # start or ending (quotation grey bar on the left)
|
||||||
|
if addQuotationDepth >= 0:
|
||||||
|
let s =
|
||||||
|
if d.target == outHtml: "<blockquote class=\"markdown-quote\">"
|
||||||
|
else: "\\begin{rstquote}"
|
||||||
|
itemPrefix = s.repeat(addQuotationDepth)
|
||||||
|
else:
|
||||||
|
let s =
|
||||||
|
if d.target == outHtml: "</blockquote>"
|
||||||
|
else: "\\end{rstquote}"
|
||||||
|
itemPrefix = s.repeat(-addQuotationDepth)
|
||||||
|
renderAux(d, n, itemPrefix & "<p>$1</p>", itemPrefix & "\n$1", result)
|
||||||
|
d.curQuotationDepth = n.quotationDepth
|
||||||
of rnLineBlock:
|
of rnLineBlock:
|
||||||
if n.sons.len == 1 and n.sons[0].lineIndent == "\n":
|
if n.sons.len == 1 and n.sons[0].lineIndent == "\n":
|
||||||
# whole line block is one empty line, no need to add extra spacing
|
# whole line block is one empty line, no need to add extra spacing
|
||||||
|
|
|
||||||
|
|
@ -567,6 +567,11 @@ blockquote {
|
||||||
border-left: 5px solid #bbc;
|
border-left: 5px solid #bbc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
blockquote.markdown-quote {
|
||||||
|
font-size: 0.9rem; /* use rem to avoid recursion */
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
.pre, span.tok {
|
.pre, span.tok {
|
||||||
font-family: "Source Code Pro", Monaco, Menlo, Consolas, "Courier New", monospace;
|
font-family: "Source Code Pro", Monaco, Menlo, Consolas, "Courier New", monospace;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,367 @@ suite "RST parsing":
|
||||||
rnLeaf ' '
|
rnLeaf ' '
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
test "RST quoted literal blocks":
|
||||||
|
let expected =
|
||||||
|
dedent"""
|
||||||
|
rnInner
|
||||||
|
rnLeaf 'Paragraph'
|
||||||
|
rnLeaf ':'
|
||||||
|
rnLiteralBlock
|
||||||
|
rnLeaf '>x'
|
||||||
|
"""
|
||||||
|
|
||||||
|
check(dedent"""
|
||||||
|
Paragraph::
|
||||||
|
|
||||||
|
>x""".toAst == expected)
|
||||||
|
|
||||||
|
check(dedent"""
|
||||||
|
Paragraph::
|
||||||
|
|
||||||
|
>x""".toAst == expected)
|
||||||
|
|
||||||
|
test "RST quoted literal blocks, :: at a separate line":
|
||||||
|
let expected =
|
||||||
|
dedent"""
|
||||||
|
rnInner
|
||||||
|
rnInner
|
||||||
|
rnLeaf 'Paragraph'
|
||||||
|
rnLiteralBlock
|
||||||
|
rnLeaf '>x
|
||||||
|
>>y'
|
||||||
|
"""
|
||||||
|
|
||||||
|
check(dedent"""
|
||||||
|
Paragraph
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
>x
|
||||||
|
>>y""".toAst == expected)
|
||||||
|
|
||||||
|
check(dedent"""
|
||||||
|
Paragraph
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
>x
|
||||||
|
>>y""".toAst == expected)
|
||||||
|
|
||||||
|
test "Markdown quoted blocks":
|
||||||
|
check(dedent"""
|
||||||
|
Paragraph.
|
||||||
|
>x""".toAst ==
|
||||||
|
dedent"""
|
||||||
|
rnInner
|
||||||
|
rnLeaf 'Paragraph'
|
||||||
|
rnLeaf '.'
|
||||||
|
rnMarkdownBlockQuote
|
||||||
|
rnMarkdownBlockQuoteItem quotationDepth=1
|
||||||
|
rnLeaf 'x'
|
||||||
|
""")
|
||||||
|
|
||||||
|
# bug #17987
|
||||||
|
check(dedent"""
|
||||||
|
foo https://github.com/nim-lang/Nim/issues/8258
|
||||||
|
|
||||||
|
> bar""".toAst ==
|
||||||
|
dedent"""
|
||||||
|
rnInner
|
||||||
|
rnInner
|
||||||
|
rnLeaf 'foo'
|
||||||
|
rnLeaf ' '
|
||||||
|
rnStandaloneHyperlink
|
||||||
|
rnLeaf 'https://github.com/nim-lang/Nim/issues/8258'
|
||||||
|
rnMarkdownBlockQuote
|
||||||
|
rnMarkdownBlockQuoteItem quotationDepth=1
|
||||||
|
rnLeaf 'bar'
|
||||||
|
""")
|
||||||
|
|
||||||
|
let expected = dedent"""
|
||||||
|
rnInner
|
||||||
|
rnLeaf 'Paragraph'
|
||||||
|
rnLeaf '.'
|
||||||
|
rnMarkdownBlockQuote
|
||||||
|
rnMarkdownBlockQuoteItem quotationDepth=1
|
||||||
|
rnInner
|
||||||
|
rnLeaf 'x1'
|
||||||
|
rnLeaf ' '
|
||||||
|
rnLeaf 'x2'
|
||||||
|
rnMarkdownBlockQuoteItem quotationDepth=2
|
||||||
|
rnInner
|
||||||
|
rnLeaf 'y1'
|
||||||
|
rnLeaf ' '
|
||||||
|
rnLeaf 'y2'
|
||||||
|
rnMarkdownBlockQuoteItem quotationDepth=1
|
||||||
|
rnLeaf 'z'
|
||||||
|
"""
|
||||||
|
|
||||||
|
check(dedent"""
|
||||||
|
Paragraph.
|
||||||
|
>x1 x2
|
||||||
|
>>y1 y2
|
||||||
|
>z""".toAst == expected)
|
||||||
|
|
||||||
|
check(dedent"""
|
||||||
|
Paragraph.
|
||||||
|
> x1 x2
|
||||||
|
>> y1 y2
|
||||||
|
> z""".toAst == expected)
|
||||||
|
|
||||||
|
check(dedent"""
|
||||||
|
>x
|
||||||
|
>y
|
||||||
|
>z""".toAst ==
|
||||||
|
dedent"""
|
||||||
|
rnMarkdownBlockQuote
|
||||||
|
rnMarkdownBlockQuoteItem quotationDepth=1
|
||||||
|
rnInner
|
||||||
|
rnLeaf 'x'
|
||||||
|
rnLeaf ' '
|
||||||
|
rnLeaf 'y'
|
||||||
|
rnLeaf ' '
|
||||||
|
rnLeaf 'z'
|
||||||
|
""")
|
||||||
|
|
||||||
|
check(dedent"""
|
||||||
|
> z
|
||||||
|
> > >y
|
||||||
|
""".toAst ==
|
||||||
|
dedent"""
|
||||||
|
rnMarkdownBlockQuote
|
||||||
|
rnMarkdownBlockQuoteItem quotationDepth=1
|
||||||
|
rnLeaf 'z'
|
||||||
|
rnMarkdownBlockQuoteItem quotationDepth=3
|
||||||
|
rnLeaf 'y'
|
||||||
|
""")
|
||||||
|
|
||||||
|
test "Markdown quoted blocks: lazy":
|
||||||
|
let expected = dedent"""
|
||||||
|
rnInner
|
||||||
|
rnMarkdownBlockQuote
|
||||||
|
rnMarkdownBlockQuoteItem quotationDepth=2
|
||||||
|
rnInner
|
||||||
|
rnLeaf 'x'
|
||||||
|
rnLeaf ' '
|
||||||
|
rnLeaf 'continuation1'
|
||||||
|
rnLeaf ' '
|
||||||
|
rnLeaf 'continuation2'
|
||||||
|
rnParagraph
|
||||||
|
rnLeaf 'newParagraph'
|
||||||
|
"""
|
||||||
|
check(dedent"""
|
||||||
|
>>x
|
||||||
|
continuation1
|
||||||
|
continuation2
|
||||||
|
|
||||||
|
newParagraph""".toAst == expected)
|
||||||
|
|
||||||
|
check(dedent"""
|
||||||
|
>> x
|
||||||
|
continuation1
|
||||||
|
continuation2
|
||||||
|
|
||||||
|
newParagraph""".toAst == expected)
|
||||||
|
|
||||||
|
# however mixing more than 1 non-lazy line and lazy one(s) splits quote
|
||||||
|
# in our parser, which appeared the easiest way to handle such cases:
|
||||||
|
var warnings = new seq[string]
|
||||||
|
check(dedent"""
|
||||||
|
>> x
|
||||||
|
>> continuation1
|
||||||
|
continuation2
|
||||||
|
|
||||||
|
newParagraph""".toAst(warnings=warnings) ==
|
||||||
|
dedent"""
|
||||||
|
rnInner
|
||||||
|
rnMarkdownBlockQuote
|
||||||
|
rnMarkdownBlockQuoteItem quotationDepth=2
|
||||||
|
rnLeaf 'x'
|
||||||
|
rnMarkdownBlockQuoteItem quotationDepth=2
|
||||||
|
rnInner
|
||||||
|
rnLeaf 'continuation1'
|
||||||
|
rnLeaf ' '
|
||||||
|
rnLeaf 'continuation2'
|
||||||
|
rnParagraph
|
||||||
|
rnLeaf 'newParagraph'
|
||||||
|
""")
|
||||||
|
check(warnings[] == @[
|
||||||
|
"input(2, 1) Warning: RST style: two or more quoted lines " &
|
||||||
|
"are followed by unquoted line 3"])
|
||||||
|
|
||||||
|
test "Markdown quoted blocks: not lazy":
|
||||||
|
# here is where we deviate from CommonMark specification: 'bar' below is
|
||||||
|
# not considered as continuation of 2-level '>> foo' quote.
|
||||||
|
check(dedent"""
|
||||||
|
>>> foo
|
||||||
|
> bar
|
||||||
|
>> baz
|
||||||
|
""".toAst() ==
|
||||||
|
dedent"""
|
||||||
|
rnMarkdownBlockQuote
|
||||||
|
rnMarkdownBlockQuoteItem quotationDepth=3
|
||||||
|
rnLeaf 'foo'
|
||||||
|
rnMarkdownBlockQuoteItem quotationDepth=1
|
||||||
|
rnLeaf 'bar'
|
||||||
|
rnMarkdownBlockQuoteItem quotationDepth=2
|
||||||
|
rnLeaf 'baz'
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
test "Markdown quoted blocks: inline markup works":
|
||||||
|
check(dedent"""
|
||||||
|
> hi **bold** text
|
||||||
|
""".toAst == dedent"""
|
||||||
|
rnMarkdownBlockQuote
|
||||||
|
rnMarkdownBlockQuoteItem quotationDepth=1
|
||||||
|
rnInner
|
||||||
|
rnLeaf 'hi'
|
||||||
|
rnLeaf ' '
|
||||||
|
rnStrongEmphasis
|
||||||
|
rnLeaf 'bold'
|
||||||
|
rnLeaf ' '
|
||||||
|
rnLeaf 'text'
|
||||||
|
""")
|
||||||
|
|
||||||
|
test "Markdown quoted blocks: blank line separator":
|
||||||
|
let expected = dedent"""
|
||||||
|
rnInner
|
||||||
|
rnMarkdownBlockQuote
|
||||||
|
rnMarkdownBlockQuoteItem quotationDepth=1
|
||||||
|
rnInner
|
||||||
|
rnLeaf 'x'
|
||||||
|
rnLeaf ' '
|
||||||
|
rnLeaf 'y'
|
||||||
|
rnMarkdownBlockQuote
|
||||||
|
rnMarkdownBlockQuoteItem quotationDepth=1
|
||||||
|
rnInner
|
||||||
|
rnLeaf 'z'
|
||||||
|
rnLeaf ' '
|
||||||
|
rnLeaf 't'
|
||||||
|
"""
|
||||||
|
check(dedent"""
|
||||||
|
>x
|
||||||
|
>y
|
||||||
|
|
||||||
|
> z
|
||||||
|
> t""".toAst == expected)
|
||||||
|
|
||||||
|
check(dedent"""
|
||||||
|
>x
|
||||||
|
y
|
||||||
|
|
||||||
|
> z
|
||||||
|
t""".toAst == expected)
|
||||||
|
|
||||||
|
test "Markdown quoted blocks: nested body blocks/elements work #1":
|
||||||
|
let expected = dedent"""
|
||||||
|
rnMarkdownBlockQuote
|
||||||
|
rnMarkdownBlockQuoteItem quotationDepth=1
|
||||||
|
rnBulletList
|
||||||
|
rnBulletItem
|
||||||
|
rnInner
|
||||||
|
rnLeaf 'x'
|
||||||
|
rnBulletItem
|
||||||
|
rnInner
|
||||||
|
rnLeaf 'y'
|
||||||
|
"""
|
||||||
|
|
||||||
|
check(dedent"""
|
||||||
|
> - x
|
||||||
|
- y
|
||||||
|
""".toAst == expected)
|
||||||
|
|
||||||
|
# TODO: if bug #17340 point 28 is resolved then this may work:
|
||||||
|
# check(dedent"""
|
||||||
|
# > - x
|
||||||
|
# - y
|
||||||
|
# """.toAst == expected)
|
||||||
|
|
||||||
|
check(dedent"""
|
||||||
|
> - x
|
||||||
|
> - y
|
||||||
|
""".toAst == expected)
|
||||||
|
|
||||||
|
check(dedent"""
|
||||||
|
>
|
||||||
|
> - x
|
||||||
|
>
|
||||||
|
> - y
|
||||||
|
>
|
||||||
|
""".toAst == expected)
|
||||||
|
|
||||||
|
test "Markdown quoted blocks: nested body blocks/elements work #2":
|
||||||
|
let expected = dedent"""
|
||||||
|
rnAdmonition adType=note
|
||||||
|
[nil]
|
||||||
|
[nil]
|
||||||
|
rnDefList
|
||||||
|
rnDefItem
|
||||||
|
rnDefName
|
||||||
|
rnLeaf 'deflist'
|
||||||
|
rnLeaf ':'
|
||||||
|
rnDefBody
|
||||||
|
rnMarkdownBlockQuote
|
||||||
|
rnMarkdownBlockQuoteItem quotationDepth=2
|
||||||
|
rnInner
|
||||||
|
rnLeaf 'quote'
|
||||||
|
rnLeaf ' '
|
||||||
|
rnLeaf 'continuation'
|
||||||
|
"""
|
||||||
|
|
||||||
|
check(dedent"""
|
||||||
|
.. Note:: deflist:
|
||||||
|
>> quote
|
||||||
|
continuation
|
||||||
|
""".toAst == expected)
|
||||||
|
|
||||||
|
check(dedent"""
|
||||||
|
.. Note::
|
||||||
|
deflist:
|
||||||
|
>> quote
|
||||||
|
continuation
|
||||||
|
""".toAst == expected)
|
||||||
|
|
||||||
|
check(dedent"""
|
||||||
|
.. Note::
|
||||||
|
deflist:
|
||||||
|
>> quote
|
||||||
|
>> continuation
|
||||||
|
""".toAst == expected)
|
||||||
|
|
||||||
|
# spaces are not significant between `>`:
|
||||||
|
check(dedent"""
|
||||||
|
.. Note::
|
||||||
|
deflist:
|
||||||
|
> > quote
|
||||||
|
> > continuation
|
||||||
|
""".toAst == expected)
|
||||||
|
|
||||||
|
test "Markdown quoted blocks: de-indent handled well":
|
||||||
|
check(dedent"""
|
||||||
|
>
|
||||||
|
> - x
|
||||||
|
> - y
|
||||||
|
>
|
||||||
|
> Paragraph.
|
||||||
|
""".toAst == dedent"""
|
||||||
|
rnMarkdownBlockQuote
|
||||||
|
rnMarkdownBlockQuoteItem quotationDepth=1
|
||||||
|
rnInner
|
||||||
|
rnBlockQuote
|
||||||
|
rnBulletList
|
||||||
|
rnBulletItem
|
||||||
|
rnInner
|
||||||
|
rnLeaf 'x'
|
||||||
|
rnBulletItem
|
||||||
|
rnInner
|
||||||
|
rnLeaf 'y'
|
||||||
|
rnParagraph
|
||||||
|
rnLeaf 'Paragraph'
|
||||||
|
rnLeaf '.'
|
||||||
|
""")
|
||||||
|
|
||||||
test "option list has priority over definition list":
|
test "option list has priority over definition list":
|
||||||
check(dedent"""
|
check(dedent"""
|
||||||
--defusages
|
--defusages
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue