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())