From: Alexandre Terrasa Date: Tue, 29 May 2018 23:55:58 +0000 (-0400) Subject: lib/markdown2: introduce markdown rendering to LaTeX X-Git-Url: http://nitlanguage.org?hp=4f37b72ded1564f1b549279d0c586805d48e1442 lib/markdown2: introduce markdown rendering to LaTeX Signed-off-by: Alexandre Terrasa --- diff --git a/lib/markdown2/markdown_latex_rendering.nit b/lib/markdown2/markdown_latex_rendering.nit new file mode 100644 index 0000000..8cd5e33 --- /dev/null +++ b/lib/markdown2/markdown_latex_rendering.nit @@ -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 index 0000000..ec85616 --- /dev/null +++ b/lib/markdown2/tests/test_markdown_latex.nit @@ -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 = """ +

+ foo +

+ """ + var tex = """ +\\begin{verbatim} +

+ foo +

+\\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 = """ + +""" + 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