fix: Pandoc-Weg – EPUB-Links, Formeln im EPUB, portables TeX (QA3-04 bis QA3-06)
Prüfung aller 5 Beispielbücher über die Befehle der Buch-README (HTML,
EPUB, PDF mit weasyprint und xelatex):
- QA3-04: Der EPUB-Befehl der Buch-README hatte kein --mathml; im
Mathebuch blieben 1072 Formeln als TeX-Text. Jetzt 4904 als MathML.
- QA3-05: Kapitel-Startanker von epub2md_merge.py begannen mit einer
Ziffer (039-…). Pandocs EPUB-Writer löst Links darauf nicht auf (C-Buch
32, Mathebuch 20 tote Links). Jetzt kap-039-…; book.lua erkennt beide.
- QA3-06: portable_tex() passt Verlags-TeX für texmath an (\"u, \O,
@{…} und \arraycolsep in array, leeres \displaystyle vor Zellenende).
Mathebuch: 1 statt 9 nicht umsetzbare Formeln.
- Cover ohne Seitenzahl (print.css, book.lua).
Neue Tests: tests/test_pandoc.py (nur mit installiertem pandoc),
Parametertests für portable_tex. Andere Bücher byte-identisch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
6fe422919f
commit
64d0cb0357
10 changed files with 157 additions and 18 deletions
|
|
@ -33,6 +33,15 @@ Versionsnummern nach [Semantic Versioning](https://semver.org/lang/de/).
|
||||||
|
|
||||||
### Behoben
|
### Behoben
|
||||||
|
|
||||||
|
- `epub2md_merge.py`: Kapitel-Startanker heißen jetzt `kap-003-name` statt `003-name`. Pandocs EPUB-Ausgabe
|
||||||
|
löste Links auf IDs mit führender Ziffer nicht auf (C-Buch 32, Mathebuch 20 tote Links im EPUB).
|
||||||
|
`pandoc/book.lua` erkennt beide Formen (QA3-05).
|
||||||
|
- Buch-README: Der EPUB-Befehl enthält bei Büchern mit Formeln `--mathml`. Ohne blieben im Mathebuch
|
||||||
|
1072 Formeln als TeX-Text stehen (QA3-04).
|
||||||
|
- TeX aus `alt`-Texten wird für Pandoc portabel gemacht (`\"u` → `ü`, `\O` → `\emptyset`, `@{…}` und
|
||||||
|
`\arraycolsep` in `array`, leeres `\displaystyle` vor Zellenende). Im Mathebuch kann Pandoc damit 4985 statt
|
||||||
|
4977 Formeln setzen; vorher blieben 9 als TeX-Text stehen (QA3-06).
|
||||||
|
- `pandoc/print.css` und `pandoc/book.lua`: keine Seitenzahl auf dem Cover.
|
||||||
- CSS-Auswertung: Es zählen nur noch Stylesheets, die ein Dokument per `<link>` (oder `@import`) einbindet.
|
- CSS-Auswertung: Es zählen nur noch Stylesheets, die ein Dokument per `<link>` (oder `@import`) einbindet.
|
||||||
Bisher wirkten alle CSS-Dateien des Manifests, auch mitgelieferte Layouts anderer Buchreihen. Im C-Buch
|
Bisher wirkten alle CSS-Dateien des Manifests, auch mitgelieferte Layouts anderer Buchreihen. Im C-Buch
|
||||||
machte das aus fetten Buchstaben Inline-Code (`` `st`an`d`ard ``) und setzte falsche `<mark>`; eine
|
machte das aus fetten Buchstaben Inline-Code (`` `st`an`d`ard ``) und setzte falsche `<mark>`; eine
|
||||||
|
|
|
||||||
14
README.md
14
README.md
|
|
@ -312,7 +312,8 @@ verwendet und das Verzeichnis gibt den Namen vor.
|
||||||
Beim Zusammenfügen passiert Folgendes:
|
Beim Zusammenfügen passiert Folgendes:
|
||||||
|
|
||||||
- Die Frontmatter der einzelnen Kapitel wird entfernt.
|
- Die Frontmatter der einzelnen Kapitel wird entfernt.
|
||||||
- Jedes Kapitel erhält einen Startanker (`<a id="003-kapitelname"></a>`).
|
- Jedes Kapitel erhält einen Startanker (`<a id="kap-003-kapitelname"></a>`). Er beginnt mit einem Buchstaben,
|
||||||
|
weil Pandocs EPUB-Ausgabe Links auf IDs mit führender Ziffer nicht auflöst.
|
||||||
- Links zwischen Kapiteln (`003-x.md#anker`) werden zu Sprunglinks in der Gesamtdatei.
|
- Links zwischen Kapiteln (`003-x.md#anker`) werden zu Sprunglinks in der Gesamtdatei.
|
||||||
- Anker, die in mehreren Kapiteln vorkommen (z. B. `Sec1` in jedem Kapitel eines Springer-Buchs), werden
|
- Anker, die in mehreren Kapiteln vorkommen (z. B. `Sec1` in jedem Kapitel eines Springer-Buchs), werden
|
||||||
ab dem zweiten Vorkommen in `c012-Sec1` umbenannt, samt aller Links darauf.
|
ab dem zweiten Vorkommen in `c012-Sec1` umbenannt, samt aller Links darauf.
|
||||||
|
|
@ -390,9 +391,8 @@ Die `README.md` eines konvertierten Buches mit Kästen enthält die Befehle bere
|
||||||
**PDF immer über HTML erzeugen.** Beim direkten Weg Markdown → LaTeX verwirft Pandoc allen rohen HTML-Code.
|
**PDF immer über HTML erzeugen.** Beim direkten Weg Markdown → LaTeX verwirft Pandoc allen rohen HTML-Code.
|
||||||
Dabei gehen HTML-Tabellen samt Inhalt verloren, und `<a id>`-Sprungziele fehlen, sodass interne Links im PDF
|
Dabei gehen HTML-Tabellen samt Inhalt verloren, und `<a id>`-Sprungziele fehlen, sodass interne Links im PDF
|
||||||
ins Leere zeigen. Liest Pandoc dagegen die HTML-Fassung, macht es daraus echte Tabellen und Sprungziele.
|
ins Leere zeigen. Liest Pandoc dagegen die HTML-Fassung, macht es daraus echte Tabellen und Sprungziele.
|
||||||
Getestet mit dem C-Buch (405 Seiten, 34 Tabellen, 728 von 755 internen Links funktionieren; die übrigen
|
Getestet mit dem C-Buch (322 Seiten, 34 Tabellen; alle 831 internen Links haben ein Ziel, kein Zeichen
|
||||||
stehen in Codeblöcken innerhalb von Tabellen). Die `README.md` jedes konvertierten Buches enthält den
|
fehlt). Die `README.md` jedes konvertierten Buches enthält den passenden Befehl.
|
||||||
passenden Befehl.
|
|
||||||
|
|
||||||
Alternativ direkt aus den Kapiteldateien:
|
Alternativ direkt aus den Kapiteldateien:
|
||||||
|
|
||||||
|
|
@ -407,8 +407,10 @@ pandoc output/programmierbuch/chapters/*.md \
|
||||||
- Ausgaben mit `--math tex` (`\(…\)`) brauchen `-f markdown+tex_math_single_backslash`.
|
- Ausgaben mit `--math tex` (`\(…\)`) brauchen `-f markdown+tex_math_single_backslash`.
|
||||||
- Das YAML-Frontmatter jeder Kapiteldatei wird von Pandoc als Metadaten gelesen. Wer die Kapiteldateien
|
- Das YAML-Frontmatter jeder Kapiteldatei wird von Pandoc als Metadaten gelesen. Wer die Kapiteldateien
|
||||||
direkt zusammen verarbeitet, sollte mit `--no-frontmatter` konvertieren oder die Gesamtdatei verwenden.
|
direkt zusammen verarbeitet, sollte mit `--no-frontmatter` konvertieren oder die Gesamtdatei verwenden.
|
||||||
- Einzelne TeX-Befehle aus EPUBs (z. B. `\O` oder `@`-Konstrukte) kennt Pandocs Formelparser nicht. Er
|
- TeX aus dem E-Book wird für Pandocs Formelparser angepasst, ohne den Inhalt zu ändern: `\"u` → `ü`,
|
||||||
belässt sie als TeX, mit der Warnung „Could not convert TeX math“.
|
`\O` (leere Menge) → `\emptyset`, keine `@{…}`-Spaltentrenner und kein `\arraycolsep` in `array`, kein
|
||||||
|
leeres `\displaystyle` vor einem Zellenende. Seltene Handkonstruktionen (z. B. `\mathinner{…\raise…}`)
|
||||||
|
bleiben als TeX stehen, mit der Warnung „Could not convert TeX math“ (Mathebuch: 1 von 4986 Formeln).
|
||||||
|
|
||||||
## Bericht und Diagnose
|
## Bericht und Diagnose
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -365,7 +365,54 @@ def strip_tex_delimiters(text: str) -> str:
|
||||||
if t.startswith(o) and t.endswith(c) and len(t) >= len(o) + len(c):
|
if t.startswith(o) and t.endswith(c) and len(t) >= len(o) + len(c):
|
||||||
t = t[len(o) : len(t) - len(c)]
|
t = t[len(o) : len(t) - len(c)]
|
||||||
break
|
break
|
||||||
return t.strip()
|
return portable_tex(t.strip())
|
||||||
|
|
||||||
|
|
||||||
|
UMLAUTS = {"a": "ä", "o": "ö", "u": "ü", "A": "Ä", "O": "Ö", "U": "Ü"}
|
||||||
|
|
||||||
|
|
||||||
|
def portable_tex(tex: str) -> str:
|
||||||
|
"""Publisher TeX → TeX that Pandoc's formula parser (texmath, MathJax, KaTeX) understands, still valid LaTeX:
|
||||||
|
text accents \\"u → ü, \\O (empty set in Springer books) → \\emptyset, no @{…} column separators in
|
||||||
|
array specifications, no \\arraycolsep assignments, no empty \\displaystyle before a cell end. Only changes
|
||||||
|
spacing, never content."""
|
||||||
|
tex = re.sub(r'\\"(?:\{([aouAOU])\}|([aouAOU]))', lambda m: UMLAUTS[m.group(1) or m.group(2)], tex)
|
||||||
|
tex = re.sub(r"\\(?:text|mbox|mathrm)\s*\{\s*\\O\s*\}", r"\\emptyset", tex)
|
||||||
|
tex = re.sub(r"\\O(?![A-Za-z])", r"\\emptyset", tex)
|
||||||
|
tex = re.sub(r"\\arraycolsep\s*-?\d*\.?\d+\s*(?:pt|em|ex|mm|cm)", "", tex)
|
||||||
|
tex = re.sub(r"\\displaystyle\s*(?=\\\\|&|\\end\b)", "", tex) # no-op before a cell end; texmath fails
|
||||||
|
out: list[str] = []
|
||||||
|
pos = 0
|
||||||
|
for m in re.finditer(r"\\begin\{array\}\{", tex):
|
||||||
|
if m.start() < pos:
|
||||||
|
continue
|
||||||
|
end = _closing_brace(tex, m.end() - 1)
|
||||||
|
if end < 0:
|
||||||
|
break
|
||||||
|
spec = tex[m.end():end]
|
||||||
|
i = spec.find("@{")
|
||||||
|
while i >= 0:
|
||||||
|
close = _closing_brace(spec, i + 1)
|
||||||
|
if close < 0:
|
||||||
|
break
|
||||||
|
spec = spec[:i] + spec[close + 1:]
|
||||||
|
i = spec.find("@{")
|
||||||
|
out.append(tex[pos:m.end()] + spec)
|
||||||
|
pos = end
|
||||||
|
return "".join(out) + tex[pos:]
|
||||||
|
|
||||||
|
|
||||||
|
def _closing_brace(text: str, open_pos: int) -> int:
|
||||||
|
"""Index of the '}' matching the '{' at ``open_pos`` (-1 if unbalanced)."""
|
||||||
|
depth = 0
|
||||||
|
for i in range(open_pos, len(text)):
|
||||||
|
if text[i] == "{":
|
||||||
|
depth += 1
|
||||||
|
elif text[i] == "}":
|
||||||
|
depth -= 1
|
||||||
|
if depth == 0:
|
||||||
|
return i
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
def is_display_tex(text: str) -> bool:
|
def is_display_tex(text: str) -> bool:
|
||||||
|
|
|
||||||
|
|
@ -188,7 +188,7 @@ def assign_anchors(chapters: list[ChapterFile]) -> int:
|
||||||
for m in TOKEN.finditer(seg):
|
for m in TOKEN.finditer(seg):
|
||||||
if m.group("attr") and m.group("aname") == "id" and m.group("avalue") not in ids:
|
if m.group("attr") and m.group("aname") == "id" and m.group("avalue") not in ids:
|
||||||
ids.append(m.group("avalue"))
|
ids.append(m.group("avalue"))
|
||||||
start = stems[ch.rel]
|
start = "kap-" + stems[ch.rel] # ids must start with a letter (Pandoc's EPUB writer breaks '#039-…')
|
||||||
while start in used or start in ids:
|
while start in used or start in ids:
|
||||||
start = "kapitel-" + start
|
start = "kapitel-" + start
|
||||||
ch.start_anchor = start
|
ch.start_anchor = start
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,7 @@ def readme(md: BookMetadata, chapters: list[Chapter], endnotes: Chapter | None,
|
||||||
f"python3 epub2md_merge.py {shlex.quote(output_dir)}",
|
f"python3 epub2md_merge.py {shlex.quote(output_dir)}",
|
||||||
"# danach in diesem Ordner:",
|
"# danach in diesem Ordner:",
|
||||||
f"pandoc {md_} -s{math}{toc}{lua} -o {html_}",
|
f"pandoc {md_} -s{math}{toc}{lua} -o {html_}",
|
||||||
f"pandoc {md_} --toc{lua} -o {shlex.quote(stem + '.epub')}",
|
f"pandoc {md_} --toc{math}{lua} -o {shlex.quote(stem + '.epub')}",
|
||||||
] + (latex + weasy if math else weasy + latex)
|
] + (latex + weasy if math else weasy + latex)
|
||||||
out.append("## Weiterverarbeitung")
|
out.append("## Weiterverarbeitung")
|
||||||
out.append("```bash\n" + "\n".join(commands) + "\n```")
|
out.append("```bash\n" + "\n".join(commands) + "\n```")
|
||||||
|
|
@ -166,7 +166,7 @@ def readme(md: BookMetadata, chapters: list[Chapter], endnotes: Chapter | None,
|
||||||
out.append(f"Das Buch enthält {callouts} {noun} (`> [!TIP]` …). Der Filter `callouts.lua` macht daraus "
|
out.append(f"Das Buch enthält {callouts} {noun} (`> [!TIP]` …). Der Filter `callouts.lua` macht daraus "
|
||||||
"in HTML, EPUB und PDF gerahmte Kästen mit Titel; ohne ihn zeigt Pandoc „[!TIP]“ als Text.")
|
"in HTML, EPUB und PDF gerahmte Kästen mit Titel; ohne ihn zeigt Pandoc „[!TIP]“ als Text.")
|
||||||
if stats.get("formulas"):
|
if stats.get("formulas"):
|
||||||
out.append("Das Buch enthält Formeln: Für HTML `--mathml` (oder `--mathjax`) verwenden, sonst belässt "
|
out.append("Das Buch enthält Formeln: Für HTML und EPUB `--mathml` (bei HTML auch `--mathjax`) verwenden, sonst belässt "
|
||||||
"Pandoc Matrizen und ähnliche Konstrukte als TeX-Text.")
|
"Pandoc Matrizen und ähnliche Konstrukte als TeX-Text.")
|
||||||
out.append("## Reproduktion")
|
out.append("## Reproduktion")
|
||||||
out.append("```bash\n" + " ".join(shlex.quote(c) for c in command) + "\n```")
|
out.append("```bash\n" + " ".join(shlex.quote(c) for c in command) + "\n```")
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ This filter, for HTML output:
|
||||||
* builds the table of contents at the placeholder (use it INSTEAD of --toc; --toc-depth is honoured),
|
* builds the table of contents at the placeholder (use it INSTEAD of --toc; --toc-depth is honoured),
|
||||||
* drops Pandoc's title block when the book has its own title page, or moves it to the placeholder,
|
* drops Pandoc's title block when the book has its own title page, or moves it to the placeholder,
|
||||||
* shows the cover (first chapter with nothing but one picture) without the caption "Cover",
|
* shows the cover (first chapter with nothing but one picture) without the caption "Cover",
|
||||||
* turns the chapter anchors <a id="001-…"></a> into <div class="chapter-start"> so that print.css can
|
* turns the chapter anchors <a id="kap-001-…"></a> into <div class="chapter-start"> so that print.css can
|
||||||
start every chapter – including cover, title page and imprint – on a new page.
|
start every chapter – including cover, title page and imprint – on a new page.
|
||||||
|
|
||||||
For LaTeX (PDF route md -> html -> pdf, filter in BOTH steps, no --toc) the table of contents becomes
|
For LaTeX (PDF route md -> html -> pdf, filter in BOTH steps, no --toc) the table of contents becomes
|
||||||
|
|
@ -55,7 +55,7 @@ local function title_block(meta)
|
||||||
return pandoc.RawBlock("html", table.concat(html, "\n"))
|
return pandoc.RawBlock("html", table.concat(html, "\n"))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Paragraph that only holds chapter start anchors (<a id="001-cover"></a>) → id of the first one.
|
-- Paragraph that only holds chapter start anchors (<a id="kap-001-cover"></a>) → id of the first one.
|
||||||
local function chapter_anchor(block)
|
local function chapter_anchor(block)
|
||||||
if block.t ~= "Para" and block.t ~= "Plain" then
|
if block.t ~= "Para" and block.t ~= "Plain" then
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -65,7 +65,8 @@ local function chapter_anchor(block)
|
||||||
if inl.t ~= "RawInline" then
|
if inl.t ~= "RawInline" then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
local found = inl.text:match('^<a id="(%d%d%d%-[^"]*)">$') or inl.text:match('^<a id="(%d%d%d%-[^"]*)"></a>$')
|
local found = inl.text:match('^<a id="(kap%-%d%d%d%-[^"]*)">$') or inl.text:match('^<a id="(kap%-%d%d%d%-[^"]*)"></a>$')
|
||||||
|
or inl.text:match('^<a id="(%d%d%d%-[^"]*)">$') or inl.text:match('^<a id="(%d%d%d%-[^"]*)"></a>$') -- before 0.2.1
|
||||||
if found then
|
if found then
|
||||||
id = id or found
|
id = id or found
|
||||||
elseif inl.text ~= "</a>" then
|
elseif inl.text ~= "</a>" then
|
||||||
|
|
@ -143,6 +144,7 @@ local function latex_book(doc)
|
||||||
blocks:insert(pandoc.RawBlock("latex", "\\clearpage"))
|
blocks:insert(pandoc.RawBlock("latex", "\\clearpage"))
|
||||||
blocks:insert(block)
|
blocks:insert(block)
|
||||||
elseif block.t == "Div" and block.classes:includes("cover") then
|
elseif block.t == "Div" and block.classes:includes("cover") then
|
||||||
|
blocks:insert(pandoc.RawBlock("latex", "\\thispagestyle{empty}")) -- no page number on the cover
|
||||||
blocks:insert(block:walk({ Image = function(img) -- cover as large as the page allows
|
blocks:insert(block:walk({ Image = function(img) -- cover as large as the page allows
|
||||||
img.attributes.width = nil
|
img.attributes.width = nil
|
||||||
img.attributes.height = "20cm"
|
img.attributes.height = "20cm"
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
--pdf-engine=weasyprint -o buch.pdf
|
--pdf-engine=weasyprint -o buch.pdf
|
||||||
book.lua places the table of contents after cover, title page and imprint and marks chapter starts. */
|
book.lua places the table of contents after cover, title page and imprint and marks chapter starts. */
|
||||||
@page { size: A4; margin: 2cm 1.8cm; @bottom-center { content: counter(page); font-size: 9pt; } }
|
@page { size: A4; margin: 2cm 1.8cm; @bottom-center { content: counter(page); font-size: 9pt; } }
|
||||||
|
@page :first { @bottom-center { content: none; } } /* no page number on the cover / title page */
|
||||||
body { max-width: none; font-size: 10.5pt; line-height: 1.4; hyphens: auto; text-align: justify;
|
body { max-width: none; font-size: 10.5pt; line-height: 1.4; hyphens: auto; text-align: justify;
|
||||||
font-family: "DejaVu Serif", "Liberation Serif", Georgia, serif; }
|
font-family: "DejaVu Serif", "Liberation Serif", Georgia, serif; }
|
||||||
#title-block-header, #title-block-header h1.title { text-align: center; margin-bottom: 2em; }
|
#title-block-header, #title-block-header h1.title { text-align: center; margin-bottom: 2em; }
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import pytest
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
from conftest import PNG, SVG, Book, Doc
|
from conftest import PNG, SVG, Book, Doc
|
||||||
from epub2md.mathml import Unsupported, mathml_to_tex
|
from epub2md.mathml import Unsupported, mathml_to_tex, portable_tex
|
||||||
|
|
||||||
M = 'xmlns="http://www.w3.org/1998/Math/MathML"'
|
M = 'xmlns="http://www.w3.org/1998/Math/MathML"'
|
||||||
|
|
||||||
|
|
@ -111,6 +111,24 @@ def test_book_readme_suggests_mathml_for_pandoc_only_with_formulas(convert):
|
||||||
readme = (run.out / "README.md").read_text(encoding="utf-8")
|
readme = (run.out / "README.md").read_text(encoding="utf-8")
|
||||||
assert "## Weiterverarbeitung" in readme and "epub2md_merge.py" in readme
|
assert "## Weiterverarbeitung" in readme and "epub2md_merge.py" in readme
|
||||||
assert "--mathml" in readme
|
assert "--mathml" in readme
|
||||||
|
pandoc_lines = [ln for ln in readme.splitlines() if ln.startswith("pandoc ")]
|
||||||
|
assert pandoc_lines and all("--mathml" in ln for ln in pandoc_lines) # EPUB too, else TeX stays text (QA3-04)
|
||||||
plain = convert(Book(docs=[Doc("a.xhtml", "<h1>A</h1><p>Text</p>")]))
|
plain = convert(Book(docs=[Doc("a.xhtml", "<h1>A</h1><p>Text</p>")]))
|
||||||
plain_readme = (plain.out / "README.md").read_text(encoding="utf-8")
|
plain_readme = (plain.out / "README.md").read_text(encoding="utf-8")
|
||||||
assert "## Weiterverarbeitung" in plain_readme and "--mathml" not in plain_readme
|
assert "## Weiterverarbeitung" in plain_readme and "--mathml" not in plain_readme
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("tex,expected", [
|
||||||
|
(r'\mbox{gr\"un}, \text{F\"{u}r}', r"\mbox{grün}, \text{Für}"),
|
||||||
|
(r'\text{ f{\"u}r } j', r"\text{ f{ü}r } j"), # braces stay balanced
|
||||||
|
(r"{\mathscr L}(A,b)={{\O }}", r"{\mathscr L}(A,b)={{\emptyset }}"),
|
||||||
|
(r"M\cap N=\text{\O }", r"M\cap N=\emptyset"),
|
||||||
|
(r"\Omega\neq\O", r"\Omega\neq\emptyset"),
|
||||||
|
(r"\begin{array}{@{}l@{\qquad \quad }l@{}} a & b\end{array}", r"\begin{array}{ll} a & b\end{array}"),
|
||||||
|
(r"\arraycolsep8pt\begin{array}{c@{\,}r} x\end{array}", r"\begin{array}{cr} x\end{array}"),
|
||||||
|
(r"a & =&\displaystyle b, \\ & \vdots &\displaystyle \\ c", r"a & =&\displaystyle b, \\ & \vdots &\\ c"),
|
||||||
|
(r"\frac{a}{b}+\{x\}", r"\frac{a}{b}+\{x\}"),
|
||||||
|
])
|
||||||
|
def test_publisher_tex_is_made_portable(tex, expected):
|
||||||
|
"""Pandoc's texmath rejects \\", \\O, @{…} and \\arraycolsep (Springer alt texts, QA3-06)."""
|
||||||
|
assert portable_tex(tex) == expected
|
||||||
|
|
|
||||||
|
|
@ -34,10 +34,10 @@ def test_merge_complete_book(convert, tmp_path):
|
||||||
# links between chapters point into the merged file; colliding id "sec" renamed in chapter B
|
# links between chapters point into the merged file; colliding id "sec" renamed in chapter B
|
||||||
assert '<a id="sec"></a>' in text and '<a id="c002-sec"></a>' in text
|
assert '<a id="sec"></a>' in text and '<a id="c002-sec"></a>' in text
|
||||||
assert "[B](#c002-sec)" in text and "[A](#sec)" in text and "[hier](#sec)" in text
|
assert "[B](#c002-sec)" in text and "[A](#sec)" in text and "[hier](#sec)" in text
|
||||||
assert "[Kapitel B](#002-kapitel-b)" in text and '<a id="002-kapitel-b"></a>' in text
|
assert "[Kapitel B](#kap-002-kapitel-b)" in text and '<a id="kap-002-kapitel-b"></a>' in text
|
||||||
# media paths relative to the merged file, also inside image links
|
# media paths relative to the merged file, also inside image links
|
||||||
assert "" in text
|
assert "" in text
|
||||||
assert "[](#002-kapitel-b)" in text
|
assert "[](#kap-002-kapitel-b)" in text
|
||||||
# code untouched
|
# code untouched
|
||||||
assert '[kein](link.md) id="sec" `code`' in text
|
assert '[kein](link.md) id="sec" `code`' in text
|
||||||
# footnote defined in both chapters -> only once in the merged file
|
# footnote defined in both chapters -> only once in the merged file
|
||||||
|
|
@ -63,8 +63,8 @@ def test_merge_to_other_location_rewrites_media_paths(convert, tmp_path):
|
||||||
text = target.read_text(encoding="utf-8")
|
text = target.read_text(encoding="utf-8")
|
||||||
assert not text.startswith("---")
|
assert not text.startswith("---")
|
||||||
assert f"" in text
|
assert f"" in text
|
||||||
assert "1. [Kapitel A](#001-kapitel-a)" in text
|
assert "1. [Kapitel A](#kap-001-kapitel-a)" in text
|
||||||
assert "001-kapitel-a" in anchors_in(text)
|
assert "kap-001-kapitel-a" in anchors_in(text)
|
||||||
|
|
||||||
|
|
||||||
def test_merge_invalid_directory(tmp_path):
|
def test_merge_invalid_directory(tmp_path):
|
||||||
|
|
|
||||||
60
tests/test_pandoc.py
Normal file
60
tests/test_pandoc.py
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
"""Pandoc path: merged file → EPUB/HTML with the filters from pandoc/ (skipped without pandoc)."""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from conftest import Book, Doc
|
||||||
|
from epub2md.merge import main as merge_main
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.skipif(shutil.which("pandoc") is None, reason="pandoc nicht installiert")
|
||||||
|
FILTERS = Path(__file__).resolve().parent.parent / "pandoc"
|
||||||
|
|
||||||
|
|
||||||
|
def merged(convert, book: Book) -> Path:
|
||||||
|
run = convert(book)
|
||||||
|
assert merge_main([str(run.out)]) == 0
|
||||||
|
return run.out / f"{run.report['input']['name'][:-5]}.md"
|
||||||
|
|
||||||
|
|
||||||
|
def pandoc(md: Path, *args: str) -> subprocess.CompletedProcess:
|
||||||
|
return subprocess.run(["pandoc", md.name, *args], cwd=md.parent, capture_output=True, text=True, check=True)
|
||||||
|
|
||||||
|
|
||||||
|
def links_and_ids(epub: Path) -> tuple[list[tuple[str, str]], dict[str, set[str]]]:
|
||||||
|
with zipfile.ZipFile(epub) as z:
|
||||||
|
docs = {n: z.read(n).decode("utf-8") for n in z.namelist() if n.endswith(".xhtml")}
|
||||||
|
ids = {n: set(re.findall(r'\bid="([^"]+)"', t)) for n, t in docs.items()}
|
||||||
|
links = [(n, h) for n, t in docs.items() for h in re.findall(r'href="([^"]+)"', t)
|
||||||
|
if not re.match(r"^[a-z]+:", h) and not h.endswith(".css")]
|
||||||
|
return links, ids
|
||||||
|
|
||||||
|
|
||||||
|
def test_epub_links_to_chapter_starts_resolve(convert):
|
||||||
|
"""Chapter start anchors must begin with a letter: Pandoc's EPUB writer breaks '#039-…' (QA3-05)."""
|
||||||
|
docs = [Doc("a.xhtml", '<h1>Kapitel A</h1><p>Siehe <a href="b.xhtml">Kapitel B</a> und <a href="c.xhtml">Bild</a>.</p>'),
|
||||||
|
Doc("b.xhtml", "<h1>Kapitel B</h1><p>Text B.</p>"),
|
||||||
|
Doc("c.xhtml", "<p>Nur eine Bildseite ohne Überschrift.</p>")]
|
||||||
|
md = merged(convert, Book(docs=docs))
|
||||||
|
pandoc(md, "--toc", "-o", "buch.epub")
|
||||||
|
links, ids = links_and_ids(md.parent / "buch.epub")
|
||||||
|
broken = []
|
||||||
|
for doc, href in links:
|
||||||
|
file, _, frag = href.partition("#")
|
||||||
|
target = str(Path(doc).parent / file).replace("\\", "/") if file else doc
|
||||||
|
target = re.sub(r"[^/]+/\.\./", "", target)
|
||||||
|
if target not in ids or (frag and frag not in ids[target]):
|
||||||
|
broken.append(f"{doc}: {href}")
|
||||||
|
assert not broken
|
||||||
|
|
||||||
|
|
||||||
|
def test_book_filter_turns_chapter_anchors_into_chapter_starts(convert):
|
||||||
|
docs = [Doc("a.xhtml", "<h1>Kapitel A</h1><p>a</p>"), Doc("b.xhtml", "<h1>Kapitel B</h1><p>b</p>")]
|
||||||
|
md = merged(convert, Book(docs=docs))
|
||||||
|
html = pandoc(md, "-s", "--lua-filter", str(FILTERS / "book.lua"), "-t", "html").stdout
|
||||||
|
assert html.count('class="chapter-start"') == 2
|
||||||
|
assert 'id="TOC"' in html and "Kapitel B" in html
|
||||||
Loading…
Reference in a new issue