diff --git a/README.md b/README.md index d69c950..60e0ece 100644 --- a/README.md +++ b/README.md @@ -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`) | diff --git a/epub2md/cli.py b/epub2md/cli.py index def2d3e..277c14d 100644 --- a/epub2md/cli.py +++ b/epub2md/cli.py @@ -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 diff --git a/epub2md/html_converter.py b/epub2md/html_converter.py index 7725ac9..b3b741f 100644 --- a/epub2md/html_converter.py +++ b/epub2md/html_converter.py @@ -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
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
grouping inside
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) diff --git a/epub2md/pipeline.py b/epub2md/pipeline.py index 6b3e02b..0ec0ea7 100644 --- a/epub2md/pipeline.py +++ b/epub2md/pipeline.py @@ -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 - parent.mkdir(parents=True, exist_ok=True) - tmp = Path(tempfile.mkdtemp(prefix=".epub2md-", dir=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 diff --git a/tests/test_chapters.py b/tests/test_chapters.py index c836683..23adb2e 100644 --- a/tests/test_chapters.py +++ b/tests/test_chapters.py @@ -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 " must not end up inside the bold markers.""" + body = ('

K

API

Glossar

' + '
API
Schnittstelle.
') + text = convert(Book(docs=[Doc("c.xhtml", body)])).chapter("002") + assert '**API:** Schnittstelle.' in text diff --git a/tests/test_integration.py b/tests/test_integration.py index 650d822..4804eed 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -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 diff --git a/tests/test_math.py b/tests/test_math.py index 3e6be79..b3eba6c 100644 --- a/tests/test_math.py +++ b/tests/test_math.py @@ -97,3 +97,10 @@ def test_math_image_mode(convert): assert text.count("![") == 3 # svg + both formula images assert " 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