diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index 07f9cec..89fdebd 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -164,10 +164,6 @@ python3 epub2md.py python-buch.epub output/python-buch --code-language python Das hilft Editoren beim Einfärben des Codes. Nachteil: Auch Programmausgaben und Shell-Befehle im Buch werden dann als Python markiert. -Zu lange Codezeilen sind im gedruckten Buch oft umbrochen und die Fortsetzung mit `↪` markiert. Das -Programm fügt solche Zeilen automatisch wieder zusammen, damit der Code lauffähig ist. Wer die -Druckfassung unverändert haben möchte, gibt `--keep-wrap-markers` an. - ### Mathematikbuch ```bash @@ -313,13 +309,13 @@ Nein. Das Programm arbeitet vollständig offline und greift nicht aufs Netz zu. **Wird das Original-E-Book verändert?** Nein, es wird nur gelesen. Mit `--copy-original` wird zusätzlich eine Kopie in den Ausgabeordner gelegt. -**Warum heißen manche Kapiteldateien „Titelseite“, „Impressum“ oder nach ihren ersten Wörtern?** -Manche E-Book-Seiten haben weder Überschrift noch Titel. Das Programm erkennt sie dann am Inhalt. -Gelingt das nicht, werden die ersten Wörter der Seite verwendet, nur im Notfall der interne Dateiname. +**Warum heißen manche Kapiteldateien seltsam (z. B. `cinnertitle`)?** +Manche E-Book-Seiten (Titelseite, Impressum) haben weder Überschrift noch Titel. Dann wird der +interne Dateiname verwendet. -**Warum ist ein Codebeispiel falsch eingerückt?** -So steht es im E-Book selbst (Satzfehler des Verlags). Der Code wird bewusst zeichengetreu -übernommen. Nur die Druck-Umbruchzeichen `↪` werden aufgelöst (abschaltbar mit `--keep-wrap-markers`). +**Warum steht in einem Codebeispiel ein `↪` oder ist es falsch eingerückt?** +So steht es im E-Book selbst (Umbruchzeichen bzw. Satzfehler des Verlags). Der Code wird bewusst +zeichengetreu übernommen. **Können Dateinamen Umlaute enthalten?** Standardmäßig werden Umlaute umschrieben (`ä` → `ae`). Mit `--slug-language none` bleiben sie erhalten, @@ -349,7 +345,6 @@ python3 epub2md_merge.py --help | `--force` | frühere Ausgabe ersetzen | | `--chapter-level N` | ab welcher Überschriftenebene eine neue Datei beginnt (Standard 1) | | `--code-language SPRACHE` | Sprache für Codeblöcke ohne erkannte Sprache | -| `--keep-wrap-markers` | Druck-Umbruchzeichen `↪` im Code stehen lassen | | `--math auto/tex/mathml/image` | Formeldarstellung | | `--footnotes end-of-chapter/end-of-book/inline/keep-links` | Fußnoten-Platzierung | | `--format gfm/commonmark` | Markdown-Variante; `commonmark` für sehr strikte Programme | diff --git a/README.md b/README.md index 0e8650e..a460f63 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,6 @@ python3 epub2md.py buch.epub output/buch --force --report berichte/buch.json | `--images {relative,flat,original-path}` | `relative` | `relative`: `media/images/0001-name.ext`; `flat`: `media/name.ext`; `original-path`: `media/` | | `--slug-language {de,en,none}` | `de` | Dateinamen: `de` ä→ae, ß→ss; `en` ä→a, ß→ss; `none` Unicode bleibt | | `--code-language SPRACHE` | – | Sprachlabel für Codeblöcke ohne erkannte Sprache (z. B. `python`, `c`; Aliase wie `py` erlaubt). Erkannte Labels haben Vorrang; im Bericht `detected_by: "option"` | -| `--keep-wrap-markers` | aus | Druck-Umbruchzeichen `↪`/`➥` in Codeblöcken stehen lassen. Standard: Fortsetzungszeilen werden wieder zur ursprünglichen Codezeile zusammengefügt (Anzahl im Bericht: `wrap_lines_joined`) | | `--keep-html` | aus | Unbekannte/nicht sicher konvertierbare Elemente als bereinigtes HTML erhalten (sonst: Text übernommen + Eintrag in `diagnostics/unsupported-elements.md`) | | `--strict` | aus | Abbruch bei kritischen Fehlern (fehlende/unlesbare Spine-Dokumente, unsichere ZIP-Pfade, ungültige Spine-Verweise) | | `--force` | aus | Bestehendes Ausgabeverzeichnis ersetzen – siehe [Sicherheit](#sicherheit-und-fehlerbehandlung) | @@ -159,12 +158,7 @@ output/mein-buch/ 3. Eine XHTML-Datei, die nur mit einer tieferen Überschrift beginnt (z. B. eine Datei pro Abschnitt `1.2`), wird an das laufende Kapitel angehängt. 4. Dateien ohne Überschrift (Cover, Titelei, Widmung) werden eigene Kapitel. Ihr Titel kommt aus - NAV/NCX, `` oder `epub:type`. Fehlt all das, entscheidet der Inhalt: - - nur ein Bild am Buchanfang → „Cover“ - - Buchtitel und Autorenname → „Titelseite“ - - ©/ISBN/„urheberrechtlich“ → „Impressum“ - - sonst die ersten acht Wörter - - erst als letzte Notlösung der Dateiname + NAV/NCX, `<title>`, `epub:type` oder dem Dateinamen. 5. Ein HTML-Inhaltsverzeichnis wird übersprungen, wenn NAV/NCX vorhanden ist (keine doppelte Ausgabe). Links darauf zeigen auf `README.md`. 6. Überschriftenebenen werden je Datei verschoben: Der Kapiteltitel ist `#`, alle weiteren beginnen bei `##`. @@ -341,14 +335,12 @@ Verlustfreiheit behauptet (`lossless: false`). ## Bekannte Einschränkungen -- Titel überschriftsloser Seiten werden aus dem Inhalt abgeleitet (Cover, Titelseite, Impressum, erste - Wörter). Das ist eine Heuristik und trifft nicht jede Seite. -- Aneinanderstoßende Feld-Spans ohne Leerraum (z. B. Autor | Titel | Reihe in Springer-Kapitelköpfen) - erhalten ein trennendes Leerzeichen. Das gilt nur bei unterschiedlichen Klassen und wenn rechts ein - Großbuchstabe oder eine Ziffer folgt. Andere direkt aneinanderstoßende Inline-Elemente bleiben - unverändert, damit Initialen und Wortteile nicht zerrissen werden. -- Fehlerhafte Einrückungen der Vorlage werden zeichengetreu übernommen. Druck-Fortsetzungszeichen - (`↪`/`➥`) am Zeilenanfang werden dagegen standardmäßig aufgelöst (siehe `--keep-wrap-markers`). +- Überschriftslose Dokumente ohne NAV-Eintrag und ohne `<title>` werden nach dem Dateinamen benannt + (z. B. `cinnertitle`). +- Aneinanderstoßende Inline-Elemente, zwischen denen auch in der Quelle kein Leerraum steht, laufen + zusammen (z. B. Autor- und Titelzeilen in Springer-Kapitelköpfen). +- Druck-Fortsetzungszeichen (`↪`) in Listings und fehlerhafte Einrückungen der Vorlage werden + zeichengetreu übernommen. - Formelbilder ohne TeX-Alternativtext bleiben Bilder. - Audio/Video sind implementiert, aber nur mit synthetischen Daten geprüft. Für Bildbände gibt es bislang keine Testbücher. diff --git a/epub2md/chapters.py b/epub2md/chapters.py index e9a767a..89472ab 100644 --- a/epub2md/chapters.py +++ b/epub2md/chapters.py @@ -86,12 +86,9 @@ def semantic_types(el: Any) -> list[str]: class ChapterBuilder: - def __init__(self, chapter_level: int, nav: list[NavEntry], diag: Diagnostics, - book_title: str = "", authors: list[str] | None = None) -> None: + def __init__(self, chapter_level: int, nav: list[NavEntry], diag: Diagnostics) -> None: self.level = chapter_level self.diag = diag - self.book_title = book_title - self.author_names = _surnames(authors or []) self.nav_titles: dict[str, str] = {} self._index_nav(nav) @@ -233,10 +230,6 @@ class ChapterBuilder: ch.title = _type_label(types) or nav or doc.title else: ch.title = nav or doc.title or _type_label(types) - if not ch.title: - ch.title, content_type = self._title_from_content(items, number) - if content_type: - types.append(content_type) ch.title = ch.title or doc.path.rsplit("/", 1)[-1].rsplit(".", 1)[0] ch.synthetic_title = True if not doc.linear and all(not d or d == doc.path for d in ch.docs[:1]): @@ -244,26 +237,6 @@ class ChapterBuilder: ch.types = list(dict.fromkeys(types)) return ch - def _title_from_content(self, items: list[tuple[str, Any]], number: int) -> tuple[str, str]: - """Title for a document without heading, nav entry and <title>: classify its content.""" - text = norm_ws(" ".join(it if isinstance(it, str) else text_content(it) - for _, it in items if isinstance(it, str) or is_element(it))) - has_image = any(is_element(it) and any(local(e.tag) in ("img", "svg", "image") for e in iter_elements(it)) - for _, it in items) - if not text and has_image: - return ("Cover", "cover") if number == 1 else ("Bild", "") - low = text.lower() - if self.book_title and len(self.book_title) >= 4 and self.book_title.lower() in low \ - and any(name.lower() in low for name in self.author_names): - return "Titelseite", "titlepage" - if re.search(r"©|\bisbn\b|copyright|urheberrechtlich|alle rechte vorbehalten|all rights reserved", low): - return "Impressum", "colophon" - words = text.split() - if not words: - return "", "" - short = " ".join(words[:8]) - return (short + " …" if len(words) > 8 else short), "" - def _finalize(self, ch: Chapter) -> None: kind = classify_title(ch.title) if kind and kind not in ch.types: @@ -303,17 +276,6 @@ def leading_heading_level(item: Any) -> int: return 0 -def _surnames(authors: list[str]) -> list[str]: - """'Adler-Olsen, Jussi' -> 'Adler-Olsen'; 'Erika Musterfrau' -> 'Musterfrau'.""" - names = [] - for a in authors: - a = a.strip() - name = a.split(",")[0].strip() if "," in a else (a.split()[-1] if a.split() else "") - if len(name) >= 3: - names.append(name) - return names - - def _meaningful(items: list[tuple[str, Any]]) -> bool: return _first_meaningful(items) is not None diff --git a/epub2md/cli.py b/epub2md/cli.py index fbcb2e5..e6defcb 100644 --- a/epub2md/cli.py +++ b/epub2md/cli.py @@ -82,9 +82,6 @@ def build_parser() -> argparse.ArgumentParser: p.add_argument("--code-language", type=code_language, default=None, metavar="SPRACHE", help="Sprachlabel für Codeblöcke ohne erkannte Sprache, z. B. python oder c " "(erkannte Labels haben Vorrang; Standard: kein Label)") - p.add_argument("--keep-wrap-markers", action="store_true", - help="Druck-Umbruchzeichen (↪/➥) in Codeblöcken nicht entfernen; Standard: fortgesetzte " - "Zeilen werden wieder zur ursprünglichen Codezeile zusammengefügt") p.add_argument("--strict", action="store_true", help="Bei kritischen Fehlern (fehlende Spine-Dateien, unsichere Pfade, kaputtes XHTML) abbrechen") p.add_argument("--force", action="store_true", @@ -117,7 +114,6 @@ def main(argv: list[str] | None = None) -> int: slug_language=args.slug_language, keep_html=args.keep_html, code_language=args.code_language or "", - keep_wrap_markers=args.keep_wrap_markers, strict=args.strict, verbose=args.verbose, dry_run=args.dry_run, diff --git a/epub2md/codeblocks.py b/epub2md/codeblocks.py index 0fcc883..65c3b13 100644 --- a/epub2md/codeblocks.py +++ b/epub2md/codeblocks.py @@ -180,27 +180,6 @@ def detect_language_from_content(code: str) -> str: return "" -WRAP_MARKER = re.compile(r"^[ \t]*[↪➥][ \t]?") - - -def join_wrap_markers(code: str) -> tuple[str, int]: - """Undo print line wrapping: a line starting with ↪/➥ continues the previous line. - - Publishers wrap over-long code lines at a space and mark the continuation; the logical line is - restored with exactly one space. Returns (code, number of joined lines). - """ - out: list[str] = [] - joined = 0 - for line in code.split("\n"): - m = WRAP_MARKER.match(line) - if m and out: - out[-1] = out[-1].rstrip(" \t") + " " + line[m.end():].lstrip(" \t") - joined += 1 - else: - out.append(line) - return "\n".join(out), joined - - def fence_for(code: str) -> str: longest = max((len(m) for m in re.findall(r"`+", code)), default=0) return "`" * max(3, longest + 1) diff --git a/epub2md/html_converter.py b/epub2md/html_converter.py index b007651..7490a17 100644 --- a/epub2md/html_converter.py +++ b/epub2md/html_converter.py @@ -24,7 +24,6 @@ from .codeblocks import ( fence_for, inline_code, is_block_code_container, - join_wrap_markers, is_inline_code, ) from .diagnostics import Diagnostics @@ -91,7 +90,6 @@ class RenderOptions: keep_html: bool = False language: str = "de" code_language: str = "" # --code-language: label for blocks without a detected language - keep_wrap_markers: bool = False # --keep-wrap-markers: do not join ↪-continued code lines def md_escape(text: str, table: bool = False) -> str: @@ -420,9 +418,6 @@ class Renderer: out = [anchors] if anchors else [] if not code.strip(): return out - joined = 0 - if not self.opt.keep_wrap_markers: - code, joined = join_wrap_markers(code) lang, how = detect_language(el) if not lang: lang = detect_language_from_content(code) @@ -434,7 +429,7 @@ class Renderer: if not lang: self.ch.stats.code_blocks_unlabeled += 1 self.code_blocks.append({"source": self._source(el), "chapter": self.file, "language": lang, - "detected_by": how, "lines": code.count("\n") + 1, "wrap_lines_joined": joined}) + "detected_by": how, "lines": code.count("\n") + 1}) fence = fence_for(code) out.append(f"{fence}{lang}\n{code}\n{fence}") return out diff --git a/epub2md/normalize.py b/epub2md/normalize.py index 6152766..886c9b0 100644 --- a/epub2md/normalize.py +++ b/epub2md/normalize.py @@ -102,35 +102,6 @@ def normalize_doc(doc: Doc, diag: Diagnostics, title_tables: bool) -> None: doc.toc_ids.update(e.get("id") for e in iter_elements(el) if e.get("id")) remove_keep_tail(el) diag.info("html-toc-removed", "HTML-Inhaltsverzeichnis entfernt (ersetzt durch README.md).", doc.path) - separated = _separate_field_spans(body) - if separated: - diag.info("inline-separator-added", - f"{separated} Leerzeichen zwischen aneinanderstoßenden Feld-Spans eingefügt.", doc.path) - - -def _separate_field_spans(body: Any) -> int: - """Insert a space between adjacent class-styled <span> "fields" that touch without whitespace. - - Publishers sometimes lay out metadata as sibling spans (author | title | series) separated only by - CSS. Narrow rule to keep drop caps ("D|as") and split words ("Haupt|bahnhof") intact: both spans - carry different classes, no text between them, both have >= 2 characters and the right one starts - with an uppercase letter or digit. - """ - count = 0 - for parent in iter_elements(body): - children = [c for c in parent if is_element(c)] - for left, right in zip(children, children[1:]): - if local(left.tag) != "span" or local(right.tag) != "span" or left.getnext() is not right: - continue - if left.tail or not classes(left) or not classes(right) or classes(left) == classes(right): - continue - lt, rt = text_content(left), text_content(right) - if len(lt.strip()) < 2 or len(rt.strip()) < 2 or lt[-1].isspace() or rt[0].isspace(): - continue - if rt[0].isupper() or rt[0].isdigit(): - left.tail = " " - count += 1 - return count def _attached(el: Any, body: Any) -> bool: diff --git a/epub2md/pipeline.py b/epub2md/pipeline.py index 60c01df..1bc1454 100644 --- a/epub2md/pipeline.py +++ b/epub2md/pipeline.py @@ -46,7 +46,6 @@ class Options: slug_language: str = "de" keep_html: bool = False code_language: str = "" - keep_wrap_markers: bool = False strict: bool = False verbose: bool = False dry_run: bool = False @@ -131,36 +130,11 @@ def _load_docs(reader: EpubReader, package: Any, diag: Diagnostics) -> list[Doc] def _convert(reader: EpubReader, input_path: Path, output_dir: Path, opts: Options, diag: Diagnostics, generated_at: str, sha: str) -> Result: package = reader.load_package() - diag.step(f"EPUB {package.version}: {package.metadata.title!r}, {len(package.spine)} Spine-Dokumente") + md = package.metadata + diag.step(f"EPUB {package.version}: {md.title!r}, {len(package.spine)} Spine-Dokumente") docs = _load_docs(reader, package, diag) - _prepare_docs(docs, package, diag) - notes, chapters, resolver = _build_structure(docs, package, opts, diag) - rendered = _render(reader, package, chapters, resolver, notes, opts, diag) - files, all_chapters, report = _assemble(input_path, output_dir, opts, diag, generated_at, sha, package, docs, - notes, chapters, resolver, rendered) - if opts.dry_run: - return Result(report=report, chapters=all_chapters, files=files, output_dir=output_dir) - _write_output(reader, input_path, output_dir, opts, files, rendered.media) - if opts.report: - report_path = Path(opts.report) - report_path.parent.mkdir(parents=True, exist_ok=True) - report_path.write_text(writer.to_json(report), encoding="utf-8") - return Result(report=report, chapters=all_chapters, files=files, output_dir=output_dir) - - -@dataclass -class _Rendered: - renderer: Renderer - media: MediaRegistry - cover_out: str - endnotes: Chapter | None - endnotes_md: str - skipped_formula_images: int - - -def _prepare_docs(docs: list[Doc], package: Any, diag: Diagnostics) -> None: - """Normalize publisher markup and drop HTML tables of contents that NAV/NCX already cover.""" + # --- normalization and HTML-TOC handling title_tables = book_uses_title_tables(docs) for doc in docs: normalize_doc(doc, diag, title_tables) @@ -181,16 +155,13 @@ def _prepare_docs(docs: list[Doc], package: Any, diag: Diagnostics) -> None: for d in docs: d.skipped = "" - -def _build_structure(docs: list[Doc], package: Any, opts: Options, - diag: Diagnostics) -> tuple[Any, list[Chapter], LinkResolver]: - """Notes, chapter units with file names, and the link resolver (incl. NAV/NCX targets).""" - md = package.metadata + # --- notes, chapters, links notes = collect_notes(docs, opts.footnotes, diag) diag.step(f"{len(notes.notes)} Anmerkungen erkannt") - chapters = ChapterBuilder(opts.chapter_level, package.toc, diag, md.title, md.authors).build(docs) + chapters = ChapterBuilder(opts.chapter_level, package.toc, diag).build(docs) _assign_filenames(chapters, opts.slug_language) - resolver = LinkResolver(docs, chapters, notes, "README.md" if opts.toc else None) + readme_path = "README.md" if opts.toc else None + resolver = LinkResolver(docs, chapters, notes, readme_path) nav_targets: list[tuple[str, str]] = [] def walk(entries: list[Any]) -> None: @@ -202,13 +173,8 @@ def _build_structure(docs: list[Doc], package: Any, opts: Options, walk(package.toc) resolver.add_targets(nav_targets) diag.step(f"{len(chapters)} Kapitel gebildet") - return notes, chapters, resolver - -def _render(reader: EpubReader, package: Any, chapters: list[Chapter], resolver: LinkResolver, notes: Any, - opts: Options, diag: Diagnostics) -> _Rendered: - """Render all chapters (and the end-of-book notes file) to Markdown and register media.""" - md = package.metadata + # --- rendering media = MediaRegistry(package, opts.images, opts.slug_language) cover_out = "" if md.cover_path and opts.extract_media and reader.exists(md.cover_path): @@ -217,7 +183,7 @@ def _render(reader: EpubReader, package: Any, chapters: list[Chapter], resolver: renderer = Renderer( RenderOptions(format=opts.format, footnotes=opts.footnotes, math=opts.math, extract_media=opts.extract_media, keep_html=opts.keep_html, language=language, - code_language=opts.code_language, keep_wrap_markers=opts.keep_wrap_markers), + code_language=opts.code_language), resolver, media, notes, diag, set(reader.names), ) width = max(3, len(str(len(chapters) + 1))) @@ -229,7 +195,6 @@ def _render(reader: EpubReader, package: Any, chapters: list[Chapter], resolver: ch.markdown = renderer.render_chapter(ch) ch.stats.word_count = _word_count(ch.markdown) _check_chapter_size(ch, opts.chapter_level, diag) - _report_wrap_markers(renderer, diag) endnotes: Chapter | None = None endnotes_md = "" if opts.footnotes == "end-of-book" and renderer.book_notes: @@ -238,23 +203,8 @@ def _render(reader: EpubReader, package: Any, chapters: list[Chapter], resolver: endnotes.filename = renderer.endnotes_file endnotes.stats.footnotes = len(renderer.book_notes) endnotes.stats.word_count = _word_count(endnotes_md) - skipped = _collect_remaining_media(reader, package, media, renderer, opts) - return _Rendered(renderer, media, cover_out, endnotes, endnotes_md, skipped) - -def _report_wrap_markers(renderer: Renderer, diag: Diagnostics) -> None: - wraps: dict[str, int] = {} - for block in renderer.code_blocks: - if block["wrap_lines_joined"]: - wraps[block["chapter"]] = wraps.get(block["chapter"], 0) + block["wrap_lines_joined"] - for chapter_file, joined in wraps.items(): - diag.info("wrap-markers-joined", f"{joined} Druck-Zeilenumbrüche (↪) in Codeblöcken zusammengeführt.", - chapter_file, chapter_file) - - -def _collect_remaining_media(reader: EpubReader, package: Any, media: MediaRegistry, renderer: Renderer, - opts: Options) -> int: - """Register fonts, CSS, audio and unreferenced images; returns the number of formula images replaced by TeX.""" + # --- remaining media (fonts, css, audio, unreferenced images) skipped_formula_images = 0 if opts.extract_media: for item in package.manifest.values(): @@ -271,18 +221,11 @@ def _collect_remaining_media(reader: EpubReader, package: Any, media: MediaRegis for mf in media.files.values(): if mf.data is None and reader.exists(mf.source): mf.size = reader.names[mf.source].file_size - return skipped_formula_images - -def _assemble(input_path: Path, output_dir: Path, opts: Options, diag: Diagnostics, generated_at: str, sha: str, - package: Any, docs: list[Doc], notes: Any, chapters: list[Chapter], resolver: LinkResolver, - rendered: _Rendered) -> tuple[dict[str, str], list[Chapter], dict[str, Any]]: - """Build all output files in memory (chapters, README, metadata, manifest, report, diagnostics).""" - md = package.metadata - renderer, media, endnotes = rendered.renderer, rendered.media, rendered.endnotes - for n in notes.notes.values(): - if n.ref_count and not n.label and notes.extracted: - diag.warn("note-not-rendered", "Anmerkung wurde referenziert, aber nicht ausgegeben.", n.key) + # --- assemble output files in memory + unresolved_notes = [n for n in notes.notes.values() if n.ref_count and not n.label and notes.extracted] + for n in unresolved_notes: + diag.warn("note-not-rendered", "Anmerkung wurde referenziert, aber nicht ausgegeben.", n.key) opf_dir = posixpath.dirname(package.opf_path) files: dict[str, str] = {} for ch in chapters: @@ -292,13 +235,13 @@ def _assemble(input_path: Path, output_dir: Path, opts: Options, diag: Diagnosti files[ch.filename] = front + ch.markdown if endnotes is not None: front = writer.chapter_frontmatter(endnotes, md, input_path.name, opf_dir, 0) if opts.frontmatter else "" - files[endnotes.filename] = front + rendered.endnotes_md + files[endnotes.filename] = front + endnotes_md all_chapters = chapters + ([endnotes] if endnotes else []) stats = _book_stats(all_chapters, media, renderer, diag) if opts.toc: - files["README.md"] = writer.readme(md, chapters, endnotes, package.toc, resolver, stats, rendered.cover_out, - package.version, input_path.name, opts.command, str(output_dir)) + files["README.md"] = writer.readme(md, chapters, endnotes, package.toc, resolver, stats, cover_out, + package.version, input_path.name, opts.command) existing = {mf.output for mf in media.files.values()} if opts.extract_media else set() if opts.copy_original: existing.add(f"source/{input_path.name}") @@ -310,14 +253,23 @@ def _assemble(input_path: Path, output_dir: Path, opts: Options, diag: Diagnosti stats["warnings"] = diag.count("warning") report = _report(input_path, sha, generated_at, package, opts, all_chapters, media, renderer, notes, diag, - docs, broken, rendered.skipped_formula_images) - files["metadata.yaml"] = writer.metadata_yaml(md, input_path.name, sha, package.version, rendered.cover_out, + docs, broken, skipped_formula_images) + files["metadata.yaml"] = writer.metadata_yaml(md, input_path.name, sha, package.version, cover_out, opts.public(), generated_at) files["manifest.json"] = writer.to_json(_manifest(md, all_chapters, media, diag)) files["conversion-report.json"] = writer.to_json(report) files["diagnostics/warnings.md"] = writer.warnings_md(diag) files["diagnostics/unsupported-elements.md"] = writer.unsupported_md(diag) - return files, all_chapters, report + + if opts.dry_run: + return Result(report=report, chapters=all_chapters, files=files, output_dir=output_dir) + + _write_output(reader, input_path, output_dir, opts, files, media) + if opts.report: + report_path = Path(opts.report) + report_path.parent.mkdir(parents=True, exist_ok=True) + report_path.write_text(writer.to_json(report), encoding="utf-8") + return Result(report=report, chapters=all_chapters, files=files, output_dir=output_dir) def _mostly_links(el: Any) -> bool: diff --git a/epub2md/writer.py b/epub2md/writer.py index 9c891e2..83f4cc5 100644 --- a/epub2md/writer.py +++ b/epub2md/writer.py @@ -87,7 +87,7 @@ def _nav_lines(entries: list[NavEntry], resolver: LinkResolver, depth: int = 0) def readme(md: BookMetadata, chapters: list[Chapter], endnotes: Chapter | None, nav: list[NavEntry], resolver: LinkResolver, stats: dict[str, int], cover: str, epub_version: str, source_name: str, - command: list[str], output_dir: str = "OUTPUT_VERZEICHNIS") -> str: + command: list[str]) -> str: out = [f"# {md_escape(md.title or source_name)}"] if md.subtitle: out.append(f"*{md_escape(md.subtitle)}*") @@ -128,20 +128,6 @@ def readme(md: BookMetadata, chapters: list[Chapter], endnotes: Chapter | None, out.append("\n".join(["| Kennzahl | Anzahl |", "| --- | ---: |"] + [f"| {lbl} | {stats.get(k, 0)} |" for k, lbl in labels])) out.append("Details: [conversion-report.json](conversion-report.json), " "[manifest.json](manifest.json), [Warnungen](diagnostics/warnings.md)") - stem = source_name[:-5] if source_name.lower().endswith(".epub") else source_name - math = " --mathml" if stats.get("formulas") else "" - commands = [ - "# Gesamtdatei erzeugen (im selben Arbeitsverzeichnis wie die Konvertierung)", - f"python3 epub2md_merge.py {shlex.quote(output_dir)}", - "# danach in diesem Ordner:", - f"pandoc {shlex.quote(stem + '.md')} --toc -s{math} -o {shlex.quote(stem + '.html')}", - f"pandoc {shlex.quote(stem + '.md')} --toc -o {shlex.quote(stem + '.epub')}", - ] - out.append("## Weiterverarbeitung") - out.append("```bash\n" + "\n".join(commands) + "\n```") - if math: - out.append("Das Buch enthält Formeln: Für HTML `--mathml` (oder `--mathjax`) verwenden, sonst belässt " - "Pandoc Matrizen und ähnliche Konstrukte als TeX-Text.") out.append("## Reproduktion") out.append("```bash\n" + " ".join(shlex.quote(c) for c in command) + "\n```") return "\n\n".join(out) + "\n" diff --git a/tests/test_chapters.py b/tests/test_chapters.py index 7c1712b..2e12493 100644 --- a/tests/test_chapters.py +++ b/tests/test_chapters.py @@ -133,34 +133,3 @@ def test_large_chapter_with_subheadings_suggests_deeper_chapter_level(convert): assert "--chapter-level 2" in hints[0]["message"] and "3 Unterabschnitte" in hints[0]["message"] small = convert(Book(docs=[Doc("t.xhtml", "<h1>Teil</h1><h2>A</h2><p>x</p><h2>B</h2><p>y</p>")])) assert not any(w["code"] == "chapter-large" for w in small.report["warnings"]) - - -def test_titles_derived_from_content_when_no_other_source(convert): - docs = [ - Doc("ccover.xhtml", '<div><img src="c.png" alt=""/></div>'), - Doc("cinner.xhtml", "<p>Erika Musterfrau</p><p>Testbuch</p><p>Roman</p>"), - Doc("cimp.xhtml", "<p>© 2024 Testverlag</p><p>Das Werk ist urheberrechtlich geschützt.</p>"), - Doc("cwid.xhtml", "<p>Für Anna, die immer an mich geglaubt hat, und für alle anderen.</p>"), - Doc("c1.xhtml", "<h1>Eins</h1><p>Text</p>"), - ] - from conftest import PNG - run = convert(Book(docs=docs, nav=[("Eins", "c1.xhtml")], media={"c.png": PNG})) - names = [p.name for p in run.chapters()] - assert names == ["001-cover.md", "002-titelseite.md", "003-impressum.md", - "004-fuer-anna-die-immer-an-mich-geglaubt-hat.md", "005-eins.md"] - types = {c["file"]: c["types"] for c in run.report["chapters"]} - assert "cover" in types["chapters/001-cover.md"] - assert "titlepage" in types["chapters/002-titelseite.md"] - assert "colophon" in types["chapters/003-impressum.md"] - - -def test_adjacent_field_spans_get_a_separator(convert): - body = ('<h1>K</h1><div class="ContextInformation"><span class="Names">J. Liesen, V. Mehrmann</span>' - '<span class="BookTitles"><span class="BookTitle">Lineare Algebra</span></span>' - '<span class="Series">Springer Studium</span></div>' - '<p><span class="dropcap">D</span><span class="rest">as Wetter</span> war schlecht.</p>' - '<p><span class="a">Haupt</span><span class="b">bahnhof</span> und x<sub>i</sub>.</p>') - text = convert(Book(docs=[Doc("c.xhtml", body)])).chapter("001") - assert "J. Liesen, V. Mehrmann Lineare Algebra Springer Studium" in text - assert "Das Wetter war schlecht." in text - assert "Hauptbahnhof und x<sub>i</sub>." in text diff --git a/tests/test_codeblocks.py b/tests/test_codeblocks.py index 0cf0ca9..704cdd6 100644 --- a/tests/test_codeblocks.py +++ b/tests/test_codeblocks.py @@ -101,20 +101,3 @@ def test_code_language_option_rejects_unknown_values(convert): run = convert(Book(docs=[Doc("c.xhtml", "<h1>C</h1><pre>x</pre>")]), "--code-language", "quatsch") assert run.rc == 1 assert "quatsch" in run.stderr - - -def test_print_wrap_markers_are_joined(convert): - body = ("<h1>W</h1><pre> return (s * (s -\n ↪ distance(a, b)) *\n s)\n" - "x = ['a', 'b',\n↪ 'c']</pre>") - run = convert(Book(docs=[Doc("c.xhtml", body)])) - (_, _, code), = fenced_blocks(run.chapter("001")) - assert code == " return (s * (s - distance(a, b)) *\n s)\nx = ['a', 'b', 'c']" - item = run.report["code_blocks"]["items"][0] - assert item["wrap_lines_joined"] == 2 - - -def test_print_wrap_markers_can_be_kept(convert): - body = "<h1>W</h1><pre>a = (1,\n↪ 2)</pre>" - run = convert(Book(docs=[Doc("c.xhtml", body)]), "--keep-wrap-markers") - (_, _, code), = fenced_blocks(run.chapter("001")) - assert code == "a = (1,\n↪ 2)" diff --git a/tests/test_math.py b/tests/test_math.py index a89df9c..b3eba6c 100644 --- a/tests/test_math.py +++ b/tests/test_math.py @@ -104,13 +104,3 @@ def test_svg_file_formula_is_reported_as_svg(convert): report = convert(math_book()).report origins = {f["origin"] for f in report["formulas"]["items"]} assert "svg" in origins - - -def test_book_readme_suggests_mathml_for_pandoc_only_with_formulas(convert): - run = convert(math_book()) - readme = (run.out / "README.md").read_text(encoding="utf-8") - assert "## Weiterverarbeitung" in readme and "epub2md_merge.py" in readme - assert "--mathml" in readme - plain = convert(Book(docs=[Doc("a.xhtml", "<h1>A</h1><p>Text</p>")])) - plain_readme = (plain.out / "README.md").read_text(encoding="utf-8") - assert "## Weiterverarbeitung" in plain_readme and "--mathml" not in plain_readme