feat: Infokästen als Callouts (P2)

Kästen werden erkannt über aside/epub:type, Klassennamen (box, kasten, tipp,
hinweis, warnung, wichtig, exkurs …) oder einen CSS-Rahmen/-Hintergrund um
kurzen Fließtext. Folgen von Tipp-Absätzen werden gruppiert. Ausgabe:
'> [!TIP]' mit fettem Titel (GitHub-Alert/Obsidian-Callout). Der Typ kommt
aus Klasse, Titel oder erstem Wort; dekorative Symbolbilder entfallen;
umrahmte Codelistings bleiben Code. Option --boxes callout|quote|html|plain.
Beispielbücher: Python-Buch 72, C-Buch 83, Bildband 32 Tipps, Mathebuch 11
MATLAB-Minuten; Code-Treue und Links unverändert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-10-06 05:00:25 +02:00
commit 4992672ca6

View file

@ -87,3 +87,43 @@ def test_p4_poems_keep_lines_and_stanzas(convert):
text = convert(Book(docs=[Doc("c.xhtml", body)])).chapter("001")
assert "> »Erste Zeile\\\n> zweite Zeile\n>\n> Zweite Strophe«" in text
assert "* * *" not in text
BOX_CSS = b""".rahmen { border: 1px solid #333; padding: 4px } .listing { border: 1px solid #ccc }"""
def boxed(body: str, *args: str, convert=None):
return convert(Book(docs=[Doc("c.xhtml", body)], media={"box.css": BOX_CSS}), *args)
def test_p2_boxes_become_callouts(convert):
body = ('<h1>K</h1><p>Vorher.</p>'
'<div class="box box_standard"><h3 class="boxheading"><span class="box_icon">[»] </span>Erwartungen</h3>'
'<p>Kein Kompendium.</p><p>Zweiter Absatz.</p></div>'
'<div class="Boxg"><div class="Box_img"><img alt="Image" src="tick.png"/></div><div class="Box_body">'
'<p><strong>Wichtig:</strong> Funktionen sind Objekte.</p></div></div>'
'<p class="p_TippU">Tipp</p><p class="p_Tipp_FT">Gelb mit Blau mischen.</p><p class="p_Tipp_FT">Mehr Weiß.</p>'
'<aside epub:type="sidebar"><h2>Exkurs</h2><p>Am Rande.</p></aside>'
'<div class="rahmen"><p>Achtung, heiß!</p></div>'
'<div class="listing"><pre>code()</pre></div><p>Nachher.</p>')
from conftest import PNG
run = convert(Book(docs=[Doc("c.xhtml", body)], media={"box.css": BOX_CSS, "tick.png": PNG}))
text = run.chapter("001")
assert "> [!NOTE]\n> **\\[»\\] Erwartungen**\n>\n> Kein Kompendium.\n>\n> Zweiter Absatz." in text
assert "> [!IMPORTANT]\n> **Wichtig:** Funktionen sind Objekte." in text and "tick" not in text
assert "> [!TIP]\n> Gelb mit Blau mischen.\n>\n> Mehr Weiß." in text # label 'Tipp' = type word, no title
assert "> [!NOTE]\n> **Exkurs**\n>\n> Am Rande." in text
assert "> [!WARNING]\n> Achtung, heiß!" in text # box recognized via CSS border, type from text
assert "```\ncode()\n```" in text and "> ```" not in text # bordered code listing is no box
assert "### " not in text # box heading no longer pollutes the outline
assert run.report["layout"]["boxes"] == 5
def test_p2_box_modes(convert):
body = '<h1>K</h1><aside epub:type="tip"><h2>Hinweis zum Pinsel</h2><p>Sauber halten.</p></aside>'
quote = boxed(body, "--boxes", "quote", convert=convert).chapter("001")
assert "> **Hinweis zum Pinsel**\n>\n> Sauber halten." in quote and "[!" not in quote
htm = boxed(body, "--boxes", "html", convert=convert).chapter("001")
assert '<div class="box box-tip">\n\n**Hinweis zum Pinsel**\n\nSauber halten.\n\n</div>' in htm
plain = boxed(body, "--boxes", "plain", convert=convert).chapter("001")
assert "## Hinweis zum Pinsel" in plain and "Sauber halten." in plain and ">" not in plain.split("---")[-1]