Compare commits

..
6 changed files with 16 additions and 68 deletions

View file

@ -29,10 +29,6 @@ Versionsnummern nach [Semantic Versioning](https://semver.org/lang/de/).
### Behoben ### Behoben
- CSS-Auswertung: Es zählen nur noch Stylesheets, die ein Dokument per `<link>` (oder `@import`) einbindet.
Bisher wirkten alle CSS-Dateien des Manifests, auch mitgelieferte Layouts anderer Buchreihen. Im C-Buch
machte das aus fetten Buchstaben Inline-Code (`` `st`an`d`ard ``) und setzte falsche `<mark>`; eine
fremde `display:none`-Regel hätte Text gelöscht (QA3-01, Regressionstests in `tests/test_styles.py`).
- `pandoc/callouts.lua`: Die Kästen blieben ohne Rahmen, wenn man Pandoc zusätzlich `-H`/`--include-in-header` - `pandoc/callouts.lua`: Die Kästen blieben ohne Rahmen, wenn man Pandoc zusätzlich `-H`/`--include-in-header`
übergab, und im EPUB hatten nur die Titelseite, nicht die Kapitel, das Kasten-CSS. Das CSS steht jetzt in übergab, und im EPUB hatten nur die Titelseite, nicht die Kapitel, das Kasten-CSS. Das CSS steht jetzt in
`pandoc/callouts.css`; der Filter setzt es bei HTML an den Anfang des Dokuments und fügt es bei EPUB den `pandoc/callouts.css`; der Filter setzt es bei HTML an den Anfang des Dokuments und fügt es bei EPUB den

View file

@ -249,8 +249,6 @@ weil CommonMark keine `[^…]`-Syntax kennt.
- **CSS-Formatierung:** Viele Verlage setzen Hervorhebungen nur über Stylesheets. epub2md wertet die - **CSS-Formatierung:** Viele Verlage setzen Hervorhebungen nur über Stylesheets. epub2md wertet die
CSS-Dateien des Buches aus: Klassen-, Element- und ID-Selektoren, Nachfahren- und Kind-Kombinatoren, 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. Spezifität und `style`-Attribute. `@media`-Blöcke und Selektoren mit Pseudoklassen werden ignoriert.
Berücksichtigt werden nur Stylesheets, die ein Dokument per `<link>` (oder `@import`) einbindet;
mitgelieferte Layouts anderer Buchreihen bleiben wie im E-Book-Reader wirkungslos (Info `css-unlinked`).
Direkt aneinanderstoßende gleiche Hervorhebungen werden zusammengefasst (`*x**y*` → `*xy*`). Direkt aneinanderstoßende gleiche Hervorhebungen werden zusammengefasst (`*x**y*` → `*xy*`).
- **Farben und Schriften:** Markdown kennt keine Farben, Schriftarten oder -größen. Mit `--styles html` - **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 bleiben sie als HTML-Stil erhalten. Übernommen wird nur, was vom Grundstil abweicht (häufigste Werte aller

View file

@ -1233,8 +1233,8 @@ class Renderer:
finally: finally:
self.strong -= 1 self.strong -= 1
if name in ("del", "s", "strike"): if name in ("del", "s", "strike"):
del_open, del_close = ("~~", "~~") if self.opt.format == "gfm" else ("<del>", "</del>") marker = ("~~", "~~") if self.opt.format == "gfm" else ("<del>", "</del>")
return pre + self._wrap(self.inline_children(el), del_open, del_close) return pre + self._wrap(self.inline_children(el), marker[0], marker[1])
if name in ("u", "ins", "mark", "kbd", "sup", "sub"): if name in ("u", "ins", "mark", "kbd", "sup", "sub"):
if name in ("sup", "sub"): if name in ("sup", "sub"):
elems = [c for c in el if is_element(c)] elems = [c for c in el if is_element(c)]

View file

@ -13,7 +13,6 @@ from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from urllib.parse import unquote
from . import TOOL_NAME, __version__ from . import TOOL_NAME, __version__
from .chapters import ChapterBuilder from .chapters import ChapterBuilder
@ -28,7 +27,7 @@ from .media import MediaRegistry
from .models import Chapter from .models import Chapter
from .normalize import Doc, book_uses_title_tables, is_html_toc_doc, make_doc, normalize_doc, remove_keep_tail from .normalize import Doc, book_uses_title_tables, is_html_toc_doc, make_doc, normalize_doc, remove_keep_tail
from .references import LinkResolver from .references import LinkResolver
from .utils import is_element, is_inside, iter_elements, local, resolve_href, sha256_file, slugify, text_content from .utils import is_element, is_inside, iter_elements, local, sha256_file, slugify, text_content
from . import writer from . import writer
MARKER_FILES = ("conversion-report.json", "manifest.json") MARKER_FILES = ("conversion-report.json", "manifest.json")
@ -234,39 +233,18 @@ def _build_structure(docs: list[Doc], package: Any, opts: Options,
return notes, chapters, resolver return notes, chapters, resolver
CSS_IMPORT = re.compile(r"""@import\s+(?:url\(\s*)?["']?([^"')\s;]+)""", re.I)
def _load_stylesheet(reader: EpubReader, package: Any, docs: list[Doc], diag: Diagnostics) -> StyleSheet: def _load_stylesheet(reader: EpubReader, package: Any, docs: list[Doc], diag: Diagnostics) -> StyleSheet:
"""The book's CSS: manifest stylesheets that a spine document links (directly or via @import), in manifest """All CSS of the book: manifest stylesheets (manifest order) plus <style> elements of the documents."""
order, plus <style> elements of the documents. Unlinked stylesheets (publishers ship the layouts of other texts: list[str] = []
series) are ignored – a reading system would not apply them either.""" for item in package.manifest.values():
pending: list[str] = [] if item.media_type == "text/css" or item.path.lower().endswith(".css"):
try:
texts.append(decode_markup(reader.read(item.path), item.path, diag))
except (KeyError, ContainerError):
diag.warn("css-unreadable", f"Stylesheet {item.path} nicht lesbar; seine Formatierung entfällt.", item.path)
for doc in docs: for doc in docs:
for e in iter_elements(doc.root): texts.extend(text_content(e) for e in iter_elements(doc.root) if local(e.tag) == "style")
if local(e.tag) == "link" and "stylesheet" in (e.get("rel") or "").lower().split(): return StyleSheet(texts)
pending.append(resolve_href(doc.path, unquote((e.get("href") or "").split("#")[0].split("?")[0])) or "")
texts: dict[str, str] = {}
while pending:
path = pending.pop()
if not path or path in texts:
continue
try:
texts[path] = decode_markup(reader.read(path), path, diag)
except (KeyError, ContainerError):
diag.warn("css-unreadable", f"Stylesheet {path} nicht lesbar; seine Formatierung entfällt.", path)
texts[path] = ""
continue
pending.extend(resolve_href(path, unquote(m)) or "" for m in CSS_IMPORT.findall(texts[path]))
css_items = [i.path for i in package.manifest.values() if i.media_type == "text/css" or i.path.lower().endswith(".css")]
ordered = [texts[p] for p in css_items if p in texts] + [t for p, t in texts.items() if p not in css_items]
unlinked = len([p for p in css_items if p not in texts])
if unlinked:
diag.info("css-unlinked", f"{unlinked} Stylesheet(s) im Manifest werden von keinem Dokument eingebunden "
"und daher nicht ausgewertet.")
for doc in docs:
ordered.extend(text_content(e) for e in iter_elements(doc.root) if local(e.tag) == "style")
return StyleSheet(ordered)
def _render(reader: EpubReader, package: Any, chapters: list[Chapter], resolver: LinkResolver, notes: Any, def _render(reader: EpubReader, package: Any, chapters: list[Chapter], resolver: LinkResolver, notes: Any,

View file

@ -3,7 +3,6 @@
from __future__ import annotations from __future__ import annotations
import json import json
import posixpath
import sys import sys
import zipfile import zipfile
from dataclasses import dataclass, field from dataclasses import dataclass, field
@ -26,7 +25,7 @@ SVG = b'<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><rect wid
XHTML = """<?xml version="1.0" encoding="utf-8"?> XHTML = """<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html> <!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" lang="de"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" lang="de">
<head><title>{title}</title>{head}</head> <head><title>{title}</title></head>
<body>{body}</body> <body>{body}</body>
</html>""" </html>"""
@ -37,7 +36,6 @@ class Doc:
body: str body: str
title: str = "" title: str = ""
linear: bool = True linear: bool = True
css: list[str] | None = None # linked stylesheets (OEBPS-relative) – default: every .css in Book.media
@dataclass @dataclass
@ -92,7 +90,7 @@ def build_epub(path: Path, book: Book) -> Path:
<spine{' toc="ncx"' if book.ncx else ''}>{''.join(spine)}{book.spine_extra}</spine> <spine{' toc="ncx"' if book.ncx else ''}>{''.join(spine)}{book.spine_extra}</spine>
</package>""" </package>"""
nav_xhtml = XHTML.format( nav_xhtml = XHTML.format(
title="Inhalt", head="", title="Inhalt",
body='<nav epub:type="toc"><ol>' + "".join(f'<li><a href="{h}">{lbl}</a></li>' for lbl, h in nav_entries) + "</ol></nav>", body='<nav epub:type="toc"><ol>' + "".join(f'<li><a href="{h}">{lbl}</a></li>' for lbl, h in nav_entries) + "</ol></nav>",
) )
ncx = ( ncx = (
@ -116,10 +114,7 @@ def build_epub(path: Path, book: Book) -> Path:
if book.ncx: if book.ncx:
z.writestr("OEBPS/toc.ncx", ncx) z.writestr("OEBPS/toc.ncx", ncx)
for d in book.docs: for d in book.docs:
css = d.css if d.css is not None else [n for n in book.media if n.lower().endswith(".css")] z.writestr(f"OEBPS/{d.name}", XHTML.format(title=d.title, body=d.body))
head = "".join(f'<link rel="stylesheet" type="text/css" href="{posixpath.relpath(c, posixpath.dirname(d.name) or ".")}"/>'
for c in css)
z.writestr(f"OEBPS/{d.name}", XHTML.format(title=d.title, head=head, body=d.body))
for name, data in book.media.items(): for name, data in book.media.items():
z.writestr(f"OEBPS/{name}", data) z.writestr(f"OEBPS/{name}", data)
for name, data in book.extra_entries.items(): for name, data in book.extra_entries.items():

View file

@ -211,22 +211,3 @@ def test_page_marker_in_box_title_goes_before_the_box(convert):
text = convert(Book(docs=[Doc("c.xhtml", body)]), "--page-markers", "visible").chapter("001") text = convert(Book(docs=[Doc("c.xhtml", body)]), "--page-markers", "visible").chapter("001")
assert "\\[S. 62\\]\n\n> [!NOTE]\n> **Wozu Deklarationen?**" in text assert "\\[S. 62\\]\n\n> [!NOTE]\n> **Wozu Deklarationen?**" in text
assert "> Weil es \\[S. 63\\]hilft." in text # markers in the box body stay in place assert "> Weil es \\[S. 63\\]hilft." in text # markers in the box body stay in place
def test_qa3_01_unlinked_stylesheets_are_ignored(convert):
"""Publishers ship layout CSS of other series in the manifest; only linked stylesheets apply (QA3-01)."""
css = {"main.css": b".bold { font-weight: bold; }",
"fremd.css": b"p > span.bold { font-weight: normal; font-family: monospace; } .weg { display: none; }"}
body = ('<h1>K</h1><p>Die (<span class="bold">st</span>an<span class="bold">d</span>ard '
'<span class="bold">in</span>put) und <span class="weg">sichtbarer Text</span>.</p>')
text = convert(Book(docs=[Doc("c.xhtml", body, css=["main.css"])], media=css)).chapter("001")
assert "`" not in text # monospace rule of fremd.css must not turn bold into code
assert "**st**an**d**ard **in**put" in text
assert "sichtbarer Text" in text # display:none of fremd.css must not hide content
def test_qa3_01_imported_stylesheets_still_apply(convert):
css = {"main.css": b'@import url("extra.css");', "extra.css": b".k { font-style: italic; }"}
body = '<h1>K</h1><p><span class="k">kursiv</span> normal.</p>'
text = convert(Book(docs=[Doc("c.xhtml", body, css=["main.css"])], media=css)).chapter("001")
assert "*kursiv* normal." in text