@liiift-studio/sanity-font-manager 2.4.0 → 2.5.1

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.
Files changed (36) hide show
  1. package/dist/UploadModal-ADNRGQUI.mjs +6 -0
  2. package/dist/UploadModal-WPK2CXLR.js +6 -0
  3. package/dist/chunk-JCDZ7SWZ.js +7711 -0
  4. package/dist/chunk-TMDE4A54.mjs +7711 -0
  5. package/dist/index.js +666 -1647
  6. package/dist/index.mjs +319 -1209
  7. package/package.json +5 -5
  8. package/src/components/BatchUploadFonts.jsx +57 -44
  9. package/src/components/BulkActions.jsx +99 -0
  10. package/src/components/ExistingDocumentResolver.jsx +152 -0
  11. package/src/components/FontReviewCard.jsx +455 -0
  12. package/src/components/SingleUploaderTool.jsx +3 -4
  13. package/src/components/UploadModal.jsx +304 -0
  14. package/src/components/UploadScriptsComponent.jsx +23 -21
  15. package/src/components/UploadStep1Settings.jsx +272 -0
  16. package/src/components/UploadStep2Review.jsx +474 -0
  17. package/src/components/UploadStep3Execute.jsx +234 -0
  18. package/src/components/UploadStep3bInstances.jsx +396 -0
  19. package/src/components/UploadSummary.jsx +196 -0
  20. package/src/index.js +46 -0
  21. package/src/utils/buildUploadPlan.js +326 -0
  22. package/src/utils/executeUploadPlan.js +430 -0
  23. package/src/utils/executionReducer.js +56 -0
  24. package/src/utils/fontHelpers.js +267 -0
  25. package/src/utils/generateCssFile.js +79 -77
  26. package/src/utils/generateFontData.js +47 -94
  27. package/src/utils/getEmptyFontKit.js +19 -17
  28. package/src/utils/parseFont.js +55 -0
  29. package/src/utils/parseVariableFontInstances.js +237 -147
  30. package/src/utils/planReducer.js +517 -0
  31. package/src/utils/planTypes.js +183 -0
  32. package/src/utils/processFontFiles.js +121 -78
  33. package/src/utils/regenerateFontData.js +2 -2
  34. package/src/utils/resolveExistingFont.js +87 -0
  35. package/src/utils/updateTypefaceDocument.js +15 -2
  36. 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 { Buffer } from 'buffer';
4
- import * as fontkit from 'fontkit';
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
- * Reads variable axes from a fontkit font object and returns:
18
- * { descriptors, skipped }
19
- * where `descriptors` is the CSS string for the @font-face block and
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
- * Edge cases handled:
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(font) {
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
- for (const [tag, axis] of Object.entries(va)) {
40
- const lo = Math.min(axis.min, axis.max)
41
- const hi = Math.max(axis.min, axis.max)
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
- cssAxes['font-stretch'] = `${lo}% ${hi}%`
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: ascending order required by CSS Fonts Level 4
52
- cssAxes['font-style'] = `oblique ${lo}deg ${hi}deg`
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 emit if font actually has italic range; slnt takes priority
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
- const descriptors = Object.entries(cssAxes).map(([k, v]) => `${k}:${v}`).join(';') + (Object.keys(cssAxes).length ? ';' : '')
66
- return { descriptors, skipped }
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 (No Classification) in their OS/2 table,
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
- /** Detects font category from the font name first, then OS/2 sFamilyClass as fallback */
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
- // fontkit v2: font['OS/2'] exposes the parsed OS/2 table directly
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
- /** Extracts metric override percentages and detects the category fallback stack from a font ArrayBuffer */
107
- function calcFallbackData(arrayBuffer, fontName) {
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
- let font = fontkit.create(Buffer.from(arrayBuffer));
110
- let upm = font.unitsPerEm;
111
- let category = detectFontCategory(font, fontName);
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: `${(font.ascent / upm * 100).toFixed(2)}%`,
115
- descentOverride: `${(Math.abs(font.descent) / upm * 100).toFixed(2)}%`,
116
- lineGapOverride: `${(font.lineGap / upm * 100).toFixed(2)}%`,
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 encoding and fontkit analysis
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
- let fontkitFont = fontkit.create(Buffer.from(arrayBuffer));
145
- let { fallbackSrc, ascentOverride, descentOverride, lineGapOverride } = calcFallbackData(arrayBuffer, fontName);
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
- let { descriptors, skipped } = buildVFDescriptors(fontkitFont);
150
- // Axes with no CSS @font-face descriptor (opsz needs font-optical-sizing:auto on elements;
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:'${fontName}';src:url(data:application/font-woff2;charset=utf-8;base64,${b64})format('woff2-variations');${descriptors}font-display:swap;}`;
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:'${fontName}';src:url(data:application/font-woff2;charset=utf-8;base64,${b64})format('woff2');font-weight:${weight};font-style:${fontStyle};font-display:swap;}`;
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 category-appropriate system font to match the custom font's
164
- // line metrics (ascent/descent/lineGap), reducing layout shift while the custom font loads.
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
- let uploadBuffer = Buffer.from(cssString + fallbackCssString, 'utf-8');
169
- let doc = await client.assets.upload('file', uploadBuffer, { filename: fileName + '.css' });
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
  }