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>
129 lines
7.6 KiB
Python
129 lines
7.6 KiB
Python
"""Typography and layout: invisible characters, CSS emphasis, scene breaks, boxes, styles, page markers."""
|
||
|
||
from conftest import Book, Doc
|
||
|
||
ZW, SHY = "", ""
|
||
|
||
|
||
def test_p6_invisible_characters_removed_outside_code(convert):
|
||
body = (f"<h1>Kapitel 1.{ZW}1</h1><p>Lese{SHY}rinnen und 1.{ZW}1 sowie{ZW}Wort.</p>"
|
||
f"<pre>a{ZW}b</pre><p><code>x{SHY}y</code></p>")
|
||
run = convert(Book(docs=[Doc("c.xhtml", body)]))
|
||
text = run.chapter("001")
|
||
assert "Leserinnen und 1.1 sowieWort." in text
|
||
assert "# Kapitel 1.1" in text and 'chapter_title: "Kapitel 1.1"' in text
|
||
assert f"a{ZW}b" in text and f"x{SHY}y" in text # code stays verbatim
|
||
assert run.report["text_cleanup"]["invisible_chars_removed"] == 4
|
||
kept = convert(Book(docs=[Doc("c.xhtml", body)]), "--keep-invisible-chars").chapter("001")
|
||
assert f"Lese{SHY}rinnen" in kept
|
||
|
||
|
||
CSS = b""".i { font-style: italic } .b { font-weight: bold } span.u { text-decoration: underline }
|
||
.s { text-decoration: line-through } .hoch { vertical-align: super } .weg { display: none }
|
||
li p.first > span.b { font-weight: normal; font-family: monospace } .schrift-c { font-family: "Courier New", monospace }
|
||
p.zitat { font-style: italic } h2.t { font-weight: bold; font-style: italic } .url { text-decoration: underline }
|
||
@media amzn-kf8 { .i { font-style: normal } }"""
|
||
|
||
|
||
def styled(body: str, *args: str, convert=None):
|
||
return convert(Book(docs=[Doc("c.xhtml", body)], media={"style.css": CSS}), *args)
|
||
|
||
|
||
def test_p1_css_classes_become_markdown_emphasis(convert):
|
||
body = ('<h1>K</h1><p>Ein <span class="i">kursives</span> und <span class="b">fettes</span> Wort, '
|
||
'<span class="u">unterstrichen</span>, <span class="s">gestrichen</span>, x<span class="hoch">2</span>, '
|
||
'<span class="weg">[»] </span>sichtbar, <span class="schrift-c">printf</span> und '
|
||
'<span class="schrift-c">zwei Wörter</span>, <span style="font-weight:700">inline</span>, '
|
||
'<span class="url"><a href="https://example.org">example.org</a></span>.</p>'
|
||
'<ul><li><p class="first"><span class="b">nicht fett im Listenkopf</span></p></li></ul>'
|
||
'<p class="zitat">Ein ganzer kursiver Absatz mit <em>Betonung</em>.</p>'
|
||
'<h2 class="t">Überschrift bleibt</h2><p><em class="i">nur einmal</em></p><pre><span class="b">code</span></pre>')
|
||
run = styled(body, convert=convert)
|
||
text = run.chapter("001")
|
||
assert "Ein *kursives* und **fettes** Wort, <u>unterstrichen</u>, ~~gestrichen~~, x<sup>2</sup>, sichtbar," in text
|
||
assert "`printf` und zwei Wörter, **inline**, [example.org](https://example.org)." in text
|
||
assert "[»]" not in text and "\\[»\\]" not in text
|
||
assert "- nicht fett im Listenkopf" in text
|
||
assert "*Ein ganzer kursiver Absatz mit Betonung.*" in text
|
||
assert "## Überschrift bleibt" in text and "*nur einmal*" in text and "**nur einmal**" not in text
|
||
assert "```\ncode\n```" in text
|
||
assert run.report["css"]["hidden_elements"] == 1
|
||
assert run.report["css"]["applied"]["italic"] >= 2
|
||
plain = styled(body, "--no-css-emphasis", convert=convert).chapter("001")
|
||
assert "Ein kursives und fettes Wort" in plain
|
||
|
||
|
||
def test_p1_css_emphasis_inside_html_tables(convert):
|
||
body = ('<h1>T</h1><table><tr><td colspan="2"><span class="i">kursiv</span> und <span class="b">fett</span></td></tr>'
|
||
'<tr><td>a</td><td>b</td></tr></table>')
|
||
text = styled(body, convert=convert).chapter("001")
|
||
assert "<em>kursiv</em> und <strong>fett</strong>" in text
|
||
|
||
|
||
def test_adjacent_emphasis_is_merged(convert):
|
||
body = ('<h1>K</h1><p>Fläche (A=r<sup>2</sup><span class="i">×</span><span class="i">π</span>) und '
|
||
'<em>x</em><em>y</em> sowie <strong>a</strong><strong>b</strong> und <em>c</em> <em>d</em>.</p>')
|
||
text = styled(body, convert=convert).chapter("001")
|
||
assert "<sup>2</sup>*×π*" in text
|
||
assert "*xy*" in text and "**ab**" in text and "*c* *d*" in text
|
||
|
||
|
||
def test_p4_scene_breaks(convert):
|
||
body = ('<h1>K</h1><p class="leerzeile"> </p><p>Erster Absatz.</p><p class="leerzeile"> </p>'
|
||
'<p class="leerzeile"> </p><p>Neue Szene.</p><p>Weiter.</p><p class="centertext">***</p>'
|
||
'<p>Nach Ornament.</p><p> </p><h2>Abschnitt</h2><p>Text</p><p>⁂</p><p>Ende.</p>')
|
||
text = convert(Book(docs=[Doc("c.xhtml", body)])).chapter("001")
|
||
body_md = text.split("# K", 1)[1]
|
||
assert "Erster Absatz.\n\n* * *\n\nNeue Szene." in body_md
|
||
assert body_md.count("* * *") == 3 # two empties collapse into one; ***, ⁂ are ornaments; none before the heading
|
||
assert "Weiter.\n\n* * *\n\nNach Ornament." in body_md
|
||
assert "\\*" not in body_md
|
||
assert body_md.strip().startswith("Erster Absatz.") # no break at chapter start
|
||
|
||
|
||
def test_p4_poems_keep_lines_and_stanzas(convert):
|
||
body = ('<h1>G</h1><p>Davor.</p><div class="poem"><p>»Erste Zeile</p><p>zweite Zeile</p>'
|
||
'<p class="leerzeile"> </p><p>Zweite Strophe«</p></div><p>Danach.</p>')
|
||
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]
|