mirror of https://github.com/vuejs/vitepress
scripts/subsetFonts.py now regenerates the woff2 subsets and fonts.css from the ranges in scripts/fontSubsets.json (pinned Inter release, byte-reproducible builds): - a new lazily-loaded `symbols` subset covers every glyph of the full font that the existing subsets skipped (arrows, math, geometric shapes, enclosed alphanumerics, keyboard symbols, ...); U+2318 and U+21A9 live in the latin subset instead as the theme's chrome uses them - Inter4CJK faces are generated as the subset ranges minus East Asian Ambiguous punctuation/symbols so that CJK fonts render those in CJK documents, and now also apply to Korean - the base typography variables move into fonts.css next to the faces; webfont mode no longer declares the Inter4CJK override (there is no such family on Google Fonts), and the redundant `font-optical-sizing: auto` is gone - fix: the `:lang(zh, ja, ko)` selector list syntax is not supported in Chrome and silently matched nothing inside the forgiving `:where()`, so the CJK font handling and `line-break: strict` never applied there Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>pull/5335/head
parent
c2be0bf936
commit
705c1be0f8
@ -0,0 +1,11 @@
|
||||
{
|
||||
"cyrillic-ext": "U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F",
|
||||
"cyrillic": "U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116",
|
||||
"greek-ext": "U+1F00-1FFF",
|
||||
"greek": "U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF",
|
||||
"vietnamese": "U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB",
|
||||
"latin-ext": "U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF",
|
||||
"latin": "U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+21A9,U+2212,U+2215,U+2318,U+FEFF,U+FFFD",
|
||||
"symbols": "U+02CD,U+02D8-02D9,U+02DB,U+0302,U+0306-0307,U+030A,U+030C,U+030F,U+0313,U+0315,U+031B,U+0326-0328,U+032C,U+0337-0338,U+0342-0343,U+0346-036F,U+0E3F,U+1DC0-1DF5,U+1DFC-1DFF,U+2070-2071,U+2074-208E,U+2090-209C,U+20DB-20DE,U+20E8,U+20F0,U+2100-2101,U+2103,U+2105-2106,U+2109,U+2117,U+211E-2121,U+2126,U+212A-212B,U+212E,U+2132,U+213B,U+214D,U+2150-217F,U+2183-2186,U+2189,U+2190,U+2192,U+2194-2199,U+21AA,U+21B0-21B1,U+21B3-21B5,U+21BA-21BB,U+21D0,U+21D2,U+21D4,U+21DE-21DF,U+21E4-21E5,U+21E7,U+21EA,U+2202,U+2205-2206,U+220F,U+2211,U+221A,U+221E,U+222B,U+2236,U+2248,U+2260,U+2264-2265,U+2295-2298,U+2303-2305,U+2325-2327,U+232B,U+2380,U+2387,U+238B,U+23CE-23CF,U+2423,U+2460-2468,U+24B6-24CF,U+24EA,U+25A0-25A2,U+25AA,U+25B2-25B3,U+25B6-25B7,U+25BA-25BD,U+25C0-25C1,U+25C4-25C7,U+25CA-25CB,U+25CF,U+25E6,U+25EF,U+2600,U+2605-2606,U+263C,U+2661,U+2665,U+266A-266B,U+26A0,U+2713,U+2717,U+2756,U+2764,U+2780-2788,U+27EF,U+27F5-27FA,U+2913,U+2A38,U+2B06,U+2B12-2B13,U+2B1C,U+2B24,U+2E18,U+A92E,U+1F12F-1F149,U+1F16A-1F16B,U+1F850,U+1F852",
|
||||
"cjkExclusions": "U+007E,U+00B7,U+02C4,U+02C7,U+02C9-02CB,U+02CD,U+02D0,U+02D8-02D9,U+02DB,U+02DD,U+02DF,U+2013-2015,U+2018-2019,U+201C-201D,U+2026,U+203B,U+2074,U+207F,U+2081-2084,U+2103,U+2105,U+2109,U+2121,U+2126,U+212B,U+2153-2154,U+215B-215E,U+2160-216B,U+2170-2179,U+2189,U+2190,U+2192,U+2194-2199,U+21D2,U+21D4,U+21E7,U+2202,U+220F,U+2211,U+221A,U+221E,U+222B,U+2236,U+2248,U+2260,U+2264-2265,U+2295,U+2460-2468,U+24B6-24CF,U+25A0-25A1,U+25B2-25B3,U+25B6-25B7,U+25BC-25BD,U+25C0-25C1,U+25C6-25C7,U+25CB,U+25CF,U+25EF,U+2605-2606,U+2661,U+2665,U+266A,U+2B1C,U+1F130-1F149"
|
||||
}
|
||||
@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = ["fonttools>=4.63", "brotli"]
|
||||
# ///
|
||||
"""Regenerate the default theme's Inter webfont subsets.
|
||||
|
||||
Reads the subset definitions from scripts/fontSubsets.json, downloads the
|
||||
pinned Inter release (cached in node_modules/.cache/inter), verifies that
|
||||
every non-PUA codepoint of the full font is covered by some subset, writes
|
||||
the woff2 files into src/client/theme-default/fonts, and regenerates the
|
||||
@font-face declarations in the theme's fonts.css.
|
||||
|
||||
Usage: scripts/subsetFonts.py
|
||||
"""
|
||||
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
import urllib.request
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
from fontTools import subset
|
||||
from fontTools.ttLib import TTFont
|
||||
|
||||
INTER_VERSION = "4.1"
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SUBSETS_JSON = ROOT / "scripts" / "fontSubsets.json"
|
||||
FONTS_DIR = ROOT / "src" / "client" / "theme-default" / "fonts"
|
||||
FONTS_CSS = ROOT / "src" / "client" / "theme-default" / "styles" / "fonts.css"
|
||||
CACHE_DIR = ROOT / "node_modules" / ".cache" / "inter"
|
||||
|
||||
STYLES = {
|
||||
"italic": ("InterVariable-Italic.ttf", "italic"),
|
||||
"roman": ("InterVariable.ttf", "normal"),
|
||||
}
|
||||
|
||||
# Inter maps its internal alternate glyphs into the Private Use Area. These
|
||||
# are intentionally not shipped: they are not real text, and declaring PUA
|
||||
# unicode-ranges would let Inter shadow icon fonts that live there.
|
||||
PUA = range(0xE000, 0xF8FF + 1)
|
||||
|
||||
BASE_VAR = """\
|
||||
:root {
|
||||
--vp-font-family-base:
|
||||
'Inter', -apple-system, BlinkMacSystemFont, sans-serif, 'Apple Color Emoji',
|
||||
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
}
|
||||
"""
|
||||
|
||||
CJK_BASE_VAR = """\
|
||||
[lang]:where(:lang(zh), :lang(ja), :lang(ko)) {
|
||||
--vp-font-family-base:
|
||||
'Inter4CJK', -apple-system, BlinkMacSystemFont, sans-serif,
|
||||
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
}
|
||||
"""
|
||||
|
||||
# used instead of everything else in this file when `useWebFonts` is enabled
|
||||
# (only the content between the markers survives) - see
|
||||
# src/node/plugins/webFontsPlugin.ts. There is no Inter4CJK on Google Fonts,
|
||||
# so CJK documents use the regular Inter there.
|
||||
WEBFONT_IMPORT = f"""\
|
||||
/* webfont-marker-begin */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap');
|
||||
|
||||
{BASE_VAR}\
|
||||
/* webfont-marker-end */
|
||||
"""
|
||||
|
||||
# The `cjkExclusions` key of the json lists characters that have both a
|
||||
# Western (proportional) and an East Asian (em-square) form with no
|
||||
# encoding-level distinction - mostly East Asian Ambiguous punctuation and
|
||||
# symbols (UAX #11). The generated Inter4CJK faces reuse the same font files
|
||||
# but leave these out of their unicode-ranges so that CJK fonts render them
|
||||
# in CJK documents. References:
|
||||
# https://www.unicode.org/L2/L2014/14006-sv-western-vs-cjk.pdf
|
||||
# https://www.unicode.org/L2/L2018/18013-svs-proposal.pdf
|
||||
# https://www.unicode.org/L2/L2018/18073-svs-proposal.pdf
|
||||
# https://www.unicode.org/L2/L2023/23212r-quotes-svs-proposal.pdf
|
||||
# https://github.com/w3c/clreq/blob/gh-pages/local.css
|
||||
# & U+2015 (used like U+2014 in Japanese), U+203B (Japanese reference mark),
|
||||
# U+007E (zh's request; both forms are fine in ja because it is unused there)
|
||||
CJK_COMMENT = """\
|
||||
/* Inter4CJK reuses the files above, but leaves out characters that should
|
||||
be rendered by CJK fonts in CJK documents - see scripts/subsetFonts.py */
|
||||
"""
|
||||
|
||||
|
||||
def parse_ranges(value: str) -> set[int]:
|
||||
cps: set[int] = set()
|
||||
for part in value.split(","):
|
||||
lo, _, hi = part.strip().removeprefix("U+").partition("-")
|
||||
cps.update(range(int(lo, 16), int(hi or lo, 16) + 1))
|
||||
return cps
|
||||
|
||||
|
||||
def format_ranges(cps: set[int]) -> str:
|
||||
ranges: list[tuple[int, int]] = []
|
||||
start = prev = -2
|
||||
for cp in sorted(cps):
|
||||
if cp != prev + 1:
|
||||
if start >= 0:
|
||||
ranges.append((start, prev))
|
||||
start = cp
|
||||
prev = cp
|
||||
ranges.append((start, prev))
|
||||
return ", ".join(
|
||||
f"U+{lo:04X}" if lo == hi else f"U+{lo:04X}-{hi:04X}" for lo, hi in ranges
|
||||
)
|
||||
|
||||
|
||||
def download_release() -> Path:
|
||||
release_dir = CACHE_DIR / f"Inter-{INTER_VERSION}"
|
||||
zip_path = CACHE_DIR / f"Inter-{INTER_VERSION}.zip"
|
||||
if not zip_path.exists():
|
||||
url = f"https://github.com/rsms/inter/releases/download/v{INTER_VERSION}/Inter-{INTER_VERSION}.zip"
|
||||
print(f"downloading {url}")
|
||||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
with urllib.request.urlopen(url) as res:
|
||||
zip_path.write_bytes(res.read())
|
||||
with zipfile.ZipFile(zip_path) as zf:
|
||||
for file, _ in STYLES.values():
|
||||
if not (release_dir / file).exists():
|
||||
zf.extract(file, release_dir)
|
||||
return release_dir
|
||||
|
||||
|
||||
def check_coverage(release: Path, subsets: dict[str, str]) -> None:
|
||||
covered = set().union(*(parse_ranges(v) for v in subsets.values()))
|
||||
ok = True
|
||||
for file, _ in STYLES.values():
|
||||
cmap = set(TTFont(release / file).getBestCmap())
|
||||
missing = sorted(cp for cp in cmap - covered if cp not in PUA)
|
||||
if missing:
|
||||
ok = False
|
||||
print(f"{file}: {len(missing)} codepoints not covered by any subset:")
|
||||
print(" " + ", ".join(f"U+{cp:04X}" for cp in missing))
|
||||
if not ok:
|
||||
sys.exit("add the missing codepoints to a subset in scripts/fontSubsets.json")
|
||||
|
||||
|
||||
def build_subsets(release: Path, subsets: dict[str, str]) -> None:
|
||||
for style, (file, _) in STYLES.items():
|
||||
for name, value in subsets.items():
|
||||
options = subset.Options()
|
||||
options.flavor = "woff2"
|
||||
options.layout_features = [*options.layout_features, "pnum", "tnum"]
|
||||
font = subset.load_font(release / file, options)
|
||||
subsetter = subset.Subsetter(options)
|
||||
subsetter.populate(unicodes=parse_ranges(value))
|
||||
subsetter.subset(font)
|
||||
buf = io.BytesIO()
|
||||
subset.save_font(font, buf, options)
|
||||
out = FONTS_DIR / f"inter-{style}-{name}.woff2"
|
||||
changed = not out.exists() or out.read_bytes() != buf.getvalue()
|
||||
if changed:
|
||||
out.write_bytes(buf.getvalue())
|
||||
print(
|
||||
f"{out.name:36} {font['maxp'].numGlyphs:4} glyphs "
|
||||
f"{buf.tell() / 1024:6.1f} KB{'' if changed else ' (unchanged)'}"
|
||||
)
|
||||
|
||||
|
||||
def face(family: str, css_style: str, file: str, ranges: str) -> str:
|
||||
return (
|
||||
"@font-face {\n"
|
||||
f" font-family: {family};\n"
|
||||
f" font-style: {css_style};\n"
|
||||
" font-weight: 100 900;\n"
|
||||
" font-display: swap;\n"
|
||||
f" src: url('../fonts/{file}') format('woff2');\n"
|
||||
f" unicode-range: {ranges};\n"
|
||||
"}\n"
|
||||
)
|
||||
|
||||
|
||||
def write_css(subsets: dict[str, str], cjk_exclusions: set[int]) -> None:
|
||||
inter = []
|
||||
cjk = []
|
||||
for style, (_, css_style) in STYLES.items():
|
||||
for name, value in subsets.items():
|
||||
file = f"inter-{style}-{name}.woff2"
|
||||
inter.append(face("Inter", css_style, file, value.replace(",", ", ")))
|
||||
cjk_cps = parse_ranges(value) - cjk_exclusions
|
||||
if cjk_cps:
|
||||
cjk.append(
|
||||
face("Inter4CJK", css_style, file, format_ranges(cjk_cps))
|
||||
)
|
||||
|
||||
FONTS_CSS.write_text(
|
||||
f"{WEBFONT_IMPORT}\n"
|
||||
"/* Generated by scripts/subsetFonts.py from scripts/fontSubsets.json */\n\n"
|
||||
+ "\n".join(inter)
|
||||
+ f"\n{CJK_COMMENT}\n"
|
||||
+ "\n".join(cjk)
|
||||
+ f"\n{BASE_VAR}\n{CJK_BASE_VAR}"
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
subsets: dict[str, str] = json.loads(SUBSETS_JSON.read_text())
|
||||
cjk_exclusions = parse_ranges(subsets.pop("cjkExclusions"))
|
||||
release = download_release()
|
||||
check_coverage(release, subsets)
|
||||
build_subsets(release, subsets)
|
||||
write_css(subsets, cjk_exclusions)
|
||||
print(f"\nwrote {len(subsets) * len(STYLES)} subsets and {FONTS_CSS.relative_to(ROOT)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in new issue