refactor: pipeline._convert in Teilschritte zerlegt (QA R5)
_convert (zuletzt 153 Zeilen) orchestriert jetzt nur noch die Teilschritte _prepare_docs, _build_structure, _render (mit _report_wrap_markers und _collect_remaining_media) und _assemble. Längste Funktion im Modul: 48 Zeilen. Keine Verhaltensänderung: Die Ausgabe aller vier Beispielbücher (Standard und end-of-book) ist bis auf Zeitstempel und Pfade byteidentisch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
bdc611eefc
commit
84821a2fcd
1 changed files with 73 additions and 33 deletions
|
|
@ -131,11 +131,36 @@ 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,
|
def _convert(reader: EpubReader, input_path: Path, output_dir: Path, opts: Options, diag: Diagnostics,
|
||||||
generated_at: str, sha: str) -> Result:
|
generated_at: str, sha: str) -> Result:
|
||||||
package = reader.load_package()
|
package = reader.load_package()
|
||||||
md = package.metadata
|
diag.step(f"EPUB {package.version}: {package.metadata.title!r}, {len(package.spine)} Spine-Dokumente")
|
||||||
diag.step(f"EPUB {package.version}: {md.title!r}, {len(package.spine)} Spine-Dokumente")
|
|
||||||
docs = _load_docs(reader, package, diag)
|
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)
|
||||||
|
|
||||||
# --- normalization and HTML-TOC handling
|
_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."""
|
||||||
title_tables = book_uses_title_tables(docs)
|
title_tables = book_uses_title_tables(docs)
|
||||||
for doc in docs:
|
for doc in docs:
|
||||||
normalize_doc(doc, diag, title_tables)
|
normalize_doc(doc, diag, title_tables)
|
||||||
|
|
@ -156,13 +181,16 @@ def _convert(reader: EpubReader, input_path: Path, output_dir: Path, opts: Optio
|
||||||
for d in docs:
|
for d in docs:
|
||||||
d.skipped = ""
|
d.skipped = ""
|
||||||
|
|
||||||
# --- notes, chapters, links
|
|
||||||
|
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 = collect_notes(docs, opts.footnotes, diag)
|
notes = collect_notes(docs, opts.footnotes, diag)
|
||||||
diag.step(f"{len(notes.notes)} Anmerkungen erkannt")
|
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, md.title, md.authors).build(docs)
|
||||||
_assign_filenames(chapters, opts.slug_language)
|
_assign_filenames(chapters, opts.slug_language)
|
||||||
readme_path = "README.md" if opts.toc else None
|
resolver = LinkResolver(docs, chapters, notes, "README.md" if opts.toc else None)
|
||||||
resolver = LinkResolver(docs, chapters, notes, readme_path)
|
|
||||||
nav_targets: list[tuple[str, str]] = []
|
nav_targets: list[tuple[str, str]] = []
|
||||||
|
|
||||||
def walk(entries: list[Any]) -> None:
|
def walk(entries: list[Any]) -> None:
|
||||||
|
|
@ -174,8 +202,13 @@ def _convert(reader: EpubReader, input_path: Path, output_dir: Path, opts: Optio
|
||||||
walk(package.toc)
|
walk(package.toc)
|
||||||
resolver.add_targets(nav_targets)
|
resolver.add_targets(nav_targets)
|
||||||
diag.step(f"{len(chapters)} Kapitel gebildet")
|
diag.step(f"{len(chapters)} Kapitel gebildet")
|
||||||
|
return notes, chapters, resolver
|
||||||
|
|
||||||
# --- rendering
|
|
||||||
|
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
|
||||||
media = MediaRegistry(package, opts.images, opts.slug_language)
|
media = MediaRegistry(package, opts.images, opts.slug_language)
|
||||||
cover_out = ""
|
cover_out = ""
|
||||||
if md.cover_path and opts.extract_media and reader.exists(md.cover_path):
|
if md.cover_path and opts.extract_media and reader.exists(md.cover_path):
|
||||||
|
|
@ -196,13 +229,7 @@ def _convert(reader: EpubReader, input_path: Path, output_dir: Path, opts: Optio
|
||||||
ch.markdown = renderer.render_chapter(ch)
|
ch.markdown = renderer.render_chapter(ch)
|
||||||
ch.stats.word_count = _word_count(ch.markdown)
|
ch.stats.word_count = _word_count(ch.markdown)
|
||||||
_check_chapter_size(ch, opts.chapter_level, diag)
|
_check_chapter_size(ch, opts.chapter_level, diag)
|
||||||
wraps: dict[str, int] = {}
|
_report_wrap_markers(renderer, diag)
|
||||||
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)
|
|
||||||
endnotes: Chapter | None = None
|
endnotes: Chapter | None = None
|
||||||
endnotes_md = ""
|
endnotes_md = ""
|
||||||
if opts.footnotes == "end-of-book" and renderer.book_notes:
|
if opts.footnotes == "end-of-book" and renderer.book_notes:
|
||||||
|
|
@ -211,8 +238,23 @@ def _convert(reader: EpubReader, input_path: Path, output_dir: Path, opts: Optio
|
||||||
endnotes.filename = renderer.endnotes_file
|
endnotes.filename = renderer.endnotes_file
|
||||||
endnotes.stats.footnotes = len(renderer.book_notes)
|
endnotes.stats.footnotes = len(renderer.book_notes)
|
||||||
endnotes.stats.word_count = _word_count(endnotes_md)
|
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)
|
||||||
|
|
||||||
# --- remaining media (fonts, css, audio, unreferenced images)
|
|
||||||
|
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."""
|
||||||
skipped_formula_images = 0
|
skipped_formula_images = 0
|
||||||
if opts.extract_media:
|
if opts.extract_media:
|
||||||
for item in package.manifest.values():
|
for item in package.manifest.values():
|
||||||
|
|
@ -229,11 +271,18 @@ def _convert(reader: EpubReader, input_path: Path, output_dir: Path, opts: Optio
|
||||||
for mf in media.files.values():
|
for mf in media.files.values():
|
||||||
if mf.data is None and reader.exists(mf.source):
|
if mf.data is None and reader.exists(mf.source):
|
||||||
mf.size = reader.names[mf.source].file_size
|
mf.size = reader.names[mf.source].file_size
|
||||||
|
return skipped_formula_images
|
||||||
|
|
||||||
# --- 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]
|
def _assemble(input_path: Path, output_dir: Path, opts: Options, diag: Diagnostics, generated_at: str, sha: str,
|
||||||
for n in unresolved_notes:
|
package: Any, docs: list[Doc], notes: Any, chapters: list[Chapter], resolver: LinkResolver,
|
||||||
diag.warn("note-not-rendered", "Anmerkung wurde referenziert, aber nicht ausgegeben.", n.key)
|
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)
|
||||||
opf_dir = posixpath.dirname(package.opf_path)
|
opf_dir = posixpath.dirname(package.opf_path)
|
||||||
files: dict[str, str] = {}
|
files: dict[str, str] = {}
|
||||||
for ch in chapters:
|
for ch in chapters:
|
||||||
|
|
@ -243,12 +292,12 @@ def _convert(reader: EpubReader, input_path: Path, output_dir: Path, opts: Optio
|
||||||
files[ch.filename] = front + ch.markdown
|
files[ch.filename] = front + ch.markdown
|
||||||
if endnotes is not None:
|
if endnotes is not None:
|
||||||
front = writer.chapter_frontmatter(endnotes, md, input_path.name, opf_dir, 0) if opts.frontmatter else ""
|
front = writer.chapter_frontmatter(endnotes, md, input_path.name, opf_dir, 0) if opts.frontmatter else ""
|
||||||
files[endnotes.filename] = front + endnotes_md
|
files[endnotes.filename] = front + rendered.endnotes_md
|
||||||
all_chapters = chapters + ([endnotes] if endnotes else [])
|
all_chapters = chapters + ([endnotes] if endnotes else [])
|
||||||
|
|
||||||
stats = _book_stats(all_chapters, media, renderer, diag)
|
stats = _book_stats(all_chapters, media, renderer, diag)
|
||||||
if opts.toc:
|
if opts.toc:
|
||||||
files["README.md"] = writer.readme(md, chapters, endnotes, package.toc, resolver, stats, cover_out,
|
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))
|
package.version, input_path.name, opts.command, str(output_dir))
|
||||||
existing = {mf.output for mf in media.files.values()} if opts.extract_media else set()
|
existing = {mf.output for mf in media.files.values()} if opts.extract_media else set()
|
||||||
if opts.copy_original:
|
if opts.copy_original:
|
||||||
|
|
@ -261,23 +310,14 @@ def _convert(reader: EpubReader, input_path: Path, output_dir: Path, opts: Optio
|
||||||
stats["warnings"] = diag.count("warning")
|
stats["warnings"] = diag.count("warning")
|
||||||
|
|
||||||
report = _report(input_path, sha, generated_at, package, opts, all_chapters, media, renderer, notes, diag,
|
report = _report(input_path, sha, generated_at, package, opts, all_chapters, media, renderer, notes, diag,
|
||||||
docs, broken, skipped_formula_images)
|
docs, broken, rendered.skipped_formula_images)
|
||||||
files["metadata.yaml"] = writer.metadata_yaml(md, input_path.name, sha, package.version, cover_out,
|
files["metadata.yaml"] = writer.metadata_yaml(md, input_path.name, sha, package.version, rendered.cover_out,
|
||||||
opts.public(), generated_at)
|
opts.public(), generated_at)
|
||||||
files["manifest.json"] = writer.to_json(_manifest(md, all_chapters, media, diag))
|
files["manifest.json"] = writer.to_json(_manifest(md, all_chapters, media, diag))
|
||||||
files["conversion-report.json"] = writer.to_json(report)
|
files["conversion-report.json"] = writer.to_json(report)
|
||||||
files["diagnostics/warnings.md"] = writer.warnings_md(diag)
|
files["diagnostics/warnings.md"] = writer.warnings_md(diag)
|
||||||
files["diagnostics/unsupported-elements.md"] = writer.unsupported_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:
|
def _mostly_links(el: Any) -> bool:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue