summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorAndrey Makarov <ph.makarov@gmail.com>2020-12-27 13:16:12 +0300
committerGitHub <noreply@github.com>2020-12-27 11:16:12 +0100
commit2bdc479622c465e570fdb87df112dd56ddc9030f (patch)
tree5b3f0abd04368714839bb4d77ff3a470361ff7e0
parent626c2bc6589101bd1b0231a2608e32b51f73abba (diff)
downloadNim-2bdc479622c465e570fdb87df112dd56ddc9030f.tar.gz
RST: implement admonitions (#16438)
-rw-r--r--config/nimdoc.tex.cfg9
-rw-r--r--doc/nimdoc.css34
-rw-r--r--lib/packages/docutils/rst.nim155
-rw-r--r--lib/packages/docutils/rstast.nim22
-rw-r--r--lib/packages/docutils/rstgen.nim24
-rw-r--r--tests/stdlib/trstgen.nim49
6 files changed, 239 insertions, 54 deletions
diff --git a/config/nimdoc.tex.cfg b/config/nimdoc.tex.cfg
index 3e5e5d38b..6d7ae413d 100644
--- a/config/nimdoc.tex.cfg
+++ b/config/nimdoc.tex.cfg
@@ -52,6 +52,15 @@ doc.file = """
 \usepackage{hyperref}
 \usepackage{enumitem}
 
+\usepackage{xcolor}
+\usepackage[tikz]{mdframed}
+\usetikzlibrary{shadows}
+\mdfsetup{%
+linewidth=3,
+topline=false,
+rightline=false,
+bottomline=false}
+
 \begin{document}
 \title{$title $version}
 \author{$author}
diff --git a/doc/nimdoc.css b/doc/nimdoc.css
index 2d9533ceb..7d4a399e1 100644
--- a/doc/nimdoc.css
+++ b/doc/nimdoc.css
@@ -14,6 +14,9 @@ Modified by Boyd Greenfield and narimiran
   --primary-background: #fff;

   --secondary-background: ghostwhite;

   --third-background: #e8e8e8;

+  --info-background: #50c050;

+  --warning-background: #c0a000;

+  --error-background: #e04040;

   --border: #dde;

   --text: #222;

   --anchor: #07b;

@@ -39,6 +42,9 @@ Modified by Boyd Greenfield and narimiran
   --primary-background: #171921;

   --secondary-background: #1e202a;

   --third-background: #2b2e3b;

+  --info-background: #008000;

+  --warning-background: #807000;

+  --error-background: #c03000;

   --border: #0e1014;

   --text: #fff;

   --anchor: #8be9fd;

@@ -609,6 +615,34 @@ table.borderless td, table.borderless th {
      The right padding separates the table cells. */

   padding: 0 0.5em 0 0 !important; }

 

+.admonition {

+    padding: 0.3em;

+    background-color: var(--secondary-background);

+    border-left: 0.4em solid #7f7f84;

+    margin-bottom: 0.5em;

+    -webkit-box-shadow: 0 5px 8px -6px rgba(0,0,0,.2);

+       -moz-box-shadow: 0 5px 8px -6px rgba(0,0,0,.2);

+            box-shadow: 0 5px 8px -6px rgba(0,0,0,.2);

+}

+.admonition-info {

+    border-color: var(--info-background);

+}

+.admonition-info-text {

+    color: var(--info-background);

+}

+.admonition-warning {

+    border-color: var(--warning-background);

+}

+.admonition-warning-text {

+    color: var(--warning-background);

+}

+.admonition-error {

+    border-color: var(--error-background);

+}

+.admonition-error-text {

+    color: var(--error-background);

+}

+

 .first {

   /* Override more specific margin styles with "! important". */

   margin-top: 0 !important; }

diff --git a/lib/packages/docutils/rst.nim b/lib/packages/docutils/rst.nim
index 0d86edcdb..7dbcaf482 100644
--- a/lib/packages/docutils/rst.nim
+++ b/lib/packages/docutils/rst.nim
@@ -7,11 +7,18 @@
 #    distribution, for details about the copyright.
 #
 
-## This module implements a `reStructuredText`:idx: parser. A large
-## subset is implemented. Some features of the `markdown`:idx: wiki syntax are
-## also supported.
+## This module implements a `reStructuredText`:idx: (RST) parser. A large
+## subset is implemented. Some features of the `markdown`:idx: syntax are
+## also supported. Nim can output the result to HTML (command ``rst2html``)
+## or Latex (command ``rst2tex``).
 ##
-## Supported RST features:
+## If you are new to RST please consider reading the following:
+##
+## 1) a short `quick introduction`_
+## 2) an `RST reference`_: a comprehensive cheatsheet for RST
+## 3) a more formal 50-page `RST specification`_.
+##
+## Supported standard RST features:
 ##
 ## * body elements
 ##   + sections
@@ -25,20 +32,29 @@
 ##   + option lists
 ##   + indented literal blocks
 ##   + simple tables
-##   + directives
-##     - image, figure
-##     - code-block
-##     - substitution definitions: replace and image
-##     - ... a few more
+##   + directives (see official documentation in `RST directives list`_):
+##     - ``image``, ``figure`` for including images and videos
+##     - ``code``
+##     - ``contents`` (table of contents), ``container``, ``raw``
+##     - ``include``
+##     - admonitions: "attention", "caution", "danger", "error", "hint",
+##       "important", "note", "tip", "warning", "admonition"
+##     - substitution definitions: `replace` and `image`
 ##   + comments
 ## * inline markup
-##   + *emphasis*, **strong emphasis**, `interpreted text`,
+##   + *emphasis*, **strong emphasis**,
 ##     ``inline literals``, hyperlink references, substitution references,
 ##     standalone hyperlinks
+##   + \`interpreted text\` with roles ``:literal:``, ``:strong:``,
+##     ``emphasis``, ``:sub:``/``:subscript:``, ``:sup:``/``:supscript:``
+##     (see `RST roles list`_ for description).
 ##
 ## Additional features:
 ##
+## * directives: ``code-block``, ``title``, ``index``
 ## * ***triple emphasis*** (bold and italic) using \*\*\*
+## * ``:idx:`` role for \`interpreted text\` to include the link to this
+##   text into an index (example: `Nim index`_).
 ##
 ## Optional additional features, turned on by ``options: RstParseOption`` in
 ## `rstParse proc <#rstParse,string,string,int,int,bool,RstParseOptions,FindFileHandler,MsgHandler>`_:
@@ -51,7 +67,11 @@
 ## * using ``1`` as auto-enumerator in enumerated lists like RST ``#``
 ##   (auto-enumerator ``1`` can not be used with ``#`` in the same list)
 ##
-## **Note:** By default nim has ``roSupportMarkdown`` turned **on**.
+## .. Note:: By default Nim has ``roSupportMarkdown`` and
+##    ``roSupportRawDirective`` turned **on**.
+##
+## .. warning:: Using Nim-specific features can cause other RST implementations
+##   to fail on your document.
 ##
 ## Limitations:
 ##
@@ -61,14 +81,32 @@
 ##   - no quoted literal blocks
 ##   - no doctest blocks
 ##   - no grid tables
-##   - directives: no support for admonitions (notes, caution)
+##   - some directives are missing (check official `RST directives list`_):
+##     ``parsed-literal``, ``sidebar``, ``topic``, ``math``, ``rubric``,
+##     ``epigraph``, ``highlights``, ``pull-quote``, ``compound``,
+##     ``table``, ``csv-table``, ``list-table``, ``section-numbering``,
+##     ``header``, ``footer``, ``meta``, ``class``
+##     - no ``role`` directives and no custom interpreted text roles
+##     - some standard roles are not supported (check `RST roles list`_)
 ##   - no footnotes & citations support
 ##   - no inline internal targets
 ## * inline markup
 ##   - no simple-inline-markup
 ##   - no embedded URI and aliases
 ##
-## **Note:** Import ``packages/docutils/rst`` to use this module
+## .. _quick introduction: https://docutils.sourceforge.io/docs/user/rst/quickstart.html
+## .. _RST reference: https://docutils.sourceforge.io/docs/user/rst/quickref.html
+## .. _RST specification: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html
+## .. _RST directives list: https://docutils.sourceforge.io/docs/ref/rst/directives.html
+## .. _RST roles list: https://docutils.sourceforge.io/docs/ref/rst/roles.html
+## .. _Nim index: https://nim-lang.org/docs/theindex.html
+##
+## See `Nim DocGen Tools Guide <docgen.html>`_ for the details about
+## ``nim doc``, ``nim rst2html`` and ``nim rst2tex`` commands.
+##
+## .. note:: Import ``packages/docutils/rst`` to use this module.
+##
+## See also `packages/docutils/rstgen module <rstgen.html>`_.
 
 import
   os, strutils, rstast
@@ -947,6 +985,7 @@ proc getDirective(p: var RstParser): string =
       result = ""             # error
   else:
     result = ""
+  result = result.toLowerAscii()
 
 proc parseComment(p: var RstParser): PRstNode =
   case currentTok(p).kind
@@ -968,21 +1007,6 @@ proc parseComment(p: var RstParser): PRstNode =
     while currentTok(p).kind notin {tkIndent, tkEof}: inc p.idx
   result = nil
 
-type
-  DirKind = enum             # must be ordered alphabetically!
-    dkNone, dkAuthor, dkAuthors, dkCode, dkCodeBlock, dkContainer, dkContents,
-    dkFigure, dkImage, dkInclude, dkIndex, dkRaw, dkTitle
-
-const
-  DirIds: array[0..12, string] = ["", "author", "authors", "code",
-    "code-block", "container", "contents", "figure", "image", "include",
-    "index", "raw", "title"]
-
-proc getDirKind(s: string): DirKind =
-  let i = find(DirIds, s)
-  if i >= 0: result = DirKind(i)
-  else: result = dkNone
-
 proc parseLine(p: var RstParser, father: PRstNode) =
   while true:
     case currentTok(p).kind
@@ -1191,7 +1215,8 @@ proc whichSection(p: RstParser): RstNodeKind =
       result = rnMarkdownTable
     elif currentTok(p).symbol == "|" and isLineBlock(p):
       result = rnLineBlock
-    elif match(p, tokenAfterNewline(p), "ai"):
+    elif match(p, tokenAfterNewline(p), "ai") and
+        isAdornmentHeadline(p, tokenAfterNewline(p)):
       result = rnHeadline
     elif predNL(p) and
         currentTok(p).symbol in ["+", "*", "-"] and nextTok(p).kind == tkWhite:
@@ -1664,8 +1689,8 @@ proc parseDirective(p: var RstParser, flags: DirFlags): 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
+  ## directive (rnDirArg), the options (rnFieldList) and the directive
+  ## content block. This proc parses the two first nodes, the 3rd is left to
   ## the outer `parseDirective` call.
   ##
   ## Both rnDirArg and rnFieldList children nodes might be nil, so you need to
@@ -1703,12 +1728,20 @@ proc indFollows(p: RstParser): bool =
 
 proc parseDirective(p: var RstParser, flags: DirFlags,
                     contentParser: SectionParser): PRstNode =
-  ## Returns a generic rnDirective tree.
+  ## A helper proc that does main work for specific directive procs.
+  ## Always returns a generic rnDirective tree with these 3 children:
+  ##
+  ## 1) rnDirArg
+  ## 2) rnFieldList
+  ## 3) a node returned by `contentParser`.
   ##
-  ## The children are rnDirArg, rnFieldList and rnLineBlock. Any might be nil.
+  ## .. warning:: Any of the 3 children may be nil.
   result = parseDirective(p, flags)
-  if not isNil(contentParser) and indFollows(p):
-    pushInd(p, currentTok(p).ival)
+  if not isNil(contentParser):
+    var nextIndent = p.tok[tokenAfterNewline(p)-1].ival
+    if nextIndent <= currInd(p):  # parse only this line
+      nextIndent = currentTok(p).col
+    pushInd(p, nextIndent)
     var content = contentParser(p)
     popInd(p)
     result.add(content)
@@ -1814,7 +1847,7 @@ proc dirCodeBlock(p: var RstParser, nimExtension = false): PRstNode =
     n.add(newRstNode(rnLeaf, readFile(path)))
     result.sons[2] = n
 
-  # Extend the field block if we are using our custom extension.
+  # Extend the field block if we are using our custom Nim extension.
   if nimExtension:
     # Create a field block if the input block didn't have any.
     if result.sons[1].isNil: result.sons[1] = newRstNode(rnFieldList)
@@ -1856,6 +1889,11 @@ proc dirIndex(p: var RstParser): PRstNode =
   result = parseDirective(p, {}, parseSectionWrapper)
   result.kind = rnIndex
 
+proc dirAdmonition(p: var RstParser, d: string): PRstNode =
+  result = parseDirective(p, {}, parseSectionWrapper)
+  result.kind = rnAdmonition
+  result.text = d
+
 proc dirRawAux(p: var RstParser, result: var PRstNode, kind: RstNodeKind,
                contentParser: SectionParser) =
   var filename = getFieldValue(result, "file")
@@ -1891,29 +1929,42 @@ proc dirRaw(p: var RstParser): PRstNode =
   else:
     dirRawAux(p, result, rnRaw, parseSectionWrapper)
 
+proc selectDir(p: var RstParser, d: string): PRstNode =
+  result = nil
+  case d
+  of "admonition", "attention", "caution": result = dirAdmonition(p, d)
+  of "code": result = dirCodeBlock(p)
+  of "code-block": result = dirCodeBlock(p, nimExtension = true)
+  of "container": result = dirContainer(p)
+  of "contents": result = dirContents(p)
+  of "danger", "error": result = dirAdmonition(p, d)
+  of "figure": result = dirFigure(p)
+  of "hint": result = dirAdmonition(p, d)
+  of "image": result = dirImage(p)
+  of "important": result = dirAdmonition(p, d)
+  of "include": result = dirInclude(p)
+  of "index": result = dirIndex(p)
+  of "note": result = dirAdmonition(p, d)
+  of "raw":
+    if roSupportRawDirective in p.s.options:
+      result = dirRaw(p)
+    else:
+      rstMessage(p, meInvalidDirective, d)
+  of "tip": result = dirAdmonition(p, d)
+  of "title": result = dirTitle(p)
+  of "warning": result = dirAdmonition(p, d)
+  else:
+    rstMessage(p, meInvalidDirective, d)
+
 proc parseDotDot(p: var RstParser): PRstNode =
+  # parse "explicit markup blocks"
   result = nil
   var col = currentTok(p).col
   inc p.idx
   var d = getDirective(p)
   if d != "":
     pushInd(p, col)
-    case getDirKind(d)
-    of dkInclude: result = dirInclude(p)
-    of dkImage: result = dirImage(p)
-    of dkFigure: result = dirFigure(p)
-    of dkTitle: result = dirTitle(p)
-    of dkContainer: result = dirContainer(p)
-    of dkContents: result = dirContents(p)
-    of dkRaw:
-      if roSupportRawDirective in p.s.options:
-        result = dirRaw(p)
-      else:
-        rstMessage(p, meInvalidDirective, d)
-    of dkCode: result = dirCodeBlock(p)
-    of dkCodeBlock: result = dirCodeBlock(p, nimExtension = true)
-    of dkIndex: result = dirIndex(p)
-    else: rstMessage(p, meInvalidDirective, d)
+    result = selectDir(p, d)
     popInd(p)
   elif match(p, p.idx, " _"):
     # hyperlink target:
diff --git a/lib/packages/docutils/rstast.nim b/lib/packages/docutils/rstast.nim
index 5e2d21c04..f01bcada1 100644
--- a/lib/packages/docutils/rstast.nim
+++ b/lib/packages/docutils/rstast.nim
@@ -41,8 +41,11 @@ type
     rnLabel,                  # used for footnotes and other things
     rnFootnote,               # a footnote
     rnCitation,               # similar to footnote
-    rnStandaloneHyperlink, rnHyperlink, rnRef, rnDirective, # a directive
-    rnDirArg, rnRaw, rnTitle, rnContents, rnImage, rnFigure, rnCodeBlock,
+    rnStandaloneHyperlink, rnHyperlink, rnRef,
+    rnDirective,              # a general directive
+    rnDirArg,                 # a directive argument (for some directives).
+                              # here are directives that are not rnDirective:
+    rnRaw, rnTitle, rnContents, rnImage, rnFigure, rnCodeBlock, rnAdmonition,
     rnRawHtml, rnRawLatex,
     rnContainer,              # ``container`` directive
     rnIndex,                  # index directve:
@@ -70,6 +73,7 @@ type
     kind*: RstNodeKind       ## the node's kind
     text*: string             ## valid for leafs in the AST; and the title of
                               ## the document or the section; and rnEnumList
+                              ## and rnAdmonition
     level*: int               ## valid for some node kinds
     sons*: RstNodeSeq        ## the node's sons
 
@@ -316,3 +320,17 @@ proc renderRstToJson*(node: PRstNode): string =
   ##     "sons":optional node array
   ##   }
   renderRstToJsonNode(node).pretty
+
+proc renderRstToStr*(node: PRstNode, indent=0): string =
+  ## Writes the parsed RST `node` into a compact string
+  ## representation in the format (one line per every sub-node):
+  ## ``indent - kind - text - level (if non-zero)``
+  ## (suitable for debugging of RST parsing).
+  if node == nil:
+    result.add " ".repeat(indent) & "[nil]\n"
+    return
+  result.add " ".repeat(indent) & $node.kind & "\t" &
+      (if node.text == "": "" else: "'" & node.text & "'") &
+      (if node.level == 0: "" else: "\tlevel=" & $node.level) & "\n"
+  for son in node.sons:
+    result.add renderRstToStr(son, indent=indent+2)
diff --git a/lib/packages/docutils/rstgen.nim b/lib/packages/docutils/rstgen.nim
index 4d056a83e..5aa2b03d4 100644
--- a/lib/packages/docutils/rstgen.nim
+++ b/lib/packages/docutils/rstgen.nim
@@ -1079,6 +1079,29 @@ proc renderEnumList(d: PDoc, n: PRstNode, result: var string) =
             "\\begin{enumerate}" & specifier & "$1\\end{enumerate}\n",
             result)
 
+proc renderAdmonition(d: PDoc, n: PRstNode, result: var string) =
+  var
+    htmlCls = "admonition_warning"
+    texSz = "\\large"
+    texColor = "orange"
+  case n.text
+  of "hint", "note", "tip":
+    htmlCls = "admonition-info"; texSz = "\\normalsize"; texColor = "green"
+  of "attention", "admonition", "important", "warning":
+    htmlCls = "admonition-warning"; texSz = "\\large"; texColor = "orange"
+  of "danger", "error":
+    htmlCls = "admonition-error"; texSz = "\\Large"; texColor = "red"
+  else: discard
+  let txt = n.text.capitalizeAscii()
+  let htmlHead = "<div class=\"admonition " & htmlCls & "\">"
+  renderAux(d, n,
+      htmlHead & "<span class=\"" & htmlCls & "-text\"><b>" & txt &
+        ":</b></span>\n" & "$1</div>\n",
+      "\n\n\\begin{mdframed}[linecolor=" & texColor & "]\n" &
+        "{" & texSz & "\\color{" & texColor & "}{\\textbf{" & txt & ":}}} " &
+        "$1\n\\end{mdframed}\n",
+      result)
+
 proc renderRstToOut(d: PDoc, n: PRstNode, result: var string) =
   if n == nil: return
   case n.kind
@@ -1143,6 +1166,7 @@ proc renderRstToOut(d: PDoc, n: PRstNode, result: var string) =
   of rnBlockQuote:
     renderAux(d, n, "<blockquote><p>$1</p></blockquote>\n",
                     "\\begin{quote}$1\\end{quote}\n", result)
+  of rnAdmonition: renderAdmonition(d, n, result)
   of rnTable, rnGridTable, rnMarkdownTable:
     renderAux(d, n,
       "<table border=\"1\" class=\"docutils\">$1</table>",
diff --git a/tests/stdlib/trstgen.nim b/tests/stdlib/trstgen.nim
index 85a96056a..3283af8c6 100644
--- a/tests/stdlib/trstgen.nim
+++ b/tests/stdlib/trstgen.nim
@@ -535,6 +535,55 @@ Test1
     assert count(output1, "<ul ") == 1
     assert count(output1, "</ul>") == 1
 
+  test "RST admonitions":
+    # check that all admonitions are implemented
+    let input0 = dedent """
+      .. admonition:: endOf admonition
+      .. attention:: endOf attention
+      .. caution:: endOf caution
+      .. danger:: endOf danger
+      .. error:: endOf error
+      .. hint:: endOf hint
+      .. important:: endOf important
+      .. note:: endOf note
+      .. tip:: endOf tip
+      .. warning:: endOf warning
+    """
+    let output0 = rstToHtml(input0, {roSupportMarkdown}, defaultConfig())
+    for a in ["admonition", "attention", "caution", "danger", "error", "hint",
+        "important", "note", "tip", "warning" ]:
+      assert "endOf " & a & "</div>" in output0
+
+    # Test that admonition does not swallow up the next paragraph.
+    let input1 = dedent """
+      .. error:: endOfError
+
+      Test paragraph.
+    """
+    let output1 = rstToHtml(input1, {roSupportMarkdown}, defaultConfig())
+    assert "endOfError</div>" in output1
+    assert "<p>Test paragraph. </p>" in output1
+    assert "class=\"admonition admonition-error\"" in output1
+
+    # Test that second line is parsed as continuation of the first line.
+    let input2 = dedent """
+      .. error:: endOfError
+        Test2p.
+
+      Test paragraph.
+    """
+    let output2 = rstToHtml(input2, {roSupportMarkdown}, defaultConfig())
+    assert "endOfError Test2p.</div>" in output2
+    assert "<p>Test paragraph. </p>" in output2
+    assert "class=\"admonition admonition-error\"" in output2
+
+    let input3 = dedent """
+      .. note:: endOfNote
+    """
+    let output3 = rstToHtml(input3, {roSupportMarkdown}, defaultConfig())
+    assert "endOfNote</div>" in output3
+    assert "class=\"admonition admonition-info\"" in output3
+
 suite "RST/Code highlight":
   test "Basic Python code highlight":
     let pythonCode = """