68 lines
2.3 KiB
Python
68 lines
2.3 KiB
Python
"""HTML / HTM -> PDF.
|
|
|
|
A standalone .html upload is rendered as-is: the document's own <style>/
|
|
<link> rules stay authoritative. app.BASE_CSS is injected at the TOP of
|
|
<head> as a fallback layer (equal-specificity rules lose to the document's
|
|
own later declarations, same layering as the EPUB path). Fragments without
|
|
a <head> ("<p>...</p>") render bare.
|
|
|
|
Declared charset is honored (windows-1252/latin-1 <meta charset> or XML
|
|
declaration) via the pinned regexes in app — no mojibake. The document
|
|
<title> becomes the export name and PDF /Title when present, else the
|
|
file stem.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
from . import common
|
|
|
|
_TITLE_RE = re.compile(r"<title[^>]*>(.*?)</title>", re.IGNORECASE | re.DOTALL)
|
|
|
|
|
|
def _extract_title(html_text: str) -> str | None:
|
|
m = _TITLE_RE.search(html_text)
|
|
if not m:
|
|
return None
|
|
title = re.sub(r"<[^>]+>", "", m.group(1))
|
|
title = " ".join(title.split())
|
|
return title or None
|
|
|
|
|
|
def _inject_base_css(html_text: str) -> str:
|
|
"""Insert BASE_CSS as a fallback <style> at the top of <head>.
|
|
|
|
Placed before the document's own styles so, at equal specificity,
|
|
the document wins (WeasyPrint resolves source order within one
|
|
stylesheet context like a browser). Fragments without <head> are
|
|
returned unchanged (a lone <style> outside <head> is legal HTML5
|
|
and still applies).
|
|
"""
|
|
import app # lazy: BASE_CSS lives with the EPUB pipeline
|
|
|
|
style = f"<style>\n{app.BASE_CSS}\n</style>"
|
|
m = re.search(r"<head[^>]*>", html_text, re.IGNORECASE)
|
|
if m:
|
|
return html_text[: m.end()] + style + html_text[m.end():]
|
|
return style + html_text
|
|
|
|
|
|
def convert(path: Path, job_id: str, progress_cb=None, output_dir=None,
|
|
source_name=None) -> str:
|
|
"""Render a standalone HTML/HTM file to a typeset PDF."""
|
|
raw = path.read_bytes()
|
|
html_text = common.decode_declared(raw)
|
|
if not html_text.strip():
|
|
raise ValueError("empty file: nothing to convert")
|
|
title = _extract_title(html_text)
|
|
|
|
def _build():
|
|
return _inject_base_css(html_text), path.name
|
|
|
|
return common.convert_single(
|
|
path, job_id, build_html=_build, title=title, css="",
|
|
progress_cb=progress_cb, output_dir=output_dir,
|
|
name_src=source_name or path.name,
|
|
)
|