pdfmd-cli 3.11.2__tar.gz → 3.15.1__tar.gz
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.
- {pdfmd_cli-3.11.2/pdfmd_cli.egg-info → pdfmd_cli-3.15.1}/PKG-INFO +3 -3
- {pdfmd_cli-3.11.2 → pdfmd_cli-3.15.1}/README.md +2 -2
- {pdfmd_cli-3.11.2 → pdfmd_cli-3.15.1}/pdfmd.py +447 -14
- {pdfmd_cli-3.11.2 → pdfmd_cli-3.15.1/pdfmd_cli.egg-info}/PKG-INFO +3 -3
- {pdfmd_cli-3.11.2 → pdfmd_cli-3.15.1}/LICENSE +0 -0
- {pdfmd_cli-3.11.2 → pdfmd_cli-3.15.1}/pdfmd_cli.egg-info/SOURCES.txt +0 -0
- {pdfmd_cli-3.11.2 → pdfmd_cli-3.15.1}/pdfmd_cli.egg-info/dependency_links.txt +0 -0
- {pdfmd_cli-3.11.2 → pdfmd_cli-3.15.1}/pdfmd_cli.egg-info/entry_points.txt +0 -0
- {pdfmd_cli-3.11.2 → pdfmd_cli-3.15.1}/pdfmd_cli.egg-info/requires.txt +0 -0
- {pdfmd_cli-3.11.2 → pdfmd_cli-3.15.1}/pdfmd_cli.egg-info/top_level.txt +0 -0
- {pdfmd_cli-3.11.2 → pdfmd_cli-3.15.1}/pyproject.toml +0 -0
- {pdfmd_cli-3.11.2 → pdfmd_cli-3.15.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pdfmd-cli
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.15.1
|
|
4
4
|
Summary: One command from Markdown to a good-looking PDF: a Pandoc wrapper with smart defaults, project-file discovery and a multi-engine fallback chain
|
|
5
5
|
Author: Ali Perdekhan
|
|
6
6
|
License-Expression: MIT
|
|
@@ -20,7 +20,7 @@ Dynamic: license-file
|
|
|
20
20
|
|
|
21
21
|
# pdfmd
|
|
22
22
|
|
|
23
|
-
[](https://github.com/aliperdehan/pdfmd/actions/workflows/publish.yml) [](https://pypi.org/project/pdfmd-cli/)
|
|
24
24
|
|
|
25
25
|
**One command from Markdown to a good-looking PDF.** `pdfmd` wraps
|
|
26
26
|
[Pandoc](https://pandoc.org) and fills in everything you would otherwise
|
|
@@ -101,7 +101,7 @@ winget install --id JohnMacFarlane.Pandoc; winget install --id Typst.Typst
|
|
|
101
101
|
|
|
102
102
|
Every push is tested on Windows, macOS and Linux (Pandoc + Typst: single
|
|
103
103
|
files, CSV tables, a book, HTML output); see the
|
|
104
|
-
[Test workflow](https://github.com/aliperdehan/pdfmd/actions/workflows/
|
|
104
|
+
[Test and publish workflow](https://github.com/aliperdehan/pdfmd/actions/workflows/publish.yml).
|
|
105
105
|
LaTeX engines aren't part of that automated test on Windows yet.
|
|
106
106
|
|
|
107
107
|
Optional extras: [Quarto](https://quarto.org) for `.qmd` files,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pdfmd
|
|
2
2
|
|
|
3
|
-
[](https://github.com/aliperdehan/pdfmd/actions/workflows/publish.yml) [](https://pypi.org/project/pdfmd-cli/)
|
|
4
4
|
|
|
5
5
|
**One command from Markdown to a good-looking PDF.** `pdfmd` wraps
|
|
6
6
|
[Pandoc](https://pandoc.org) and fills in everything you would otherwise
|
|
@@ -81,7 +81,7 @@ winget install --id JohnMacFarlane.Pandoc; winget install --id Typst.Typst
|
|
|
81
81
|
|
|
82
82
|
Every push is tested on Windows, macOS and Linux (Pandoc + Typst: single
|
|
83
83
|
files, CSV tables, a book, HTML output); see the
|
|
84
|
-
[Test workflow](https://github.com/aliperdehan/pdfmd/actions/workflows/
|
|
84
|
+
[Test and publish workflow](https://github.com/aliperdehan/pdfmd/actions/workflows/publish.yml).
|
|
85
85
|
LaTeX engines aren't part of that automated test on Windows yet.
|
|
86
86
|
|
|
87
87
|
Optional extras: [Quarto](https://quarto.org) for `.qmd` files,
|
|
@@ -449,7 +449,7 @@ Automatic source backups (--backup, v3.8.0; formats v3.9.0):
|
|
|
449
449
|
# unreliable 1.x history from those gaps, versioning restarts at 2.0.0 here
|
|
450
450
|
# (2026-09-16, the author's call) as an honest baseline: this is where real
|
|
451
451
|
# changelog tracking begins, not a claim about how many changes preceded it.
|
|
452
|
-
PDFMD_VERSION = "3.
|
|
452
|
+
PDFMD_VERSION = "3.15.1"
|
|
453
453
|
import argparse
|
|
454
454
|
import filecmp
|
|
455
455
|
from fnmatch import fnmatchcase
|
|
@@ -1585,14 +1585,39 @@ def wrap_latex_header_includes(text: str) -> str:
|
|
|
1585
1585
|
|
|
1586
1586
|
|
|
1587
1587
|
@contextmanager
|
|
1588
|
-
def prepared_latex_inputs(paths: list[Path], latex_engine: bool
|
|
1589
|
-
|
|
1588
|
+
def prepared_latex_inputs(paths: list[Path], latex_engine: bool,
|
|
1589
|
+
typst_engine: bool = False, doc_count: int = 1) -> Iterator[list[Path]]:
|
|
1590
|
+
"""Yield temporary, corrected inputs for LaTeX-family renders when
|
|
1591
|
+
needed -- or, with `typst_engine` instead, to make a margin setting
|
|
1592
|
+
written for the OTHER engine family reach Typst's own template (see
|
|
1593
|
+
fix_typst_margin()). The two are mutually exclusive; at most one
|
|
1594
|
+
corrector ever applies to a given render, same as `latex_engine` alone
|
|
1595
|
+
before this.
|
|
1596
|
+
|
|
1597
|
+
`paths[:doc_count]` are the main document(s) being rendered -- a
|
|
1598
|
+
single `title_source` everywhere except report/book mode, where every
|
|
1599
|
+
chapter file counts (``doc_count=len(files)``, passed explicitly by
|
|
1600
|
+
those two call sites; every other call site keeps the default of 1).
|
|
1601
|
+
Every path after that is a linked --metadata-file, which Pandoc
|
|
1602
|
+
accepts EITHER fenced (``---``/``...``-delimited, same as a
|
|
1603
|
+
document's own front matter) OR completely bare (no delimiters at
|
|
1604
|
+
all -- see the Pandoc manual's own "metadata file" section).
|
|
1605
|
+
fix_typst_margin() is told which is which (`is_metadata`) so it can
|
|
1606
|
+
safely treat a bare metadata file as pure YAML, while a bare MAIN
|
|
1607
|
+
DOCUMENT (no delimiters at all) is definitely markdown body text,
|
|
1608
|
+
never YAML, and is always left alone regardless.
|
|
1609
|
+
"""
|
|
1590
1610
|
temporary_paths: list[Path] = []
|
|
1591
1611
|
prepared: list[Path] = []
|
|
1592
1612
|
try:
|
|
1593
|
-
for path in paths:
|
|
1613
|
+
for index, path in enumerate(paths):
|
|
1594
1614
|
text = path.read_text(encoding="utf-8-sig")
|
|
1595
|
-
|
|
1615
|
+
if latex_engine:
|
|
1616
|
+
corrected = wrap_latex_header_includes(text)
|
|
1617
|
+
elif typst_engine:
|
|
1618
|
+
corrected = fix_typst_margin(text, is_metadata=(index >= doc_count))
|
|
1619
|
+
else:
|
|
1620
|
+
corrected = text
|
|
1596
1621
|
if corrected == text:
|
|
1597
1622
|
prepared.append(path)
|
|
1598
1623
|
continue
|
|
@@ -1949,6 +1974,189 @@ def has_geometry(md_path: Path, variables: list[str], metadata_files: list[Path]
|
|
|
1949
1974
|
for preamble in preamble_files)
|
|
1950
1975
|
|
|
1951
1976
|
|
|
1977
|
+
# Shared by frontmatter_margin_geometry_options() and fix_typst_margin()
|
|
1978
|
+
# below to translate a margin setting written for ONE engine family into
|
|
1979
|
+
# the variable the OTHER one actually reads -- geometry: (LaTeX) and
|
|
1980
|
+
# margin: (Typst) each mean nothing at all to the other's template, so a
|
|
1981
|
+
# document written/tested against one engine silently loses its margin
|
|
1982
|
+
# the moment it's rendered through the other (has_geometry() already
|
|
1983
|
+
# stops pdfmd's own DEFAULT_MARGIN from stepping on either, which is
|
|
1984
|
+
# correct, but does nothing to make the document's OWN setting reach the
|
|
1985
|
+
# engine that can't read it natively). Both directions normalize through
|
|
1986
|
+
# one canonical shape: a {top, bottom, left, right} dict of raw dimension
|
|
1987
|
+
# strings, physical sides only -- Typst's binding-aware `inside`/`outside`
|
|
1988
|
+
# and `rest` have no LaTeX equivalent at all, so a `margin:` mapping using
|
|
1989
|
+
# any of those three is left completely alone rather than guessed at.
|
|
1990
|
+
MARGIN_SIDE_ALIASES = {
|
|
1991
|
+
"x": ("left", "right"),
|
|
1992
|
+
"y": ("top", "bottom"),
|
|
1993
|
+
"top": ("top",),
|
|
1994
|
+
"bottom": ("bottom",),
|
|
1995
|
+
"left": ("left",),
|
|
1996
|
+
"right": ("right",),
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
# geometry package option names -- includes the older t/b/l/r-margin
|
|
2000
|
+
# spellings alongside the more common ones actually seen in practice.
|
|
2001
|
+
GEOMETRY_SIDE_ALIASES = {
|
|
2002
|
+
"margin": ("top", "bottom", "left", "right"),
|
|
2003
|
+
"hmargin": ("left", "right"),
|
|
2004
|
+
"vmargin": ("top", "bottom"),
|
|
2005
|
+
"top": ("top",), "tmargin": ("top",),
|
|
2006
|
+
"bottom": ("bottom",), "bmargin": ("bottom",),
|
|
2007
|
+
"left": ("left",), "lmargin": ("left",),
|
|
2008
|
+
"right": ("right",), "rmargin": ("right",),
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
|
|
2012
|
+
def parse_margin_sides(value) -> dict[str, str] | None:
|
|
2013
|
+
"""Normalize a parsed (via PyYAML) Typst-shaped ``margin:`` value into
|
|
2014
|
+
a ``{top, bottom, left, right}`` dict of raw dimension strings -- or
|
|
2015
|
+
None when `value` is a scalar-shaped None/absent, or a mapping that
|
|
2016
|
+
uses ``inside``/``outside``/``rest`` (Typst's own binding-aware
|
|
2017
|
+
margins, with no LaTeX equivalent) or any other key this doesn't
|
|
2018
|
+
recognize, or isn't a scalar or mapping at all.
|
|
2019
|
+
|
|
2020
|
+
A scalar (already the case fix_typst_margin() itself exists to
|
|
2021
|
+
correct on the Typst side) is uniform on all four sides.
|
|
2022
|
+
"""
|
|
2023
|
+
if isinstance(value, (str, int, float)) and not isinstance(value, bool):
|
|
2024
|
+
side_value = str(value)
|
|
2025
|
+
return {"top": side_value, "bottom": side_value, "left": side_value, "right": side_value}
|
|
2026
|
+
if not isinstance(value, dict):
|
|
2027
|
+
return None
|
|
2028
|
+
sides: dict[str, str] = {}
|
|
2029
|
+
for key, side_value in value.items():
|
|
2030
|
+
aliases = MARGIN_SIDE_ALIASES.get(str(key).strip().casefold())
|
|
2031
|
+
if not aliases:
|
|
2032
|
+
return None
|
|
2033
|
+
for side in aliases:
|
|
2034
|
+
sides[side] = str(side_value)
|
|
2035
|
+
return sides or None
|
|
2036
|
+
|
|
2037
|
+
|
|
2038
|
+
def parse_geometry_sides(value) -> dict[str, str] | None:
|
|
2039
|
+
"""Normalize a parsed (via PyYAML) ``geometry:`` value -- a bare
|
|
2040
|
+
"key=val" scalar, a comma-joined "key=val,key=val" scalar, or a YAML
|
|
2041
|
+
list of such strings, the three shapes Pandoc's own LaTeX template
|
|
2042
|
+
accepts -- into a ``{top, bottom, left, right}`` dict of raw dimension
|
|
2043
|
+
strings, or None when it contains anything this can't safely
|
|
2044
|
+
interpret: an option with no ``key=`` at all, or a key that isn't
|
|
2045
|
+
about a margin (``includehead``, ``showframe``, ...) -- the geometry
|
|
2046
|
+
package has many options that aren't page margins, and guessing wrong
|
|
2047
|
+
on those would be worse than leaving Typst's own defaults in place.
|
|
2048
|
+
"""
|
|
2049
|
+
if isinstance(value, str):
|
|
2050
|
+
items = [value]
|
|
2051
|
+
elif isinstance(value, list) and all(isinstance(item, str) for item in value):
|
|
2052
|
+
items = value
|
|
2053
|
+
else:
|
|
2054
|
+
return None
|
|
2055
|
+
sides: dict[str, str] = {}
|
|
2056
|
+
for item in items:
|
|
2057
|
+
for part in item.split(","):
|
|
2058
|
+
part = part.strip()
|
|
2059
|
+
if not part:
|
|
2060
|
+
continue
|
|
2061
|
+
if "=" not in part:
|
|
2062
|
+
return None
|
|
2063
|
+
key, _, side_value = part.partition("=")
|
|
2064
|
+
aliases = GEOMETRY_SIDE_ALIASES.get(key.strip().casefold())
|
|
2065
|
+
if not aliases:
|
|
2066
|
+
return None
|
|
2067
|
+
for side in aliases:
|
|
2068
|
+
sides[side] = side_value.strip()
|
|
2069
|
+
return sides or None
|
|
2070
|
+
|
|
2071
|
+
|
|
2072
|
+
def sides_to_geometry_options(sides: dict[str, str]) -> list[str]:
|
|
2073
|
+
"""Format a {top,bottom,left,right} dict as the geometry: option list
|
|
2074
|
+
Pandoc's LaTeX template expects -- collapsed to one ``margin=...``
|
|
2075
|
+
when all four sides given are equal, one option per side otherwise
|
|
2076
|
+
(only the sides actually present -- a mapping that only set ``x:``
|
|
2077
|
+
leaves top/bottom to LaTeX's own default, same as it would for Typst).
|
|
2078
|
+
"""
|
|
2079
|
+
if len(sides) == 4 and len(set(sides.values())) == 1:
|
|
2080
|
+
return [f"margin={next(iter(sides.values()))}"]
|
|
2081
|
+
return [f"{side}={sides[side]}" for side in ("top", "bottom", "left", "right") if side in sides]
|
|
2082
|
+
|
|
2083
|
+
|
|
2084
|
+
def frontmatter_margin_geometry_options(md_path: Path, variables: list[str],
|
|
2085
|
+
metadata_files: list[Path] = ()) -> list[str] | None:
|
|
2086
|
+
"""Return the geometry: option list a document's own (or a linked
|
|
2087
|
+
--metadata-file's) ``margin:`` value translates to for LaTeX-family
|
|
2088
|
+
engines -- or None when there's nothing to translate.
|
|
2089
|
+
|
|
2090
|
+
``margin:`` is a real Pandoc variable for the TYPST template only (see
|
|
2091
|
+
fix_typst_margin() below) -- Pandoc's LaTeX template never reads it at
|
|
2092
|
+
all, only ``geometry:`` does. has_geometry() already treats a bare
|
|
2093
|
+
``margin:`` (in the document's own front matter OR a metadata file --
|
|
2094
|
+
it checks both) as "a margin setting exists" (correctly, so
|
|
2095
|
+
DEFAULT_MARGIN isn't injected on top of it), but nothing translated
|
|
2096
|
+
that value into the one variable LaTeX's own template actually
|
|
2097
|
+
consumes -- so a document with ONLY ``margin: 2.54cm`` (or a
|
|
2098
|
+
``top:``/``bottom:``/``left:``/``right:``/``x:``/``y:`` breakdown) and
|
|
2099
|
+
no ``geometry:`` silently kept LaTeX's own much wider article-class
|
|
2100
|
+
default margins on every LaTeX-family engine, the requested value
|
|
2101
|
+
never taking effect at all -- confirmed directly (2026-09-28) alongside
|
|
2102
|
+
the Typst-side bug the scalar case triggers (see fix_typst_margin()).
|
|
2103
|
+
|
|
2104
|
+
Same precedence pdf-engine resolution uses (see
|
|
2105
|
+
frontmatter_pdfmd_options()'s callers): the document's own front
|
|
2106
|
+
matter is checked first; if it sets no ``margin:`` at all, each
|
|
2107
|
+
linked --metadata-file is checked next, in order, so a shared
|
|
2108
|
+
metadata.yaml can set ``margin:`` once for every document that finds
|
|
2109
|
+
it -- leaving it out entirely, the way this function's own first
|
|
2110
|
+
version did, meant a document with margin ONLY in a shared metadata
|
|
2111
|
+
file lost it silently on both engine families at once, worse than the
|
|
2112
|
+
bug this function exists to fix. A real ``geometry:`` anywhere -- the
|
|
2113
|
+
document's own front matter, any metadata file, or -V -- always wins,
|
|
2114
|
+
untouched, checked before any ``margin:`` in any source, same as
|
|
2115
|
+
has_geometry().
|
|
2116
|
+
|
|
2117
|
+
Returns None when: no ``margin:`` in any source; a real ``geometry:``
|
|
2118
|
+
is set anywhere; PyYAML isn't installed (falls back to the
|
|
2119
|
+
scalar-only regex this function used before parse_margin_sides()
|
|
2120
|
+
existed, since a real parse is needed for the mapping case -- checked
|
|
2121
|
+
per source, same order); or every ``margin:`` found uses Typst's
|
|
2122
|
+
``inside``/``outside``/``rest`` keys, which have no LaTeX equivalent
|
|
2123
|
+
(see parse_margin_sides()).
|
|
2124
|
+
"""
|
|
2125
|
+
if any(variable.startswith(("geometry=", "geometry:")) for variable in variables):
|
|
2126
|
+
return None
|
|
2127
|
+
text = md_path.read_text(encoding="utf-8-sig")
|
|
2128
|
+
front_matter = re.match(r"^---\s*\n(.*?)\n---\s*(?:\n|$)", text, re.DOTALL)
|
|
2129
|
+
blocks = [front_matter.group(1)] if front_matter else []
|
|
2130
|
+
for metadata_file in metadata_files:
|
|
2131
|
+
meta_text = metadata_file.read_text(encoding="utf-8-sig")
|
|
2132
|
+
# A --metadata-file may be fenced (---...---, same as front matter)
|
|
2133
|
+
# or completely bare -- Pandoc accepts either. yaml.safe_load()
|
|
2134
|
+
# chokes on the fenced shape as-is (a second "---" reads as a
|
|
2135
|
+
# second YAML document, "expected a single document in the
|
|
2136
|
+
# stream"), so unwrap it the same way md_path's own front matter
|
|
2137
|
+
# is, and only fall back to the raw text when it isn't fenced.
|
|
2138
|
+
meta_front_matter = re.match(r"^---\s*\n(.*?)\n---\s*(?:\n|$)", meta_text, re.DOTALL)
|
|
2139
|
+
blocks.append(meta_front_matter.group(1) if meta_front_matter else meta_text)
|
|
2140
|
+
if any(re.search(r"^geometry\s*:", block, re.MULTILINE) for block in blocks):
|
|
2141
|
+
return None
|
|
2142
|
+
for block in blocks:
|
|
2143
|
+
if yaml is None:
|
|
2144
|
+
match = re.search(r"^margin\s*:[ \t]*(\S.*?)[ \t]*$", block, re.MULTILINE)
|
|
2145
|
+
if not match:
|
|
2146
|
+
continue
|
|
2147
|
+
value = match.group(1).strip().strip("'\"")
|
|
2148
|
+
return [f"margin={value}"] if value else None
|
|
2149
|
+
try:
|
|
2150
|
+
data = yaml.safe_load(block)
|
|
2151
|
+
except yaml.YAMLError:
|
|
2152
|
+
continue
|
|
2153
|
+
if not isinstance(data, dict) or "margin" not in data:
|
|
2154
|
+
continue
|
|
2155
|
+
sides = parse_margin_sides(data["margin"])
|
|
2156
|
+
return sides_to_geometry_options(sides) if sides else None
|
|
2157
|
+
return None
|
|
2158
|
+
|
|
2159
|
+
|
|
1952
2160
|
def pagesize_typo_value(md_path: Path, variables: list[str]) -> str | None:
|
|
1953
2161
|
"""Return a document's own ``pagesize:`` value, IF ``papersize:`` isn't
|
|
1954
2162
|
already set anywhere Pandoc would see it -- a -V variable or the
|
|
@@ -2031,6 +2239,112 @@ def typst_papersize_translation(value: str | None) -> str | None:
|
|
|
2031
2239
|
return TYPST_PAPERSIZE_ALIASES.get(value.strip().casefold())
|
|
2032
2240
|
|
|
2033
2241
|
|
|
2242
|
+
TYPST_MARGIN_RE = re.compile(r"^(?P<indent>[ \t]*)margin(?P<colon>[ \t]*:[ \t]*)(?P<val>\S.*?)[ \t]*$",
|
|
2243
|
+
re.MULTILINE)
|
|
2244
|
+
|
|
2245
|
+
|
|
2246
|
+
def sides_to_typst_margin_yaml(sides: dict[str, str], indent: str, newline: str) -> str:
|
|
2247
|
+
"""Format a {top,bottom,left,right} dict as the YAML ``margin:``
|
|
2248
|
+
mapping Pandoc's Typst template needs -- collapsed to ``x:``/``y:``
|
|
2249
|
+
when all four sides given are equal, one line per side otherwise
|
|
2250
|
+
(only the sides actually present, in a fixed top/bottom/left/right
|
|
2251
|
+
order for a stable, readable rewrite)."""
|
|
2252
|
+
if len(sides) == 4 and len(set(sides.values())) == 1:
|
|
2253
|
+
value = next(iter(sides.values()))
|
|
2254
|
+
return f"{indent}margin:{newline}{indent} x: {value}{newline}{indent} y: {value}"
|
|
2255
|
+
lines = [f"{indent}margin:"]
|
|
2256
|
+
lines += [f"{indent} {side}: {sides[side]}" for side in ("top", "bottom", "left", "right")
|
|
2257
|
+
if side in sides]
|
|
2258
|
+
return newline.join(lines)
|
|
2259
|
+
|
|
2260
|
+
|
|
2261
|
+
def fix_typst_margin(text: str, is_metadata: bool = False) -> str:
|
|
2262
|
+
"""Make a document's or linked --metadata-file's own page-margin
|
|
2263
|
+
setting reach Pandoc's Typst template, whichever engine family it was
|
|
2264
|
+
actually written for.
|
|
2265
|
+
|
|
2266
|
+
Two distinct problems, both from the same root cause -- ``margin:``
|
|
2267
|
+
is a real Pandoc variable for the TYPST template only, and
|
|
2268
|
+
``geometry:`` for the LATEX template only, neither reads the other's
|
|
2269
|
+
key at all:
|
|
2270
|
+
|
|
2271
|
+
1. A bare scalar ``margin:`` (``margin: 2.54cm``) is rewritten into
|
|
2272
|
+
the ``x:``/``y:`` mapping Typst's template requires -- it always
|
|
2273
|
+
renders margin as ``($for(margin/pairs)$...$endfor$)``, iterating
|
|
2274
|
+
key/value PAIRS a scalar has none of, so it comes out as the
|
|
2275
|
+
literal, invalid ``margin: (: ,)``: a Typst syntax error
|
|
2276
|
+
("unexpected comma") instead of a page margin. Confirmed directly
|
|
2277
|
+
(2026-09-28): a document with ``margin: 2.54cm`` and
|
|
2278
|
+
``pdfmd-options: {engine: typst}`` failed outright with exactly
|
|
2279
|
+
that error, while rendering fine (albeit at the WRONG margin --
|
|
2280
|
+
see frontmatter_margin_geometry_options()) on every LaTeX-family
|
|
2281
|
+
engine.
|
|
2282
|
+
|
|
2283
|
+
2. A document with ONLY a ``geometry:`` (no ``margin:`` at all) --
|
|
2284
|
+
written for and tested against a LaTeX-family engine -- reaches
|
|
2285
|
+
Typst with no margin variable set at all: Typst's template never
|
|
2286
|
+
reads ``geometry:``, so it silently falls back to its own built-in
|
|
2287
|
+
default (1.25in) instead of erroring, the same "wrong margin, no
|
|
2288
|
+
warning" failure frontmatter_margin_geometry_options() fixes in the
|
|
2289
|
+
other direction. Parsed via parse_geometry_sides() and added as a
|
|
2290
|
+
new ``margin:`` mapping -- ``geometry:`` itself is left in place;
|
|
2291
|
+
it's inert for Typst, not harmful.
|
|
2292
|
+
|
|
2293
|
+
Neither fix touches a ``margin:`` that's already a YAML mapping --
|
|
2294
|
+
Pandoc's ``/pairs`` filter already turns that straight into valid
|
|
2295
|
+
Typst key/value pairs, and it may use Typst's own ``inside``/
|
|
2296
|
+
``outside``/``rest`` keys, which parse_margin_sides() can't safely
|
|
2297
|
+
reinterpret but Typst's own template needs no help with anyway.
|
|
2298
|
+
|
|
2299
|
+
`is_metadata` marks a linked --metadata-file (passed by
|
|
2300
|
+
prepared_latex_inputs() using its own `doc_count`), which Pandoc
|
|
2301
|
+
accepts EITHER fenced (front-matter-shaped, handled the same as a
|
|
2302
|
+
document below) OR completely bare -- no ``---``/``...`` delimiters
|
|
2303
|
+
at all, just the YAML directly. A bare MAIN document (`is_metadata`
|
|
2304
|
+
False) is always left alone instead: with no front matter at all,
|
|
2305
|
+
that's markdown BODY text, never YAML, and must never be parsed as
|
|
2306
|
+
such.
|
|
2307
|
+
"""
|
|
2308
|
+
front_matter = re.match(r"^---\s*\n(.*?)\n---\s*(?:\n|$)", text, re.DOTALL)
|
|
2309
|
+
if front_matter:
|
|
2310
|
+
block = front_matter.group(1)
|
|
2311
|
+
prefix = text[:front_matter.start(1)]
|
|
2312
|
+
suffix = text[front_matter.end(1):]
|
|
2313
|
+
elif is_metadata:
|
|
2314
|
+
block = text
|
|
2315
|
+
prefix = ""
|
|
2316
|
+
suffix = ""
|
|
2317
|
+
else:
|
|
2318
|
+
return text
|
|
2319
|
+
newline = "\r\n" if "\r\n" in block else "\n"
|
|
2320
|
+
|
|
2321
|
+
if yaml is not None:
|
|
2322
|
+
try:
|
|
2323
|
+
data = yaml.safe_load(block)
|
|
2324
|
+
except yaml.YAMLError:
|
|
2325
|
+
data = None
|
|
2326
|
+
if isinstance(data, dict) and "margin" not in data and "geometry" in data:
|
|
2327
|
+
sides = parse_geometry_sides(data["geometry"])
|
|
2328
|
+
if not sides:
|
|
2329
|
+
return text
|
|
2330
|
+
insertion = sides_to_typst_margin_yaml(sides, "", newline) + newline
|
|
2331
|
+
return prefix + insertion + block + suffix
|
|
2332
|
+
if isinstance(data, dict) and isinstance(data.get("margin"), dict):
|
|
2333
|
+
return text # already a mapping -- Typst's template handles it as-is
|
|
2334
|
+
|
|
2335
|
+
def replace(match: re.Match) -> str:
|
|
2336
|
+
value = match.group("val").strip().strip("'\"")
|
|
2337
|
+
if not value or value.startswith(("{", "[")):
|
|
2338
|
+
return match.group(0)
|
|
2339
|
+
indent = match.group("indent")
|
|
2340
|
+
return f"{indent}margin:{newline}{indent} x: {value}{newline}{indent} y: {value}"
|
|
2341
|
+
|
|
2342
|
+
fixed_block = TYPST_MARGIN_RE.sub(replace, block, count=1)
|
|
2343
|
+
if fixed_block == block:
|
|
2344
|
+
return text
|
|
2345
|
+
return prefix + fixed_block + suffix
|
|
2346
|
+
|
|
2347
|
+
|
|
2034
2348
|
def has_monofont(md_path: Path, variables: list[str]) -> bool:
|
|
2035
2349
|
if any(variable.startswith("monofont=") for variable in variables):
|
|
2036
2350
|
return True
|
|
@@ -2482,6 +2796,51 @@ def frontmatter_extra_preambles(md_path: Path) -> list[Path]:
|
|
|
2482
2796
|
return resolved
|
|
2483
2797
|
|
|
2484
2798
|
|
|
2799
|
+
def frontmatter_extra_lua_filters(md_path: Path) -> list[Path]:
|
|
2800
|
+
"""Read ``pdfmd-options: lua-filter: ...`` -- a document naming its own
|
|
2801
|
+
Pandoc Lua filter(s) explicitly, instead of relying on
|
|
2802
|
+
find_lua_filters()'s fixed auto-discovery names (``<doc-stem>.lua`` or
|
|
2803
|
+
``nulabreport.lua``).
|
|
2804
|
+
|
|
2805
|
+
Accepts a bare string or a YAML list of strings; each path is resolved
|
|
2806
|
+
relative to `md_path`'s own directory (not cwd -- a document should be
|
|
2807
|
+
runnable from any directory), same convention as
|
|
2808
|
+
frontmatter_extra_preambles. A path that doesn't exist is a hard
|
|
2809
|
+
SystemExit, not a silent skip: naming a filter explicitly means it's
|
|
2810
|
+
required, unlike auto-discovery's own best-effort search.
|
|
2811
|
+
|
|
2812
|
+
Merged with (not a replacement for) whatever find_lua_filters() already
|
|
2813
|
+
auto-discovered -- see this function's one call site in convert_one,
|
|
2814
|
+
which appends these AFTER the auto-discovered ones and dedupes by
|
|
2815
|
+
resolved path. Both land on the command line after --citeproc (see
|
|
2816
|
+
run()'s own comment on that ordering) -- required for a filter, like
|
|
2817
|
+
fullcite.lua, that consumes citeproc's resolved bibliography div.
|
|
2818
|
+
|
|
2819
|
+
Reuses the existing "lua" --no-auto KIND, same as
|
|
2820
|
+
frontmatter_extra_preambles reuses "preamble": a pdfmd-options.lua-filter
|
|
2821
|
+
key is written IN the document, so --no-auto lua (or a bare --no-auto)
|
|
2822
|
+
suppresses it the same as auto-discovery.
|
|
2823
|
+
|
|
2824
|
+
Added 2026-09-28 for a real case: an annotated-bibliography.md using a
|
|
2825
|
+
citeproc-dependent filter (fullcite.lua) that isn't named after the
|
|
2826
|
+
document's own stem, so find_lua_filters() alone never picked it up --
|
|
2827
|
+
the document had to name it explicitly instead.
|
|
2828
|
+
"""
|
|
2829
|
+
value = frontmatter_pdfmd_options(md_path).get("lua-filter")
|
|
2830
|
+
if value is None:
|
|
2831
|
+
return []
|
|
2832
|
+
names = [value] if isinstance(value, str) else (
|
|
2833
|
+
[str(item) for item in value] if isinstance(value, list) else [])
|
|
2834
|
+
resolved = []
|
|
2835
|
+
for name in names:
|
|
2836
|
+
path = (md_path.parent / name).resolve()
|
|
2837
|
+
if not path.is_file():
|
|
2838
|
+
raise SystemExit(f"{md_path}: pdfmd-options.lua-filter names {name!r}, "
|
|
2839
|
+
f"which doesn't exist at {path}")
|
|
2840
|
+
resolved.append(path)
|
|
2841
|
+
return resolved
|
|
2842
|
+
|
|
2843
|
+
|
|
2485
2844
|
# --stamp / pdfmd-options.stamp: appends (or updates) a "BUILD NOTES" HTML
|
|
2486
2845
|
# comment at/near the end of a document recording what compiled it and when
|
|
2487
2846
|
# -- a lab-notebook-style provenance note, invisible in the rendered output
|
|
@@ -3193,6 +3552,15 @@ def stamp_pdf_metadata_posthoc(pdf_path: Path, options: dict, texts: list[str],
|
|
|
3193
3552
|
warnings.simplefilter("ignore")
|
|
3194
3553
|
reader = pypdf.PdfReader(pdf_path)
|
|
3195
3554
|
existing = dict(reader.metadata) if reader.metadata else {}
|
|
3555
|
+
# Credit pdfmd in /Creator, appended in pandoc's own
|
|
3556
|
+
# "<tool> via <wrapper>" order ("LaTeX via pandoc" -> "LaTeX
|
|
3557
|
+
# via pandoc via pdfmd-cli"; "Typst 0.15.1" -> "Typst 0.15.1
|
|
3558
|
+
# via pdfmd-cli"). Named by its PyPI distribution name so it
|
|
3559
|
+
# is searchable; the version is already in PdfmdVersions.
|
|
3560
|
+
# Idempotent (a re-stamp doesn't append twice).
|
|
3561
|
+
creator = str(existing.get("/Creator") or "").strip()
|
|
3562
|
+
if not creator.endswith("pdfmd-cli"):
|
|
3563
|
+
existing["/Creator"] = f"{creator} via pdfmd-cli" if creator else "pdfmd-cli"
|
|
3196
3564
|
writer = pypdf.PdfWriter()
|
|
3197
3565
|
writer.append(reader)
|
|
3198
3566
|
writer.add_metadata({
|
|
@@ -3885,11 +4253,15 @@ def convert_via_soffice_bridge(md_path: Path, output: Path, effective_from: str
|
|
|
3885
4253
|
cmd += resource_path_option(md_path.parent, pandoc_cwd, metadata_files)
|
|
3886
4254
|
for variable in variables:
|
|
3887
4255
|
cmd += ["-V", variable]
|
|
3888
|
-
cmd += pandoc_options
|
|
3889
4256
|
cmd += crossref_filter_args(md_path, pandoc_options, no_auto, str(md_path))
|
|
3890
4257
|
cmd += csv_table_filter_args(md_path, no_auto, csv_filter)
|
|
3891
4258
|
if contains_citations(md_path) and "--citeproc" not in pandoc_options and not CITEPROC_DISABLED:
|
|
3892
4259
|
cmd.append("--citeproc")
|
|
4260
|
+
# pandoc_options after --citeproc: any --lua-filter/--filter a caller
|
|
4261
|
+
# passes through needs resolved citations already in the AST, same
|
|
4262
|
+
# invariant as the auto-discovered lua_filters below (see run()'s
|
|
4263
|
+
# matching comment in convert_one).
|
|
4264
|
+
cmd += pandoc_options
|
|
3893
4265
|
for lua_filter in lua_filters:
|
|
3894
4266
|
cmd += ["--lua-filter", str(lua_filter)]
|
|
3895
4267
|
log_cmd(cmd, pandoc_cwd, verbose)
|
|
@@ -4115,6 +4487,10 @@ def _convert_one(md_path: Path, out_dir: Path | None, presentation: bool, font:
|
|
|
4115
4487
|
if reader_reason:
|
|
4116
4488
|
note("READER", reader_reason)
|
|
4117
4489
|
lua_filters = [] if auto_disabled(no_auto, "lua") else find_lua_filters(md_path, metadata_files)
|
|
4490
|
+
if not auto_disabled(no_auto, "lua"):
|
|
4491
|
+
for extra_filter in frontmatter_extra_lua_filters(md_path):
|
|
4492
|
+
if extra_filter not in lua_filters:
|
|
4493
|
+
lua_filters.append(extra_filter)
|
|
4118
4494
|
for lua_filter in lua_filters:
|
|
4119
4495
|
note("LUA", str(lua_filter))
|
|
4120
4496
|
|
|
@@ -4166,6 +4542,11 @@ def _convert_one(md_path: Path, out_dir: Path | None, presentation: bool, font:
|
|
|
4166
4542
|
if is_tex_target and geometry_needed:
|
|
4167
4543
|
note("MARGIN", f"{md_path}: no geometry/margin set; "
|
|
4168
4544
|
f"using geometry:margin={DEFAULT_MARGIN} on LaTeX-family targets")
|
|
4545
|
+
margin_options = (None if auto_disabled(no_auto, "margin")
|
|
4546
|
+
else frontmatter_margin_geometry_options(md_path, variables, metadata_files))
|
|
4547
|
+
if is_tex_target and margin_options:
|
|
4548
|
+
note("MARGIN", f"{md_path}: margin: isn't a Pandoc variable LaTeX-family targets "
|
|
4549
|
+
f"read (geometry: is) -- using geometry:{','.join(margin_options)}")
|
|
4169
4550
|
if is_tex_target and monofont_needed:
|
|
4170
4551
|
note("MONOFONT", f"{md_path}: has code but no monofont set; "
|
|
4171
4552
|
f"using {default_monofont()} on LaTeX-family targets")
|
|
@@ -4176,7 +4557,8 @@ def _convert_one(md_path: Path, out_dir: Path | None, presentation: bool, font:
|
|
|
4176
4557
|
f"(papersize: is) -- using papersize={pagesize_typo} on LaTeX-family "
|
|
4177
4558
|
"targets. Set papersize: yourself, or --no-auto papersize, to silence "
|
|
4178
4559
|
"this and keep the Letter default")
|
|
4179
|
-
with prepared_latex_inputs([title_source, *metadata_files], is_tex_target
|
|
4560
|
+
with prepared_latex_inputs([title_source, *metadata_files], is_tex_target,
|
|
4561
|
+
typst_engine=(target_format == "typst")) as prepared, \
|
|
4180
4562
|
document_header_file(md_path, (bool(preamble_files) or bool(pdf_meta_snippet_text))
|
|
4181
4563
|
and is_tex_target) as header_file, \
|
|
4182
4564
|
pdf_metadata_header_file(pdf_meta_snippet_text if is_tex_target else None) as pdf_meta_file:
|
|
@@ -4212,6 +4594,9 @@ def _convert_one(md_path: Path, out_dir: Path | None, presentation: bool, font:
|
|
|
4212
4594
|
cmd += ["-V", f"mainfont={first_font}"]
|
|
4213
4595
|
if geometry_needed:
|
|
4214
4596
|
cmd += ["-V", f"geometry:margin={DEFAULT_MARGIN}"]
|
|
4597
|
+
elif margin_options:
|
|
4598
|
+
for margin_option in margin_options:
|
|
4599
|
+
cmd += ["-V", f"geometry:{margin_option}"]
|
|
4215
4600
|
if monofont_needed:
|
|
4216
4601
|
cmd += ["-V", f"monofont={default_monofont()}"]
|
|
4217
4602
|
if pagesize_typo:
|
|
@@ -4230,7 +4615,6 @@ def _convert_one(md_path: Path, out_dir: Path | None, presentation: bool, font:
|
|
|
4230
4615
|
cmd += ["-V", f"papersize={typst_size}"]
|
|
4231
4616
|
if shift_heading:
|
|
4232
4617
|
cmd += ["--shift-heading-level-by=-1"]
|
|
4233
|
-
cmd += pandoc_options
|
|
4234
4618
|
if preamble_files and is_tex_target:
|
|
4235
4619
|
for preamble_file in preamble_files:
|
|
4236
4620
|
cmd += ["--include-in-header", str(preamble_file)]
|
|
@@ -4256,6 +4640,11 @@ def _convert_one(md_path: Path, out_dir: Path | None, presentation: bool, font:
|
|
|
4256
4640
|
cmd.append(f"--{citation_engine}")
|
|
4257
4641
|
else:
|
|
4258
4642
|
cmd.append("--citeproc")
|
|
4643
|
+
# pandoc_options after the citation-engine flag above: a
|
|
4644
|
+
# caller-supplied --lua-filter/--filter needs resolved
|
|
4645
|
+
# citations already in the AST (same invariant as the
|
|
4646
|
+
# auto-discovered lua_filters below).
|
|
4647
|
+
cmd += pandoc_options
|
|
4259
4648
|
if is_tex_target and tablewidth_auto:
|
|
4260
4649
|
cmd += ["--lua-filter", str(width_filter)]
|
|
4261
4650
|
for lua_filter in lua_filters:
|
|
@@ -4276,6 +4665,11 @@ def _convert_one(md_path: Path, out_dir: Path | None, presentation: bool, font:
|
|
|
4276
4665
|
if geometry_needed:
|
|
4277
4666
|
note("MARGIN", f"{md_path}: no geometry/margin set; "
|
|
4278
4667
|
f"using geometry:margin={DEFAULT_MARGIN} on LaTeX-family engines")
|
|
4668
|
+
margin_options = (None if auto_disabled(no_auto, "margin")
|
|
4669
|
+
else frontmatter_margin_geometry_options(md_path, variables, metadata_files))
|
|
4670
|
+
if margin_options:
|
|
4671
|
+
note("MARGIN", f"{md_path}: margin: isn't a Pandoc variable LaTeX-family engines "
|
|
4672
|
+
f"read (geometry: is) -- using geometry:{','.join(margin_options)}")
|
|
4279
4673
|
monofont_needed = (not auto_disabled(no_auto, "monofont")
|
|
4280
4674
|
and has_code_spans(md_path.read_text(encoding="utf-8-sig"))
|
|
4281
4675
|
and not has_monofont(md_path, variables))
|
|
@@ -4343,7 +4737,8 @@ def _convert_one(md_path: Path, out_dir: Path | None, presentation: bool, font:
|
|
|
4343
4737
|
remaining = [e for e in engines[engine_index + 1:] if ENGINE_FAMILY.get(e, e) not in failed_families]
|
|
4344
4738
|
report_engine_failure(str(md_path), engine, result, remaining, debug)
|
|
4345
4739
|
continue
|
|
4346
|
-
with prepared_latex_inputs([title_source, *metadata_files], engine in LATEX_ENGINES
|
|
4740
|
+
with prepared_latex_inputs([title_source, *metadata_files], engine in LATEX_ENGINES,
|
|
4741
|
+
typst_engine=(engine == "typst")) as prepared, \
|
|
4347
4742
|
document_header_file(md_path, (bool(preamble_files) or bool(pdf_meta_snippet_text))
|
|
4348
4743
|
and engine in LATEX_ENGINES) as header_file, \
|
|
4349
4744
|
pdf_metadata_header_file(pdf_meta_snippet_text if engine in LATEX_ENGINES else None) as pdf_meta_file:
|
|
@@ -4367,6 +4762,9 @@ def _convert_one(md_path: Path, out_dir: Path | None, presentation: bool, font:
|
|
|
4367
4762
|
cmd += ["-V", f"mainfontfallback={fallback_font()}"]
|
|
4368
4763
|
if geometry_needed and engine in LATEX_ENGINES:
|
|
4369
4764
|
cmd += ["-V", f"geometry:margin={DEFAULT_MARGIN}"]
|
|
4765
|
+
elif margin_options and engine in LATEX_ENGINES:
|
|
4766
|
+
for margin_option in margin_options:
|
|
4767
|
+
cmd += ["-V", f"geometry:{margin_option}"]
|
|
4370
4768
|
if monofont_needed and engine in LATEX_ENGINES:
|
|
4371
4769
|
cmd += ["-V", f"monofont={default_monofont()}"]
|
|
4372
4770
|
if pagesize_typo and engine in LATEX_ENGINES:
|
|
@@ -4385,7 +4783,6 @@ def _convert_one(md_path: Path, out_dir: Path | None, presentation: bool, font:
|
|
|
4385
4783
|
cmd += ["-V", f"papersize={typst_size}"]
|
|
4386
4784
|
if shift_heading:
|
|
4387
4785
|
cmd += ["--shift-heading-level-by=-1"]
|
|
4388
|
-
cmd += pandoc_options
|
|
4389
4786
|
if preamble_files and engine in LATEX_ENGINES:
|
|
4390
4787
|
for preamble_file in preamble_files:
|
|
4391
4788
|
cmd += ["--include-in-header", str(preamble_file)]
|
|
@@ -4402,6 +4799,15 @@ def _convert_one(md_path: Path, out_dir: Path | None, presentation: bool, font:
|
|
|
4402
4799
|
cmd += crossref_filter_args(md_path, pandoc_options, no_auto, str(md_path))
|
|
4403
4800
|
if contains_citations(md_path) and "--citeproc" not in pandoc_options and not CITEPROC_DISABLED:
|
|
4404
4801
|
cmd.append("--citeproc")
|
|
4802
|
+
# pandoc_options after --citeproc, same reason as below: a
|
|
4803
|
+
# caller-supplied --lua-filter/--filter needs resolved
|
|
4804
|
+
# citations already in the AST. (This used to sit before
|
|
4805
|
+
# the --citeproc append above, which silently broke any
|
|
4806
|
+
# citeproc-dependent filter passed via extra CLI args --
|
|
4807
|
+
# e.g. `--lua-filter=some.lua` landed ahead of --citeproc
|
|
4808
|
+
# on the actual pandoc command line. Confirmed via
|
|
4809
|
+
# --verbose CMD output before this fix.)
|
|
4810
|
+
cmd += pandoc_options
|
|
4405
4811
|
# Lua filters go last: pandoc applies --citeproc and filters in
|
|
4406
4812
|
# command-line order, and a filter that renders cell contents to
|
|
4407
4813
|
# LaTeX needs the citations already resolved. csv-table has to
|
|
@@ -4948,13 +5354,22 @@ def main() -> None:
|
|
|
4948
5354
|
if report_geometry_needed:
|
|
4949
5355
|
report_note("MARGIN", "REPORT: no geometry/margin set; "
|
|
4950
5356
|
f"using geometry:margin={DEFAULT_MARGIN} on LaTeX-family targets")
|
|
5357
|
+
report_margin_options = (None if auto_disabled(report_no_auto, "margin")
|
|
5358
|
+
else frontmatter_margin_geometry_options(files[0], variables,
|
|
5359
|
+
metadata_files))
|
|
5360
|
+
if is_tex_target and report_margin_options:
|
|
5361
|
+
report_note("MARGIN", "REPORT: margin: isn't a Pandoc variable LaTeX-family "
|
|
5362
|
+
"targets read (geometry: is) -- using "
|
|
5363
|
+
f"geometry:{','.join(report_margin_options)}")
|
|
4951
5364
|
report_monofont_needed = (is_tex_target and not auto_disabled(report_no_auto, "monofont")
|
|
4952
5365
|
and any(has_code_spans(file.read_text(encoding="utf-8-sig")) for file in files)
|
|
4953
5366
|
and not has_monofont(files[0], variables))
|
|
4954
5367
|
if report_monofont_needed:
|
|
4955
5368
|
report_note("MONOFONT", "REPORT: has code but no monofont set; "
|
|
4956
5369
|
f"using {default_monofont()} on LaTeX-family targets")
|
|
4957
|
-
with prepared_latex_inputs([*files, *metadata_files], is_tex_target
|
|
5370
|
+
with prepared_latex_inputs([*files, *metadata_files], is_tex_target,
|
|
5371
|
+
typst_engine=(target_format == "typst"),
|
|
5372
|
+
doc_count=len(files)) as prepared, \
|
|
4958
5373
|
document_header_file(files[0], (bool(report_preambles) or bool(report_pdf_meta_snippet_text))
|
|
4959
5374
|
and is_tex_target) as report_header_file, \
|
|
4960
5375
|
pdf_metadata_header_file(report_pdf_meta_snippet_text) as report_pdf_meta_file:
|
|
@@ -4982,11 +5397,13 @@ def main() -> None:
|
|
|
4982
5397
|
cmd += ["-V", f"mainfont={report_first_font}"]
|
|
4983
5398
|
if report_geometry_needed:
|
|
4984
5399
|
cmd += ["-V", f"geometry:margin={DEFAULT_MARGIN}"]
|
|
5400
|
+
elif report_margin_options:
|
|
5401
|
+
for margin_option in report_margin_options:
|
|
5402
|
+
cmd += ["-V", f"geometry:{margin_option}"]
|
|
4985
5403
|
if report_monofont_needed:
|
|
4986
5404
|
cmd += ["-V", f"monofont={default_monofont()}"]
|
|
4987
5405
|
for variable in variables:
|
|
4988
5406
|
cmd += ["-V", variable]
|
|
4989
|
-
cmd += pandoc_options
|
|
4990
5407
|
if report_preambles and is_tex_target:
|
|
4991
5408
|
for preamble in report_preambles:
|
|
4992
5409
|
cmd += ["--include-in-header", str(preamble)]
|
|
@@ -5004,6 +5421,9 @@ def main() -> None:
|
|
|
5004
5421
|
and "--citeproc" not in pandoc_options
|
|
5005
5422
|
and not CITEPROC_DISABLED):
|
|
5006
5423
|
cmd.append("--citeproc")
|
|
5424
|
+
# pandoc_options after --citeproc: see convert_one's
|
|
5425
|
+
# matching comment.
|
|
5426
|
+
cmd += pandoc_options
|
|
5007
5427
|
if is_tex_target and not auto_disabled(report_no_auto, "tablewidth"):
|
|
5008
5428
|
cmd += ["--lua-filter", str(width_filter)]
|
|
5009
5429
|
log_cmd(cmd, pandoc_cwd, args.verbose)
|
|
@@ -5027,6 +5447,12 @@ def main() -> None:
|
|
|
5027
5447
|
if geometry_needed:
|
|
5028
5448
|
report_note("MARGIN", "REPORT: no geometry/margin set; "
|
|
5029
5449
|
f"using geometry:margin={DEFAULT_MARGIN} on LaTeX-family engines")
|
|
5450
|
+
margin_options = (None if auto_disabled(report_no_auto, "margin")
|
|
5451
|
+
else frontmatter_margin_geometry_options(files[0], variables, metadata_files))
|
|
5452
|
+
if margin_options:
|
|
5453
|
+
report_note("MARGIN", "REPORT: margin: isn't a Pandoc variable LaTeX-family "
|
|
5454
|
+
"engines read (geometry: is) -- using "
|
|
5455
|
+
f"geometry:{','.join(margin_options)}")
|
|
5030
5456
|
monofont_needed = (not auto_disabled(report_no_auto, "monofont")
|
|
5031
5457
|
and any(has_code_spans(file.read_text(encoding="utf-8-sig")) for file in files)
|
|
5032
5458
|
and not has_monofont(files[0], variables))
|
|
@@ -5057,7 +5483,9 @@ def main() -> None:
|
|
|
5057
5483
|
print(f"SKIP REPORT: {engine} shares the {family} engine with an earlier "
|
|
5058
5484
|
f"failure; skipping", file=sys.stderr)
|
|
5059
5485
|
continue
|
|
5060
|
-
with prepared_latex_inputs([*files, *metadata_files], engine in LATEX_ENGINES
|
|
5486
|
+
with prepared_latex_inputs([*files, *metadata_files], engine in LATEX_ENGINES,
|
|
5487
|
+
typst_engine=(engine == "typst"),
|
|
5488
|
+
doc_count=len(files)) as prepared, \
|
|
5061
5489
|
pdf_metadata_header_file(
|
|
5062
5490
|
report_pdf_meta_snippet_text if engine in LATEX_ENGINES else None
|
|
5063
5491
|
) as report_pdf_meta_file:
|
|
@@ -5072,11 +5500,13 @@ def main() -> None:
|
|
|
5072
5500
|
cmd += report_resource_path
|
|
5073
5501
|
if geometry_needed and engine in LATEX_ENGINES:
|
|
5074
5502
|
cmd += ["-V", f"geometry:margin={DEFAULT_MARGIN}"]
|
|
5503
|
+
elif margin_options and engine in LATEX_ENGINES:
|
|
5504
|
+
for margin_option in margin_options:
|
|
5505
|
+
cmd += ["-V", f"geometry:{margin_option}"]
|
|
5075
5506
|
if monofont_needed and engine in LATEX_ENGINES:
|
|
5076
5507
|
cmd += ["-V", f"monofont={default_monofont()}"]
|
|
5077
5508
|
for variable in variables:
|
|
5078
5509
|
cmd += ["-V", variable]
|
|
5079
|
-
cmd += pandoc_options
|
|
5080
5510
|
if preambles and engine in LATEX_ENGINES:
|
|
5081
5511
|
for preamble in preambles:
|
|
5082
5512
|
cmd += ["--include-in-header", str(preamble)]
|
|
@@ -5088,6 +5518,9 @@ def main() -> None:
|
|
|
5088
5518
|
and "--citeproc" not in pandoc_options
|
|
5089
5519
|
and not CITEPROC_DISABLED):
|
|
5090
5520
|
cmd.append("--citeproc")
|
|
5521
|
+
# pandoc_options after --citeproc: see convert_one's
|
|
5522
|
+
# matching comment.
|
|
5523
|
+
cmd += pandoc_options
|
|
5091
5524
|
if report_tablewidth_auto and engine in LATEX_ENGINES:
|
|
5092
5525
|
cmd += ["--lua-filter", str(width_filter)]
|
|
5093
5526
|
log_cmd(cmd, pandoc_cwd, args.verbose)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pdfmd-cli
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.15.1
|
|
4
4
|
Summary: One command from Markdown to a good-looking PDF: a Pandoc wrapper with smart defaults, project-file discovery and a multi-engine fallback chain
|
|
5
5
|
Author: Ali Perdekhan
|
|
6
6
|
License-Expression: MIT
|
|
@@ -20,7 +20,7 @@ Dynamic: license-file
|
|
|
20
20
|
|
|
21
21
|
# pdfmd
|
|
22
22
|
|
|
23
|
-
[](https://github.com/aliperdehan/pdfmd/actions/workflows/publish.yml) [](https://pypi.org/project/pdfmd-cli/)
|
|
24
24
|
|
|
25
25
|
**One command from Markdown to a good-looking PDF.** `pdfmd` wraps
|
|
26
26
|
[Pandoc](https://pandoc.org) and fills in everything you would otherwise
|
|
@@ -101,7 +101,7 @@ winget install --id JohnMacFarlane.Pandoc; winget install --id Typst.Typst
|
|
|
101
101
|
|
|
102
102
|
Every push is tested on Windows, macOS and Linux (Pandoc + Typst: single
|
|
103
103
|
files, CSV tables, a book, HTML output); see the
|
|
104
|
-
[Test workflow](https://github.com/aliperdehan/pdfmd/actions/workflows/
|
|
104
|
+
[Test and publish workflow](https://github.com/aliperdehan/pdfmd/actions/workflows/publish.yml).
|
|
105
105
|
LaTeX engines aren't part of that automated test on Windows yet.
|
|
106
106
|
|
|
107
107
|
Optional extras: [Quarto](https://quarto.org) for `.qmd` files,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|