symbols.py

3.7 kB · python · 113 lines

1import html2import os3import re4import shutil5import subprocess6import sys7from pathlib import Path89from fontTools.subset import Options, Subsetter10from fontTools.ttLib import TTFont1112ROOT = Path(__file__).resolve().parents[1]13SITE = ROOT / "site"14FONTS = SITE / "kit" / "ui" / "fonts"15MASTER = ROOT / "files" / "fonts" / "symbols.ttf"16SHIPPED = FONTS / "symbols.woff2"17CSS = FONTS / "fonts.css"18CONFIG = SITE / "site.json"19DIST = Path(os.environ.get("MRLY_DIST") or SITE / "dist")20SKIP = {"git", "raw"}21FAMILY = "Noto Sans Symbols 2"22DROP = re.compile(r"<(script|style)\b[^>]*>.*?</\1>", re.S | re.I)23TAG = re.compile(r"<[^>]*>", re.S)2425def pages(dist):26    out = []27    for here, dirs, files in os.walk(dist):28        if Path(here) == dist:29            dirs[:] = [d for d in dirs if d not in SKIP]30        out += [Path(here) / name for name in files if name.endswith(".html")]31    return sorted(out)3233def used(dist):34    seen = set()35    found = pages(dist)36    for file in found:37        text = html.unescape(TAG.sub(" ", DROP.sub(" ", file.read_text(encoding="utf8", errors="replace"))))38        seen |= {ord(ch) for ch in set(text)}39    return seen, len(found)4041def source():42    here = FONTS / "symbols.ttf"43    if here.exists():44        MASTER.parent.mkdir(parents=True, exist_ok=True)45        if subprocess.run(["git", "mv", str(here), str(MASTER)], cwd=ROOT, capture_output=True).returncode:46            shutil.move(str(here), str(MASTER))47        print(f"symbols: master kept at {MASTER.relative_to(ROOT)}")48    if MASTER.exists():49        return MASTER50    if SHIPPED.exists():51        print("symbols: no master ttf, narrowing the shipped woff2")52        return SHIPPED53    raise SystemExit("symbols: nothing to subset")5455def ranges(points):56    out = []57    for cp in points:58        if out and cp == out[-1][1] + 1:59            out[-1][1] = cp60        else:61            out.append([cp, cp])62    return out6364def spell(spans):65    return ", ".join(f"U+{a:04x}" if a == b else f"U+{a:04x}-{b:04x}" for a, b in spans)6667def face(text, ranged):68    block = re.compile(r'@font-face \{\n  font-family: "' + FAMILY + r'";.*?\n\}', re.S)69    found = block.search(text)70    if not found:71        raise SystemExit(f"symbols: no @font-face for {FAMILY} in {CSS}")72    body = re.sub(73        r"  src: url\([^)]*\) format\([^)]*\);\n(?:  unicode-range: [^\n]*\n)?",74        f'  src: url(symbols.woff2) format("woff2");\n  unicode-range: {ranged};\n',75        found.group(0),76    )77    return text[: found.start()] + body + text[found.end() :]7879def listed(text):80    return text.replace('"fonts/symbols.ttf"', '"fonts/symbols.woff2"')8182def main():83    if not DIST.exists():84        raise SystemExit(f"symbols: no dist at {DIST}; build the site first")85    seen, count = used(DIST)86    file = source()87    font = TTFont(file, recalcTimestamp=False)88    keep = sorted(cp for cp in seen & set(font.getBestCmap()) if cp > 0x7F)89    if not keep:90        raise SystemExit("symbols: no page needs a symbol glyph")91    options = Options()92    options.layout_features = ["*"]93    options.name_IDs = ["*"]94    options.hinting = False95    options.flavor = "woff2"96    cut = Subsetter(options=options)97    cut.populate(unicodes=keep)98    cut.subset(font)99    font.flavor = "woff2"100    font.save(SHIPPED)101    font.close()102    spans = ranges(keep)103    ranged = spell(spans)104    CSS.write_text(face(CSS.read_text(encoding="utf8"), ranged), encoding="utf8")105    CONFIG.write_text(listed(CONFIG.read_text(encoding="utf8")), encoding="utf8")106    was = file.stat().st_size107    now = SHIPPED.stat().st_size108    print(f"symbols: {count} pages, {len(keep)} glyphs, {len(spans)} ranges, {was:,} -> {now:,} bytes")109    print(f"symbols: unicode-range: {ranged}")110    return 0111112if __name__ == "__main__":113    sys.exit(main())