@liiift-studio/sanity-font-manager 2.4.0 → 2.5.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.
- package/dist/UploadModal-6LIX7XOK.js +6 -0
- package/dist/UploadModal-NME2W53V.mjs +6 -0
- package/dist/chunk-646WCBRR.mjs +7276 -0
- package/dist/chunk-FH4QKHOH.js +7276 -0
- package/dist/index.js +664 -1647
- package/dist/index.mjs +317 -1209
- package/package.json +5 -5
- package/src/components/BatchUploadFonts.jsx +57 -44
- package/src/components/BulkActions.jsx +99 -0
- package/src/components/ExistingDocumentResolver.jsx +152 -0
- package/src/components/FontReviewCard.jsx +415 -0
- package/src/components/SingleUploaderTool.jsx +3 -4
- package/src/components/UploadModal.jsx +268 -0
- package/src/components/UploadScriptsComponent.jsx +23 -21
- package/src/components/UploadStep1Settings.jsx +272 -0
- package/src/components/UploadStep2Review.jsx +472 -0
- package/src/components/UploadStep3Execute.jsx +234 -0
- package/src/components/UploadSummary.jsx +196 -0
- package/src/index.js +45 -0
- package/src/utils/buildUploadPlan.js +325 -0
- package/src/utils/executeUploadPlan.js +437 -0
- package/src/utils/executionReducer.js +56 -0
- package/src/utils/fontHelpers.js +267 -0
- package/src/utils/generateCssFile.js +79 -77
- package/src/utils/generateFontData.js +47 -94
- package/src/utils/getEmptyFontKit.js +19 -17
- package/src/utils/parseFont.js +55 -0
- package/src/utils/planReducer.js +517 -0
- package/src/utils/planTypes.js +183 -0
- package/src/utils/processFontFiles.js +120 -78
- package/src/utils/regenerateFontData.js +2 -2
- package/src/utils/resolveExistingFont.js +87 -0
- package/src/utils/uploadFontFiles.js +405 -405
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// Shared helpers for extracting data from lib-font parsed fonts — the ONLY code that touches font.opentype.tables.*
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Name record lookup cache — avoids repeated linear scans of nameRecords.
|
|
5
|
+
* Keyed by font instance (WeakMap), values are { [nameID]: string } maps.
|
|
6
|
+
* @type {WeakMap<object, Object.<number, string>>}
|
|
7
|
+
*/
|
|
8
|
+
const nameCache = new WeakMap();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get a name table string by numeric name ID.
|
|
12
|
+
* Prefers Windows/Unicode/English (platform 3, language 0x0409),
|
|
13
|
+
* falls back to Mac/Roman/English (platform 1, language 0),
|
|
14
|
+
* then first available record.
|
|
15
|
+
*
|
|
16
|
+
* @param {object} font - lib-font Font instance
|
|
17
|
+
* @param {number} nameID - OpenType name ID (0=copyright, 1=family, 2=subfamily, 4=fullName, 6=postscript, 16=prefFamily, 17=prefSubfamily)
|
|
18
|
+
* @returns {string} Decoded name string, or empty string if not found
|
|
19
|
+
*/
|
|
20
|
+
export function getNameString(font, nameID) {
|
|
21
|
+
if (!nameCache.has(font)) nameCache.set(font, {});
|
|
22
|
+
const cache = nameCache.get(font);
|
|
23
|
+
if (nameID in cache) return cache[nameID];
|
|
24
|
+
|
|
25
|
+
const records = font.opentype?.tables?.name?.nameRecords || [];
|
|
26
|
+
|
|
27
|
+
// Priority 1: Windows Unicode English
|
|
28
|
+
const win = records.find(r => r.nameID === nameID && r.platformID === 3 && r.languageID === 0x0409);
|
|
29
|
+
if (win?.string) { cache[nameID] = win.string; return win.string; }
|
|
30
|
+
|
|
31
|
+
// Priority 2: Mac Roman English
|
|
32
|
+
const mac = records.find(r => r.nameID === nameID && r.platformID === 1 && r.languageID === 0);
|
|
33
|
+
if (mac?.string) { cache[nameID] = mac.string; return mac.string; }
|
|
34
|
+
|
|
35
|
+
// Priority 3: First record with this nameID
|
|
36
|
+
const any = records.find(r => r.nameID === nameID);
|
|
37
|
+
const result = any?.string || '';
|
|
38
|
+
cache[nameID] = result;
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get all OpenType feature tags from GSUB and GPOS tables.
|
|
44
|
+
* Traverses scripts → langsys → features, deduplicates, and caches.
|
|
45
|
+
* Equivalent to fontkit's font.availableFeatures.
|
|
46
|
+
*
|
|
47
|
+
* @param {object} font - lib-font Font instance
|
|
48
|
+
* @returns {string[]} Array of unique 4-character feature tag strings (e.g. ['kern', 'liga', 'smcp'])
|
|
49
|
+
*/
|
|
50
|
+
export function getAllFeatureTags(font) {
|
|
51
|
+
const tags = new Set();
|
|
52
|
+
const tables = font.opentype?.tables;
|
|
53
|
+
for (const layoutTable of [tables?.GSUB, tables?.GPOS]) {
|
|
54
|
+
if (!layoutTable) continue;
|
|
55
|
+
try {
|
|
56
|
+
for (const scriptTag of layoutTable.getSupportedScripts()) {
|
|
57
|
+
const script = layoutTable.getScriptTable(scriptTag);
|
|
58
|
+
for (const langTag of layoutTable.getSupportedLangSys(script)) {
|
|
59
|
+
const langsys = layoutTable.getLangSysTable(script, langTag);
|
|
60
|
+
for (const feature of layoutTable.getFeatures(langsys)) {
|
|
61
|
+
tags.add(feature.featureTag.trim());
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.warn(`Error reading ${layoutTable === tables.GSUB ? 'GSUB' : 'GPOS'} features:`, err.message);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return [...tags];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get character set as array of code points from the cmap table.
|
|
74
|
+
* Uses Windows/Unicode BMP subtable (platform 3, encoding 1).
|
|
75
|
+
*
|
|
76
|
+
* @param {object} font - lib-font Font instance
|
|
77
|
+
* @returns {number[]} Array of supported Unicode code points
|
|
78
|
+
*/
|
|
79
|
+
export function getCharacterSet(font) {
|
|
80
|
+
const cmap = font.opentype?.tables?.cmap;
|
|
81
|
+
if (!cmap) return [];
|
|
82
|
+
try {
|
|
83
|
+
return cmap.getSupportedCharCodes(3, 1);
|
|
84
|
+
} catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Build a variation axis map from fvar table.
|
|
91
|
+
* Filters out degenerate axes (min === max). Returns null if not a variable font.
|
|
92
|
+
*
|
|
93
|
+
* @param {object} font - lib-font Font instance
|
|
94
|
+
* @returns {{ [tag: string]: { min: number, max: number, default: number, name: string } } | null}
|
|
95
|
+
*/
|
|
96
|
+
export function getVariationAxes(font) {
|
|
97
|
+
const fvar = font.opentype?.tables?.fvar;
|
|
98
|
+
if (!fvar?.axes) return null;
|
|
99
|
+
|
|
100
|
+
const axes = {};
|
|
101
|
+
for (const axis of fvar.axes) {
|
|
102
|
+
if (axis.minValue === axis.maxValue) continue;
|
|
103
|
+
axes[axis.tag] = {
|
|
104
|
+
min: axis.minValue,
|
|
105
|
+
max: axis.maxValue,
|
|
106
|
+
default: axis.defaultValue,
|
|
107
|
+
name: getNameString(font, axis.axisNameID) || axis.tag,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return Object.keys(axes).length > 0 ? axes : null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get named instances from fvar table.
|
|
115
|
+
* Resolves subfamilyNameID and postScriptNameID via the name table.
|
|
116
|
+
*
|
|
117
|
+
* @param {object} font - lib-font Font instance
|
|
118
|
+
* @returns {Array<{ name: string, coordinates: number[], postScriptName: string }>}
|
|
119
|
+
*/
|
|
120
|
+
export function getNamedInstances(font) {
|
|
121
|
+
const fvar = font.opentype?.tables?.fvar;
|
|
122
|
+
if (!fvar?.instances) return [];
|
|
123
|
+
return fvar.instances.map(inst => ({
|
|
124
|
+
name: getNameString(font, inst.subfamilyNameID),
|
|
125
|
+
coordinates: inst.coordinates,
|
|
126
|
+
postScriptName: getNameString(font, inst.postScriptNameID || 0),
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Build font metrics object matching the Sanity document shape.
|
|
132
|
+
* Uses OS/2 typo metrics when USE_TYPO_METRICS bit is set, otherwise hhea.
|
|
133
|
+
*
|
|
134
|
+
* @param {object} font - lib-font Font instance
|
|
135
|
+
* @returns {{ unitsPerEm: number, ascender: number, descender: number, lineGap: number, underlinePosition: number, underlineThickness: number, italicAngle: number, capHeight: number, xHeight: number, boundingBox: { xMin: number, yMin: number, xMax: number, yMax: number } }}
|
|
136
|
+
*/
|
|
137
|
+
export function getFontMetrics(font) {
|
|
138
|
+
const tables = font.opentype?.tables;
|
|
139
|
+
const os2 = tables?.['OS/2'];
|
|
140
|
+
const head = tables?.head;
|
|
141
|
+
const post = tables?.post;
|
|
142
|
+
const hhea = tables?.hhea;
|
|
143
|
+
|
|
144
|
+
// USE_TYPO_METRICS flag (fsSelection bit 7) — when set, use OS/2 typo metrics
|
|
145
|
+
const useTypo = os2 ? (os2.fsSelection & 0x80) !== 0 : false;
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
unitsPerEm: head?.unitsPerEm || 1000,
|
|
149
|
+
ascender: useTypo ? (os2?.sTypoAscender || 0) : (hhea?.ascender ?? os2?.sTypoAscender ?? 0),
|
|
150
|
+
descender: useTypo ? (os2?.sTypoDescender || 0) : (hhea?.descender ?? os2?.sTypoDescender ?? 0),
|
|
151
|
+
lineGap: useTypo ? (os2?.sTypoLineGap || 0) : (hhea?.lineGap ?? os2?.sTypoLineGap ?? 0),
|
|
152
|
+
underlinePosition: post?.underlinePosition || 0,
|
|
153
|
+
underlineThickness: post?.underlineThickness || 0,
|
|
154
|
+
italicAngle: post?.italicAngle || 0,
|
|
155
|
+
capHeight: (os2?.version >= 2) ? (os2?.sCapHeight || 0) : 0,
|
|
156
|
+
xHeight: (os2?.version >= 2) ? (os2?.sxHeight || 0) : 0,
|
|
157
|
+
boundingBox: {
|
|
158
|
+
xMin: head?.xMin || 0,
|
|
159
|
+
yMin: head?.yMin || 0,
|
|
160
|
+
xMax: head?.xMax || 0,
|
|
161
|
+
yMax: head?.yMax || 0,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Build font metadata object matching the Sanity document shape.
|
|
168
|
+
*
|
|
169
|
+
* @param {object} font - lib-font Font instance
|
|
170
|
+
* @returns {{ postscriptName: string, fullName: string, familyName: string, subfamilyName: string, copyright: string, version: string, genDate: string }}
|
|
171
|
+
*/
|
|
172
|
+
export function getFontMetadata(font) {
|
|
173
|
+
return {
|
|
174
|
+
postscriptName: getNameString(font, 6),
|
|
175
|
+
fullName: getNameString(font, 4),
|
|
176
|
+
familyName: getNameString(font, 1),
|
|
177
|
+
subfamilyName: getNameString(font, 2),
|
|
178
|
+
copyright: getNameString(font, 0),
|
|
179
|
+
version: getNameString(font, 5),
|
|
180
|
+
genDate: new Date().toISOString(),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get the OS/2 usWeightClass value.
|
|
186
|
+
*
|
|
187
|
+
* @param {object} font - lib-font Font instance
|
|
188
|
+
* @returns {number|null} Weight class (1-1000) or null if OS/2 table is missing
|
|
189
|
+
*/
|
|
190
|
+
export function getWeightClass(font) {
|
|
191
|
+
return font.opentype?.tables?.['OS/2']?.usWeightClass || null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get the OS/2 fsSelection flags as a raw uint16.
|
|
196
|
+
*
|
|
197
|
+
* @param {object} font - lib-font Font instance
|
|
198
|
+
* @returns {number} fsSelection bitmask (0 if OS/2 table is missing)
|
|
199
|
+
*/
|
|
200
|
+
export function getFsSelection(font) {
|
|
201
|
+
return font.opentype?.tables?.['OS/2']?.fsSelection || 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get the head macStyle flags as a uint16 bitmask.
|
|
206
|
+
* lib-font returns macStyle as a bit array (big-endian order: index 15 = bit 0).
|
|
207
|
+
* This helper converts it back to a standard uint16 for bitwise testing.
|
|
208
|
+
*
|
|
209
|
+
* @param {object} font - lib-font Font instance
|
|
210
|
+
* @returns {number} macStyle bitmask (0 if head table is missing)
|
|
211
|
+
*/
|
|
212
|
+
export function getMacStyle(font) {
|
|
213
|
+
const macStyle = font.opentype?.tables?.head?.macStyle;
|
|
214
|
+
if (!macStyle) return 0;
|
|
215
|
+
// lib-font returns a bit array or a number depending on version
|
|
216
|
+
if (typeof macStyle === 'number') return macStyle;
|
|
217
|
+
// Convert bit array (big-endian) to uint16: index 15 = bit 0, index 14 = bit 1, etc.
|
|
218
|
+
if (typeof macStyle === 'object') {
|
|
219
|
+
let value = 0;
|
|
220
|
+
for (let i = 0; i < 16; i++) {
|
|
221
|
+
if (macStyle[i]) value |= (1 << (15 - i));
|
|
222
|
+
}
|
|
223
|
+
return value;
|
|
224
|
+
}
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get the post table italic angle.
|
|
230
|
+
*
|
|
231
|
+
* @param {object} font - lib-font Font instance
|
|
232
|
+
* @returns {number} Italic angle in degrees (0 for upright fonts)
|
|
233
|
+
*/
|
|
234
|
+
export function getItalicAngle(font) {
|
|
235
|
+
return font.opentype?.tables?.post?.italicAngle || 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get glyph count from maxp table.
|
|
240
|
+
*
|
|
241
|
+
* @param {object} font - lib-font Font instance
|
|
242
|
+
* @returns {number} Number of glyphs
|
|
243
|
+
*/
|
|
244
|
+
export function getGlyphCount(font) {
|
|
245
|
+
return font.opentype?.tables?.maxp?.numGlyphs || 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get the OS/2 sFamilyClass value for font category detection.
|
|
250
|
+
*
|
|
251
|
+
* @param {object} font - lib-font Font instance
|
|
252
|
+
* @returns {number} sFamilyClass value (0 if missing)
|
|
253
|
+
*/
|
|
254
|
+
export function getFamilyClass(font) {
|
|
255
|
+
return font.opentype?.tables?.['OS/2']?.sFamilyClass || 0;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Escape a font name for safe interpolation into CSS font-family declarations.
|
|
260
|
+
* Prevents CSS injection via crafted name table strings.
|
|
261
|
+
*
|
|
262
|
+
* @param {string} name - Raw font name from the name table
|
|
263
|
+
* @returns {string} Escaped name safe for CSS string context
|
|
264
|
+
*/
|
|
265
|
+
export function escapeCssFontName(name) {
|
|
266
|
+
return name.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/;/g, '');
|
|
267
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Builds a @font-face CSS file from a WOFF2 blob — URL or base64 src, variable font axis descriptors, metric-tuned fallback @font-face for CLS reduction
|
|
2
|
+
|
|
2
3
|
import base64 from 'base-64';
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
4
|
+
import { parseFont } from './parseFont';
|
|
5
|
+
import { getVariationAxes, getFontMetrics, getFamilyClass, escapeCssFontName } from './fontHelpers';
|
|
5
6
|
|
|
6
7
|
function _arrayBufferToBase64(buffer) {
|
|
7
8
|
var binary = '';
|
|
@@ -14,88 +15,83 @@ function _arrayBufferToBase64(buffer) {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* `skipped` lists axis tags that have no CSS descriptor (opsz, custom axes, etc.)
|
|
21
|
-
* so callers can surface them in comments.
|
|
18
|
+
* Builds CSS @font-face axis descriptors from a variation axis map.
|
|
19
|
+
* Accepts a pre-built axis map (from getVariationAxes) rather than a raw font object,
|
|
20
|
+
* so callers can share a single parse result.
|
|
22
21
|
*
|
|
23
|
-
*
|
|
24
|
-
* - Degenerate axes (min === max): skipped — no actual variation range
|
|
25
|
-
* - ital with max === 0: skipped — axis exists but font has no italic
|
|
26
|
-
* - slnt ordering: sorted ascending as CSS spec requires
|
|
27
|
-
* - ital + slnt coexistence: slnt takes priority (more expressive)
|
|
28
|
-
* - min > max from corrupt font data: clamped with Math.min/Math.max
|
|
29
|
-
* @param {Object} font - fontkit font instance
|
|
22
|
+
* @param {object|null} axisMap - Keyed axis map { wght: { min, max, default }, ... } or null
|
|
30
23
|
* @returns {{ descriptors: string, skipped: string[] }}
|
|
31
24
|
*/
|
|
32
|
-
export function buildVFDescriptors(
|
|
33
|
-
const cssAxes = {}
|
|
34
|
-
const skipped = []
|
|
35
|
-
try {
|
|
36
|
-
const va = font.variationAxes
|
|
37
|
-
if (!va) return { descriptors: '', skipped: [] }
|
|
25
|
+
export function buildVFDescriptors(axisMap) {
|
|
26
|
+
const cssAxes = {};
|
|
27
|
+
const skipped = [];
|
|
38
28
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
if (!axisMap) return { descriptors: '', skipped: [] };
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
for (const [tag, axis] of Object.entries(axisMap)) {
|
|
33
|
+
const lo = Math.min(axis.min, axis.max);
|
|
34
|
+
const hi = Math.max(axis.min, axis.max);
|
|
42
35
|
|
|
43
36
|
// Skip degenerate axes — no actual range
|
|
44
|
-
if (lo === hi) { skipped.push(tag); continue }
|
|
37
|
+
if (lo === hi) { skipped.push(tag); continue; }
|
|
45
38
|
|
|
46
39
|
if (tag === 'wght') {
|
|
47
|
-
cssAxes['font-weight'] = `${lo} ${hi}
|
|
40
|
+
cssAxes['font-weight'] = `${lo} ${hi}`;
|
|
48
41
|
} else if (tag === 'wdth') {
|
|
49
|
-
|
|
42
|
+
// Clamp to CSS font-stretch range (50-200%)
|
|
43
|
+
cssAxes['font-stretch'] = `${Math.max(50, lo)}% ${Math.min(200, hi)}%`;
|
|
50
44
|
} else if (tag === 'slnt') {
|
|
51
|
-
// slnt
|
|
52
|
-
|
|
45
|
+
// OpenType slnt is counter-clockwise positive; CSS oblique is clockwise positive
|
|
46
|
+
// Negate values and ensure ascending order
|
|
47
|
+
cssAxes['font-style'] = `oblique ${-hi}deg ${-lo}deg`;
|
|
53
48
|
} else if (tag === 'ital' && !cssAxes['font-style']) {
|
|
54
|
-
// ital: only
|
|
55
|
-
if (hi > 0) cssAxes['font-style'] = 'italic'
|
|
56
|
-
else skipped.push(tag)
|
|
49
|
+
// ital: only note if font has italic range; slnt takes priority
|
|
50
|
+
if (hi > 0) cssAxes['font-style'] = 'italic';
|
|
51
|
+
else skipped.push(tag);
|
|
57
52
|
} else {
|
|
58
53
|
// opsz, GRAD, XTRA, XHGT and all other custom axes have no CSS @font-face descriptor
|
|
59
|
-
skipped.push(tag)
|
|
54
|
+
skipped.push(tag);
|
|
60
55
|
}
|
|
61
56
|
}
|
|
62
57
|
} catch (_) {
|
|
63
58
|
// axes unreadable — no descriptors
|
|
64
59
|
}
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
|
|
61
|
+
const descriptors = Object.entries(cssAxes).map(([k, v]) => `${k}:${v}`).join(';') + (Object.keys(cssAxes).length ? ';' : '');
|
|
62
|
+
return { descriptors, skipped };
|
|
67
63
|
}
|
|
68
64
|
|
|
69
65
|
// Cross-platform fallback font stacks by category.
|
|
70
|
-
// Multiple local() sources ensure the first available system font is used.
|
|
71
|
-
// Liberation Sans covers Linux; Roboto covers Android; Georgia is universal for serifs.
|
|
72
66
|
const FALLBACK_STACKS = {
|
|
73
67
|
'sans-serif': "local('Arial'), local('Helvetica Neue'), local('Roboto'), local('Liberation Sans')",
|
|
74
68
|
'serif': "local('Georgia'), local('Times New Roman'), local('Times')",
|
|
75
69
|
'monospace': "local('Courier New'), local('Courier'), local('Menlo'), local('Monaco')",
|
|
76
|
-
// Display and script fonts have no universally suitable system fallback; default to sans-serif
|
|
77
70
|
'default': "local('Arial'), local('Helvetica Neue'), local('Roboto'), local('Liberation Sans')",
|
|
78
71
|
};
|
|
79
72
|
|
|
80
73
|
// OS/2 sFamilyClass high byte → FALLBACK_STACKS key.
|
|
81
|
-
// Classes 1–5,7 = serif variants; 8 = sans-serif; 9 = ornamental; 10 = script; 12 = symbolic.
|
|
82
74
|
const FAMILY_CLASS_MAP = {
|
|
83
75
|
1: 'serif', 2: 'serif', 3: 'serif', 4: 'serif', 5: 'serif', 7: 'serif',
|
|
84
76
|
8: 'sans-serif',
|
|
85
77
|
};
|
|
86
78
|
|
|
87
|
-
// Darden Studio fonts have sFamilyClass: 0
|
|
79
|
+
// Darden Studio fonts have sFamilyClass: 0 in their OS/2 table,
|
|
88
80
|
// so name-based matching is used as the primary signal.
|
|
89
81
|
const SERIF_NAMES = /jubilat|corundum|dapifer|birra|daith/i;
|
|
90
82
|
const SANS_NAMES = /halyard|gamay|omnes|kit/i;
|
|
91
83
|
|
|
92
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* Detects font category from the font name first, then OS/2 sFamilyClass as fallback.
|
|
86
|
+
* @param {object} font - lib-font Font instance
|
|
87
|
+
* @param {string} fontName
|
|
88
|
+
* @returns {string}
|
|
89
|
+
*/
|
|
93
90
|
function detectFontCategory(font, fontName) {
|
|
94
91
|
if (fontName && SERIF_NAMES.test(fontName)) return 'serif';
|
|
95
92
|
if (fontName && SANS_NAMES.test(fontName)) return 'sans-serif';
|
|
96
93
|
try {
|
|
97
|
-
|
|
98
|
-
const familyClass = font['OS/2']?.sFamilyClass ?? 0;
|
|
94
|
+
const familyClass = getFamilyClass(font);
|
|
99
95
|
const highByte = (familyClass >> 8) & 0xFF;
|
|
100
96
|
return FAMILY_CLASS_MAP[highByte] ?? 'default';
|
|
101
97
|
} catch {
|
|
@@ -103,17 +99,23 @@ function detectFontCategory(font, fontName) {
|
|
|
103
99
|
}
|
|
104
100
|
}
|
|
105
101
|
|
|
106
|
-
/**
|
|
107
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Extracts metric override percentages and detects the category fallback stack.
|
|
104
|
+
* Accepts a pre-parsed font to avoid double-parsing.
|
|
105
|
+
* @param {object} font - lib-font Font instance
|
|
106
|
+
* @param {string} fontName
|
|
107
|
+
* @returns {{ fallbackSrc: string, ascentOverride: string, descentOverride: string, lineGapOverride: string }}
|
|
108
|
+
*/
|
|
109
|
+
function calcFallbackData(font, fontName) {
|
|
108
110
|
try {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
const metrics = getFontMetrics(font);
|
|
112
|
+
const upm = metrics.unitsPerEm;
|
|
113
|
+
const category = detectFontCategory(font, fontName);
|
|
112
114
|
return {
|
|
113
115
|
fallbackSrc: FALLBACK_STACKS[category],
|
|
114
|
-
ascentOverride: `${(
|
|
115
|
-
descentOverride: `${(Math.abs(
|
|
116
|
-
lineGapOverride: `${(
|
|
116
|
+
ascentOverride: `${(metrics.ascender / upm * 100).toFixed(2)}%`,
|
|
117
|
+
descentOverride: `${(Math.abs(metrics.descender) / upm * 100).toFixed(2)}%`,
|
|
118
|
+
lineGapOverride: `${(metrics.lineGap / upm * 100).toFixed(2)}%`,
|
|
117
119
|
};
|
|
118
120
|
} catch (err) {
|
|
119
121
|
console.error('Failed to extract fallback font data:', err);
|
|
@@ -138,35 +140,38 @@ export default async function generateCssFile({
|
|
|
138
140
|
client,
|
|
139
141
|
}) {
|
|
140
142
|
try {
|
|
141
|
-
// Read the file once; reuse the same buffer for base64
|
|
143
|
+
// Read the file once; reuse the same buffer for base64 and font analysis
|
|
142
144
|
let arrayBuffer = await woff2File.arrayBuffer();
|
|
143
145
|
let b64 = _arrayBufferToBase64(arrayBuffer);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
+
|
|
147
|
+
// Parse once — share result between axis descriptors and fallback metrics
|
|
148
|
+
let font = await parseFont(arrayBuffer, fileName + '.woff2');
|
|
149
|
+
let { fallbackSrc, ascentOverride, descentOverride, lineGapOverride } = calcFallbackData(font, fontName);
|
|
150
|
+
|
|
151
|
+
// Escape font name for CSS injection prevention
|
|
152
|
+
const safeFontName = escapeCssFontName(fontName);
|
|
146
153
|
|
|
147
154
|
let cssString;
|
|
148
155
|
if (variableFont) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
// custom axes like GRAD require CSS @font-face syntax not yet standardised).
|
|
156
|
+
const axisMap = getVariationAxes(font);
|
|
157
|
+
let { descriptors, skipped } = buildVFDescriptors(axisMap);
|
|
152
158
|
let skipComment = skipped.length
|
|
153
159
|
? `/* axes present but have no @font-face descriptor: ${skipped.join(', ')}` +
|
|
154
160
|
(skipped.includes('opsz') ? ' — add font-optical-sizing:auto to your element CSS' : '') +
|
|
155
161
|
' */'
|
|
156
|
-
: ''
|
|
157
|
-
cssString = `${skipComment}@font-face{font-family:'${
|
|
162
|
+
: '';
|
|
163
|
+
cssString = `${skipComment}@font-face{font-family:'${safeFontName}';src:url(data:application/font-woff2;charset=utf-8;base64,${b64})format('woff2');${descriptors}font-display:swap;}`;
|
|
158
164
|
} else {
|
|
159
165
|
let fontStyle = style === 'Italic' ? 'italic' : 'normal';
|
|
160
|
-
cssString = `@font-face{font-family:'${
|
|
166
|
+
cssString = `@font-face{font-family:'${safeFontName}';src:url(data:application/font-woff2;charset=utf-8;base64,${b64})format('woff2');font-weight:${weight};font-style:${fontStyle};font-display:swap;}`;
|
|
161
167
|
}
|
|
162
168
|
|
|
163
|
-
// Fallback @font-face: tunes a
|
|
164
|
-
|
|
165
|
-
// Customers who reference fontSrc in their own projects get this fallback automatically.
|
|
166
|
-
let fallbackCssString = `@font-face{font-family:'${fontName} Fallback';src:${fallbackSrc};ascent-override:${ascentOverride};descent-override:${descentOverride};line-gap-override:${lineGapOverride};}`;
|
|
169
|
+
// Fallback @font-face: tunes a system font to match the custom font's metrics for CLS reduction
|
|
170
|
+
let fallbackCssString = `@font-face{font-family:'${safeFontName} Fallback';src:${fallbackSrc};ascent-override:${ascentOverride};descent-override:${descentOverride};line-gap-override:${lineGapOverride};}`;
|
|
167
171
|
|
|
168
|
-
|
|
169
|
-
|
|
172
|
+
// Upload as a text buffer (no Buffer polyfill needed — TextEncoder is native)
|
|
173
|
+
const cssBytes = new TextEncoder().encode(cssString + fallbackCssString);
|
|
174
|
+
let doc = await client.assets.upload('file', new Blob([cssBytes]), { filename: fileName + '.css' });
|
|
170
175
|
|
|
171
176
|
let newFileInput = language == null ?
|
|
172
177
|
{
|
|
@@ -175,9 +180,9 @@ export default async function generateCssFile({
|
|
|
175
180
|
_type: 'file',
|
|
176
181
|
asset: {
|
|
177
182
|
_type: 'reference',
|
|
178
|
-
_ref: doc._id
|
|
179
|
-
}
|
|
180
|
-
}
|
|
183
|
+
_ref: doc._id,
|
|
184
|
+
},
|
|
185
|
+
},
|
|
181
186
|
}
|
|
182
187
|
:
|
|
183
188
|
{
|
|
@@ -188,18 +193,15 @@ export default async function generateCssFile({
|
|
|
188
193
|
_type: 'file',
|
|
189
194
|
asset: {
|
|
190
195
|
_type: 'reference',
|
|
191
|
-
_ref: doc._id
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
+
_ref: doc._id,
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
};
|
|
196
201
|
|
|
197
202
|
return newFileInput;
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
catch (err) {
|
|
203
|
+
} catch (err) {
|
|
201
204
|
console.error(err);
|
|
202
205
|
throw err;
|
|
203
206
|
}
|
|
204
|
-
|
|
205
207
|
}
|