Compare commits
6 changed files with 16 additions and 68 deletions
|
|
@ -29,10 +29,6 @@ Versionsnummern nach [Semantic Versioning](https://semver.org/lang/de/).
|
|||
|
||||
### 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`
|
||||
ü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
|
||||
|
|
|
|||
|
|
@ -249,8 +249,6 @@ weil CommonMark keine `[^…]`-Syntax kennt.
|
|||
- **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,
|
||||
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*`).
|
||||
- **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
|
||||
|
|
|
|||
|
|
@ -1233,8 +1233,8 @@ class Renderer:
|
|||
finally:
|
||||
self.strong -= 1
|
||||
if name in ("del", "s", "strike"):
|
||||
del_open, del_close = ("~~", "~~") if self.opt.format == "gfm" else ("<del>", "</del>")
|
||||
return pre + self._wrap(self.inline_children(el), del_open, del_close)
|
||||
marker = ("~~", "~~") if self.opt.format == "gfm" else ("<del>", "</del>")
|
||||
return pre + self._wrap(self.inline_children(el), marker[0], marker[1])
|
||||
if name in ("u", "ins", "mark", "kbd", "sup", "sub"):
|
||||
if name in ("sup", "sub"):
|
||||
elems = [c for c in el if is_element(c)]
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ from dataclasses import asdict, dataclass, field
|
|||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib.parse import unquote
|
||||
|
||||
from . import TOOL_NAME, __version__
|
||||
from .chapters import ChapterBuilder
|
||||
|
|
@ -28,7 +27,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_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
|
||||
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
"""The book's CSS: manifest stylesheets that a spine document links (directly or via @import), in manifest
|
||||
order, plus <style> elements of the documents. Unlinked stylesheets (publishers ship the layouts of other
|
||||
series) are ignored – a reading system would not apply them either."""
|
||||
pending: list[str] = []
|
||||
"""All CSS of the book: manifest stylesheets (manifest order) plus <style> elements of the documents."""
|
||||
texts: list[str] = []
|
||||
for item in package.manifest.values():
|
||||
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 e in iter_elements(doc.root):
|
||||
if local(e.tag) == "link" and "stylesheet" in (e.get("rel") or "").lower().split():
|
||||
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)
|
||||
texts.extend(text_content(e) for e in iter_elements(doc.root) if local(e.tag) == "style")
|
||||
return StyleSheet(texts)
|
||||
|
||||
|
||||
def _render(reader: EpubReader, package: Any, chapters: list[Chapter], resolver: LinkResolver, notes: Any,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import posixpath
|
||||
import sys
|
||||
import zipfile
|
||||
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"?>
|
||||
<!DOCTYPE html>
|
||||
<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>
|
||||
</html>"""
|
||||
|
||||
|
|
@ -37,7 +36,6 @@ class Doc:
|
|||
body: str
|
||||
title: str = ""
|
||||
linear: bool = True
|
||||
css: list[str] | None = None # linked stylesheets (OEBPS-relative) – default: every .css in Book.media
|
||||
|
||||
|
||||
@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>
|
||||
</package>"""
|
||||
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>",
|
||||
)
|
||||
ncx = (
|
||||
|
|
@ -116,10 +114,7 @@ def build_epub(path: Path, book: Book) -> Path:
|
|||
if book.ncx:
|
||||
z.writestr("OEBPS/toc.ncx", ncx)
|
||||
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")]
|
||||
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))
|
||||
z.writestr(f"OEBPS/{d.name}", XHTML.format(title=d.title, body=d.body))
|
||||
for name, data in book.media.items():
|
||||
z.writestr(f"OEBPS/{name}", data)
|
||||
for name, data in book.extra_entries.items():
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in a new issue