feat: Sprachlabels für Codeblöcke aus Inhalt und Buchkontext (R1)

- Gedruckte Zeilennummern ("00  #include …") werden für die Erkennung
  ignoriert, bleiben aber im Block stehen.
- Python zusätzlich per ast.parse (Kontrollstrukturen, def/class, Import
  mit Code) und an >>>-Sitzungen erkannt; YAML zählt nicht.
- Vordurchlauf bestimmt die Sprache des Buches aus den sicher erkannten
  Blöcken; passt sie, reichen für kurze Schnipsel schwächere Signale
  (detected_by: "book-context"). Programmausgaben bleiben ohne Label.

Ohne Label: C-Buch 518 -> 132 von 574, Python-Buch 492 -> 326 von 747.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-10-06 23:46:50 +02:00
commit 2526ee6789
2 changed files with 207 additions and 7 deletions

View file

@ -118,3 +118,57 @@ def test_print_wrap_markers_can_be_kept(convert):
run = convert(Book(docs=[Doc("c.xhtml", body)]), "--keep-wrap-markers")
(_, _, code), = fenced_blocks(run.chapter("001"))
assert code == "a = (1,\n↪ 2)"
def test_printed_line_numbers_do_not_hide_the_language(convert):
listing = ("00 // Kapitel1/hallo.c\n01 #include &lt;stdio.h&gt;\n \n02 int main(void) {\n"
"03 printf(\"Hallo\\n\");\n04 return 0;\n05 }")
run = convert(Book(docs=[Doc("c.xhtml", f"<h1>N</h1><pre>{listing}</pre>")]))
(_, lang, code), = fenced_blocks(run.chapter("001"))
assert lang == "c"
assert code.startswith("00 // Kapitel1/hallo.c\n01 #include <stdio.h>") # numbers stay in the block
def test_python_is_recognized_by_parsing(convert):
blocks = ["from collections import ChainMap\n\nchain = ChainMap({}, {})\nprint(chain)",
"zahl = -1\nif zahl &lt; 0:\n print(\"negativ\")",
"&gt;&gt;&gt; 2 + 3\n5",
"name: epub2md\nversion: 1", # YAML is valid Python syntax, but no Python code
"[1, 4, 9]", # program output
"x = compute()"] # Python, but too weak without book context
body = "<h1>P</h1>" + "".join(f"<pre>{b}</pre>" for b in blocks)
run = convert(Book(docs=[Doc("c.xhtml", body)]))
langs = [lang for _, lang, _ in fenced_blocks(run.chapter("001"))]
assert langs == ["python", "python", "python", "", "", ""]
def test_weak_signals_follow_the_book_language(convert):
c_listing = "#include &lt;stdio.h&gt;\nint main(void) { return 0; }"
snippets = ["char ch = 65; // 'A'", "if (a || b) {\n x();\n}", "int remove(const char *pathname);",
"struct node {\n int value;\n};", "Ergebnis: 12345", "|00003.14|", "gcc -Wall -o prog prog.c",
"Dezimal: 65; Zeichen: A"]
body = "<h1>C</h1>" + "".join(f"<pre>{c_listing}</pre>" for _ in range(5))
body += "".join(f"<pre>{s}</pre>" for s in snippets)
run = convert(Book(docs=[Doc("c.xhtml", body)]))
langs = [lang for _, lang, _ in fenced_blocks(run.chapter("001"))][5:]
assert langs == ["c", "c", "c", "c", "", "", "", ""]
by = [item["detected_by"] for item in run.report["code_blocks"]["items"]][5:9]
assert by == ["book-context"] * 4
def test_python_book_labels_assignments_but_not_output(convert):
py = "def f(x):\n return x"
snippets = ["foo = decorate(foo)", "random.sample(population, k)", "[x * x for x in range(5)]",
"True", "('A', 'B', 'C')", "Hallo Welt", "4\n3\nnohtyP", "Jane\n42\nPerson(name='Jane')"]
body = "<h1>P</h1>" + "".join(f"<pre>{py}</pre>" for _ in range(5))
body += "".join(f"<pre>{s}</pre>" for s in snippets)
run = convert(Book(docs=[Doc("c.xhtml", body)]))
langs = [lang for _, lang, _ in fenced_blocks(run.chapter("001"))][5:]
assert langs == ["python", "python", "python", "", "", "", "", ""]
def test_mixed_book_gets_no_context_labels(convert):
body = "<h1>M</h1>" + "<pre>def f(x):\n return x</pre>" * 3
body += "<pre>#include &lt;stdio.h&gt;\nint main(void) { return 0; }</pre>" * 3 + "<pre>x = compute();</pre>"
run = convert(Book(docs=[Doc("c.xhtml", body)]))
assert fenced_blocks(run.chapter("001"))[-1][1] == ""

View file

@ -39,7 +39,7 @@ def test_qa2_10_book_readme_recommends_pdf_via_html(convert):
def test_qa2_11_code_language_hint(convert):
py = "def f(x):\n return x\n\nclass A:\n pass"
body = "<h1>C</h1>" + "".join(f"<pre>{py}\n# {i}</pre><pre>print({i})</pre>" for i in range(10))
body = "<h1>C</h1>" + "".join(f"<pre>{py}\n# {i}</pre><pre>Ergebnis: {i}</pre>" for i in range(10))
run = convert(Book(docs=[Doc("c.xhtml", body)]))
hints = [i for i in run.report["infos"] if i["code"] == "code-language-hint"]
assert hints and "--code-language python" in hints[0]["message"]