@turntrout/subfont 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,145 @@
1
+ const LinesAndColumns = require('lines-and-columns').default;
2
+ const getFontInfo = require('./getFontInfo');
3
+ const unicodeRange = require('./unicodeRange');
4
+
5
+ async function warnAboutMissingGlyphs(
6
+ htmlOrSvgAssetTextsWithProps,
7
+ assetGraph
8
+ ) {
9
+ const missingGlyphsErrors = [];
10
+
11
+ // Collect all unique subset buffers and parse them concurrently.
12
+ // getFontInfo internally serializes harfbuzzjs WASM calls, so
13
+ // Promise.all just queues them up rather than running in parallel.
14
+ const uniqueSubsetBuffers = new Map();
15
+ for (const { fontUsages } of htmlOrSvgAssetTextsWithProps) {
16
+ for (const fontUsage of fontUsages) {
17
+ if (!fontUsage.subsets) continue;
18
+ const subsetBuffer = Object.values(fontUsage.subsets)[0];
19
+ if (!uniqueSubsetBuffers.has(subsetBuffer)) {
20
+ uniqueSubsetBuffers.set(
21
+ subsetBuffer,
22
+ getFontInfo(subsetBuffer)
23
+ .then((info) => new Set(info.characterSet))
24
+ .catch((err) => {
25
+ assetGraph.warn(err);
26
+ return null;
27
+ })
28
+ );
29
+ }
30
+ }
31
+ }
32
+ const subsetCharSetCache = new Map();
33
+ await Promise.all(
34
+ [...uniqueSubsetBuffers.entries()].map(async ([buffer, promise]) => {
35
+ subsetCharSetCache.set(buffer, await promise);
36
+ })
37
+ );
38
+
39
+ for (const {
40
+ htmlOrSvgAsset,
41
+ fontUsages,
42
+ accumulatedFontFaceDeclarations,
43
+ } of htmlOrSvgAssetTextsWithProps) {
44
+ let linesAndColumns;
45
+ for (const fontUsage of fontUsages) {
46
+ if (!fontUsage.subsets) continue;
47
+ const subsetBuffer = Object.values(fontUsage.subsets)[0];
48
+ const characterSetLookup = subsetCharSetCache.get(subsetBuffer);
49
+ if (!characterSetLookup) continue; // getFontInfo failed on subset; already warned
50
+
51
+ let missedAny = false;
52
+ // Note: location search below is best-effort — it searches the raw
53
+ // HTML source, so it may match characters inside <script>, attributes,
54
+ // or other non-rendered contexts rather than the actual styled text.
55
+ for (const char of fontUsage.pageText) {
56
+ // Turns out that browsers don't mind that these are missing:
57
+ if (char === '\t' || char === '\n') {
58
+ continue;
59
+ }
60
+
61
+ const codePoint = char.codePointAt(0);
62
+
63
+ const isMissing = !characterSetLookup.has(codePoint);
64
+
65
+ if (isMissing) {
66
+ // Find all occurrences of the missing character in the source,
67
+ // not just the first, so that every location is reported.
68
+ const locations = [];
69
+ const sourceText = htmlOrSvgAsset.text;
70
+ if (char.length > 0) {
71
+ let searchIdx = 0;
72
+ while (true) {
73
+ const charIdx = sourceText.indexOf(char, searchIdx);
74
+ if (charIdx === -1) break;
75
+ if (!linesAndColumns) {
76
+ linesAndColumns = new LinesAndColumns(sourceText);
77
+ }
78
+ const position = linesAndColumns.locationForIndex(charIdx);
79
+ locations.push(
80
+ `${htmlOrSvgAsset.urlOrDescription}:${position.line + 1}:${
81
+ position.column + 1
82
+ }`
83
+ );
84
+ searchIdx = charIdx + char.length;
85
+ }
86
+ }
87
+
88
+ if (locations.length === 0) {
89
+ locations.push(
90
+ `${htmlOrSvgAsset.urlOrDescription} (generated content)`
91
+ );
92
+ }
93
+
94
+ for (const location of locations) {
95
+ missingGlyphsErrors.push({
96
+ codePoint,
97
+ char,
98
+ htmlOrSvgAsset,
99
+ fontUsage,
100
+ location,
101
+ });
102
+ }
103
+ missedAny = true;
104
+ }
105
+ }
106
+ if (missedAny) {
107
+ const fontFaces = accumulatedFontFaceDeclarations.filter((fontFace) =>
108
+ fontUsage.fontFamilies.has(fontFace['font-family'])
109
+ );
110
+ for (const fontFace of fontFaces) {
111
+ const cssFontFaceSrc = fontFace.relations[0];
112
+ const fontFaceDeclaration = cssFontFaceSrc.node;
113
+ if (
114
+ !fontFaceDeclaration.some((node) => node.prop === 'unicode-range')
115
+ ) {
116
+ fontFaceDeclaration.append({
117
+ prop: 'unicode-range',
118
+ value: unicodeRange(fontUsage.codepoints.original),
119
+ });
120
+ cssFontFaceSrc.from.markDirty();
121
+ }
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ if (missingGlyphsErrors.length) {
128
+ const errorLog = missingGlyphsErrors.map(
129
+ ({ char, fontUsage, location }) =>
130
+ `- \\u{${char.codePointAt(0).toString(16)}} (${char}) in font-family '${
131
+ fontUsage.props['font-family']
132
+ }' (${fontUsage.props['font-weight']}/${
133
+ fontUsage.props['font-style']
134
+ }) at ${location}`
135
+ );
136
+
137
+ const message = `Missing glyph fallback detected.
138
+ When your primary webfont doesn't contain the glyphs you use, browsers that don't support unicode-range will load your fallback fonts, which will be a potential waste of bandwidth.
139
+ These glyphs are used on your site, but they don't exist in the font you applied to them:`;
140
+
141
+ assetGraph.info(new Error(`${message}\n${errorLog.join('\n')}`));
142
+ }
143
+ }
144
+
145
+ module.exports = warnAboutMissingGlyphs;
@@ -0,0 +1,11 @@
1
+ // Shared serialization queue for harfbuzzjs WASM calls.
2
+ // harfbuzzjs returns corrupt results when multiple calls run concurrently
3
+ // on the shared module instance. This queue ensures only one WASM
4
+ // operation runs at a time across getFontInfo and collectFeatureGlyphIds.
5
+ let queue = Promise.resolve();
6
+
7
+ function enqueue(fn) {
8
+ return (queue = queue.then(fn, fn));
9
+ }
10
+
11
+ module.exports = enqueue;
package/package.json ADDED
@@ -0,0 +1,113 @@
1
+ {
2
+ "name": "@turntrout/subfont",
3
+ "version": "1.0.0",
4
+ "description": "Automatically subset web fonts to only the characters used on your pages. Fork of Munter/subfont with modern defaults.",
5
+ "packageManager": "pnpm@10.29.3",
6
+ "engines": {
7
+ "node": ">=18.0.0"
8
+ },
9
+ "scripts": {
10
+ "lint": "eslint . && prettier --check '**/*.{js,md}'",
11
+ "test": "mocha && npm run lint",
12
+ "coverage": "nyc --reporter=lcov --reporter=text -- mocha",
13
+ "check-coverage": "nyc check-coverage",
14
+ "preversion": "offline-github-changelog --next=${npm_new_version} > CHANGELOG.md && git add CHANGELOG.md",
15
+ "prepare": "husky || true"
16
+ },
17
+ "bin": {
18
+ "subfont": "lib/cli.js"
19
+ },
20
+ "main": "lib/subfont.js",
21
+ "files": [
22
+ "lib",
23
+ "*.md"
24
+ ],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/alexander-turner/subfont.git"
28
+ },
29
+ "keywords": [
30
+ "google",
31
+ "font",
32
+ "fonts",
33
+ "webfont",
34
+ "webfonts",
35
+ "subset",
36
+ "subsetting",
37
+ "commandline",
38
+ "cli",
39
+ "automation",
40
+ "woff",
41
+ "woff2",
42
+ "preload"
43
+ ],
44
+ "author": "Alexander Turner",
45
+ "license": "MIT",
46
+ "bugs": {
47
+ "url": "https://github.com/alexander-turner/subfont/issues"
48
+ },
49
+ "homepage": "https://github.com/alexander-turner/subfont#readme",
50
+ "dependencies": {
51
+ "@gustavnikolaj/async-main-wrap": "^3.0.1",
52
+ "@hookun/parse-animation-shorthand": "^0.1.5",
53
+ "@puppeteer/browsers": "^2.0.0",
54
+ "assetgraph": "7.13.0",
55
+ "css-font-parser": "^2.0.0",
56
+ "css-font-weight-names": "^0.2.1",
57
+ "css-list-helpers": "^2.0.0",
58
+ "font-snapper": "^1.2.0",
59
+ "font-tracer": "^3.7.0",
60
+ "fontverter": "^2.0.0",
61
+ "gettemporaryfilepath": "^1.0.1",
62
+ "harfbuzzjs": "^0.5.0",
63
+ "lines-and-columns": "^1.1.6",
64
+ "memoizesync": "^1.1.1",
65
+ "p-limit": "^3.0.0",
66
+ "parse5": "^7.0.0",
67
+ "postcss": "^8.3.11",
68
+ "postcss-value-parser": "^4.0.2",
69
+ "pretty-bytes": "^5.1.0",
70
+ "puppeteer-core": "^24.39.1",
71
+ "specificity": "^0.4.1",
72
+ "subset-font": "~2.3.0",
73
+ "urltools": "^0.4.1",
74
+ "yargs": "^15.4.0"
75
+ },
76
+ "devDependencies": {
77
+ "canvas": "npm:@napi-rs/canvas@^0.1.97",
78
+ "combos": "^0.2.0",
79
+ "coveralls": "^3.0.9",
80
+ "css-generators": "^0.2.0",
81
+ "eslint": "^9.0.0",
82
+ "eslint-config-prettier": "^10.0.0",
83
+ "eslint-plugin-mocha": "^10.0.0",
84
+ "neostandard": "^0.12.0",
85
+ "html-generators": "^1.0.3",
86
+ "httpception": "^3.0.0",
87
+ "husky": "^9.1.7",
88
+ "magicpen-prism": "^3.0.2",
89
+ "mocha": "^11.0.0",
90
+ "nyc": "^15.1.0",
91
+ "offline-github-changelog": "^1.6.1",
92
+ "prettier": "^3.0.0",
93
+ "proxyquire": "^2.1.1",
94
+ "puppeteer": "^24.39.1",
95
+ "sinon": "^19.0.0",
96
+ "unexpected": "^11.8.1",
97
+ "unexpected-check": "^2.3.1",
98
+ "unexpected-resemble": "^5.0.1",
99
+ "unexpected-set": "^2.0.1",
100
+ "unexpected-sinon": "^10.11.2"
101
+ },
102
+ "pnpm": {
103
+ "overrides": {
104
+ "canvas": "npm:@napi-rs/canvas@^0.1.97"
105
+ },
106
+ "onlyBuiltDependencies": [
107
+ "core-js",
108
+ "es5-ext",
109
+ "puppeteer",
110
+ "unrs-resolver"
111
+ ]
112
+ }
113
+ }