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:
parent
9b0f060af2
commit
4992672ca6
6 changed files with 218 additions and 4 deletions
|
|
@ -209,6 +209,25 @@ python3 epub2md.py bildband.epub output/bildband --chapter-level 3
|
|||
- Bilder, die im E-Book zwar enthalten, aber nirgends eingebunden sind, werden trotzdem gespeichert.
|
||||
Im Bericht sind sie als `unreferenced` gekennzeichnet.
|
||||
|
||||
### Infokästen (Tipps, Hinweise, Warnungen)
|
||||
|
||||
Kästen aus dem Buch, etwa „Tipp“, „Achtung“, „Exkurs“ oder „Die MATLAB-Minute“, werden als hervorgehobene
|
||||
Blöcke übernommen:
|
||||
|
||||
```markdown
|
||||
> [!TIP]
|
||||
> **Erwartungen an dieses Buch**
|
||||
>
|
||||
> Text des Kastens …
|
||||
```
|
||||
|
||||
GitHub und Obsidian zeigen das als farbigen Kasten mit Symbol; andere Programme als eingerückten Zitatblock.
|
||||
Andere Darstellungen wählst du mit `--boxes`:
|
||||
|
||||
- `quote`: Zitatblock ohne `[!TIP]`
|
||||
- `html`: HTML-`<div>`
|
||||
- `plain`: wie normaler Text
|
||||
|
||||
### Buch mit vielen Fußnoten
|
||||
|
||||
Standardmäßig stehen die Fußnoten am Ende des jeweiligen Kapitels. Alternativen:
|
||||
|
|
@ -441,6 +460,7 @@ python3 epub2md_merge.py --help
|
|||
| `--code-language SPRACHE` | Sprache für Codeblöcke ohne erkannte Sprache |
|
||||
| `--keep-wrap-markers` | Druck-Umbruchzeichen `↪` im Code stehen lassen |
|
||||
| `--keep-invisible-chars` | unsichtbare Zeichen (Nullbreiten-Leerzeichen, weiche Trennstriche) behalten |
|
||||
| `--boxes callout/quote/html/plain` | Darstellung von Infokästen |
|
||||
| `--no-css-emphasis` | Hervorhebungen nur aus HTML-Tags übernehmen, nicht aus dem Stylesheet des Buches |
|
||||
| `--math auto/tex/mathml/image` | Formeldarstellung |
|
||||
| `--footnotes end-of-chapter/end-of-book/inline/keep-links` | Fußnoten-Platzierung |
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ python3 epub2md.py buch.epub output/buch --force --report berichte/buch.json
|
|||
| `--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-wrap-markers` | aus | Druck-Umbruchzeichen `↪`/`➥` in Codeblöcken stehen lassen. Standard: Fortsetzungszeilen werden wieder zur ursprünglichen Codezeile zusammengefügt (Anzahl im Bericht: `wrap_lines_joined`) |
|
||||
| `--boxes {callout,quote,html,plain}` | `callout` | Infokästen (Tipp, Hinweis, Warnung, Exkurs, Merke …): `callout` = `> [!TIP]` mit fettem Titel (GitHub-Alerts, Obsidian-Callouts; anderswo ein Zitatblock), `quote` = Zitatblock mit fettem Titel, `html` = `<div class="box box-tip">`, `plain` = normaler Text wie bisher |
|
||||
| `--css-emphasis` / `--no-css-emphasis` | an | Fett, kursiv, unterstrichen, durchgestrichen, hoch-/tiefgestellt auch übernehmen, wenn das Buch sie nur per CSS-Klasse setzt (z. B. `span.italic`). Einzelwörter in Monospace-Schrift werden zu Inline-Code; per CSS ausgeblendete Elemente (`display:none`) entfallen. Statistik im Bericht unter `css` |
|
||||
| `--keep-invisible-chars` | aus | Nullbreiten-Zeichen und weiche Trennstriche im Fließtext behalten. Standard: entfernen, weil sie Suche und RAG stören (Anzahl im Bericht: `text_cleanup`). Code bleibt immer unverändert |
|
||||
| `--keep-html` | aus | Unbekannte/nicht sicher konvertierbare Elemente als bereinigtes HTML erhalten (sonst: Text übernommen + Eintrag in `diagnostics/unsupported-elements.md`) |
|
||||
|
|
@ -243,6 +244,14 @@ weil CommonMark keine `[^…]`-Syntax kennt.
|
|||
CSS-Dateien des Buches aus: Klassen-, Element- und ID-Selektoren, Nachfahren- und Kind-Kombinatoren,
|
||||
Spezifität und `style`-Attribute. `@media`-Blöcke und Selektoren mit Pseudoklassen werden ignoriert.
|
||||
Direkt aneinanderstoßende gleiche Hervorhebungen werden zusammengefasst (`*x**y*` → `*xy*`).
|
||||
- **Infokästen:**
|
||||
- **Erkennung:** `aside`/`epub:type` (sidebar, tip, warning …), Klassennamen (box, kasten, tipp, hinweis,
|
||||
warnung, achtung, wichtig, merke, exkurs …) oder ein CSS-Rahmen bzw. -Hintergrund um kurzen Fließtext.
|
||||
Folgen von Tipp-Absätzen ohne Rahmen werden zu einem Kasten zusammengefasst.
|
||||
- **Titel und Symbole:** Kastenüberschriften werden zum fetten Titel und tauchen damit nicht mehr in der
|
||||
Gliederung auf. Dekorative Symbolbilder entfallen.
|
||||
- **Typ:** Er ergibt sich aus Klasse, Titel oder erstem Wort: TIP, IMPORTANT, WARNING, CAUTION, sonst NOTE.
|
||||
- **Ausnahmen:** Umrahmte Codelistings und Bildrahmen gelten nicht als Kasten.
|
||||
- **Szenenwechsel und Verse:**
|
||||
- Leere Absätze zwischen zwei Textabsätzen werden zu `* * *`; mehrere hintereinander ergeben einen
|
||||
Wechsel. Ebenso werden Ornament-Absätze wie `***` oder `⁂` zu `* * *`.
|
||||
|
|
|
|||
|
|
@ -85,6 +85,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
p.add_argument("--keep-wrap-markers", action="store_true",
|
||||
help="Druck-Umbruchzeichen (↪/➥) in Codeblöcken nicht entfernen; Standard: fortgesetzte "
|
||||
"Zeilen werden wieder zur ursprünglichen Codezeile zusammengefügt")
|
||||
p.add_argument("--boxes", choices=["callout", "quote", "html", "plain"], default="callout",
|
||||
help="Infokästen (Tipp, Hinweis, Warnung, Exkurs …): callout = '> [!TIP]' (GitHub/Obsidian), "
|
||||
"quote = Zitatblock mit fettem Titel, html = <div class=\"box …\">, plain = normaler Text "
|
||||
"(Standard: callout)")
|
||||
p.add_argument("--css-emphasis", action=argparse.BooleanOptionalAction, default=True,
|
||||
help="Fett/kursiv/unterstrichen/hoch-/tiefgestellt auch übernehmen, wenn das Buch sie nur per CSS "
|
||||
"setzt; per CSS ausgeblendete Elemente weglassen (Standard: an)")
|
||||
|
|
@ -127,6 +131,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
keep_wrap_markers=args.keep_wrap_markers,
|
||||
keep_invisible_chars=args.keep_invisible_chars,
|
||||
css_emphasis=args.css_emphasis,
|
||||
boxes=args.boxes,
|
||||
strict=args.strict,
|
||||
verbose=args.verbose,
|
||||
dry_run=args.dry_run,
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ from .utils import (
|
|||
rel_link,
|
||||
remove_invisible,
|
||||
resolve_href,
|
||||
roles,
|
||||
text_content,
|
||||
url_path,
|
||||
)
|
||||
|
|
@ -97,6 +98,7 @@ class RenderOptions:
|
|||
code_language: str = "" # --code-language: label for blocks without a detected language
|
||||
keep_wrap_markers: bool = False # --keep-wrap-markers: do not join ↪-continued code lines
|
||||
keep_invisible_chars: bool = False # --keep-invisible-chars: keep zero-width chars and soft hyphens
|
||||
boxes: str = "callout" # --boxes {callout,quote,html,plain}
|
||||
css_emphasis: bool = True # --no-css-emphasis: ignore bold/italic/… that only the stylesheet expresses
|
||||
|
||||
|
||||
|
|
@ -195,6 +197,30 @@ def _collapse_blank_lines(text: str) -> str:
|
|||
return "\n".join(out)
|
||||
|
||||
|
||||
CONSUMED = "data-e2m-consumed"
|
||||
BOX_TAGS = {"div", "section", "aside", "p"}
|
||||
BOX_CLASS = re.compile(r"(?:^|[-_])(box[a-z]?|kasten|infobox|infokasten|tipps?|tip|hint|hinweis|notiz|warn|warning|"
|
||||
r"warnung|achtung|caution|vorsicht|important|wichtig|merke|merksatz|exkurs|sidebar|callout|"
|
||||
r"alert)(?:[-_\d]|$)")
|
||||
CALLOUT = {"note": "NOTE", "tip": "TIP", "important": "IMPORTANT", "warning": "WARNING", "caution": "CAUTION"}
|
||||
TYPE_WORDS = {"tipp", "tip", "tipps", "hinweis", "note", "notiz", "info", "achtung", "warnung", "warning",
|
||||
"wichtig", "important", "vorsicht", "caution", "gefahr", "merke"}
|
||||
|
||||
|
||||
def _box_type(hint: str) -> str:
|
||||
"""Callout type from class names, epub:type, a title or the first words of the box."""
|
||||
h = hint.lower()
|
||||
if re.search(r"vorsicht|caution|gefahr|danger", h):
|
||||
return "caution"
|
||||
if re.search(r"warn|achtung|attention", h):
|
||||
return "warning"
|
||||
if re.search(r"wichtig|important|merke", h):
|
||||
return "important"
|
||||
if re.search(r"tipp|\btip|hint", h):
|
||||
return "tip"
|
||||
return ""
|
||||
|
||||
|
||||
ORNAMENT = re.compile(r"(?:[*∗⁂❖✻✱✳◆◇•·~][ \t]*){1,9}")
|
||||
POEM_CLASS = re.compile(r"(?:^|[-_])(poem|poetry|verse|vers|lyrics|stanza|gedicht|lied|song)(?:$|[-_\d])")
|
||||
ITALIC_TAGS = {"em", "i", "cite", "dfn", "var"}
|
||||
|
|
@ -267,6 +293,8 @@ class Renderer:
|
|||
self.css = stylesheet
|
||||
self.scene_breaks = 0
|
||||
self.poems = 0
|
||||
self.boxes = 0
|
||||
self.in_box = False
|
||||
self.css_applied: Counter[str] = Counter()
|
||||
self.hidden_elements = 0
|
||||
self.invisible_removed = 0
|
||||
|
|
@ -337,8 +365,8 @@ class Renderer:
|
|||
anchor = self.resolver.anchor_for(self.doc, item.element)
|
||||
if anchor:
|
||||
buf.append(("r", self._anchor(anchor)))
|
||||
elif isinstance(item, Marker) or not is_element(item):
|
||||
continue
|
||||
elif isinstance(item, Marker) or not is_element(item) or item.get(CONSUMED):
|
||||
continue # markers, comments, siblings already rendered as part of a box
|
||||
elif self.is_block(item):
|
||||
flush()
|
||||
blocks.extend(self.block(item))
|
||||
|
|
@ -409,6 +437,10 @@ class Renderer:
|
|||
return ([anchor] if anchor else []) + ["* * *"]
|
||||
if name in ("div", "section", "blockquote") and self._is_poem(el):
|
||||
return self.poem(el)
|
||||
if self.opt.boxes != "plain" and not self.in_box and not self.in_table and name in BOX_TAGS:
|
||||
kind = self._box_kind(el)
|
||||
if kind is not None:
|
||||
return self.box(el, kind)
|
||||
ch = self.ch
|
||||
if ch is not None and el is ch.title_el and not heading_level(el):
|
||||
return self.heading(el, forced_level=1)
|
||||
|
|
@ -522,6 +554,112 @@ class Renderer:
|
|||
text = "\n>\n".join("\\\n".join(f"> {line}" for line in stanza) for stanza in stanzas)
|
||||
return ([anchor] if anchor else []) + [text]
|
||||
|
||||
# ------------------------------------------------------------------ info boxes (P2)
|
||||
|
||||
def _box_kind(self, el: Any) -> str | None:
|
||||
"""'' for a box without type hint, a type keyword, or None if ``el`` is no box."""
|
||||
name = local(el.tag)
|
||||
types = set(epub_types(el)) | {r.removeprefix("doc-") for r in roles(el)}
|
||||
hint = " ".join(sorted(types)) + " " + " ".join(classes(el))
|
||||
semantic = types & {"sidebar", "tip", "notice", "warning", "caution", "important", "help", "example"}
|
||||
by_class = any(BOX_CLASS.search(c) for c in classes(el))
|
||||
if name == "p":
|
||||
return _box_type(hint) if by_class and not self._is_empty(el) else None
|
||||
if not (semantic or by_class or self._css_framed(el)):
|
||||
return None
|
||||
if not text_content(el).strip() or any(local(e.tag) == "pre" for e in iter_elements(el)) \
|
||||
and not any(local(e.tag) == "p" and text_content(e).strip() for e in iter_elements(el)):
|
||||
return None # bordered code listings or image frames are no info boxes
|
||||
if self.ch is not None and any(e is self.ch.title_el for e in iter_elements(el)):
|
||||
return None # never swallow the chapter heading
|
||||
return _box_type(hint)
|
||||
|
||||
def _css_framed(self, el: Any) -> bool:
|
||||
"""Block with a visible border or background in the book's CSS, holding short prose."""
|
||||
if self.css is None or local(el.tag) not in ("div", "section", "aside"):
|
||||
return False
|
||||
style = self.css.computed(el)
|
||||
border = any(k.startswith("border") and not re.search(r"\bnone\b|^0(px)?\b|hidden", v)
|
||||
for k, v in style.items() if k in ("border", "border-top", "border-left", "border-style",
|
||||
"border-width", "border-bottom", "border-right"))
|
||||
bg = style.get("background-color") or style.get("background") or ""
|
||||
background = bool(bg) and not re.search(r"transparent|none|inherit|initial|^#?fff(fff)?$|white", bg.lower())
|
||||
if not (border or background):
|
||||
return False
|
||||
text = norm_ws(text_content(el))
|
||||
return 0 < len(text) <= 3000 and any(local(e.tag) == "p" for e in iter_elements(el)) \
|
||||
and not any(heading_level(e) in (1, 2) for e in iter_elements(el))
|
||||
|
||||
def box(self, el: Any, kind: str) -> list[str]:
|
||||
anchor = self.anchors_of(el)
|
||||
members = [el]
|
||||
if local(el.tag) == "p": # paragraph-style boxes: group following siblings of the same kind
|
||||
for sib in self._element_siblings(el, forward=True):
|
||||
if local(sib.tag) == "p" and any(BOX_CLASS.search(c) for c in classes(sib)) \
|
||||
and _box_type(" ".join(classes(sib))) == kind:
|
||||
members.append(sib)
|
||||
else:
|
||||
break
|
||||
title_el = self._box_title(members)
|
||||
title = ""
|
||||
if title_el is not None:
|
||||
title = join_inline(self.inline_children(title_el), hard_break=" ").strip()
|
||||
kind = kind or _box_type(norm_ws(text_content(title_el)))
|
||||
saved = self.in_box
|
||||
self.in_box = True
|
||||
try:
|
||||
blocks: list[str] = []
|
||||
for member in members:
|
||||
if member is title_el:
|
||||
continue
|
||||
if local(member.tag) == "p":
|
||||
blocks.extend(self.flow(member))
|
||||
else:
|
||||
blocks.extend(self.render_items(
|
||||
[(None, x) for x in self._items_without(member, title_el) if not self._box_icon(x)]))
|
||||
finally:
|
||||
self.in_box = saved
|
||||
for sib in members[1:]:
|
||||
sib.set(CONSUMED, "1")
|
||||
if not kind and blocks:
|
||||
kind = _box_type(norm_ws(re.sub(r"[*_\\]", "", blocks[0]))[:30])
|
||||
kind = kind or "note"
|
||||
if title and norm_ws(re.sub(r"[*_\\:]", "", title)).lower() in TYPE_WORDS:
|
||||
title = "" # 'Tipp' as title of a TIP box adds nothing
|
||||
self.boxes += 1
|
||||
out = [anchor] if anchor else []
|
||||
head = [f"**{title}**"] if title else []
|
||||
if self.opt.boxes == "html":
|
||||
out.append(f'<div class="box box-{kind}">\n\n' + "\n\n".join(head + blocks) + "\n\n</div>")
|
||||
return out
|
||||
body = "\n\n".join(head + blocks)
|
||||
if self.opt.boxes == "callout":
|
||||
body = f"[!{CALLOUT[kind]}]\n" + body
|
||||
out.append("\n".join("> " + line if line.strip() else ">" for line in body.split("\n")))
|
||||
return out
|
||||
|
||||
def _box_title(self, members: list[Any]) -> Any:
|
||||
first = members[0]
|
||||
if local(first.tag) == "p":
|
||||
text = norm_ws(text_content(first))
|
||||
return first if len(members) > 1 and 0 < len(text) <= 40 and not text.endswith((".", "!", "?")) else None
|
||||
for child in first:
|
||||
if not is_element(child) or self._box_icon(child):
|
||||
continue
|
||||
if heading_level(child) or any(re.search(r"heading|title|head$|ueberschrift", c) for c in classes(child)):
|
||||
return child
|
||||
return None
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _box_icon(node: Any) -> bool:
|
||||
"""Decorative icon container inside a box (e.g. <div class="Box_img"><img alt="Image"></div>)."""
|
||||
if not is_element(node) or text_content(node).strip():
|
||||
return False
|
||||
imgs = [e for e in iter_elements(node) if local(e.tag) == "img"]
|
||||
return bool(imgs) and (any(re.search(r"icon|box_?img|symbol", c) for c in classes(node))
|
||||
or all((i.get("alt") or "").strip().lower() in ("", "image", "icon", "bild") for i in imgs))
|
||||
|
||||
def _flow_with_anchor(self, el: Any) -> list[str]:
|
||||
anchor = self.anchors_of(el)
|
||||
if self.ch is not None and {"index", "index-persons", "glossary"} & set(self.ch.types):
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ class Options:
|
|||
keep_wrap_markers: bool = False
|
||||
keep_invisible_chars: bool = False
|
||||
css_emphasis: bool = True
|
||||
boxes: str = "callout"
|
||||
strict: bool = False
|
||||
verbose: bool = False
|
||||
dry_run: bool = False
|
||||
|
|
@ -254,7 +255,8 @@ def _render(reader: EpubReader, package: Any, chapters: list[Chapter], resolver:
|
|||
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, keep_wrap_markers=opts.keep_wrap_markers,
|
||||
keep_invisible_chars=opts.keep_invisible_chars, css_emphasis=opts.css_emphasis),
|
||||
keep_invisible_chars=opts.keep_invisible_chars, css_emphasis=opts.css_emphasis,
|
||||
boxes=opts.boxes),
|
||||
resolver, media, notes, diag, set(reader.names), reader.entry_problem, stylesheet,
|
||||
)
|
||||
width = max(3, len(str(len(chapters) + 1)))
|
||||
|
|
@ -517,7 +519,7 @@ def _report(input_path: Path, sha: str, generated_at: str, package: Any, opts: O
|
|||
"text_cleanup": {"invisible_chars_removed": renderer.invisible_removed},
|
||||
"css": {"rules": len(renderer.css.rules) if renderer.css else 0, "applied": dict(renderer.css_applied),
|
||||
"hidden_elements": renderer.hidden_elements},
|
||||
"layout": {"scene_breaks": renderer.scene_breaks, "poems": renderer.poems},
|
||||
"layout": {"scene_breaks": renderer.scene_breaks, "poems": renderer.poems, "boxes": renderer.boxes},
|
||||
"warnings": warnings,
|
||||
"errors": errors,
|
||||
"infos": [d.as_dict() for d in diag.items if d.level == "info"],
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Reference in a new issue