Markdown to pdf renderer
"""Markdown to pdf converter baset on markdown_it and fitz."""
import io
from markdown_it import MarkdownIt
import fitz


class Section:
    """Markdown section."""

    def __init__(self, text, toc=True, root=".", paper_size="A4", borders=(36, 36, -36, -36)):
        """Create md section with given properties."""
        self.text = text
        self.toc = toc
        self.root = root
        # https://pymupdf.readthedocs.io/en/latest/functions.html#paper_size
        self.paper_size = paper_size
        self.borders = borders


class MarkdownPdf:
    """Converter class."""

    meta = {
      "creationDate": fitz.get_pdf_now(),
      "modDate": fitz.get_pdf_now(),
      "creator": "PyMuPDF library: https://pypi.org/project/PyMuPDF",
      "producer": None,
      "title": None,
      "author": None,
      "subject": None,
      "keywords": None,
    }

    def __init__(self, toc_level=6, mode='commonmark'):
        """Create md -> pdf converter with given TOC level and mode of md parsing."""
        self.toc_level = toc_level
        self.toc = []

        # zero, commonmark, js-default, gfm-like
        # https://markdown-it-py.readthedocs.io/en/latest/using.html#quick-start
        self.m_d = (MarkdownIt(mode))

        self.out_file = io.BytesIO()
        self.writer = fitz.DocumentWriter(self.out_file)
        self.page = 0

    @staticmethod
    def recorder(elpos):
        """Call function invoked during story.place() for making a TOC."""
        if not elpos.open_close & 1:  # only consider "open" items
            return
        if not elpos.toc:
            return

        if 0 < elpos.heading <= elpos.pdfile.toc_level:  # this is a header (h1 - h6)
            elpos.pdfile.toc.append((
                elpos.heading,
                elpos.text,
                elpos.pdfile.page,
                elpos.rect[1],  # top of written rectangle (use for TOC)
            ))

    def add_section(self, section):
        """Add markdown section to pdf."""
        rect = fitz.paper_rect(section.paper_size)
        where = rect + section.borders
        story = fitz.Story(html=self.m_d.render(section.text), archive=section.root)
        more = 1
        while more:  # loop outputting the story
            self.page += 1
            device = self.writer.begin_page(rect)
            more, _ = story.place(where)  # layout into allowed rectangle
            story.element_positions(self.recorder, {"toc": section.toc, "pdfile": self})
            story.draw(device)
            self.writer.end_page()

    def save(self, file_name):
        """Save pdf to file."""
        self.writer.close()
        doc = fitz.open("pdf", self.out_file)
        doc.set_metadata(self.meta)
        if self.toc_level > 0:
            doc.set_toc(self.toc)
        doc.save(file_name)
        doc.close()

markdown_pdf/__init__.py
vb64/markdown-pdf
"""Root class for testing."""
import os
from unittest import TestCase


class TestBase(TestCase):
    """Base class for tests."""

    @staticmethod
    def fixture(*file_name):
        """Return path to fixture file."""
        return os.path.join('fixture', *file_name)

    @staticmethod
    def build(file_name):
        """Return path to file in 'build' folder."""
        return os.path.join('build', file_name)

tests/test/__init__.py
vb64/markdown-pdf
"""Package markdown_pdf tests.

make test T=test_converter.py
"""
from . import TestBase


class TestConverter(TestBase):
    """Converter markdown_pdf."""

    def test_with_toc(self):
        """Convert md to pdf."""
        from markdown_pdf import Section, MarkdownPdf

        pdf = MarkdownPdf(toc_level=2)
        pdf.add_section(Section("# Title\n", toc=False))
        pdf.add_section(Section("# Head1\n\nbody\n"))
        pdf.add_section(Section("## Head2\n\n### Head3\n\n"))
        pdf.save(self.build("with_toc.pdf"))

    def test_no_toc(self):
        """Convert md to pdf."""
        from markdown_pdf import Section, MarkdownPdf

        pdf = MarkdownPdf(toc_level=0)
        pdf.add_section(Section("# Title\n"))
        pdf.save(self.build("no_toc.pdf"))

tests/test/test_converter.py
vb64/markdown-pdf