feat: Farben, Schriften, Größen und Sprache per --styles (P3, P7)
--styles semantic (Standard): Hintergrundmarkierung wird zu <mark>, sonst nur bedeutungstragende Gestaltung. --styles html: Farbe, Hintergrund, Schriftgröße/-art, Kapitälchen und Ausrichtung bleiben als <span style>/<div style> erhalten, fremdsprachige Passagen als <span lang>. Übernommen wird nur, was vom Grundstil abweicht (häufigste Werte aller Absätze, Schriftgröße ab ±15 %); Formel- und Bildabsätze bekommen keine Ausrichtung; unsichere Werte werden verworfen. Gilt im Fließtext, für Absätze und in HTML-Tabellen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4992672ca6
commit
5cef9d7885
7 changed files with 227 additions and 8 deletions
|
|
@ -228,6 +228,21 @@ Andere Darstellungen wählst du mit `--boxes`:
|
|||
- `html`: HTML-`<div>`
|
||||
- `plain`: wie normaler Text
|
||||
|
||||
### Farben, Schriftarten und Schriftgrößen
|
||||
|
||||
Markdown kennt keine Farben, Schriftarten oder Schriftgrößen. Standardmäßig übernimmt das Programm daher
|
||||
nur, was eine Bedeutung trägt:
|
||||
|
||||
- **fett** und *kursiv*, auch wenn das Buch sie nur über sein Stylesheet setzt
|
||||
- unterstrichen, hoch- und tiefgestellt
|
||||
- farbig hinterlegter Text als `<mark>`-Markierung
|
||||
|
||||
Möchtest du das Aussehen möglichst erhalten, etwa für eine HTML-, EPUB- oder PDF-Ausgabe mit Pandoc oder zum
|
||||
Lesen in Obsidian, wandle mit `--styles html` um. Dann bleiben Farbe, Hintergrund, Schriftgröße,
|
||||
Schriftart, Kapitälchen und zentrierte oder rechtsbündige Absätze als HTML erhalten. Das gilt nur dort, wo
|
||||
sie vom normalen Text abweichen. Fremdsprachige Passagen werden als solche gekennzeichnet. GitHub zeigt
|
||||
diese Angaben nicht an.
|
||||
|
||||
### Buch mit vielen Fußnoten
|
||||
|
||||
Standardmäßig stehen die Fußnoten am Ende des jeweiligen Kapitels. Alternativen:
|
||||
|
|
@ -461,6 +476,7 @@ python3 epub2md_merge.py --help
|
|||
| `--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 |
|
||||
| `--styles semantic/html` | Farben, Schriften, Größen, Ausrichtung und Sprache als HTML übernehmen (`html`) |
|
||||
| `--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 |
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ python3 epub2md.py buch.epub output/buch --force --report berichte/buch.json
|
|||
| `--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 |
|
||||
| `--styles {semantic,html}` | `semantic` | `semantic`: nur bedeutungstragende Gestaltung (Hintergrundmarkierung → `<mark>`). `html`: zusätzlich Farbe, Hintergrund, Schriftgröße, Schriftart, Kapitälchen, Ausrichtung als `<span style>`/`<div style>` und fremdsprachige Passagen als `<span lang>`, jeweils nur, wo sie vom normalen Text des Buches abweichen. Obsidian, VS Code und Pandoc (HTML/PDF/EPUB) zeigen das an, GitHub nicht |
|
||||
| `--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`) |
|
||||
|
|
@ -244,6 +245,10 @@ 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*`).
|
||||
- **Farben und Schriften:** Markdown kennt keine Farben, Schriftarten oder -größen. Mit `--styles html`
|
||||
bleiben sie als HTML-Stil erhalten. Übernommen wird nur, was vom Grundstil abweicht (häufigste Werte aller
|
||||
Absätze; Schriftgröße erst ab ±15 %). Abgesetzte Formeln und Bildabsätze bekommen keine Ausrichtung. Werte
|
||||
mit `url(...)`, `expression` usw. werden verworfen.
|
||||
- **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.
|
||||
|
|
|
|||
|
|
@ -89,6 +89,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
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("--styles", choices=["semantic", "html"], default="semantic",
|
||||
help="semantic: nur Bedeutung übernehmen (Hintergrundmarkierung → <mark>); html: zusätzlich Farbe, "
|
||||
"Hintergrund, Schriftgröße/-art, Kapitälchen, Ausrichtung und Fremdsprache als <span style>/"
|
||||
"<span lang> (wo sie vom normalen Text abweichen; GitHub zeigt das nicht an) (Standard: semantic)")
|
||||
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)")
|
||||
|
|
@ -132,6 +136,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
keep_invisible_chars=args.keep_invisible_chars,
|
||||
css_emphasis=args.css_emphasis,
|
||||
boxes=args.boxes,
|
||||
styles=args.styles,
|
||||
strict=args.strict,
|
||||
verbose=args.verbose,
|
||||
dry_run=args.dry_run,
|
||||
|
|
|
|||
|
|
@ -192,3 +192,95 @@ def emphasis(style: dict[str, str]) -> dict[str, bool]:
|
|||
"mono": bool(MONOSPACE.search(style.get("font-family", ""))),
|
||||
"hidden": style.get("display", "").lower() == "none",
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------- visual styles (--styles html)
|
||||
|
||||
STYLE_PROPS = ("color", "background-color", "font-size", "font-family", "font-variant", "text-align")
|
||||
UNSAFE_VALUE = re.compile(r"url\(|expression|javascript|[;<>{}\\]", re.I)
|
||||
TRIVIAL_BG = re.compile(r"^(transparent|none|inherit|initial|unset|white|#fff|#ffffff|rgba?\(\s*255\s*,\s*255\s*,\s*255.*)$")
|
||||
COLOR_TOKEN = re.compile(r"(#[0-9a-f]{3,8}\b|rgba?\([^)]*\)|hsla?\([^)]*\)|\b[a-z]+\b)", re.I)
|
||||
KEYWORD_SIZES = {"xx-small": 0.6, "x-small": 0.75, "small": 0.89, "medium": 1.0, "large": 1.2, "x-large": 1.5,
|
||||
"xx-large": 2.0, "smaller": 0.83, "larger": 1.2}
|
||||
|
||||
|
||||
def _font_size_em(value: str) -> float | None:
|
||||
v = value.strip().lower()
|
||||
if v in KEYWORD_SIZES:
|
||||
return KEYWORD_SIZES[v]
|
||||
m = re.fullmatch(r"([\d.]+)\s*(em|rem|%|px|pt)?", v)
|
||||
if not m:
|
||||
return None
|
||||
n = float(m.group(1))
|
||||
return {"em": n, "rem": n, "%": n / 100, "px": n / 16, "pt": n / 12, None: None}.get(m.group(2), None)
|
||||
|
||||
|
||||
def _background_color(style: dict[str, str]) -> str:
|
||||
bg = style.get("background-color", "")
|
||||
if not bg and style.get("background"):
|
||||
for tok in COLOR_TOKEN.findall(style["background"]):
|
||||
if tok.lower() not in ("none", "repeat", "no", "fixed", "scroll", "top", "left", "center", "right",
|
||||
"bottom", "url"):
|
||||
bg = tok
|
||||
break
|
||||
bg = bg.strip()
|
||||
return "" if not bg or TRIVIAL_BG.match(bg.lower()) else bg
|
||||
|
||||
|
||||
def _family(value: str) -> str:
|
||||
parts = [p.strip().strip("'\"") for p in value.split(",") if p.strip()]
|
||||
return ", ".join(f"'{p}'" if " " in p else p for p in parts)
|
||||
|
||||
|
||||
def _is_black(value: str) -> bool:
|
||||
return value.strip().lower() in ("black", "#000", "#000000", "rgb(0,0,0)", "rgb(0, 0, 0)", "windowtext")
|
||||
|
||||
|
||||
def has_highlight(style: dict[str, str]) -> bool:
|
||||
return bool(_background_color(style))
|
||||
|
||||
|
||||
def base_style(stylesheet: StyleSheet, paragraphs: list[Any]) -> dict[str, str]:
|
||||
"""Most frequent value per visual property over the book's paragraphs (the 'normal' text style)."""
|
||||
counters: dict[str, dict[str, int]] = {p: {} for p in STYLE_PROPS}
|
||||
for el in paragraphs:
|
||||
style = stylesheet.computed(el)
|
||||
for prop in STYLE_PROPS:
|
||||
value = _background_color(style) if prop == "background-color" else style.get(prop, "")
|
||||
counters[prop][value] = counters[prop].get(value, 0) + 1
|
||||
return {p: max(c, key=lambda v: c[v]) for p, c in counters.items() if c}
|
||||
|
||||
|
||||
def style_props(style: dict[str, str], base: dict[str, str], block: bool = False) -> list[tuple[str, str]]:
|
||||
"""Visual properties of ``style`` that differ from the book's base text style (sanitized)."""
|
||||
out: list[tuple[str, str]] = []
|
||||
for prop in STYLE_PROPS:
|
||||
if prop == "text-align" and not block:
|
||||
continue
|
||||
value = _background_color(style) if prop == "background-color" else style.get(prop, "").strip()
|
||||
if not value or UNSAFE_VALUE.search(value) or value.lower() in ("inherit", "initial", "unset", "currentcolor"):
|
||||
continue
|
||||
reference = base.get(prop, "")
|
||||
if prop == "color" and not reference:
|
||||
reference = "black" # no colour declared = default text colour
|
||||
if prop == "color" and _is_black(value) and _is_black(reference):
|
||||
continue
|
||||
if prop == "font-size":
|
||||
size, ref = _font_size_em(value), _font_size_em(reference) if reference else 1.0
|
||||
if size is None or ref is None or abs(size / (ref or 1.0) - 1) < 0.15:
|
||||
continue
|
||||
elif prop == "font-family":
|
||||
if _family(value).lower() == _family(reference).lower():
|
||||
continue
|
||||
value = _family(value)
|
||||
elif prop == "font-variant":
|
||||
if "small-caps" not in value.lower() or "small-caps" in reference.lower():
|
||||
continue
|
||||
value = "small-caps"
|
||||
elif prop == "text-align":
|
||||
if value.lower() not in ("center", "right", "end") or value.lower() == reference.lower():
|
||||
continue
|
||||
elif value.lower() == reference.lower():
|
||||
continue
|
||||
out.append((prop, value))
|
||||
return out
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ from urllib.parse import unquote
|
|||
from lxml import etree
|
||||
|
||||
from .chapters import IdCarrier, Marker, heading_level
|
||||
from .css import StyleSheet, emphasis, inline_style
|
||||
from .css import StyleSheet, emphasis, has_highlight, inline_style, style_props
|
||||
from .codeblocks import (
|
||||
code_text,
|
||||
detect_language,
|
||||
|
|
@ -99,6 +99,7 @@ class RenderOptions:
|
|||
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}
|
||||
styles: str = "semantic" # --styles {semantic,html}
|
||||
css_emphasis: bool = True # --no-css-emphasis: ignore bold/italic/… that only the stylesheet expresses
|
||||
|
||||
|
||||
|
|
@ -228,6 +229,10 @@ BOLD_TAGS = {"strong", "b"}
|
|||
CSS_SKIP_TAGS = {"br", "wbr", "img", "svg", "image", "math", "code", "samp", "tt", "kbd", "pre"}
|
||||
|
||||
|
||||
def _style_attr(props: list[tuple[str, str]]) -> str:
|
||||
return html.escape("; ".join(f"{k}: {v}" for k, v in props), quote=False).replace('"', "'")
|
||||
|
||||
|
||||
def _wrap_text(text: str, marker: str) -> str:
|
||||
"""Wrap a finished paragraph in emphasis markers (whitespace stays outside)."""
|
||||
core = text.strip()
|
||||
|
|
@ -288,7 +293,8 @@ class Renderer:
|
|||
def __init__(self, options: RenderOptions, resolver: LinkResolver, media: MediaRegistry,
|
||||
notes: NoteIndex, diag: Diagnostics, zip_names: set[str],
|
||||
entry_problem: Callable[[str], str | None] | None = None,
|
||||
stylesheet: StyleSheet | None = None) -> None:
|
||||
stylesheet: StyleSheet | None = None, base_style: dict[str, str] | None = None) -> None:
|
||||
self.base_style = base_style or {}
|
||||
self.entry_problem = entry_problem or (lambda path: None)
|
||||
self.css = stylesheet
|
||||
self.scene_breaks = 0
|
||||
|
|
@ -684,8 +690,9 @@ class Renderer:
|
|||
if flag:
|
||||
blocks[0] = _wrap_text(blocks[0], marker)
|
||||
self.css_applied[key] += 1
|
||||
blocks = self._block_styles(el, blocks)
|
||||
if anchor:
|
||||
if blocks and not blocks[0].startswith(("#", "`", "|", "<table", "> ", "- ", "$$", "\\[", "* * *")) \
|
||||
if blocks and not blocks[0].startswith(("#", "`", "|", "<table", "<div", "> ", "- ", "$$", "\\[", "* * *")) \
|
||||
and not re.match(r"\d+\. ", blocks[0]):
|
||||
blocks[0] = anchor + blocks[0]
|
||||
else:
|
||||
|
|
@ -1057,14 +1064,61 @@ class Renderer:
|
|||
return True
|
||||
|
||||
def inline(self, el: Any) -> list[Piece]:
|
||||
"""Inline rendering plus formatting that only the stylesheet expresses (P1)."""
|
||||
"""Inline rendering plus formatting that only the stylesheet expresses (P1, P3, P7)."""
|
||||
name = local(el.tag)
|
||||
if not self.opt.css_emphasis or name in CSS_SKIP_TAGS:
|
||||
if name in CSS_SKIP_TAGS:
|
||||
return self._inline_core(el)
|
||||
if self._hidden(el):
|
||||
return []
|
||||
if self.in_heading or is_inline_code(el) or not text_content(el).strip():
|
||||
return self._inline_core(el)
|
||||
pieces = self._inline_emphasis(el) if self.opt.css_emphasis else self._inline_core(el)
|
||||
return self._inline_styles(el, pieces)
|
||||
|
||||
def _inline_styles(self, el: Any, pieces: list[Piece]) -> list[Piece]:
|
||||
"""--styles semantic: background highlight → <mark>; --styles html: deviating visual properties and
|
||||
foreign-language spans as <span style>/<span lang>."""
|
||||
if self.css is None or (pieces and pieces[0][0] == "r" and pieces[0][1].startswith("`")):
|
||||
return pieces # no stylesheet, or rendered as inline code
|
||||
style = self.css.computed(el)
|
||||
if self.opt.styles == "html":
|
||||
props = style_props(style, self.base_style)
|
||||
if props:
|
||||
pieces = self._wrap(pieces, f'<span style="{_style_attr(props)}">', "</span>")
|
||||
self.css_applied["style"] += 1
|
||||
lang = self._foreign_lang(el)
|
||||
if lang:
|
||||
pieces = self._wrap(pieces, f'<span lang="{html.escape(lang)}">', "</span>")
|
||||
self.css_applied["lang"] += 1
|
||||
elif has_highlight(style):
|
||||
pieces = self._wrap(pieces, "<mark>", "</mark>")
|
||||
self.css_applied["mark"] += 1
|
||||
return pieces
|
||||
|
||||
def _foreign_lang(self, el: Any) -> str:
|
||||
lang = (el.get("lang") or el.get("{http://www.w3.org/XML/1998/namespace}lang") or "").strip()
|
||||
if lang and re.fullmatch(r"[A-Za-z]{2,3}(-[A-Za-z0-9]{1,8})*", lang) \
|
||||
and lang.split("-")[0].lower() != self.opt.language.split("-")[0].lower():
|
||||
return lang
|
||||
return ""
|
||||
|
||||
def _block_styles(self, el: Any, blocks: list[str]) -> list[str]:
|
||||
"""--styles html: paragraph alignment, colour, size, font and language as a wrapping <div>."""
|
||||
if self.opt.styles != "html" or self.css is None or self.in_table or not blocks or local(el.tag) not in ("p", "div") \
|
||||
or any(local(d.tag) in BLOCK_TAGS for d in iter_elements(el) if d is not el):
|
||||
return blocks
|
||||
if all(b.startswith(("$$", "\\[", "![", "<img", "<math", "<a id")) for b in blocks):
|
||||
return blocks # display formulas and image paragraphs are set apart anyway
|
||||
props = style_props(self.css.computed(el), self.base_style, block=True)
|
||||
lang = self._foreign_lang(el)
|
||||
if not props and not lang:
|
||||
return blocks
|
||||
attrs = (f' style="{_style_attr(props)}"' if props else "") + (f' lang="{html.escape(lang)}"' if lang else "")
|
||||
self.css_applied["style" if props else "lang"] += 1
|
||||
return [f"<div{attrs}>"] + blocks + ["</div>"]
|
||||
|
||||
def _inline_emphasis(self, el: Any) -> list[Piece]:
|
||||
name = local(el.tag)
|
||||
e = self._emphasis(el)
|
||||
if e["mono"] and name != "a" and re.fullmatch(r"\S+", norm_ws(text_content(el))) \
|
||||
and not any(local(d.tag) in ("img", "svg", "math", "a") for d in iter_elements(el) if d is not el):
|
||||
|
|
@ -1577,6 +1631,15 @@ class Renderer:
|
|||
if flag:
|
||||
content = f"<{tag}>{content}</{tag}>"
|
||||
self.css_applied[key] += 1
|
||||
if self.css is not None and content.strip():
|
||||
style = self.css.computed(el)
|
||||
props = style_props(style, self.base_style) if self.opt.styles == "html" else []
|
||||
if props:
|
||||
content = f'<span style="{_style_attr(props)}">{content}</span>'
|
||||
self.css_applied["style"] += 1
|
||||
elif self.opt.styles != "html" and has_highlight(style):
|
||||
content = f"<mark>{content}</mark>"
|
||||
self.css_applied["mark"] += 1
|
||||
return anchor + content
|
||||
attrs = []
|
||||
for k, v in el.attrib.items():
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ from typing import Any
|
|||
from . import TOOL_NAME, __version__
|
||||
from .chapters import ChapterBuilder
|
||||
from .diagnostics import Diagnostics
|
||||
from .css import StyleSheet
|
||||
from .css import StyleSheet, base_style
|
||||
from .epub_reader import EpubReader, decode_markup, parse_xml
|
||||
from .errors import ContainerError, InputError, StructureError
|
||||
from .footnotes import collect_notes
|
||||
|
|
@ -26,7 +26,7 @@ from .media import MediaRegistry
|
|||
from .models import Chapter
|
||||
from .normalize import Doc, book_uses_title_tables, is_html_toc_doc, make_doc, normalize_doc, remove_keep_tail
|
||||
from .references import LinkResolver
|
||||
from .utils import is_inside, iter_elements, local, sha256_file, slugify, text_content
|
||||
from .utils import is_element, is_inside, iter_elements, local, sha256_file, slugify, text_content
|
||||
from . import writer
|
||||
|
||||
MARKER_FILES = ("conversion-report.json", "manifest.json")
|
||||
|
|
@ -51,6 +51,7 @@ class Options:
|
|||
keep_invisible_chars: bool = False
|
||||
css_emphasis: bool = True
|
||||
boxes: str = "callout"
|
||||
styles: str = "semantic"
|
||||
strict: bool = False
|
||||
verbose: bool = False
|
||||
dry_run: bool = False
|
||||
|
|
@ -256,8 +257,10 @@ def _render(reader: EpubReader, package: Any, chapters: list[Chapter], resolver:
|
|||
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,
|
||||
boxes=opts.boxes),
|
||||
boxes=opts.boxes, styles=opts.styles),
|
||||
resolver, media, notes, diag, set(reader.names), reader.entry_problem, stylesheet,
|
||||
base_style(stylesheet, [p for c in chapters for _, it in c.items if is_element(it)
|
||||
for p in iter_elements(it) if local(p.tag) == "p"][:20000]) if stylesheet else None,
|
||||
)
|
||||
width = max(3, len(str(len(chapters) + 1)))
|
||||
notes_title = "Anmerkungen" if language.startswith("de") else "Notes"
|
||||
|
|
|
|||
|
|
@ -127,3 +127,38 @@ def test_p2_box_modes(convert):
|
|||
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]
|
||||
|
||||
|
||||
STYLE_CSS = b"""p { font-family: "Aldus", serif; font-size: 0.9em; color: #000 }
|
||||
.rot { color: #942f6a } .gelb { background-color: #ffff00 } .gross { font-size: 1.5em }
|
||||
.kap { font-variant: small-caps } p.mitte { text-align: center } .script { font-family: "Mulled Wine", cursive }"""
|
||||
|
||||
|
||||
def styles_book(body: str, *args: str, convert=None):
|
||||
return convert(Book(docs=[Doc("c.xhtml", body)], media={"s.css": STYLE_CSS}), *args)
|
||||
|
||||
|
||||
BODY_STYLES = ('<h1>S</h1><p>Normal <span class="rot">rot</span>, <span class="gelb">markiert</span>, '
|
||||
'<span class="gross">groß</span>, <span class="kap">Kapitälchen</span>, <span class="script">Zierschrift</span>, '
|
||||
'<span lang="en">English words</span> und <span lang="de">deutsch</span>.</p>'
|
||||
'<p>Zweiter Absatz.</p><p>Dritter Absatz.</p><p class="mitte">Widmung</p>')
|
||||
|
||||
|
||||
def test_p3_semantic_styles_default(convert):
|
||||
text = styles_book(BODY_STYLES, convert=convert).chapter("001")
|
||||
assert "Normal rot, <mark>markiert</mark>, groß, Kapitälchen, Zierschrift, English words und deutsch." in text
|
||||
assert "style=" not in text and "lang=" not in text
|
||||
|
||||
|
||||
def test_p3_p7_html_styles(convert):
|
||||
run = styles_book(BODY_STYLES, "--styles", "html", convert=convert)
|
||||
text = run.chapter("001")
|
||||
assert '<span style="color: #942f6a">rot</span>' in text
|
||||
assert '<span style="background-color: #ffff00">markiert</span>' in text
|
||||
assert '<span style="font-size: 1.5em">groß</span>' in text
|
||||
assert '<span style="font-variant: small-caps">Kapitälchen</span>' in text
|
||||
assert "<span style=\"font-family: 'Mulled Wine', cursive\">Zierschrift</span>" in text
|
||||
assert '<span lang="en">English words</span> und deutsch.' in text
|
||||
assert '<div style="text-align: center">\n\nWidmung\n\n</div>' in text
|
||||
assert "Zweiter Absatz." in text and "Aldus" not in text # base paragraph style is not repeated
|
||||
assert run.report["css"]["applied"]["style"] >= 5
|
||||
|
|
|
|||
Loading…
Reference in a new issue