towishy_Owen-Graphite/dev/scripts/audit_pdf_header_footer.py
2026-05-17 12:26:21 +09:00

662 lines
No EOL
27 KiB
Python

#!/usr/bin/env python3
"""Audit the PDF header/footer marginalia contract.
This guard codifies the v3.1.32+ PDF marginalia design: print-only anchor
pseudos, no Chromium-fragile margin boxes/string-set/list tricks, and stable
Style Settings + token plumbing.
Exits with code 1 if any contract violation is detected, 0 otherwise.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
SRC_DIR = ROOT / "src"
STYLE_SETTINGS = SRC_DIR / "features" / "40-style-settings.css"
PDF_OWNER = SRC_DIR / "features" / "41-feature-presets.css"
PAGE_OWNER = SRC_DIR / "features" / "43-print-base.css"
LIGHT_TOKENS = SRC_DIR / "tokens" / "00-light-tokens.css"
DARK_TOKENS = SRC_DIR / "tokens" / "01-dark-tokens.css"
REQUIRED_SETTING_IDS = (
"ogd-settings-pdf-marginalia",
"ogd-pdf-marginalia-preset",
"ogd-pdf-label-layout",
"ogd-pdf-header-enabled",
"ogd-pdf-header-text",
"ogd-pdf-header-value",
"ogd-pdf-header-dual-pair",
"ogd-pdf-header2-key-palette",
"ogd-pdf-header2-value-palette",
"ogd-pdf-header-text-2",
"ogd-pdf-header-value-2",
"ogd-pdf-footer-enabled",
"ogd-pdf-footer-text",
"ogd-pdf-footer-value",
"ogd-pdf-marginalia-accent",
"ogd-pdf-marginalia-style",
"ogd-pdf-header-key-palette",
"ogd-pdf-header-value-palette",
"ogd-pdf-footer-key-palette",
"ogd-pdf-footer-value-palette",
"ogd-pdf-marginalia-size",
"ogd-pdf-header-position",
)
REQUIRED_SETTING_VALUES = (
"ogd-pdf-preset-custom",
"ogd-pdf-preset-prepared-confidential",
"ogd-pdf-preset-draft-internal",
"ogd-pdf-preset-final-end",
"ogd-pdf-preset-status-end",
"ogd-pdf-label-single",
"ogd-pdf-label-segmented",
"ogd-pdf-label-segmented-dual",
"ogd-pdf-label-minimal",
"ogd-pdf-label-bordered",
"ogd-pdf-label-filled",
"ogd-pdf-label-badge",
"ogd-pdf-header-key-graphite",
"ogd-pdf-header-key-slate",
"ogd-pdf-header-key-sky",
"ogd-pdf-header-key-teal",
"ogd-pdf-header-key-mint",
"ogd-pdf-header-key-violet",
"ogd-pdf-header-key-rose",
"ogd-pdf-header-key-amber",
"ogd-pdf-header-value-graphite",
"ogd-pdf-header-value-slate",
"ogd-pdf-header-value-sky",
"ogd-pdf-header-value-teal",
"ogd-pdf-header-value-mint",
"ogd-pdf-header-value-violet",
"ogd-pdf-header-value-rose",
"ogd-pdf-header-value-amber",
"ogd-pdf-header2-key-graphite",
"ogd-pdf-header2-key-slate",
"ogd-pdf-header2-key-sky",
"ogd-pdf-header2-key-teal",
"ogd-pdf-header2-key-mint",
"ogd-pdf-header2-key-violet",
"ogd-pdf-header2-key-rose",
"ogd-pdf-header2-key-amber",
"ogd-pdf-header2-value-graphite",
"ogd-pdf-header2-value-slate",
"ogd-pdf-header2-value-sky",
"ogd-pdf-header2-value-teal",
"ogd-pdf-header2-value-mint",
"ogd-pdf-header2-value-violet",
"ogd-pdf-header2-value-rose",
"ogd-pdf-header2-value-amber",
"ogd-pdf-footer-key-graphite",
"ogd-pdf-footer-key-slate",
"ogd-pdf-footer-key-sky",
"ogd-pdf-footer-key-teal",
"ogd-pdf-footer-key-mint",
"ogd-pdf-footer-key-violet",
"ogd-pdf-footer-key-rose",
"ogd-pdf-footer-key-amber",
"ogd-pdf-footer-value-graphite",
"ogd-pdf-footer-value-slate",
"ogd-pdf-footer-value-sky",
"ogd-pdf-footer-value-teal",
"ogd-pdf-footer-value-mint",
"ogd-pdf-footer-value-violet",
"ogd-pdf-footer-value-rose",
"ogd-pdf-footer-value-amber",
"ogd-pdf-label-compact",
"ogd-pdf-label-standard",
"ogd-pdf-header-top-right",
"ogd-pdf-header-top-center",
)
REQUIRED_IMPLEMENTED_CLASSES = (
"ogd-pdf-preset-prepared-confidential",
"ogd-pdf-preset-draft-internal",
"ogd-pdf-preset-final-end",
"ogd-pdf-preset-status-end",
"ogd-pdf-label-minimal",
"ogd-pdf-label-filled",
"ogd-pdf-label-badge",
"ogd-pdf-label-segmented-dual",
"ogd-pdf-header-key-graphite",
"ogd-pdf-header-key-slate",
"ogd-pdf-header-key-sky",
"ogd-pdf-header-key-teal",
"ogd-pdf-header-key-mint",
"ogd-pdf-header-key-violet",
"ogd-pdf-header-key-rose",
"ogd-pdf-header-key-amber",
"ogd-pdf-header-value-graphite",
"ogd-pdf-header-value-slate",
"ogd-pdf-header-value-sky",
"ogd-pdf-header-value-teal",
"ogd-pdf-header-value-mint",
"ogd-pdf-header-value-violet",
"ogd-pdf-header-value-rose",
"ogd-pdf-header-value-amber",
"ogd-pdf-header2-key-graphite",
"ogd-pdf-header2-key-slate",
"ogd-pdf-header2-key-sky",
"ogd-pdf-header2-key-teal",
"ogd-pdf-header2-key-mint",
"ogd-pdf-header2-key-violet",
"ogd-pdf-header2-key-rose",
"ogd-pdf-header2-key-amber",
"ogd-pdf-header2-value-graphite",
"ogd-pdf-header2-value-slate",
"ogd-pdf-header2-value-sky",
"ogd-pdf-header2-value-teal",
"ogd-pdf-header2-value-mint",
"ogd-pdf-header2-value-violet",
"ogd-pdf-header2-value-rose",
"ogd-pdf-header2-value-amber",
"ogd-pdf-footer-key-graphite",
"ogd-pdf-footer-key-slate",
"ogd-pdf-footer-key-sky",
"ogd-pdf-footer-key-teal",
"ogd-pdf-footer-key-mint",
"ogd-pdf-footer-key-violet",
"ogd-pdf-footer-key-rose",
"ogd-pdf-footer-key-amber",
"ogd-pdf-footer-value-graphite",
"ogd-pdf-footer-value-slate",
"ogd-pdf-footer-value-sky",
"ogd-pdf-footer-value-teal",
"ogd-pdf-footer-value-mint",
"ogd-pdf-footer-value-violet",
"ogd-pdf-footer-value-rose",
"ogd-pdf-footer-value-amber",
"ogd-pdf-label-compact",
"ogd-pdf-header-top-center",
"ogd-pdf-header-dual-pair",
)
REQUIRED_LIGHT_TOKENS = (
"--ogd-pdf-header-text",
"--ogd-pdf-header-value",
"--ogd-pdf-header-text-2",
"--ogd-pdf-header-value-2",
"--ogd-pdf-footer-text",
"--ogd-pdf-footer-value",
"--ogd-pdf-marginalia-accent",
"--ogd-pdf-marginalia-bg",
"--ogd-pdf-marginalia-border",
"--ogd-pdf-marginalia-shadow",
"--ogd-pdf-header-top",
"--ogd-pdf-header-right",
"--ogd-pdf-header-left",
"--ogd-pdf-header-transform",
"--ogd-pdf-header-font-size",
"--ogd-pdf-footer-font-size",
"--ogd-pdf-label-letter-spacing",
"--ogd-pdf-label-line-height",
"--ogd-pdf-label-radius",
"--ogd-pdf-header-pad-y",
"--ogd-pdf-header-pad-x",
"--ogd-pdf-footer-pad-y",
"--ogd-pdf-footer-pad-x",
"--ogd-pdf-footer-reserve",
"--ogd-pdf-footer-offset",
"--ogd-pdf-footer-max-width",
"--ogd-pdf-segment-key-width",
"--ogd-pdf-segment-value-width",
"--ogd-pdf-segment-half-width",
"--ogd-pdf-header-pair-gap",
"--ogd-pdf-header-half-pair-gap",
"--ogd-pdf-segment-key-bg",
"--ogd-pdf-segment-key-text",
"--ogd-pdf-segment-key-border",
"--ogd-pdf-segment-value-bg",
"--ogd-pdf-segment-value-text",
"--ogd-pdf-segment-value-border",
"--ogd-pdf-header-segment-key-bg",
"--ogd-pdf-header-segment-key-text",
"--ogd-pdf-header-segment-key-border",
"--ogd-pdf-header-segment-value-bg",
"--ogd-pdf-header-segment-value-text",
"--ogd-pdf-header-segment-value-border",
"--ogd-pdf-header2-segment-key-bg",
"--ogd-pdf-header2-segment-key-text",
"--ogd-pdf-header2-segment-key-border",
"--ogd-pdf-header2-segment-value-bg",
"--ogd-pdf-header2-segment-value-text",
"--ogd-pdf-header2-segment-value-border",
"--ogd-pdf-footer-segment-key-bg",
"--ogd-pdf-footer-segment-key-text",
"--ogd-pdf-footer-segment-key-border",
"--ogd-pdf-footer-segment-value-bg",
"--ogd-pdf-footer-segment-value-text",
"--ogd-pdf-footer-segment-value-border",
)
REQUIRED_DARK_TOKENS = (
"--ogd-pdf-marginalia-accent",
"--ogd-pdf-marginalia-bg",
"--ogd-pdf-marginalia-border",
"--ogd-pdf-marginalia-shadow",
"--ogd-pdf-segment-key-bg",
"--ogd-pdf-segment-key-text",
"--ogd-pdf-segment-key-border",
"--ogd-pdf-segment-value-bg",
"--ogd-pdf-segment-value-text",
"--ogd-pdf-segment-value-border",
"--ogd-pdf-header-segment-key-bg",
"--ogd-pdf-header-segment-key-text",
"--ogd-pdf-header-segment-key-border",
"--ogd-pdf-header-segment-value-bg",
"--ogd-pdf-header-segment-value-text",
"--ogd-pdf-header-segment-value-border",
"--ogd-pdf-header2-segment-key-bg",
"--ogd-pdf-header2-segment-key-text",
"--ogd-pdf-header2-segment-key-border",
"--ogd-pdf-header2-segment-value-bg",
"--ogd-pdf-header2-segment-value-text",
"--ogd-pdf-header2-segment-value-border",
"--ogd-pdf-footer-segment-key-bg",
"--ogd-pdf-footer-segment-key-text",
"--ogd-pdf-footer-segment-key-border",
"--ogd-pdf-footer-segment-value-bg",
"--ogd-pdf-footer-segment-value-text",
"--ogd-pdf-footer-segment-value-border",
)
MARGIN_BOX_RE = re.compile(r"^@(top|bottom|left|right)(?:-|$)", re.I)
VIEWPORT_UNIT_RE = re.compile(r"(?:^|[^a-zA-Z-])-?\d*\.?\d+v(?:h|w|min|max)\b", re.I)
@dataclass(frozen=True)
class CssRule:
path: Path
selector: str
body: str
at_context: str
line: int
@dataclass(frozen=True)
class AtBlock:
path: Path
header: str
at_context: str
line: int
def normalize_ws(text: str) -> str:
return re.sub(r"\s+", " ", text).strip()
def strip_comments(css: str) -> str:
def replace(match: re.Match[str]) -> str:
return "\n" * match.group(0).count("\n")
return re.sub(r"/\*.*?\*/", replace, css, flags=re.S)
def declarations(body: str) -> dict[str, str]:
parsed: dict[str, str] = {}
for raw_declaration in body.split(";"):
if ":" not in raw_declaration:
continue
property_name, _, value = raw_declaration.partition(":")
parsed[property_name.strip().lower()] = value.strip()
return parsed
def tokenize_css(path: Path, css: str) -> tuple[list[CssRule], list[AtBlock]]:
rules: list[CssRule] = []
at_blocks: list[AtBlock] = []
def walk(fragment: str, line_offset: int, at_stack: list[str]) -> None:
index = 0
line_number = 1
css_length = len(fragment)
while index < css_length:
if fragment.startswith("/*", index):
comment_end = fragment.find("*/", index + 2)
if comment_end == -1:
return
line_number += fragment.count("\n", index, comment_end + 2)
index = comment_end + 2
continue
current_char = fragment[index]
if current_char.isspace():
if current_char == "\n":
line_number += 1
index += 1
continue
if current_char == "@":
header_start = index
while index < css_length and fragment[index] not in "{;":
if fragment[index] == "\n":
line_number += 1
index += 1
if index >= css_length:
return
if fragment[index] == ";":
index += 1
continue
header = normalize_ws(fragment[header_start:index])
absolute_line = line_offset + line_number - 1
at_blocks.append(AtBlock(path, header, " >> ".join(at_stack), absolute_line))
block_start = index + 1
index = block_start
depth = 1
while index < css_length and depth > 0:
if fragment.startswith("/*", index):
comment_end = fragment.find("*/", index + 2)
if comment_end == -1:
return
line_number += fragment.count("\n", index, comment_end + 2)
index = comment_end + 2
continue
if fragment[index] == "{":
depth += 1
elif fragment[index] == "}":
depth -= 1
if depth == 0:
break
if fragment[index] == "\n":
line_number += 1
index += 1
inner_css = fragment[block_start:index]
inner_line_offset = line_offset + fragment.count("\n", 0, block_start)
walk(inner_css, inner_line_offset, [*at_stack, header])
index += 1
continue
selector_start = index
while index < css_length:
if fragment.startswith("/*", index):
comment_end = fragment.find("*/", index + 2)
if comment_end == -1:
return
line_number += fragment.count("\n", index, comment_end + 2)
index = comment_end + 2
continue
if fragment[index] in "{}":
break
index += 1
if index >= css_length or fragment[index] != "{":
index += 1
continue
selector = normalize_ws(fragment[selector_start:index])
rule_line = line_offset + line_number - 1
body_start = index + 1
index = body_start
depth = 1
while index < css_length and depth > 0:
if fragment.startswith("/*", index):
comment_end = fragment.find("*/", index + 2)
if comment_end == -1:
return
line_number += fragment.count("\n", index, comment_end + 2)
index = comment_end + 2
continue
if fragment[index] == "{":
depth += 1
elif fragment[index] == "}":
depth -= 1
if depth == 0:
break
if fragment[index] == "\n":
line_number += 1
index += 1
body = normalize_ws(fragment[body_start:index])
if selector and body:
rules.append(CssRule(path, selector, body, " >> ".join(at_stack), rule_line))
index += 1
walk(css, 1, [])
return rules, at_blocks
def rel(path: Path) -> str:
return path.relative_to(ROOT).as_posix()
def location(path: Path, line: int) -> str:
return f"{rel(path)}:{line}"
def has_body_class(selector: str, class_name: str) -> bool:
return re.search(rf"\bbody(?=[^{{,]*\.{re.escape(class_name)}\b)", selector) is not None
def require_substring(failures: list[str], text: str, token: str, label: str) -> None:
if token not in text:
failures.append(f"missing {label}: `{token}`")
def audit_settings_and_tokens(failures: list[str]) -> None:
settings_text = STYLE_SETTINGS.read_text(encoding="utf-8")
owner_text = PDF_OWNER.read_text(encoding="utf-8")
light_text = LIGHT_TOKENS.read_text(encoding="utf-8")
dark_text = DARK_TOKENS.read_text(encoding="utf-8")
for setting_id in REQUIRED_SETTING_IDS:
if not re.search(rf"\bid:\s*{re.escape(setting_id)}\b", settings_text):
failures.append(f"missing Style Settings id `{setting_id}` in {rel(STYLE_SETTINGS)}")
for setting_value in REQUIRED_SETTING_VALUES:
if not re.search(rf"\bvalue:\s*{re.escape(setting_value)}\b", settings_text):
failures.append(f"missing Style Settings value `{setting_value}` in {rel(STYLE_SETTINGS)}")
for class_name in REQUIRED_IMPLEMENTED_CLASSES:
if not re.search(rf"\bbody(?:\.[\w-]+)*\.{re.escape(class_name)}\b", owner_text):
failures.append(f"missing PDF marginalia implementation class `body.{class_name}` in {rel(PDF_OWNER)}")
for token in REQUIRED_LIGHT_TOKENS:
require_substring(failures, light_text, token, f"light token in {rel(LIGHT_TOKENS)}")
for token in REQUIRED_DARK_TOKENS:
require_substring(failures, dark_text, token, f"dark token in {rel(DARK_TOKENS)}")
def audit_at_rules(failures: list[str], at_blocks: list[AtBlock], uncommented_sources: dict[Path, str]) -> None:
for block in at_blocks:
header = block.header.lower()
if header.startswith("@page"):
if block.path != PAGE_OWNER:
failures.append(f"{location(block.path, block.line)}: @page is owned by {rel(PAGE_OWNER)}")
if "@media print" not in block.at_context.lower():
failures.append(f"{location(block.path, block.line)}: @page must be nested under @media print")
if MARGIN_BOX_RE.match(header):
failures.append(f"{location(block.path, block.line)}: forbidden @page margin box `{block.header}`")
for path, css in uncommented_sources.items():
for pattern, label in (
(r"\bstring-set\b", "string-set"),
(r"\bcontent\s*\(", "CSS content() function"),
):
match = re.search(pattern, css, flags=re.I)
if match:
line = css.count("\n", 0, match.start()) + 1
failures.append(f"{location(path, line)}: forbidden PDF-fragile primitive `{label}`")
def audit_pdf_rules(failures: list[str], rules: list[CssRule]) -> None:
header_rules: list[CssRule] = []
header_segment_rules: list[CssRule] = []
header_value_rules: list[CssRule] = []
header_pair2_key_rules: list[CssRule] = []
header_pair2_value_rules: list[CssRule] = []
footer_rules: list[CssRule] = []
footer_segment_rules: list[CssRule] = []
footer_value_rules: list[CssRule] = []
footer_reserve_rules: list[CssRule] = []
for rule in rules:
selector = rule.selector
body = rule.body
mentions_header_toggle = has_body_class(selector, "ogd-pdf-header-enabled")
mentions_footer_toggle = has_body_class(selector, "ogd-pdf-footer-enabled")
mentions_pdf_toggle = mentions_header_toggle or mentions_footer_toggle
mentions_pdf_token = "--ogd-pdf-" in body or "--ogd-pdf-" in selector
if mentions_pdf_toggle and rule.path != PDF_OWNER:
failures.append(f"{location(rule.path, rule.line)}: PDF marginalia selector is owned by {rel(PDF_OWNER)}")
if mentions_pdf_toggle and "@media print" not in rule.at_context.lower():
failures.append(f"{location(rule.path, rule.line)}: PDF marginalia selector must be inside @media print")
if "markdown-source-view.mod-cm6" in selector and (mentions_pdf_toggle or mentions_pdf_token):
failures.append(f"{location(rule.path, rule.line)}: PDF marginalia must not target Live Preview / CM6")
if mentions_pdf_toggle:
declarations_map = declarations(body)
if "!important" in body:
failures.append(f"{location(rule.path, rule.line)}: !important is forbidden in PDF marginalia")
if VIEWPORT_UNIT_RE.search(body):
failures.append(f"{location(rule.path, rule.line)}: viewport units are forbidden in PDF marginalia")
if re.search(r"::(?:first-line|marker)\b", selector):
failures.append(f"{location(rule.path, rule.line)}: forbidden pseudo in PDF marginalia selector")
for property_name, value in declarations_map.items():
if property_name in ("backdrop-filter", "-webkit-backdrop-filter"):
failures.append(f"{location(rule.path, rule.line)}: backdrop-filter is forbidden in PDF output")
if property_name == "position" and value.split()[0].lower() == "fixed":
failures.append(f"{location(rule.path, rule.line)}: position: fixed is forbidden in PDF marginalia")
if property_name == "display" and "list-item" in value:
failures.append(f"{location(rule.path, rule.line)}: list-item trick is forbidden in PDF marginalia")
if property_name.startswith("list-style"):
failures.append(f"{location(rule.path, rule.line)}: list-style trick is forbidden in PDF marginalia")
if property_name == "content" and "\\A" in value:
failures.append(f"{location(rule.path, rule.line)}: multiline generated content is forbidden in PDF marginalia")
if mentions_header_toggle and "::before" in selector and "content" in declarations_map:
content = declarations_map.get("content", "")
if "--ogd-pdf-header-text-2" in content:
header_pair2_key_rules.append(rule)
elif "--ogd-pdf-header-value-2" in content:
header_pair2_value_rules.append(rule)
elif "--ogd-pdf-header-value" in content:
header_value_rules.append(rule)
else:
header_rules.append(rule)
if mentions_header_toggle and "::after" in selector and "content" in declarations_map:
content = declarations_map.get("content", "")
if "--ogd-pdf-header-text-2" in content:
header_pair2_key_rules.append(rule)
elif "--ogd-pdf-header-value-2" in content:
header_pair2_value_rules.append(rule)
else:
header_segment_rules.append(rule)
if mentions_footer_toggle and "::after" in selector and "content" in declarations_map:
if "--ogd-pdf-footer-value" in declarations_map.get("content", ""):
footer_value_rules.append(rule)
else:
footer_rules.append(rule)
if mentions_footer_toggle and "::before" in selector and "content" in declarations_map:
footer_segment_rules.append(rule)
if mentions_footer_toggle and "::before" not in selector and "::after" not in selector:
footer_reserve_rules.append(rule)
if len(header_rules) != 1:
failures.append(f"expected exactly one PDF header anchor pseudo; found {len(header_rules)}")
else:
require_anchor_contract(failures, header_rules[0], "--ogd-pdf-header-text")
if len(footer_rules) != 1:
failures.append(f"expected exactly one PDF footer anchor pseudo; found {len(footer_rules)}")
else:
require_anchor_contract(failures, footer_rules[0], "--ogd-pdf-footer-text")
if len(footer_reserve_rules) != 1:
failures.append(f"expected exactly one PDF footer reserve rule; found {len(footer_reserve_rules)}")
else:
reserve_declarations = declarations(footer_reserve_rules[0].body)
margin_bottom = reserve_declarations.get("margin-bottom", "")
if "mm" not in margin_bottom:
failures.append(f"{location(footer_reserve_rules[0].path, footer_reserve_rules[0].line)}: footer reserve must use mm margin-bottom")
if len(header_segment_rules) != 1:
failures.append(f"expected exactly one PDF header segmented key pseudo; found {len(header_segment_rules)}")
else:
require_anchor_contract(
failures,
header_segment_rules[0],
"--ogd-pdf-header-text",
check_geometry=False,
expected_display="inline-flex",
)
if len(header_value_rules) != 1:
failures.append(f"expected exactly one PDF header segmented value override; found {len(header_value_rules)}")
if len(header_pair2_key_rules) != 1:
failures.append(f"expected exactly one PDF header pair-2 key pseudo; found {len(header_pair2_key_rules)}")
if len(header_pair2_value_rules) != 1:
failures.append(f"expected exactly one PDF header pair-2 value pseudo; found {len(header_pair2_value_rules)}")
if len(footer_segment_rules) != 1:
failures.append(f"expected exactly one PDF footer segmented key pseudo; found {len(footer_segment_rules)}")
else:
require_anchor_contract(
failures,
footer_segment_rules[0],
"--ogd-pdf-footer-text",
check_geometry=False,
expected_display="inline-flex",
)
if len(footer_value_rules) != 1:
failures.append(f"expected exactly one PDF footer segmented value override; found {len(footer_value_rules)}")
def require_anchor_contract(
failures: list[str],
rule: CssRule,
text_token: str,
check_geometry: bool = True,
expected_display: str = "inline-block",
) -> None:
decl = declarations(rule.body)
expected_exact = {
"position": "absolute",
"pointer-events": "none",
"display": expected_display,
"white-space": "nowrap",
"overflow": "hidden",
"text-overflow": "ellipsis",
"font-style": "normal",
}
for property_name, expected_value in expected_exact.items():
actual = decl.get(property_name, "").split()[0].lower() if decl.get(property_name) else ""
if actual != expected_value:
failures.append(f"{location(rule.path, rule.line)}: expected `{property_name}: {expected_value}`")
content = decl.get("content", "")
if normalize_ws(content) != f'var({text_token}, "")':
failures.append(f"{location(rule.path, rule.line)}: content must be `var({text_token}, \"\")`")
if "exact" not in decl.get("print-color-adjust", "") or "exact" not in decl.get("-webkit-print-color-adjust", ""):
failures.append(f"{location(rule.path, rule.line)}: both print-color-adjust properties must be exact")
if not check_geometry:
return
if text_token == "--ogd-pdf-header-text":
expected_header_values = {
"top": "var(--ogd-pdf-header-top, 11mm)",
"right": "var(--ogd-pdf-header-right, 13mm)",
"left": "var(--ogd-pdf-header-left, auto)",
"transform": "var(--ogd-pdf-header-transform, none)",
}
for property_name, expected_value in expected_header_values.items():
if normalize_ws(decl.get(property_name, "")) != expected_value:
failures.append(f"{location(rule.path, rule.line)}: header must keep `{property_name}: {expected_value}`")
if text_token == "--ogd-pdf-footer-text":
expected_footer_values = {
"left": "50%",
"bottom": "var(--ogd-pdf-footer-offset, -22mm)",
"transform": "translateX(-50%)",
"max-width": "var(--ogd-pdf-footer-max-width, 90%)",
}
for property_name, expected_value in expected_footer_values.items():
if normalize_ws(decl.get(property_name, "")) != expected_value:
failures.append(f"{location(rule.path, rule.line)}: footer must keep `{property_name}: {expected_value}`")
def main() -> int:
failures: list[str] = []
rules: list[CssRule] = []
at_blocks: list[AtBlock] = []
uncommented_sources: dict[Path, str] = {}
for css_path in sorted(SRC_DIR.rglob("*.css")):
css = css_path.read_text(encoding="utf-8")
module_rules, module_at_blocks = tokenize_css(css_path, css)
rules.extend(module_rules)
at_blocks.extend(module_at_blocks)
uncommented_sources[css_path] = strip_comments(css)
audit_settings_and_tokens(failures)
audit_at_rules(failures, at_blocks, uncommented_sources)
audit_pdf_rules(failures, rules)
if failures:
print("FAIL: PDF header/footer contract violations:")
for failure in failures:
print(f" - {failure}")
return 1
print("OK: PDF header/footer contract clean")
return 0
if __name__ == "__main__":
raise SystemExit(main())