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 |
|
||||
| --- | --- |
|
||||
| 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`) |
|
||||
| 3 | OPF-/Spine-/XHTML-Fehler (z. B. leere Spine; mit `--strict` auch einzelne defekte Dokumente) |
|
||||
| 4 | Unerwarteter interner Fehler (Traceback mit `--verbose`) |
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ kapitelweiser Markdown-Dateien mit Medien, Metadaten und Konvertierungsbericht.
|
|||
EPILOG = """\
|
||||
Rückgabecodes:
|
||||
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)
|
||||
3 OPF-/Spine-/XHTML-Verarbeitungsfehler
|
||||
4 Unerwarteter interner Fehler
|
||||
|
|
|
|||
|
|
@ -510,31 +510,33 @@ class Renderer:
|
|||
anchor = self.anchors_of(el)
|
||||
out: list[str] = [anchor] if anchor else []
|
||||
terms: list[str] = []
|
||||
term_anchors = "" # anchors of <dt> elements go in front of the bold term, not inside it
|
||||
for child in el:
|
||||
if not is_element(child):
|
||||
continue
|
||||
name = local(child.tag)
|
||||
if name == "dt":
|
||||
t = join_inline(self.inline_children(child), hard_break=" ")
|
||||
a = self.anchors_of(child)
|
||||
terms.append(a + t)
|
||||
terms.append(join_inline(self.inline_children(child), hard_break=" "))
|
||||
term_anchors += self.anchors_of(child)
|
||||
elif name == "dd":
|
||||
blocks = self.flow(child)
|
||||
a = self.anchors_of(child)
|
||||
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(("```", "|", "<")):
|
||||
out.append(f"**{term}:** {a}{blocks[0]}")
|
||||
out.append(f"{ta}**{term}:** {a}{blocks[0]}")
|
||||
else:
|
||||
if term:
|
||||
out.append(f"**{term}**")
|
||||
out.append(f"{ta}**{term}**")
|
||||
elif ta:
|
||||
out.append(ta)
|
||||
if a:
|
||||
out.append(a)
|
||||
out.extend(blocks)
|
||||
elif name == "div": # HTML5 allows <div> grouping inside <dl>
|
||||
out.extend(self.definition_list(child))
|
||||
for t in terms:
|
||||
out.append(f"**{t}**")
|
||||
if terms:
|
||||
out.append(term_anchors + " / ".join(f"**{t}**" for t in terms))
|
||||
return out
|
||||
|
||||
def figure(self, el: Any) -> list[str]:
|
||||
|
|
@ -1008,7 +1010,8 @@ class Renderer:
|
|||
self.ch.stats.math_block += 1
|
||||
else:
|
||||
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()})
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
files: dict[str, str], media: MediaRegistry) -> None:
|
||||
parent = output_dir.resolve().parent
|
||||
try:
|
||||
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:
|
||||
for rel, content in files.items():
|
||||
_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:
|
||||
output_dir.rmdir()
|
||||
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:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
raise
|
||||
|
|
|
|||
|
|
@ -114,3 +114,11 @@ def test_layout_table_headings_are_recognized(convert):
|
|||
assert "# 3 Grundlagen" in text
|
||||
assert "## 3.1 Einführung" 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"]:
|
||||
assert (run.out / m["file"]).is_file()
|
||||
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 "<math" in text # pure MathML without image is not lost
|
||||
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