feat: Option --code-language für Codeblöcke ohne erkannte Sprache
Weist Blöcken ohne erkannte Sprache ein vom Nutzer gewähltes Label zu. Aliase wie py werden normalisiert, unbekannte Werte ergeben Exit 1. Erkannte Labels haben Vorrang; im Bericht detected_by "option" (QA-Befund R1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8afa1cc6d4
commit
cc36372329
5 changed files with 39 additions and 2 deletions
|
|
@ -78,6 +78,7 @@ python3 epub2md.py buch.epub output/buch --force --report berichte/buch.json
|
||||||
| `--math {auto,tex,mathml,image}` | `auto` | siehe [Mathematik](#math-modi) |
|
| `--math {auto,tex,mathml,image}` | `auto` | siehe [Mathematik](#math-modi) |
|
||||||
| `--images {relative,flat,original-path}` | `relative` | `relative`: `media/images/0001-name.ext`; `flat`: `media/name.ext`; `original-path`: `media/<Pfad im EPUB>` |
|
| `--images {relative,flat,original-path}` | `relative` | `relative`: `media/images/0001-name.ext`; `flat`: `media/name.ext`; `original-path`: `media/<Pfad im EPUB>` |
|
||||||
| `--slug-language {de,en,none}` | `de` | Dateinamen: `de` ä→ae, ß→ss; `en` ä→a, ß→ss; `none` Unicode bleibt |
|
| `--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-html` | aus | Unbekannte/nicht sicher konvertierbare Elemente als bereinigtes HTML erhalten (sonst: Text übernommen + Eintrag in `diagnostics/unsupported-elements.md`) |
|
| `--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) |
|
| `--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) |
|
| `--force` | aus | Bestehendes Ausgabeverzeichnis ersetzen – siehe [Sicherheit](#sicherheit-und-fehlerbehandlung) |
|
||||||
|
|
@ -200,7 +201,9 @@ weil CommonMark keine `[^…]`-Syntax kennt.
|
||||||
- **Syntax-Highlighting:** Farb-Spans, `<strong>`/`<em>` in Listings und Zeilennummern werden entfernt.
|
- **Syntax-Highlighting:** Farb-Spans, `<strong>`/`<em>` in Listings und Zeilennummern werden entfernt.
|
||||||
Einrückung, Tabs, `<`, `>`, `&`, `$`, `|`, Backslashes und Backticks bleiben exakt erhalten. Die Sprache
|
Einrückung, Tabs, `<`, `>`, `&`, `$`, `|`, Backslashes und Backticks bleiben exakt erhalten. Die Sprache
|
||||||
kommt aus Klassen/Attributen (`language-python`, `data-lang`, …) oder starken Inhaltssignalen
|
kommt aus Klassen/Attributen (`language-python`, `data-lang`, …) oder starken Inhaltssignalen
|
||||||
(`#include`, `def …:`, Shebang, …). Ist sie unsicher, bleibt das Label leer.
|
(`#include`, `def …:`, Shebang, …). Ist sie unsicher, bleibt das Label leer. Bei einsprachigen
|
||||||
|
Büchern kann `--code-language python` (o. Ä.) alle übrigen Blöcke kennzeichnen. Achtung: Das betrifft
|
||||||
|
auch Programmausgaben und Shell-Befehle im Buch.
|
||||||
- **Gescannte Texte:** nur Bilder, kein Text (keine OCR).
|
- **Gescannte Texte:** nur Bilder, kein Text (keine OCR).
|
||||||
- **DRM:** wird erkannt und abgelehnt.
|
- **DRM:** wird erkannt und abgelehnt.
|
||||||
- **Register:** Buchstabengruppen werden zu `##`-Überschriften, Untereinträge erhalten `– `. Interne
|
- **Register:** Buchstabengruppen werden zu `##`-Überschriften, Untereinträge erhalten `– `. Interne
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ from pathlib import Path
|
||||||
from typing import NoReturn
|
from typing import NoReturn
|
||||||
|
|
||||||
from . import __version__
|
from . import __version__
|
||||||
|
from .codeblocks import LANG_ALIASES
|
||||||
from .errors import EXIT_INPUT, EXIT_INTERNAL, Epub2MdError
|
from .errors import EXIT_INPUT, EXIT_INTERNAL, Epub2MdError
|
||||||
from .pipeline import Options, convert, print_summary
|
from .pipeline import Options, convert, print_summary
|
||||||
|
|
||||||
|
|
@ -37,6 +38,14 @@ class _Parser(argparse.ArgumentParser):
|
||||||
sys.exit(EXIT_INPUT)
|
sys.exit(EXIT_INPUT)
|
||||||
|
|
||||||
|
|
||||||
|
def code_language(value: str) -> str:
|
||||||
|
lang = LANG_ALIASES.get(value.strip().lower())
|
||||||
|
if lang is None:
|
||||||
|
known = ", ".join(sorted(set(LANG_ALIASES.values())))
|
||||||
|
raise argparse.ArgumentTypeError(f"unbekannte Sprache {value!r} (bekannt: {known})")
|
||||||
|
return lang
|
||||||
|
|
||||||
|
|
||||||
def build_parser() -> argparse.ArgumentParser:
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
p = _Parser(
|
p = _Parser(
|
||||||
prog="epub2md.py",
|
prog="epub2md.py",
|
||||||
|
|
@ -70,6 +79,9 @@ def build_parser() -> argparse.ArgumentParser:
|
||||||
help="Dateinamen-Regeln: de (ä→ae), en (ä→a), none (Unicode behalten) (Standard: de)")
|
help="Dateinamen-Regeln: de (ä→ae), en (ä→a), none (Unicode behalten) (Standard: de)")
|
||||||
p.add_argument("--keep-html", action="store_true",
|
p.add_argument("--keep-html", action="store_true",
|
||||||
help="Nicht zuverlässig konvertierbare Elemente als bereinigtes HTML erhalten")
|
help="Nicht zuverlässig konvertierbare Elemente als bereinigtes HTML erhalten")
|
||||||
|
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("--strict", action="store_true",
|
p.add_argument("--strict", action="store_true",
|
||||||
help="Bei kritischen Fehlern (fehlende Spine-Dateien, unsichere Pfade, kaputtes XHTML) abbrechen")
|
help="Bei kritischen Fehlern (fehlende Spine-Dateien, unsichere Pfade, kaputtes XHTML) abbrechen")
|
||||||
p.add_argument("--force", action="store_true",
|
p.add_argument("--force", action="store_true",
|
||||||
|
|
@ -101,6 +113,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
images=args.images,
|
images=args.images,
|
||||||
slug_language=args.slug_language,
|
slug_language=args.slug_language,
|
||||||
keep_html=args.keep_html,
|
keep_html=args.keep_html,
|
||||||
|
code_language=args.code_language or "",
|
||||||
strict=args.strict,
|
strict=args.strict,
|
||||||
verbose=args.verbose,
|
verbose=args.verbose,
|
||||||
dry_run=args.dry_run,
|
dry_run=args.dry_run,
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ class RenderOptions:
|
||||||
extract_media: bool = True
|
extract_media: bool = True
|
||||||
keep_html: bool = False
|
keep_html: bool = False
|
||||||
language: str = "de"
|
language: str = "de"
|
||||||
|
code_language: str = "" # --code-language: label for blocks without a detected language
|
||||||
|
|
||||||
|
|
||||||
def md_escape(text: str, table: bool = False) -> str:
|
def md_escape(text: str, table: bool = False) -> str:
|
||||||
|
|
@ -421,6 +422,8 @@ class Renderer:
|
||||||
if not lang:
|
if not lang:
|
||||||
lang = detect_language_from_content(code)
|
lang = detect_language_from_content(code)
|
||||||
how = "content" if lang else ""
|
how = "content" if lang else ""
|
||||||
|
if not lang and self.opt.code_language:
|
||||||
|
lang, how = self.opt.code_language, "option"
|
||||||
if self.ch:
|
if self.ch:
|
||||||
self.ch.stats.code_blocks += 1
|
self.ch.stats.code_blocks += 1
|
||||||
if not lang:
|
if not lang:
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ class Options:
|
||||||
images: str = "relative"
|
images: str = "relative"
|
||||||
slug_language: str = "de"
|
slug_language: str = "de"
|
||||||
keep_html: bool = False
|
keep_html: bool = False
|
||||||
|
code_language: str = ""
|
||||||
strict: bool = False
|
strict: bool = False
|
||||||
verbose: bool = False
|
verbose: bool = False
|
||||||
dry_run: bool = False
|
dry_run: bool = False
|
||||||
|
|
@ -180,7 +181,9 @@ def _convert(reader: EpubReader, input_path: Path, output_dir: Path, opts: Optio
|
||||||
cover_out = media.register(md.cover_path, role="cover", hint="cover").output
|
cover_out = media.register(md.cover_path, role="cover", hint="cover").output
|
||||||
language = (md.language or "de").lower()
|
language = (md.language or "de").lower()
|
||||||
renderer = Renderer(
|
renderer = Renderer(
|
||||||
RenderOptions(opts.format, opts.footnotes, opts.math, opts.extract_media, opts.keep_html, language),
|
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),
|
||||||
resolver, media, notes, diag, set(reader.names),
|
resolver, media, notes, diag, set(reader.names),
|
||||||
)
|
)
|
||||||
width = max(3, len(str(len(chapters) + 1)))
|
width = max(3, len(str(len(chapters) + 1)))
|
||||||
|
|
|
||||||
|
|
@ -86,3 +86,18 @@ def test_prose_is_not_forced_into_code(convert):
|
||||||
def test_unlabeled_code_is_reported(convert):
|
def test_unlabeled_code_is_reported(convert):
|
||||||
run = convert(Book(docs=[Doc("c.xhtml", "<h1>U</h1><pre>foo bar</pre>")]))
|
run = convert(Book(docs=[Doc("c.xhtml", "<h1>U</h1><pre>foo bar</pre>")]))
|
||||||
assert run.report["code_blocks"]["languages"] == {"(keine)": 1}
|
assert run.report["code_blocks"]["languages"] == {"(keine)": 1}
|
||||||
|
|
||||||
|
|
||||||
|
def test_code_language_option_fills_only_unlabeled_blocks(convert):
|
||||||
|
body = "<h1>C</h1><pre>x = compute()</pre><pre>#include <stdio.h>\nint main(void) { return 0; }</pre>"
|
||||||
|
run = convert(Book(docs=[Doc("c.xhtml", body)]), "--code-language", "py")
|
||||||
|
langs = [lang for _, lang, _ in fenced_blocks(run.chapter("001"))]
|
||||||
|
assert langs == ["python", "c"]
|
||||||
|
by = {item["language"]: item["detected_by"] for item in run.report["code_blocks"]["items"]}
|
||||||
|
assert by == {"python": "option", "c": "content"}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue