git @ Cat's Eye Technologies Cleandown / master src / marko / ext / latex_renderer.py
master

Tree @master (Download .tar.gz)

latex_renderer.py @masterraw · history · blame

# Copyright (c) 2019 Frost Ming
#
# SPDX-License-Identifier: LicenseRef-MIT-X-Marko

"""
LaTeX renderer
"""

from __future__ import annotations

import logging
from typing import Iterable

from marko.helpers import MarkoExtension
from marko.renderer import Renderer

_logger = logging.getLogger(__name__)


class LatexRendererMixin:
    """Render the parsed Markdown to LaTeX format."""

    _packages: set[str]

    def __init__(self):
        super().__init__()
        self._packages = set()

    def __enter__(self):
        self._packages_back = self._packages.copy()
        return super().__enter__()

    def __exit__(self, *args):
        self._packages = self._packages_back
        super().__exit__(*args)

    def render_document(self, element):
        # should come first to collect needed packages
        children = self.render_children(element)
        # create document parts
        items = ["\\documentclass{article}"]
        # add used packages
        items.extend(f"\\usepackage{{{p}}}" for p in self._packages)
        # add inner content
        items.append(self._environment("document", children))
        return "\n".join(items)

    def render_paragraph(self, element):
        children = self.render_children(element)
        return children if element._tight else f"{children}\n"

    def render_blank_line(self, element):
        return "\n"

    def render_line_break(self, element):
        return "\n" if element.soft else "\\\\\n"

    def render_list(self, element):
        children = self.render_children(element)
        env = "enumerate" if element.ordered else "itemize"
        # TODO: check how to handle element.start with ordered list
        if element.start and element.start != 1:
            _logger.warning("Setting the starting number of the list is not supported!")
        return self._environment(env, children)

    def render_list_item(self, element):
        children = self.render_children(element)
        return f"\\item {children}\n"

    def render_quote(self, element):
        self._packages.add("csquotes")
        children = self.render_children(element)
        return self._environment("displayquote", children)

    def render_fenced_code(self, element):
        self._packages.add("listings")
        language = self._escape_latex(element.lang)
        return self._environment(
            "lstlisting", element.children[0].children, [f"language={language}"]
        )

    def render_code_block(self, element):
        return self._environment("verbatim", element.children[0].children)

    def render_thematic_break(self, element):
        return "\\noindent\\rule{\\textwidth}{1pt}\n"

    def render_heading(self, element):
        children = self.render_children(element)
        headers = [
            "part",
            "section",
            "subsection",
            "subsubsection",
            "paragraph",
            "subparagraph",
        ]
        header = headers[element.level - 1] + "*"
        return f"\\{header}{{{children}}}\n"

    def render_setext_heading(self, element):
        return self.render_heading(element)

    def render_emphasis(self, element):
        children = self.render_children(element)
        return f"\\textit{{{children}}}"

    def render_strong_emphasis(self, element):
        children = self.render_children(element)
        return f"\\textbf{{{children}}}"

    def render_code_span(self, element):
        children = self._escape_latex(element.children)
        return f"\\texttt{{{children}}}"

    def render_link(self, element):
        if element.title:
            _logger.warning("Setting a title for links is not supported!")
        body = self.render_children(element)
        return f"\\href{{{element.dest}}}{{{body}}}"

    def render_auto_link(self, element):
        return f"\\url{{{element.dest}}}"

    def render_link_ref_def(self, element):
        return ""

    def render_image(self, element):
        self._packages.add("graphicx")
        # TODO: check how alt (element.children) and element.title could be used!
        return f"\\includegraphics{{{element.dest}}}"

    def render_html_block(self, element):
        _logger.warning("Rendering HTML is not supported!")
        return ""

    def render_inline_html(self, element):
        _logger.warning("Rendering HTML is not supported!")
        return ""

    def render_literal(self, element):
        return self.render_raw_text(element)

    def render_raw_text(self, element):
        return self._escape_latex(element.children)

    @staticmethod
    def _escape_latex(text: str) -> str:
        # Special LaTeX Character:  # $ % ^ & _ { } ~ \
        specials = {
            "#": "\\#",
            "$": "\\$",
            "%": "\\%",
            "&": "\\&",
            "_": "\\_",
            "{": "\\{",
            "}": "\\}",
            "^": "\\^{}",
            "~": "\\~{}",
            "\\": "\\textbackslash{}",
        }

        return "".join(specials.get(s, s) for s in text)

    @staticmethod
    def _environment(env_name: str, content: str, options: Iterable[str] = ()) -> str:
        options_str = f"[{','.join(options)}]" if options else ""
        return f"\\begin{{{env_name}}}{options_str}\n{content}\\end{{{env_name}}}\n"


class LatexRenderer(LatexRendererMixin, Renderer):
    """Render the parsed Markdown to LaTeX format."""


def make_extension():
    return MarkoExtension(renderer_mixins=[LatexRendererMixin])