fix: Befunde aus der QA-Prüfung (Prompt_Kontrolle.md)
- F1: Nicht beschreibbares Ausgabeziel liefert eine verständliche Meldung und Exit 1 statt "Interner Fehler" (Exit 4). - F2: Anker von <dt id> stehen vor dem fett gesetzten Glossarbegriff statt innerhalb der **-Marker. - F3: Formeln als <img src="*.svg"> erscheinen im Bericht mit origin "svg" statt "image". Regressionstests für alle drei Befunde ergänzt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
02449734a2
commit
3b07fcf01a
7 changed files with 54 additions and 13 deletions
|
|
@ -91,7 +91,7 @@ python3 epub2md.py buch.epub output/buch --force --report berichte/buch.json
|
||||||
| Code | Bedeutung |
|
| Code | Bedeutung |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 0 | Erfolgreich, ggf. mit nicht-kritischen Warnungen |
|
| 0 | Erfolgreich, ggf. mit nicht-kritischen Warnungen |
|
||||||
| 1 | Ungültige Parameter, Eingabedatei fehlt/nicht lesbar, Ausgabeverzeichnis belegt |
|
| 1 | Ungültige Parameter, Eingabedatei fehlt/nicht lesbar, Ausgabeverzeichnis belegt oder nicht beschreibbar (Schreibfehler) |
|
||||||
| 2 | ZIP-/Container-Fehler, DRM, unsichere ZIP-Pfade (mit `--strict`) |
|
| 2 | ZIP-/Container-Fehler, DRM, unsichere ZIP-Pfade (mit `--strict`) |
|
||||||
| 3 | OPF-/Spine-/XHTML-Fehler (z. B. leere Spine; mit `--strict` auch einzelne defekte Dokumente) |
|
| 3 | OPF-/Spine-/XHTML-Fehler (z. B. leere Spine; mit `--strict` auch einzelne defekte Dokumente) |
|
||||||
| 4 | Unerwarteter interner Fehler (Traceback mit `--verbose`) |
|
| 4 | Unerwarteter interner Fehler (Traceback mit `--verbose`) |
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ kapitelweiser Markdown-Dateien mit Medien, Metadaten und Konvertierungsbericht.
|
||||||
EPILOG = """\
|
EPILOG = """\
|
||||||
Rückgabecodes:
|
Rückgabecodes:
|
||||||
0 Erfolgreich (ggf. mit nicht-kritischen Warnungen)
|
0 Erfolgreich (ggf. mit nicht-kritischen Warnungen)
|
||||||
1 Ungültige Parameter, Eingabedatei nicht lesbar oder Ausgabeverzeichnis belegt
|
1 Ungültige Parameter, Eingabedatei nicht lesbar, Ausgabeverzeichnis belegt/nicht beschreibbar
|
||||||
2 EPUB-/ZIP-/Containerfehler (auch DRM, unsichere ZIP-Pfade mit --strict)
|
2 EPUB-/ZIP-/Containerfehler (auch DRM, unsichere ZIP-Pfade mit --strict)
|
||||||
3 OPF-/Spine-/XHTML-Verarbeitungsfehler
|
3 OPF-/Spine-/XHTML-Verarbeitungsfehler
|
||||||
4 Unerwarteter interner Fehler
|
4 Unerwarteter interner Fehler
|
||||||
|
|
|
||||||
|
|
@ -510,31 +510,33 @@ class Renderer:
|
||||||
anchor = self.anchors_of(el)
|
anchor = self.anchors_of(el)
|
||||||
out: list[str] = [anchor] if anchor else []
|
out: list[str] = [anchor] if anchor else []
|
||||||
terms: list[str] = []
|
terms: list[str] = []
|
||||||
|
term_anchors = "" # anchors of <dt> elements go in front of the bold term, not inside it
|
||||||
for child in el:
|
for child in el:
|
||||||
if not is_element(child):
|
if not is_element(child):
|
||||||
continue
|
continue
|
||||||
name = local(child.tag)
|
name = local(child.tag)
|
||||||
if name == "dt":
|
if name == "dt":
|
||||||
t = join_inline(self.inline_children(child), hard_break=" ")
|
terms.append(join_inline(self.inline_children(child), hard_break=" "))
|
||||||
a = self.anchors_of(child)
|
term_anchors += self.anchors_of(child)
|
||||||
terms.append(a + t)
|
|
||||||
elif name == "dd":
|
elif name == "dd":
|
||||||
blocks = self.flow(child)
|
blocks = self.flow(child)
|
||||||
a = self.anchors_of(child)
|
a = self.anchors_of(child)
|
||||||
term = " / ".join(terms) if terms else ""
|
term = " / ".join(terms) if terms else ""
|
||||||
terms = []
|
ta, terms, term_anchors = term_anchors, [], ""
|
||||||
if term and len(blocks) == 1 and "\n" not in blocks[0] and not blocks[0].startswith(("```", "|", "<")):
|
if term and len(blocks) == 1 and "\n" not in blocks[0] and not blocks[0].startswith(("```", "|", "<")):
|
||||||
out.append(f"**{term}:** {a}{blocks[0]}")
|
out.append(f"{ta}**{term}:** {a}{blocks[0]}")
|
||||||
else:
|
else:
|
||||||
if term:
|
if term:
|
||||||
out.append(f"**{term}**")
|
out.append(f"{ta}**{term}**")
|
||||||
|
elif ta:
|
||||||
|
out.append(ta)
|
||||||
if a:
|
if a:
|
||||||
out.append(a)
|
out.append(a)
|
||||||
out.extend(blocks)
|
out.extend(blocks)
|
||||||
elif name == "div": # HTML5 allows <div> grouping inside <dl>
|
elif name == "div": # HTML5 allows <div> grouping inside <dl>
|
||||||
out.extend(self.definition_list(child))
|
out.extend(self.definition_list(child))
|
||||||
for t in terms:
|
if terms:
|
||||||
out.append(f"**{t}**")
|
out.append(term_anchors + " / ".join(f"**{t}**" for t in terms))
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def figure(self, el: Any) -> list[str]:
|
def figure(self, el: Any) -> list[str]:
|
||||||
|
|
@ -1008,7 +1010,8 @@ class Renderer:
|
||||||
self.ch.stats.math_block += 1
|
self.ch.stats.math_block += 1
|
||||||
else:
|
else:
|
||||||
self.ch.stats.math_inline += 1
|
self.ch.stats.math_inline += 1
|
||||||
self.formulas.append({"source": source, "chapter": self.file, "origin": "image",
|
origin = "svg" if path.lower().endswith(".svg") else "image"
|
||||||
|
self.formulas.append({"source": source, "chapter": self.file, "origin": origin,
|
||||||
"representation": "image", "display": str(display).lower()})
|
"representation": "image", "display": str(display).lower()})
|
||||||
if not tex:
|
if not tex:
|
||||||
self.diag.warn("formula-image", f"Formel liegt nur als Bild vor ({path.rsplit('/', 1)[-1]}); als Bild referenziert, keine semantische Textextraktion.", source, self.file)
|
self.diag.warn("formula-image", f"Formel liegt nur als Bild vor ({path.rsplit('/', 1)[-1]}); als Bild referenziert, keine semantische Textextraktion.", source, self.file)
|
||||||
|
|
|
||||||
|
|
@ -400,8 +400,11 @@ def _report(input_path: Path, sha: str, generated_at: str, package: Any, opts: O
|
||||||
def _write_output(reader: EpubReader, input_path: Path, output_dir: Path, opts: Options,
|
def _write_output(reader: EpubReader, input_path: Path, output_dir: Path, opts: Options,
|
||||||
files: dict[str, str], media: MediaRegistry) -> None:
|
files: dict[str, str], media: MediaRegistry) -> None:
|
||||||
parent = output_dir.resolve().parent
|
parent = output_dir.resolve().parent
|
||||||
parent.mkdir(parents=True, exist_ok=True)
|
try:
|
||||||
tmp = Path(tempfile.mkdtemp(prefix=".epub2md-", dir=parent))
|
parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
tmp = Path(tempfile.mkdtemp(prefix=".epub2md-", dir=parent))
|
||||||
|
except OSError as exc:
|
||||||
|
raise InputError(f"Ausgabeverzeichnis nicht beschreibbar: {parent} ({exc.strerror or exc})") from exc
|
||||||
try:
|
try:
|
||||||
for rel, content in files.items():
|
for rel, content in files.items():
|
||||||
_write_file(tmp, rel, content.encode("utf-8"))
|
_write_file(tmp, rel, content.encode("utf-8"))
|
||||||
|
|
@ -419,6 +422,9 @@ def _write_output(reader: EpubReader, input_path: Path, output_dir: Path, opts:
|
||||||
else:
|
else:
|
||||||
output_dir.rmdir()
|
output_dir.rmdir()
|
||||||
os.replace(tmp, output_dir)
|
os.replace(tmp, output_dir)
|
||||||
|
except OSError as exc:
|
||||||
|
shutil.rmtree(tmp, ignore_errors=True)
|
||||||
|
raise InputError(f"Schreibfehler im Ziel {output_dir}: {exc.strerror or exc}") from exc
|
||||||
except BaseException:
|
except BaseException:
|
||||||
shutil.rmtree(tmp, ignore_errors=True)
|
shutil.rmtree(tmp, ignore_errors=True)
|
||||||
raise
|
raise
|
||||||
|
|
|
||||||
|
|
@ -114,3 +114,11 @@ def test_layout_table_headings_are_recognized(convert):
|
||||||
assert "# 3 Grundlagen" in text
|
assert "# 3 Grundlagen" in text
|
||||||
assert "## 3.1 Einführung" in text
|
assert "## 3.1 Einführung" in text
|
||||||
assert "<table" not in text
|
assert "<table" not in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_glossary_anchor_is_placed_before_bold_term(convert):
|
||||||
|
"""QA-F2: the anchor of a <dt id> must not end up inside the bold markers."""
|
||||||
|
body = ('<h1>K</h1><p><a href="#g">API</a></p><section epub:type="glossary"><h2>Glossar</h2>'
|
||||||
|
'<dl><dt id="g">API</dt><dd>Schnittstelle.</dd></dl></section>')
|
||||||
|
text = convert(Book(docs=[Doc("c.xhtml", body)])).chapter("002")
|
||||||
|
assert '<a id="g"></a>**API:** Schnittstelle.' in text
|
||||||
|
|
|
||||||
|
|
@ -124,3 +124,20 @@ def test_example_books_structural_invariants(convert, epub: Path):
|
||||||
for m in report["media"]["files"]:
|
for m in report["media"]["files"]:
|
||||||
assert (run.out / m["file"]).is_file()
|
assert (run.out / m["file"]).is_file()
|
||||||
assert sum(c["stats"]["word_count"] for c in report["chapters"]) > 1000
|
assert sum(c["stats"]["word_count"] for c in report["chapters"]) > 1000
|
||||||
|
|
||||||
|
|
||||||
|
def test_unwritable_output_parent_gives_clear_error(convert, tmp_path):
|
||||||
|
"""QA-F1: a permission problem at the target must not be reported as internal error (exit 4)."""
|
||||||
|
import os
|
||||||
|
import stat
|
||||||
|
|
||||||
|
parent = tmp_path / "readonly"
|
||||||
|
parent.mkdir()
|
||||||
|
os.chmod(parent, stat.S_IRUSR | stat.S_IXUSR)
|
||||||
|
try:
|
||||||
|
run = convert(simple_book(), out=parent / "ziel")
|
||||||
|
finally:
|
||||||
|
os.chmod(parent, stat.S_IRWXU)
|
||||||
|
assert run.rc == 1
|
||||||
|
assert "Interner Fehler" not in run.stderr
|
||||||
|
assert "nicht beschreibbar" in run.stderr or "Schreibfehler" in run.stderr
|
||||||
|
|
|
||||||
|
|
@ -97,3 +97,10 @@ def test_math_image_mode(convert):
|
||||||
assert text.count("![") == 3 # svg + both formula images
|
assert text.count("![") == 3 # svg + both formula images
|
||||||
assert "<math" in text # pure MathML without image is not lost
|
assert "<math" in text # pure MathML without image is not lost
|
||||||
assert any(w["code"] == "mathml-no-image" for w in run.report["warnings"])
|
assert any(w["code"] == "mathml-no-image" for w in run.report["warnings"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_svg_file_formula_is_reported_as_svg(convert):
|
||||||
|
"""QA-F3: <img src="*.svg"> inside a formula container is an SVG formula in the report."""
|
||||||
|
report = convert(math_book()).report
|
||||||
|
origins = {f["origin"] for f in report["formulas"]["items"]}
|
||||||
|
assert "svg" in origins
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue