lib/markdown2: introduce markdown rendering to LaTeX
authorAlexandre Terrasa <alexandre@moz-code.org>
Tue, 29 May 2018 23:55:58 +0000 (19:55 -0400)
committerAlexandre Terrasa <alexandre@moz-code.org>
Thu, 21 Jun 2018 00:58:45 +0000 (20:58 -0400)
Signed-off-by: Alexandre Terrasa <alexandre@moz-code.org>

lib/markdown2/markdown_latex_rendering.nit [new file with mode: 0644]
lib/markdown2/tests/test_markdown_latex.nit [new file with mode: 0644]

diff --git a/lib/markdown2/markdown_latex_rendering.nit b/lib/markdown2/markdown_latex_rendering.nit
new file mode 100644 (file)
index 0000000..8cd5e33
--- /dev/null
@@ -0,0 +1,404 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# LaTeX rendering of Markdown documents
+module markdown_latex_rendering
+
+import markdown_rendering
+
+# Markdown document renderer to LaTeX
+class LatexRenderer
+       super MdRenderer
+
+       # Generate the LaTeX document wrapper
+       #
+       # The header includes:
+       #  * document class
+       #  * package importation
+       #  * begin and end document directives
+       var wrap_document = false is optional, writable
+
+       # LaTeX document class
+       #
+       # Default is `article`.
+       var document_class = "article" is optional, writable
+
+       # LaTeX document page format
+       #
+       # Default is `letter`.
+       var page_format = "letter" is optional, writable
+
+       # LaTeX font size
+       #
+       # Default is `10pt`.
+       var font_size = "10pt" is optional, writable
+
+       # Use `listings` package for code blocks?
+       var use_listings = false is optional, writable
+
+       # LaTeX output under construction
+       private var latex: Buffer is noinit
+
+       # Render `document` as LaTeX
+       redef fun render(document) do
+               latex = new Buffer
+               enter_visit(document)
+               return latex.write_to_string
+       end
+
+       redef fun visit(node) do node.render_latex(self)
+
+       # Indentation level
+       var indent = 0
+
+       # Add a raw `string` to the output
+       #
+       # Raw means that the string will not be escaped.
+       fun add_raw(string: String) do latex.append string
+
+       # Add `text` string to the output
+       #
+       # The string will be escaped.
+       fun add_text(text: String) do latex.append latex_escape(text)
+
+       # Add a blank line to the output
+       fun add_line do
+               if not latex.is_empty and latex.last != '\n' then
+                       latex.add '\n'
+               end
+       end
+
+       # Add an indentation depending on `ident` level
+       fun add_indent do latex.append " " * indent
+
+       # Escape `string` to LaTeX
+       fun latex_escape(string: String): String do
+               var buffer = new Buffer
+               for i in [0 .. string.length[ do
+                       var c = string.chars[i]
+                       if c == '>' then
+                               buffer.append "\\textgreater"
+                               continue
+                       else if c == '<' then
+                               buffer.append "\\textless"
+                               continue
+                       else if c == '\\' then
+                               buffer.append "\\textbackslash"
+                               continue
+                       else if escaped_chars.has(c) then
+                               buffer.add '\\'
+                       end
+                       buffer.add c
+               end
+               return buffer.to_s
+       end
+
+       # LaTeX characters to escape
+       var escaped_chars = ['%', '$', '{', '}', '_', '#', '&']
+end
+
+redef class MdNode
+
+       # Render `self` as HTML
+       fun render_latex(v: LatexRenderer) do visit_all(v)
+end
+
+# Blocks
+
+redef class MdDocument
+       redef fun render_latex(v) do
+               var wrap_document = v.wrap_document
+               if v.wrap_document then
+                       v.add_line
+                       v.add_raw "\\documentclass[{v.page_format},{v.font_size}]\{{v.document_class}\}\n\n"
+                       v.add_raw "\\usepackage[utf8]\{inputenc\}\n"
+                       if v.use_listings then
+                               v.add_raw "\\usepackage\{listings\}\n"
+                       end
+                       v.add_raw "\\usepackage\{hyperref\}\n"
+                       v.add_raw "\\usepackage\{graphicx\}\n"
+                       v.add_raw "\\usepackage\{ulem\}\n\n"
+                       v.add_raw "\\begin\{document\}\n\n"
+               end
+               var node = first_child
+               while node != null do
+                       v.enter_visit node
+                       node = node.next
+                       if node != null then v.add_raw "\n"
+               end
+               if wrap_document then
+                       v.add_raw "\n\\end\{document\}\n"
+               end
+       end
+end
+
+redef class MdHeading
+       redef fun render_latex(v) do
+               var level = self.level
+               v.add_indent
+               v.add_line
+               if level == 1 then
+                       v.add_raw "\\section\{"
+               else if level == 2 then
+                       v.add_raw "\\subsection\{"
+               else if level == 3 then
+                       v.add_raw "\\subsubsection\{"
+               else if level == 4 then
+                       v.add_raw "\\paragraph\{"
+               else if level == 5 then
+                       v.add_raw "\\subparagraph\{"
+               else
+                       # use bold for level 6 headings
+                       v.add_raw "\\textbf\{"
+               end
+               v.add_indent
+               visit_all(v)
+               v.add_raw "\}"
+               v.add_line
+       end
+end
+
+redef class MdBlockQuote
+       redef fun render_latex(v) do
+               v.add_line
+               v.add_indent
+               v.add_raw "\\begin\{quote\}"
+               v.add_line
+               v.indent += 2
+               visit_all(v)
+               v.indent -= 2
+               v.add_line
+               v.add_indent
+               v.add_raw "\\end\{quote\}"
+               v.add_line
+       end
+end
+
+redef class MdIndentedCodeBlock
+       redef fun render_latex(v) do
+               var directive = if v.use_listings then "lstlisting" else "verbatim"
+               v.add_line
+               v.add_indent
+               v.add_raw "\\begin\{{directive}\}"
+               v.add_line
+               v.add_raw literal or else ""
+               v.add_line
+               v.add_indent
+               v.add_raw "\\end\{{directive}\}"
+               v.add_line
+       end
+end
+
+redef class MdFencedCodeBlock
+       redef fun render_latex(v) do
+               var info = self.info
+               var lstlistings = v.use_listings
+               var directive = if lstlistings then "lstlisting" else "verbatim"
+               v.add_line
+               v.add_indent
+               v.add_raw "\\begin\{{directive}\}"
+               if lstlistings and info != null and not info.is_empty then
+                       v.add_raw "[language={info}]"
+               end
+               v.add_line
+               v.add_raw literal or else ""
+               v.add_line
+               v.add_indent
+               v.add_raw "\\end\{{directive}\}"
+               v.add_line
+       end
+end
+
+redef class MdOrderedList
+       redef fun render_latex(v) do
+               var start = self.start_number
+               v.add_line
+               v.add_indent
+               v.add_raw "\\begin\{enumerate\}"
+               v.indent += 2
+               v.add_line
+               if start != 1 then
+                       v.add_indent
+                       v.add_raw "\\setcounter\{enum{nesting_level}\}\{{start}\}"
+                       v.add_line
+               end
+               visit_all(v)
+               v.indent -= 2
+               v.add_line
+               v.add_indent
+               v.add_raw "\\end\{enumerate\}"
+               v.add_line
+       end
+
+       # Depth of ordered list
+       #
+       # Used to compute the `setcounter` level.
+       fun nesting_level: String do
+               var nesting = 1
+
+               var parent = self.parent
+               while parent != null do
+                       if parent isa MdOrderedList then nesting += 1
+                       parent = parent.parent
+               end
+
+               if nesting <= 3 then
+                       return "i" * nesting
+               end
+               return "iv"
+       end
+end
+
+redef class MdUnorderedList
+       redef fun render_latex(v) do
+               v.add_line
+               v.add_indent
+               v.add_raw "\\begin\{itemize\}"
+               v.add_line
+               v.indent += 2
+               visit_all(v)
+               v.indent -= 2
+               v.add_line
+               v.add_indent
+               v.add_raw "\\end\{itemize\}"
+               v.add_line
+       end
+end
+
+redef class MdListItem
+       redef fun render_latex(v) do
+               v.add_indent
+               v.add_raw "\\item"
+               v.add_line
+               v.indent += 2
+               visit_all(v)
+               v.indent -= 2
+               v.add_line
+       end
+end
+
+redef class MdThematicBreak
+       redef fun render_latex(v) do
+               v.add_line
+               v.add_indent
+               v.add_raw "\\begin\{center\}\\rule\{3in\}\{0.4pt\}\\end\{center\}"
+               v.add_line
+       end
+end
+
+redef class MdParagraph
+       redef fun render_latex(v) do
+               v.add_indent
+               visit_all(v)
+               v.add_line
+       end
+end
+
+
+redef class MdHtmlBlock
+       redef fun render_latex(v) do
+               v.add_line
+               v.add_indent
+               v.add_raw "\\begin\{verbatim\}"
+               v.add_line
+               v.add_indent
+               v.add_raw literal or else ""
+               v.add_line
+               v.add_indent
+               v.add_raw "\\end\{verbatim\}"
+               v.add_line
+       end
+end
+
+# Inlines
+
+redef class MdLineBreak
+       redef fun render_latex(v) do
+               v.add_line
+               v.add_indent
+       end
+end
+
+redef class MdCode
+       redef fun render_latex(v) do
+               v.add_raw "\\texttt\{"
+               v.add_text literal
+               v.add_raw "\}"
+       end
+end
+
+redef class MdEmphasis
+       redef fun render_latex(v) do
+               v.add_raw "\\textit\{"
+               visit_all(v)
+               v.add_raw "\}"
+       end
+end
+
+redef class MdStrongEmphasis
+       redef fun render_latex(v) do
+               v.add_raw "\\textbf\{"
+               visit_all(v)
+               v.add_raw "\}"
+       end
+end
+
+redef class MdHtmlInline
+       redef fun render_latex(v) do
+               v.add_raw "\\texttt\{"
+               v.add_raw v.latex_escape(literal)
+               v.add_raw "\}"
+       end
+end
+
+redef class MdImage
+       redef fun render_latex(v) do
+               v.add_raw "\\includegraphics\{"
+               v.add_text destination
+               v.add_raw "\}"
+       end
+
+       private fun alt_text: String do
+               var v = new RawTextVisitor
+               return v.render(self)
+       end
+end
+
+redef class MdLink
+       redef fun render_latex(v) do
+               if is_autolink then
+                       v.add_raw "\\url\{"
+                       v.add_text destination
+                       v.add_raw "\}"
+                       return
+               end
+               var title = self.title
+               v.add_raw "\\href\{"
+               v.add_text destination
+               v.add_raw "\}\{"
+               visit_all(v)
+               if title != null and not title.is_empty then
+                       v.add_raw " ("
+                       v.add_text title
+                       v.add_raw ")"
+               end
+               v.add_raw "\}"
+       end
+end
+
+redef class MdText
+       redef fun render_latex(v) do
+               v.add_text literal
+       end
+end
diff --git a/lib/markdown2/tests/test_markdown_latex.nit b/lib/markdown2/tests/test_markdown_latex.nit
new file mode 100644 (file)
index 0000000..ec85616
--- /dev/null
@@ -0,0 +1,542 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Tests for markdown rendering to LaTeX
+module test_markdown_latex is test
+
+import test_markdown
+import markdown_latex_rendering
+
+# Abstract test class that defines the test methods for LaTeX rendering
+abstract class TestMarkdownLatex
+       super TestMarkdown
+
+       # LaTeX renderer used in tests
+       var tex_renderer = new LatexRenderer
+
+       # Render the `md` string as LaTeX format
+       fun md_to_tex(md: String): String do
+               var node = parse_md(md)
+               return tex_renderer.render(node)
+       end
+end
+
+class TestLatexRendering
+       super TestMarkdownLatex
+       test
+
+       fun after_test is after do
+               tex_renderer.wrap_document = false
+               tex_renderer.use_listings = false
+       end
+
+       fun test_document_wrapper is test do
+               var md = """
+This example needs a document wrapper.
+"""
+
+               var tex = """
+\\documentclass[letter,10pt]{article}
+
+\\usepackage[utf8]{inputenc}
+\\usepackage{hyperref}
+\\usepackage{graphicx}
+\\usepackage{ulem}
+
+\\begin{document}
+
+This example needs a document wrapper.
+
+\\end{document}
+"""
+               tex_renderer.wrap_document = true
+               assert md_to_tex(md) == tex
+       end
+
+       fun test_atx_headings is test do
+               var md = """
+# Title 1
+## Title 2
+### Title 3
+#### Title 4
+##### Title 5
+###### Title 6
+"""
+               var tex = """
+\\section{Title 1}
+
+\\subsection{Title 2}
+
+\\subsubsection{Title 3}
+
+\\paragraph{Title 4}
+
+\\subparagraph{Title 5}
+
+\\textbf{Title 6}
+"""
+               assert md_to_tex(md) == tex
+       end
+
+       fun test_blockquotes is test do
+               var md = """
+> this is a
+> multiline quote
+"""
+               var tex = """
+\\begin{quote}
+  this is a
+  multiline quote
+\\end{quote}
+"""
+               assert md_to_tex(md) == tex
+       end
+
+       fun test_thematic_breaks is test do
+               var md = """
+* * *
+"""
+               var tex = """
+\\begin{center}\\rule{3in}{0.4pt}\\end{center}
+"""
+               assert md_to_tex(md) == tex
+       end
+
+       fun test_paragraphs is test do
+               var md = """
+a paragraph
+on two lines
+
+another paragraph
+"""
+               var tex = """
+a paragraph
+on two lines
+
+another paragraph
+"""
+               assert md_to_tex(md) == tex
+       end
+
+       fun test_html_block is test do
+               var md = """
+<p>
+       <a href="url">foo</a>
+</p>
+               """
+               var tex = """
+\\begin{verbatim}
+<p>
+       <a href="url">foo</a>
+</p>
+\\end{verbatim}
+"""
+               assert md_to_tex(md) == tex
+       end
+
+       fun test_indented_code is test do
+               var md = """
+    first line
+    second line
+"""
+               var tex = """
+\\begin{verbatim}
+first line
+second line
+\\end{verbatim}
+"""
+               assert md_to_tex(md) == tex
+       end
+
+       fun test_indented_code_with_listings is test do
+               var md = """
+    first line
+    second line
+"""
+               var tex = """
+\\begin{lstlisting}
+first line
+second line
+\\end{lstlisting}
+"""
+               tex_renderer.use_listings = true
+               assert md_to_tex(md) == tex
+       end
+
+       fun test_fenced_code is test do
+               var md = """
+~~~
+first line
+second line
+~~~
+"""
+               var tex = """
+\\begin{verbatim}
+first line
+second line
+\\end{verbatim}
+"""
+               assert md_to_tex(md) == tex
+       end
+
+       fun test_fenced_code_with_listings is test do
+               var md = """
+~~~
+first line
+second line
+~~~
+"""
+               var tex = """
+\\begin{lstlisting}
+first line
+second line
+\\end{lstlisting}
+"""
+               tex_renderer.use_listings = true
+               assert md_to_tex(md) == tex
+       end
+
+       fun test_fenced_code_with_listings_and_language is test do
+               var md = """
+~~~c
+first line
+second line
+~~~
+"""
+               var tex = """
+\\begin{lstlisting}[language=c]
+first line
+second line
+\\end{lstlisting}
+"""
+               tex_renderer.use_listings = true
+               assert md_to_tex(md) == tex
+       end
+
+       fun test_list_ordered is test do
+               var md = """
+1) item 1
+2) item 2
+"""
+               var tex = """
+\\begin{enumerate}
+  \\item
+    item 1
+  \\item
+    item 2
+\\end{enumerate}
+"""
+               assert md_to_tex(md) == tex
+       end
+
+       fun test_list_ordered_with_starting_number is test do
+               var md = """
+4) item 1
+5) item 2
+"""
+               var tex = """
+\\begin{enumerate}
+  \\setcounter{enumi}{4}
+  \\item
+    item 1
+  \\item
+    item 2
+\\end{enumerate}
+"""
+               assert md_to_tex(md) == tex
+       end
+
+       fun test_list_unordered is test do
+               var md = """
+* item 1
+* item 2
+"""
+               var tex = """
+\\begin{itemize}
+  \\item
+    item 1
+  \\item
+    item 2
+\\end{itemize}
+"""
+               assert md_to_tex(md) == tex
+       end
+
+       fun test_list_nested is test do
+               var md = """
+* item 1
+* item 2
+   1) item 3
+   2) item 4
+"""
+               var tex = """
+\\begin{itemize}
+  \\item
+    item 1
+  \\item
+    item 2
+    \\begin{enumerate}
+      \\item
+        item 3
+      \\item
+        item 4
+    \\end{enumerate}
+\\end{itemize}
+"""
+               assert md_to_tex(md) == tex
+       end
+
+       fun test_ordered_list_nested is test do
+               var md = """
+4) item 1
+5) item 2
+
+   4) item 3
+   5) item 4
+
+      4) item 3
+      5) item 4
+
+         4) item 3
+         5) item 4
+"""
+               var tex = """
+\\begin{enumerate}
+  \\setcounter{enumi}{4}
+  \\item
+    item 1
+  \\item
+    item 2
+    \\begin{enumerate}
+      \\setcounter{enumii}{4}
+      \\item
+        item 3
+      \\item
+        item 4
+        \\begin{enumerate}
+          \\setcounter{enumiii}{4}
+          \\item
+            item 3
+          \\item
+            item 4
+            \\begin{enumerate}
+              \\setcounter{enumiv}{4}
+              \\item
+                item 3
+              \\item
+                item 4
+            \\end{enumerate}
+        \\end{enumerate}
+    \\end{enumerate}
+\\end{enumerate}
+"""
+               assert md_to_tex(md) == tex
+       end
+
+       fun test_list_and_blockquote is test do
+               var md = """
+* item 1
+* item 2
+   > quote 1
+   > quote 2
+"""
+               var tex = """
+\\begin{itemize}
+  \\item
+    item 1
+  \\item
+    item 2
+    \\begin{quote}
+      quote 1
+      quote 2
+    \\end{quote}
+\\end{itemize}
+"""
+               assert md_to_tex(md) == tex
+       end
+
+       fun test_blockquote_and_list is test do
+               var md = """
+> line 1
+> line 2
+> * item 1
+> * item 2
+"""
+               var tex = """
+\\begin{quote}
+  line 1
+  line 2
+  \\begin{itemize}
+    \\item
+      item 1
+    \\item
+      item 2
+  \\end{itemize}
+\\end{quote}
+"""
+               assert md_to_tex(md) == tex
+       end
+
+       fun test_code is test do
+               var md = """
+An `inline code`.
+"""
+               var tex = """
+An \\texttt{inline code}.
+"""
+               assert md_to_tex(md) == tex
+       end
+
+       fun test_emphasis is test do
+               var md = """
+An *emphasis* and a **strong emphasis**.
+"""
+               var tex = """
+An \\textit{emphasis} and a \\textbf{strong emphasis}.
+"""
+               assert md_to_tex(md) == tex
+       end
+
+       fun test_autolink is test do
+               var md = """
+<http://test>
+"""
+               var tex = """
+\\url{http://test}
+"""
+               assert md_to_tex(md) == tex
+       end
+
+       fun test_link is test do
+               var md = """
+A [link](url/).
+"""
+               var tex = """
+A \\href{url/}{link}.
+"""
+               assert md_to_tex(md) == tex
+       end
+
+       fun test_link_with_title is test do
+               var md = """
+A [link](url/ "with a title").
+"""
+               var tex = """
+A \\href{url/}{link (with a title)}.
+"""
+               assert md_to_tex(md) == tex
+       end
+
+       fun test_image is test do
+               var md = """
+![image](url/).
+"""
+               var tex = """
+\\includegraphics{url/}.
+"""
+               assert md_to_tex(md) == tex
+       end
+
+       fun test_softbreak is test do
+               var md = """
+A soft
+break.
+"""
+               var tex = """
+A soft
+break.
+"""
+               assert md_to_tex(md) == tex
+       end
+
+       fun test_hardbreak is test do
+               var md = """
+A hard\\
+break.
+"""
+               var tex = """
+A hard
+break.
+"""
+               assert md_to_tex(md) == tex
+       end
+
+       fun test_escaped is test do
+               var md = """
+An escaped \\*.
+"""
+               var tex = """
+An escaped *.
+"""
+               assert md_to_tex(md) == tex
+       end
+
+       fun test_forbidden_chars is test do
+               var md = """
+%${_><#&}\\
+"""
+               var tex = """
+\\%\\$\\{\\_\\textgreater\\textless\\#\\&\\}\\textbackslash
+"""
+               assert md_to_tex(md) == tex
+       end
+
+       fun test_full_document is test do
+               var md = """
+# Title
+
+A paragraph.
+
+## Another title
+
+A list:
+
+1. item 1
+2. item 2
+
+A code example:
+
+    line 1
+       line 2
+
+Another paragraph.
+"""
+               var tex = """
+\\section{Title}
+
+A paragraph.
+
+\\subsection{Another title}
+
+A list:
+
+\\begin{enumerate}
+  \\item
+    item 1
+  \\item
+    item 2
+\\end{enumerate}
+
+A code example:
+
+\\begin{verbatim}
+line 1
+line 2
+\\end{verbatim}
+
+Another paragraph.
+"""
+               assert md_to_tex(md) == tex
+       end
+end