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) |
|
||||
| `--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 |
|
||||
| `--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`) |
|
||||
| `--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) |
|
||||
|
|
@ -200,7 +201,9 @@ weil CommonMark keine `[^…]`-Syntax kennt.
|
|||
- **Syntax-Highlighting:** Farb-Spans, `<strong>`/`<em>` in Listings und Zeilennummern werden entfernt.
|
||||
Einrückung, Tabs, `<`, `>`, `&`, `$`, `|`, Backslashes und Backticks bleiben exakt erhalten. Die Sprache
|
||||
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).
|
||||
- **DRM:** wird erkannt und abgelehnt.
|
||||
- **Register:** Buchstabengruppen werden zu `##`-Überschriften, Untereinträge erhalten `– `. Interne
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from pathlib import Path
|
|||
from typing import NoReturn
|
||||
|
||||
from . import __version__
|
||||
from .codeblocks import LANG_ALIASES
|
||||
from .errors import EXIT_INPUT, EXIT_INTERNAL, Epub2MdError
|
||||
from .pipeline import Options, convert, print_summary
|
||||
|
||||
|
|
@ -37,6 +38,14 @@ class _Parser(argparse.ArgumentParser):
|
|||
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:
|
||||
p = _Parser(
|
||||
prog="epub2md.py",
|
||||
|
|
@ -70,6 +79,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
help="Dateinamen-Regeln: de (ä→ae), en (ä→a), none (Unicode behalten) (Standard: de)")
|
||||
p.add_argument("--keep-html", action="store_true",
|
||||
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",
|
||||
help="Bei kritischen Fehlern (fehlende Spine-Dateien, unsichere Pfade, kaputtes XHTML) abbrechen")
|
||||
p.add_argument("--force", action="store_true",
|
||||
|
|
@ -101,6 +113,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
images=args.images,
|
||||
slug_language=args.slug_language,
|
||||
keep_html=args.keep_html,
|
||||
code_language=args.code_language or "",
|
||||
strict=args.strict,
|
||||
verbose=args.verbose,
|
||||
dry_run=args.dry_run,
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ class RenderOptions:
|
|||
extract_media: bool = True
|
||||
keep_html: bool = False
|
||||
language: str = "de"
|
||||
code_language: str = "" # --code-language: label for blocks without a detected language
|
||||
|
||||
|
||||
def md_escape(text: str, table: bool = False) -> str:
|
||||
|
|
@ -421,6 +422,8 @@ class Renderer:
|
|||
if not lang:
|
||||
lang = detect_language_from_content(code)
|
||||
how = "content" if lang else ""
|
||||
if not lang and self.opt.code_language:
|
||||
lang, how = self.opt.code_language, "option"
|
||||
if self.ch:
|
||||
self.ch.stats.code_blocks += 1
|
||||
if not lang:
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ class Options:
|
|||
images: str = "relative"
|
||||
slug_language: str = "de"
|
||||
keep_html: bool = False
|
||||
code_language: str = ""
|
||||
strict: bool = False
|
||||
verbose: 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
|
||||
language = (md.language or "de").lower()
|
||||
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),
|
||||
)
|
||||
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):
|
||||
run = convert(Book(docs=[Doc("c.xhtml", "<h1>U</h1><pre>foo bar</pre>")]))
|
||||
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