"""Comparing a site's stylesheet against the default one. SPEC.md 5.2 lets a site host its own copy of the default stylesheet and change nothing in it, except to add @font-face rules that load fonts from the site itself. Checking that means comparing text, since a site may legitimately differ in line endings and trailing whitespace after a copy-paste. """ from dataclasses import dataclass, field import difflib from functools import cache from importlib import resources import re FONT_FACE = re.compile(r"@font-face\b", re.IGNORECASE) URL_IN_SRC = re.compile(r"url\(\s*['\"]?([^'\")]+)", re.IGNORECASE) IMPORT_RULE = re.compile(r"@import\b", re.IGNORECASE) @dataclass class Comparison: """What a candidate stylesheet differs from the default by.""" equal: bool diff_line: int | None = None diff: str = "" font_faces: list[str] = field(default_factory=list) has_import: bool = False @property def font_urls(self) -> list[str]: """Every url() the added @font-face rules load.""" return [ match.group(1).strip() for block in self.font_faces for match in URL_IN_SRC.finditer(block) ] @cache def canonical() -> str: """Return the normalised default stylesheet shipped with this package.""" data = resources.files("mews.data").joinpath("mews-0.1.css").read_text("utf-8") return normalise(data) def normalise(text: str) -> str: """Strip the differences a copy may pick up without changing any rule.""" text = text.lstrip("").replace("\r\n", "\n").replace("\r", "\n") lines = [line.rstrip() for line in text.split("\n")] out: list[str] = [] for line in lines: if line or (out and out[-1]): out.append(line) return "\n".join(out).strip("\n") def strip_font_faces(text: str) -> tuple[str, list[str]]: """Remove top-level @font-face blocks, returning the rest and the blocks. Only brace depth zero counts, so an @font-face inside a media query is left in place and shows up as a difference. """ blocks: list[str] = [] out: list[str] = [] index = 0 depth = 0 while index < len(text): if depth == 0 and FONT_FACE.match(text, index): end = _block_end(text, index) if end is None: break blocks.append(text[index:end]) index = end continue character = text[index] if character == "{": depth += 1 elif character == "}": depth = max(0, depth - 1) out.append(character) index += 1 out.append(text[index:]) return "".join(out), blocks def _block_end(text: str, start: int) -> int | None: """Index just past the brace-balanced block beginning at start.""" opened = text.find("{", start) if opened == -1: return None depth = 0 for index in range(opened, len(text)): if text[index] == "{": depth += 1 elif text[index] == "}": depth -= 1 if depth == 0: return index + 1 return None def compare(candidate: str) -> Comparison: """Compare a stylesheet against the default, allowing added @font-face rules.""" expected = canonical() if normalise(candidate) == expected: return Comparison(equal=True) remainder, blocks = strip_font_faces(candidate) found = normalise(remainder) has_import = bool(IMPORT_RULE.search(candidate)) if found == expected: return Comparison(equal=True, font_faces=blocks, has_import=has_import) diff = list( difflib.unified_diff( expected.split("\n"), found.split("\n"), "mews-0.1.css", "your copy", n=1 ) ) line = next( ( index + 1 for index, (left, right) in enumerate( zip(expected.split("\n"), found.split("\n"), strict=False) ) if left != right ), min(len(expected.split("\n")), len(found.split("\n"))) + 1, ) return Comparison( equal=False, diff_line=line, diff="\n".join(diff[:8]), font_faces=blocks, has_import=has_import, )