mdmint 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mdmint/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """mdmint — export Markdown files to DOCX and/or PDF.
2
+
3
+ Pure-Python, cross-platform, minimal dependencies. The two features that cannot
4
+ be rendered statically in pure Python — mermaid diagrams and LaTeX math — use an
5
+ optional renderer if present and fall back gracefully (with a warning) otherwise.
6
+
7
+ Public API
8
+ ----------
9
+ from mdmint import convert, MarkdownConverter
10
+
11
+ convert(["doc.md"], formats=["pdf", "docx"], output="out/")
12
+ convert_mkdocs("mkdocs.yml", formats=["docx"], template="brand.docx")
13
+ """
14
+
15
+ from .core import MarkdownConverter, convert, convert_mkdocs
16
+
17
+ __version__ = "0.2.0"
18
+ __all__ = ["MarkdownConverter", "convert", "convert_mkdocs", "__version__"]
mdmint/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Enable ``python -m mdmint``."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
mdmint/admonitions.py ADDED
@@ -0,0 +1,70 @@
1
+ """Shared colour table for Markdown admonitions (``!!!`` / ``???`` callouts).
2
+
3
+ Both writers need to agree on what a "warning" looks like, but they style it
4
+ through completely different mechanisms: :mod:`mdmint.render` emits inline CSS that
5
+ the PDF backend honours, while :mod:`mdmint.backends` paints the DOCX cell with
6
+ low-level OOXML after the fact. Keeping the palette here means the two paths can
7
+ never drift, and adding a type is a one-line change.
8
+
9
+ Type names follow python-markdown / mkdocs-Material conventions; unknown tags
10
+ degrade to the neutral ``note`` styling rather than raising.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ # Canonical type -> (accent colour, light background tint), both ``#rrggbb``.
16
+ _CANONICAL: dict[str, tuple[str, str]] = {
17
+ "note": ("#448aff", "#eaf1ff"),
18
+ "abstract": ("#00b0ff", "#e5f6ff"),
19
+ "info": ("#00b8d4", "#e3f8fc"),
20
+ "tip": ("#00bfa5", "#e3f8f4"),
21
+ "success": ("#00c853", "#e6f9ec"),
22
+ "question": ("#64dd17", "#eef9e3"),
23
+ "warning": ("#ff9100", "#fff3e0"),
24
+ "failure": ("#ff5252", "#ffecec"),
25
+ "danger": ("#ff1744", "#ffe9ed"),
26
+ "bug": ("#f50057", "#ffe6ef"),
27
+ "example": ("#7c4dff", "#f0ebff"),
28
+ "quote": ("#9e9e9e", "#f5f5f5"),
29
+ }
30
+
31
+ # Extra spellings mkdocs/python-markdown accept, mapped to a canonical key.
32
+ _ALIASES: dict[str, str] = {
33
+ "summary": "abstract",
34
+ "tldr": "abstract",
35
+ "hint": "tip",
36
+ "important": "tip",
37
+ "check": "success",
38
+ "done": "success",
39
+ "help": "question",
40
+ "faq": "question",
41
+ "caution": "warning",
42
+ "attention": "warning",
43
+ "fail": "failure",
44
+ "missing": "failure",
45
+ "error": "danger",
46
+ "cite": "quote",
47
+ }
48
+
49
+ DEFAULT = "note"
50
+
51
+
52
+ def resolve(tag: str) -> str:
53
+ """Map any admonition tag (or alias) to a canonical key, defaulting to note."""
54
+ tag = (tag or "").strip().lower()
55
+ if tag in _CANONICAL:
56
+ return tag
57
+ return _ALIASES.get(tag, DEFAULT)
58
+
59
+
60
+ def style_for(tag: str) -> tuple[str, str]:
61
+ """Return ``(accent, background)`` hex colours for an admonition tag."""
62
+ return _CANONICAL[resolve(tag)]
63
+
64
+
65
+ # Accent colour (upper-case, no ``#``) -> canonical type. The DOCX post-pass uses
66
+ # this to recognise an admonition table by its title colour, which is the only
67
+ # marker that survives htmldocx's class-stripping conversion.
68
+ ACCENTS: dict[str, str] = {
69
+ accent.lstrip("#").upper(): key for key, (accent, _bg) in _CANONICAL.items()
70
+ }
mdmint/assets.py ADDED
@@ -0,0 +1,84 @@
1
+ """Image asset resolution.
2
+
3
+ After a Markdown body is rendered to HTML, every ``<img src>`` is rewritten to an
4
+ absolute local filesystem path so that both writers (htmldocx and the PDF backends)
5
+ resolve images identically:
6
+
7
+ * relative paths → resolved against the source ``.md`` file's directory
8
+ * ``http(s)`` URLs → downloaded into the work directory (offline-safe thereafter)
9
+ * absolute paths → kept (these include mermaid/math images we generated)
10
+ * ``data:`` URIs → left untouched
11
+
12
+ Anything that cannot be resolved is left as-is with a warning rather than aborting.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import hashlib
18
+ import logging
19
+ import os
20
+ import re
21
+ import urllib.request
22
+ from pathlib import Path
23
+
24
+ log = logging.getLogger("mdmint")
25
+
26
+ # Matches the src attribute of an <img> tag, capturing the quote and the value.
27
+ _IMG_SRC = re.compile(r'(<img\b[^>]*?\ssrc=)(["\'])(.*?)\2', re.IGNORECASE)
28
+
29
+ _EXT_BY_TYPE = {
30
+ "image/png": ".png",
31
+ "image/jpeg": ".jpg",
32
+ "image/gif": ".gif",
33
+ "image/svg+xml": ".svg",
34
+ "image/webp": ".webp",
35
+ }
36
+
37
+
38
+ def _download(url: str, work_dir: Path) -> str | None:
39
+ digest = hashlib.sha1(url.encode("utf-8"), usedforsecurity=False).hexdigest()[:16]
40
+ try:
41
+ req = urllib.request.Request(url, headers={"User-Agent": "mdmint"}) # noqa: S310
42
+ with urllib.request.urlopen(req, timeout=30) as resp: # noqa: S310 (user-provided URL)
43
+ data = resp.read()
44
+ ctype = resp.headers.get_content_type()
45
+ except Exception as exc:
46
+ log.warning("image: could not download %s (%s); leaving as-is.", url, exc)
47
+ return None
48
+
49
+ ext = _EXT_BY_TYPE.get(ctype) or os.path.splitext(url.split("?")[0])[1] or ".img"
50
+ out_path = work_dir / f"remote-{digest}{ext}"
51
+ out_path.write_bytes(data)
52
+ return str(out_path)
53
+
54
+
55
+ def _resolve_one(src: str, base_dir: Path, work_dir: Path) -> str:
56
+ low = src.lower()
57
+ if low.startswith("data:"):
58
+ return src
59
+ if low.startswith(("http://", "https://")):
60
+ local = _download(src, work_dir)
61
+ return local if local else src
62
+ if low.startswith("file://"):
63
+ return urllib.request.url2pathname(src[7:])
64
+
65
+ # Local path: absolute as-is, relative against the markdown file's directory.
66
+ path = Path(src)
67
+ if not path.is_absolute():
68
+ path = base_dir / path
69
+ abspath = os.path.abspath(path)
70
+ if not os.path.exists(abspath):
71
+ log.warning("image: file not found: %s (from src %r)", abspath, src)
72
+ return abspath
73
+
74
+
75
+ def resolve_images(html_body: str, base_dir: Path, work_dir: Path) -> str:
76
+ """Rewrite every ``<img src>`` in *html_body* to an absolute local path."""
77
+
78
+ def repl(m: re.Match[str]) -> str:
79
+ prefix, quote, src = m.group(1), m.group(2), m.group(3)
80
+ resolved = _resolve_one(src.strip(), base_dir, work_dir)
81
+ # Keep the original quote style; the path itself contains no quote chars.
82
+ return f"{prefix}{quote}{resolved}{quote}"
83
+
84
+ return _IMG_SRC.sub(repl, html_body)
mdmint/backends.py ADDED
@@ -0,0 +1,244 @@
1
+ """Output writers: HTML → DOCX and HTML → PDF.
2
+
3
+ * DOCX uses ``htmldocx`` (built on ``python-docx``) — pure Python, no native deps.
4
+ * PDF uses ``xhtml2pdf`` by default (pure Python) or ``weasyprint`` if explicitly
5
+ requested (higher fidelity, but needs native Pango/Cairo libraries).
6
+
7
+ Both PDF backends are imported lazily so a missing optional backend never affects
8
+ the default path.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import re
15
+ from importlib.resources import files as _res_files
16
+ from pathlib import Path
17
+ from typing import TYPE_CHECKING
18
+
19
+ from . import admonitions
20
+
21
+ if TYPE_CHECKING:
22
+ from docx.document import Document as DocxDocument
23
+ from docx.oxml.table import CT_TcPr
24
+
25
+ _IMG_SRC = re.compile(r'(<img\b[^>]*?\ssrc=)(["\'])(.*?)\2', re.IGNORECASE)
26
+
27
+
28
+ def load_css() -> str:
29
+ """Return the bundled default stylesheet."""
30
+ return _res_files("mdmint.resources").joinpath("default.css").read_text(encoding="utf-8")
31
+
32
+
33
+ def wrap_html(body: str, title: str = "", css: str | None = None) -> str:
34
+ """Wrap an HTML body fragment in a complete, styled HTML document."""
35
+ if css is None:
36
+ css = load_css()
37
+ safe_title = (title or "Document").replace("<", "&lt;").replace(">", "&gt;")
38
+ return (
39
+ "<!DOCTYPE html>\n<html><head>"
40
+ '<meta charset="utf-8" />'
41
+ f"<title>{safe_title}</title>"
42
+ f"<style>{css}</style>"
43
+ f"</head><body>{body}</body></html>"
44
+ )
45
+
46
+
47
+ # --------------------------------------------------------------------------- DOCX
48
+
49
+
50
+ def _fit_images_to_page(document: DocxDocument) -> None:
51
+ """Scale down any inline image larger than the page's text area.
52
+
53
+ ``htmldocx`` adds pictures at their native size, and python-docx assumes 72 DPI
54
+ for images without DPI metadata (mermaid/math PNGs), so wide diagrams overflow
55
+ the margins. We shrink anything bigger than the printable area to fit, keeping
56
+ aspect ratio and never upscaling — the DOCX counterpart of CSS ``max-width:100%``.
57
+ """
58
+ s = document.sections[0]
59
+ max_w = (s.page_width or 0) - (s.left_margin or 0) - (s.right_margin or 0)
60
+ max_h = (s.page_height or 0) - (s.top_margin or 0) - (s.bottom_margin or 0)
61
+ if max_w <= 0 or max_h <= 0:
62
+ return
63
+ for shape in document.inline_shapes:
64
+ w, h = shape.width, shape.height
65
+ if not w or not h:
66
+ continue
67
+ scale = min(1.0, max_w / w, max_h / h)
68
+ if scale < 1.0:
69
+ shape.width = int(w * scale)
70
+ shape.height = int(h * scale)
71
+
72
+
73
+ def _set_cell_shading(tcPr: CT_TcPr, fill: str) -> None:
74
+ from docx.oxml import OxmlElement
75
+ from docx.oxml.ns import qn
76
+
77
+ shd = OxmlElement("w:shd")
78
+ shd.set(qn("w:val"), "clear")
79
+ shd.set(qn("w:color"), "auto")
80
+ shd.set(qn("w:fill"), fill)
81
+ tcPr.append(shd)
82
+
83
+
84
+ def _set_cell_borders(tcPr: CT_TcPr, accent: str) -> None:
85
+ """A thick accent rule on the left, hairline grey on the other three sides."""
86
+ from docx.oxml import OxmlElement
87
+ from docx.oxml.ns import qn
88
+
89
+ borders = OxmlElement("w:tcBorders")
90
+ edges = {
91
+ "left": ("24", accent), # 24 eighths = 3pt accent bar
92
+ "top": ("4", "DDDDDD"),
93
+ "bottom": ("4", "DDDDDD"),
94
+ "right": ("4", "DDDDDD"),
95
+ }
96
+ for edge, (size, color) in edges.items():
97
+ e = OxmlElement(f"w:{edge}")
98
+ e.set(qn("w:val"), "single")
99
+ e.set(qn("w:sz"), size)
100
+ e.set(qn("w:space"), "0")
101
+ e.set(qn("w:color"), color)
102
+ borders.append(e)
103
+ tcPr.append(borders)
104
+
105
+
106
+ def _style_admonitions(document: DocxDocument) -> None:
107
+ """Paint admonition callouts in a freshly built DOCX.
108
+
109
+ htmldocx renders our admonitions as bare single-cell tables and discards the
110
+ CSS that colours them in PDF. We recognise them after the fact — a 1x1 table
111
+ whose title run carries one of our accent colours (see :mod:`mdmint.admonitions`)
112
+ — and add the matching cell shading and left accent bar. This is the DOCX twin
113
+ of the inline CSS emitted by ``render._admonition_open``; anything that is not
114
+ an admonition (real user tables) is left untouched.
115
+ """
116
+ for table in document.tables:
117
+ if len(table.rows) != 1 or len(table.columns) != 1:
118
+ continue
119
+ cell = table.cell(0, 0)
120
+ title = next((p for p in cell.paragraphs if p.text.strip()), None)
121
+ run = title and next((r for r in title.runs if r.text.strip()), None)
122
+ color = run and run.font.color
123
+ if not color or color.rgb is None:
124
+ continue
125
+ accent = str(color.rgb).upper()
126
+ if accent not in admonitions.ACCENTS:
127
+ continue
128
+ _accent, bg = admonitions.style_for(admonitions.ACCENTS[accent])
129
+ tcPr = cell._tc.get_or_add_tcPr()
130
+ _set_cell_shading(tcPr, bg.lstrip("#").upper())
131
+ _set_cell_borders(tcPr, accent)
132
+
133
+
134
+ def _flatten_loose_list_items(body: str) -> str:
135
+ """Unwrap the leading ``<p>`` inside each ``<li>`` for the DOCX path.
136
+
137
+ markdown-it wraps list-item content in ``<p>`` whenever the list is *loose*
138
+ (items separated by blank lines or spanning several lines). htmldocx turns
139
+ such an ``<li>`` into two paragraphs — an empty ``List Number``/``List Bullet``
140
+ paragraph carrying the marker, then a separate ``Normal`` paragraph with the
141
+ text — so the number ends up on its own line. PDF is unaffected (CSS keeps the
142
+ ``<p>`` inline), so this runs only here. Dropping the leading ``<p>`` wrapper
143
+ puts the first line back beside its marker; any *further* paragraphs in a
144
+ multi-block item are left as continuation paragraphs.
145
+ """
146
+ from bs4 import BeautifulSoup
147
+
148
+ soup = BeautifulSoup(body, "html.parser")
149
+ for li in soup.find_all("li"):
150
+ first_p = li.find("p", recursive=False)
151
+ if first_p is not None:
152
+ first_p.unwrap()
153
+ return str(soup)
154
+
155
+
156
+ def to_docx(
157
+ body: str,
158
+ out_path: str | os.PathLike[str],
159
+ *,
160
+ template: str | os.PathLike[str] | None = None,
161
+ meta: dict[str, str] | None = None,
162
+ styles: dict[str, str] | None = None,
163
+ ) -> None:
164
+ """Write *body* (an HTML fragment) to a .docx file.
165
+
166
+ With a *template* (.docx/.dotx) the template-aware writer in
167
+ :mod:`mdmint.docxtemplate` is used instead of htmldocx, so the output carries the
168
+ template's cover, headers/footers and named styles (*meta* fills its
169
+ ``{{PLACEHOLDERS}}``, *styles* overrides role → style-name choices).
170
+ """
171
+ if template is not None:
172
+ from . import docxtemplate
173
+
174
+ docxtemplate.to_docx(body, out_path, template, meta=meta, styles=styles)
175
+ return
176
+
177
+ from docx import Document
178
+ from htmldocx import HtmlToDocx
179
+
180
+ document = Document()
181
+ parser = HtmlToDocx()
182
+ parser.add_html_to_document(_flatten_loose_list_items(body), document)
183
+ _fit_images_to_page(document)
184
+ _style_admonitions(document)
185
+ document.save(os.fspath(out_path))
186
+
187
+
188
+ # ---------------------------------------------------------------------------- PDF
189
+
190
+
191
+ def _xhtml2pdf_link_callback(uri: str, rel: str) -> str:
192
+ """Resolve resource URIs for xhtml2pdf. Our image srcs are absolute paths."""
193
+ if uri.startswith("file://"):
194
+ import urllib.request
195
+
196
+ return urllib.request.url2pathname(uri[7:])
197
+ return uri
198
+
199
+
200
+ def _to_file_uris(body: str) -> str:
201
+ """Convert absolute filesystem image paths to file:// URIs (for weasyprint)."""
202
+
203
+ def repl(m: re.Match[str]) -> str:
204
+ prefix, quote, src = m.group(1), m.group(2), m.group(3)
205
+ if os.path.isabs(src) and os.path.exists(src):
206
+ src = Path(src).as_uri()
207
+ return f"{prefix}{quote}{src}{quote}"
208
+
209
+ return _IMG_SRC.sub(repl, body)
210
+
211
+
212
+ def to_pdf(
213
+ body: str,
214
+ out_path: str | os.PathLike[str],
215
+ *,
216
+ backend: str = "xhtml2pdf",
217
+ title: str = "",
218
+ work_dir: Path | None = None,
219
+ ) -> None:
220
+ """Write *body* (an HTML fragment) to a .pdf file using the chosen backend."""
221
+ if backend == "weasyprint":
222
+ from weasyprint import HTML # lazy: optional native-dependency backend
223
+
224
+ html_doc = wrap_html(_to_file_uris(body), title=title)
225
+ base = (work_dir or Path.cwd()).as_uri()
226
+ HTML(string=html_doc, base_url=base).write_pdf(os.fspath(out_path))
227
+ return
228
+
229
+ if backend == "xhtml2pdf":
230
+ from xhtml2pdf import pisa # lazy
231
+
232
+ html_doc = wrap_html(body, title=title)
233
+ with open(os.fspath(out_path), "wb") as fh:
234
+ status = pisa.CreatePDF(
235
+ src=html_doc,
236
+ dest=fh,
237
+ encoding="utf-8",
238
+ link_callback=_xhtml2pdf_link_callback,
239
+ )
240
+ if status.err:
241
+ raise RuntimeError(f"xhtml2pdf failed to render {out_path}")
242
+ return
243
+
244
+ raise ValueError(f"Unknown PDF backend: {backend!r} (use 'xhtml2pdf' or 'weasyprint')")
mdmint/cli.py ADDED
@@ -0,0 +1,164 @@
1
+ """Command-line interface for mdmint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import logging
7
+ import sys
8
+ from collections.abc import Sequence
9
+
10
+ from . import __version__
11
+ from .core import VALID_FORMATS, MarkdownConverter
12
+ from .mkdocs import SECTION_STYLES
13
+
14
+
15
+ def _parse_formats(value: str) -> list[str]:
16
+ fmts = [f.strip().lower() for f in value.split(",") if f.strip()]
17
+ bad = [f for f in fmts if f not in VALID_FORMATS]
18
+ if bad:
19
+ raise argparse.ArgumentTypeError(
20
+ f"invalid format(s): {', '.join(bad)} (choose from {', '.join(VALID_FORMATS)})"
21
+ )
22
+ if not fmts:
23
+ raise argparse.ArgumentTypeError("no formats given")
24
+ return fmts
25
+
26
+
27
+ def _key_value(value: str) -> tuple[str, str]:
28
+ key, sep, val = value.partition("=")
29
+ if not sep or not key.strip():
30
+ raise argparse.ArgumentTypeError(f"expected KEY=VALUE, got {value!r}")
31
+ return key.strip(), val.strip()
32
+
33
+
34
+ def build_parser() -> argparse.ArgumentParser:
35
+ parser = argparse.ArgumentParser(
36
+ prog="mdmint",
37
+ description="Export Markdown file(s) to DOCX and/or PDF.",
38
+ formatter_class=argparse.RawDescriptionHelpFormatter,
39
+ epilog=(
40
+ "examples:\n"
41
+ " mdmint notes.md # -> notes.pdf\n"
42
+ " mdmint notes.md -f docx,pdf -o out/ # both formats into out/\n"
43
+ " mdmint a.md b.md --merge -o book.pdf # one merged PDF\n"
44
+ " mdmint a.md b.md -f docx # a.docx and b.docx\n"
45
+ " mdmint --mkdocs mkdocs.yml -f docx --template brand.docx -o out/\n"
46
+ " # one branded DOCX from the nav\n"
47
+ ),
48
+ )
49
+ parser.add_argument("inputs", nargs="*", metavar="INPUT", help="Markdown file(s)")
50
+ parser.add_argument(
51
+ "-f",
52
+ "--format",
53
+ dest="formats",
54
+ type=_parse_formats,
55
+ default=["pdf"],
56
+ metavar="FMT[,FMT]",
57
+ help="output format(s): pdf, docx, or both (comma-separated). Default: pdf",
58
+ )
59
+ parser.add_argument(
60
+ "-o",
61
+ "--output",
62
+ default=None,
63
+ help="output directory, or a file path. Default: alongside each input.",
64
+ )
65
+ parser.add_argument(
66
+ "--merge",
67
+ action="store_true",
68
+ help="with multiple inputs, produce a single merged file per format",
69
+ )
70
+ parser.add_argument(
71
+ "--toc",
72
+ action="store_true",
73
+ help="insert a table of contents (or honour a [TOC] marker)",
74
+ )
75
+ parser.add_argument(
76
+ "--mkdocs",
77
+ metavar="MKDOCS_YML",
78
+ default=None,
79
+ help="build one document from an mkdocs config's nav (instead of INPUTs)",
80
+ )
81
+ parser.add_argument(
82
+ "--section-style",
83
+ choices=SECTION_STYLES,
84
+ default=None,
85
+ help="with --mkdocs: top-level nav sections become part dividers (part, "
86
+ "default) or every section is a heading above its pages (heading)",
87
+ )
88
+ parser.add_argument(
89
+ "--template",
90
+ metavar="DOCX",
91
+ default=None,
92
+ help="Word template (.docx/.dotx) for DOCX output: its cover, headers, "
93
+ "footers and styles are used",
94
+ )
95
+ parser.add_argument(
96
+ "--meta",
97
+ metavar="KEY=VALUE",
98
+ type=_key_value,
99
+ action="append",
100
+ default=[],
101
+ help="value for a template {{KEY}} placeholder (repeatable); overrides "
102
+ "front matter / mkdocs.yml",
103
+ )
104
+ parser.add_argument(
105
+ "--style",
106
+ metavar="ROLE=STYLE",
107
+ type=_key_value,
108
+ action="append",
109
+ default=[],
110
+ help="use template style STYLE for ROLE, e.g. code='My Code' (repeatable)",
111
+ )
112
+ parser.add_argument(
113
+ "--pdf-backend",
114
+ choices=("xhtml2pdf", "weasyprint"),
115
+ default="xhtml2pdf",
116
+ help="PDF engine. xhtml2pdf (default, pure-Python) or weasyprint (extra).",
117
+ )
118
+ parser.add_argument("-v", "--verbose", action="store_true", help="verbose logging")
119
+ parser.add_argument("--version", action="version", version=f"mdmint {__version__}")
120
+ return parser
121
+
122
+
123
+ def main(argv: Sequence[str] | None = None) -> int:
124
+ parser = build_parser()
125
+ args = parser.parse_args(argv)
126
+ if args.mkdocs and args.inputs:
127
+ parser.error("give either INPUT files or --mkdocs, not both")
128
+ if not args.mkdocs and not args.inputs:
129
+ parser.error("no INPUT files given (or use --mkdocs MKDOCS_YML)")
130
+ if args.section_style and not args.mkdocs:
131
+ parser.error("--section-style only applies with --mkdocs")
132
+ logging.basicConfig(
133
+ level=logging.INFO if args.verbose else logging.WARNING,
134
+ format="%(levelname)s: %(message)s",
135
+ )
136
+ try:
137
+ converter = MarkdownConverter(
138
+ args.formats,
139
+ pdf_backend=args.pdf_backend,
140
+ merge=args.merge,
141
+ toc=args.toc,
142
+ template=args.template,
143
+ meta=dict(args.meta),
144
+ styles=dict(args.style),
145
+ section_style=args.section_style,
146
+ )
147
+ if args.mkdocs:
148
+ written = converter.convert_mkdocs(args.mkdocs, output=args.output)
149
+ else:
150
+ written = converter.convert(args.inputs, output=args.output)
151
+ except (ValueError, FileNotFoundError) as exc:
152
+ print(f"error: {exc}", file=sys.stderr)
153
+ return 2
154
+ except ImportError as exc:
155
+ print(f"error: missing optional dependency — {exc}", file=sys.stderr)
156
+ return 3
157
+
158
+ for path in written:
159
+ print(path)
160
+ return 0
161
+
162
+
163
+ if __name__ == "__main__":
164
+ raise SystemExit(main())