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 "