diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index 89fdebd..54dccfd 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -164,6 +164,10 @@ 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,9 +317,9 @@ Nein, es wird nur gelesen. Mit `--copy-original` wird zusätzlich eine Kopie in Manche E-Book-Seiten (Titelseite, Impressum) haben weder Überschrift noch Titel. Dann wird der interne Dateiname verwendet. -**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. +**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`). **Können Dateinamen Umlaute enthalten?** Standardmäßig werden Umlaute umschrieben (`ä` → `ae`). Mit `--slug-language none` bleiben sie erhalten, @@ -345,6 +349,7 @@ 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 a460f63..aadaa0f 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ 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) | @@ -339,8 +340,8 @@ Verlustfreiheit behauptet (`lossless: false`). (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. +- Fehlerhafte Einrückungen der Vorlage werden zeichengetreu übernommen. Druck-Fortsetzungszeichen + (`↪`/`➥`) am Zeilenanfang werden dagegen standardmäßig aufgelöst (siehe `--keep-wrap-markers`). - 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/cli.py b/epub2md/cli.py index e6defcb..fbcb2e5 100644 --- a/epub2md/cli.py +++ b/epub2md/cli.py @@ -82,6 +82,9 @@ 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", @@ -114,6 +117,7 @@ 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 65c3b13..0fcc883 100644 --- a/epub2md/codeblocks.py +++ b/epub2md/codeblocks.py @@ -180,6 +180,27 @@ 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 7490a17..b007651 100644 --- a/epub2md/html_converter.py +++ b/epub2md/html_converter.py @@ -24,6 +24,7 @@ from .codeblocks import ( fence_for, inline_code, is_block_code_container, + join_wrap_markers, is_inline_code, ) from .diagnostics import Diagnostics @@ -90,6 +91,7 @@ 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: @@ -418,6 +420,9 @@ 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) @@ -429,7 +434,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}) + "detected_by": how, "lines": code.count("\n") + 1, "wrap_lines_joined": joined}) fence = fence_for(code) out.append(f"{fence}{lang}\n{code}\n{fence}") return out diff --git a/epub2md/pipeline.py b/epub2md/pipeline.py index 1bc1454..ec5574e 100644 --- a/epub2md/pipeline.py +++ b/epub2md/pipeline.py @@ -46,6 +46,7 @@ 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 @@ -183,7 +184,7 @@ def _convert(reader: EpubReader, input_path: Path, output_dir: Path, opts: Optio 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), + code_language=opts.code_language, keep_wrap_markers=opts.keep_wrap_markers), resolver, media, notes, diag, set(reader.names), ) width = max(3, len(str(len(chapters) + 1))) @@ -195,6 +196,13 @@ def _convert(reader: EpubReader, input_path: Path, output_dir: Path, opts: Optio ch.markdown = renderer.render_chapter(ch) ch.stats.word_count = _word_count(ch.markdown) _check_chapter_size(ch, opts.chapter_level, diag) + 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) endnotes: Chapter | None = None endnotes_md = "" if opts.footnotes == "end-of-book" and renderer.book_notes: diff --git a/tests/test_codeblocks.py b/tests/test_codeblocks.py index 704cdd6..0cf0ca9 100644 --- a/tests/test_codeblocks.py +++ b/tests/test_codeblocks.py @@ -101,3 +101,20 @@ def test_code_language_option_rejects_unknown_values(convert): run = convert(Book(docs=[Doc("c.xhtml", "

C

x
")]), "--code-language", "quatsch") assert run.rc == 1 assert "quatsch" in run.stderr + + +def test_print_wrap_markers_are_joined(convert): + body = ("

W

    return (s * (s -\n    ↪ distance(a, b)) *\n        s)\n"
+            "x = ['a', 'b',\n↪ 'c']
") + 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 = "

W

a = (1,\n↪ 2)
" + run = convert(Book(docs=[Doc("c.xhtml", body)]), "--keep-wrap-markers") + (_, _, code), = fenced_blocks(run.chapter("001")) + assert code == "a = (1,\n↪ 2)"