from __future__ import annotations
import logging
import re
from bs4 import BeautifulSoup, Tag
from header_styles import (
SHARED_HEADER_CSS,
inject_header_css,
inject_header_layout,
)
logger = logging.getLogger(__name__)
# Single source of truth: (field_name, css_var, default_value).
# To add a new theme property, add one entry here — nothing else needs updating.
_THEME_PROPS: list[tuple[str, str, str]] = [
("primary", "--theme-primary", "#1e3a5f"),
("accent", "--theme-accent", "#2563eb"),
("secondary", "--theme-secondary", "#475569"),
("bg", "--theme-bg", "#ffffff"),
("surface", "--theme-surface", "#f8fafc"),
("border", "--theme-border", "#e2e8f0"),
("text", "--theme-text", "#1a1a1a"),
("text_muted", "--theme-text-muted", "#6b7280"),
("heading", "--theme-heading", "#111827"),
("font", "--theme-font", "'Helvetica Neue', Arial, sans-serif"),
("font_size_base", "--theme-font-size-base", "10pt"),
("font_size_heading", "--theme-font-size-heading", "14pt"),
("font_weight_body", "--theme-font-weight-body", "400"),
("font_weight_heading", "--theme-font-weight-heading", "700"),
("font_weight_bold", "--theme-font-weight-bold", "600"),
("line_height", "--theme-line-height", "1.4"),
("page_margin", "--theme-page-margin", "20mm"),
]
_FIELD_TO_VAR: dict[str, str] = {field: var for field, var, _ in _THEME_PROPS}
_DEFAULTS: dict[str, str] = {var: default for _, var, default in _THEME_PROPS}
DEFAULT_THEME_CSS: str = ":root {\n" + "\n".join(f" {var}: {val};" for var, val in _DEFAULTS.items()) + "\n}"
def build_theme_css(overrides: dict[str, str] | None = None) -> str:
"""Build a :root { } block of CSS custom properties with optional overrides."""
if not overrides:
return DEFAULT_THEME_CSS
props = {**_DEFAULTS, **{_FIELD_TO_VAR.get(k, k): v for k, v in overrides.items()}}
return ":root {\n" + "\n".join(f" {var}: {val};" for var, val in props.items()) + "\n}"
def inject_theme(html_content: str, theme_overrides: dict[str, str] | None = None) -> str:
"""
Inject theme CSS variables into an HTML document.
If the template contains {{THEME_CSS}}, it is replaced with the theme block.
Otherwise, the style block is injected before .
A body-level cascade is included so that typography CSS variables take effect
on existing templates without requiring per-template changes.
"""
theme_css = build_theme_css(theme_overrides)
body_cascade = (
# html background covers edge-to-edge (including @page margin area) because
# Chrome applies the html element background to the entire page in print.
# Do NOT zero out @page margins here — body padding only applies to the
# first/last page, so using it as a substitute for @page margins breaks
# the top margin on all pages after a page break.
"html { "
"background: var(--theme-bg) !important; "
"-webkit-print-color-adjust: exact !important; "
"print-color-adjust: exact !important; "
"} "
"body { "
"background: var(--theme-bg) !important; "
"-webkit-print-color-adjust: exact !important; "
"print-color-adjust: exact !important; "
"font-size: var(--theme-font-size-base) !important; "
"line-height: var(--theme-line-height) !important; "
"} "
"h1, .doc-title, .name { color: var(--theme-primary) !important; } "
"h2, h3, h4, h5, h6, .section-heading { color: var(--theme-primary) !important; } "
".party-card-header, .entry-org, .job-title, .sender-company { color: var(--theme-accent) !important; }"
)
full_css = f"{theme_css}\n{body_cascade}"
logger.info("[inject_theme] overrides=%s", theme_overrides)
logger.info("[inject_theme] full CSS block being injected:\n%s", full_css)
if "{{THEME_CSS}}" in html_content:
logger.info("[inject_theme] replacing {{THEME_CSS}} placeholder")
return html_content.replace("{{THEME_CSS}}", full_css)
style_tag = f""
if "" in html_content:
logger.info("[inject_theme] injecting