@liiift-studio/sanity-font-manager 2.7.0 → 2.7.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 (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +695 -437
  3. package/dist/index.js +1634 -17839
  4. package/dist/index.mjs +1551 -17745
  5. package/package.json +83 -83
  6. package/src/components/BatchUploadFonts.jsx +655 -655
  7. package/src/components/BulkActions.jsx +99 -99
  8. package/src/components/ExistingDocumentResolver.jsx +152 -152
  9. package/src/components/FontReviewCard.jsx +455 -455
  10. package/src/components/FontScriptUploaderComponent.jsx +463 -463
  11. package/src/components/GenerateCollectionsPairsComponent.jsx +259 -259
  12. package/src/components/KeyValueInput.jsx +95 -95
  13. package/src/components/KeyValueReferenceInput.jsx +267 -267
  14. package/src/components/NestedObjectArraySelector.jsx +146 -146
  15. package/src/components/PriceInput.jsx +26 -26
  16. package/src/components/PrimaryCollectionGeneratorTypeface.jsx +116 -116
  17. package/src/components/RegenerateSubfamiliesComponent.jsx +185 -185
  18. package/src/components/SetOTF.jsx +87 -87
  19. package/src/components/SingleUploaderTool.jsx +674 -674
  20. package/src/components/StatusDisplay.jsx +26 -26
  21. package/src/components/StyleCountInput.jsx +16 -16
  22. package/src/components/UpdateScriptsComponent.jsx +76 -76
  23. package/src/components/UploadButton.jsx +43 -43
  24. package/src/components/UploadModal.jsx +309 -309
  25. package/src/components/UploadScriptsComponent.jsx +539 -539
  26. package/src/components/UploadStep1Settings.jsx +272 -272
  27. package/src/components/UploadStep2Review.jsx +478 -478
  28. package/src/components/UploadStep3Execute.jsx +234 -234
  29. package/src/components/UploadStep3bInstances.jsx +396 -396
  30. package/src/components/UploadSummary.jsx +196 -196
  31. package/src/components/VariableInstanceReferencesInput.jsx +190 -190
  32. package/src/hooks/useNestedObjects.js +92 -92
  33. package/src/hooks/useSanityClient.js +9 -9
  34. package/src/index.js +120 -120
  35. package/src/schema/openTypeField.js +1995 -1995
  36. package/src/schema/styleCountField.js +12 -12
  37. package/src/schema/stylesField.js +302 -302
  38. package/src/schema/stylisticSetField.js +301 -301
  39. package/src/utils/buildUploadPlan.js +326 -326
  40. package/src/utils/executeUploadPlan.js +430 -430
  41. package/src/utils/executionReducer.js +56 -56
  42. package/src/utils/fontHelpers.js +281 -281
  43. package/src/utils/generateCssFile.js +207 -207
  44. package/src/utils/generateFontData.js +98 -98
  45. package/src/utils/generateFontFile.js +38 -38
  46. package/src/utils/generateKeywords.js +185 -185
  47. package/src/utils/generateSubset.js +45 -45
  48. package/src/utils/getEmptyFontKit.js +101 -101
  49. package/src/utils/parseFont.js +56 -56
  50. package/src/utils/parseVariableFontInstances.js +301 -301
  51. package/src/utils/planReducer.js +531 -531
  52. package/src/utils/planTypes.js +183 -183
  53. package/src/utils/processFontFiles.js +530 -530
  54. package/src/utils/regenerateFontData.js +146 -146
  55. package/src/utils/resolveExistingFont.js +87 -87
  56. package/src/utils/retitleFontEntries.js +154 -154
  57. package/src/utils/sanitizeForSanityId.js +65 -65
  58. package/src/utils/setupDecompressors.js +27 -27
  59. package/src/utils/updateFontPrices.js +94 -94
  60. package/src/utils/updateTypefaceDocument.js +162 -162
  61. package/src/utils/uploadFontFiles.js +405 -405
  62. package/src/utils/utils.js +24 -24
@@ -1,56 +1,56 @@
1
- // Execution reducer — manages ExecutionState separately from the plan to prevent progress ticks from re-rendering the review UI
2
-
3
- import { EXECUTION_STATUS } from './planTypes';
4
-
5
- /**
6
- * Creates the initial execution state.
7
- * @returns {object}
8
- */
9
- export function createInitialExecutionState() {
10
- return {
11
- status: 'idle',
12
- progress: {},
13
- error: null,
14
- };
15
- }
16
-
17
- /**
18
- * Execution reducer for useReducer. Manages per-font upload progress.
19
- * Mounted in UploadStep3Execute — isolated from the plan/review UI.
20
- *
21
- * @param {object} state - Current ExecutionState
22
- * @param {object} action - Dispatched action
23
- * @returns {object} New ExecutionState
24
- */
25
- export function executionReducer(state, action) {
26
- switch (action.type) {
27
- case 'SET_EXECUTION_STATUS': {
28
- return { ...state, status: action.status };
29
- }
30
-
31
- case 'SET_FONT_EXECUTION_PROGRESS': {
32
- const { tempId, progress } = action;
33
- return {
34
- ...state,
35
- progress: {
36
- ...state.progress,
37
- [tempId]: {
38
- ...(state.progress[tempId] || {}),
39
- ...progress,
40
- },
41
- },
42
- };
43
- }
44
-
45
- case 'SET_EXECUTION_ERROR': {
46
- return {
47
- ...state,
48
- status: 'error',
49
- error: action.error,
50
- };
51
- }
52
-
53
- default:
54
- return state;
55
- }
56
- }
1
+ // Execution reducer — manages ExecutionState separately from the plan to prevent progress ticks from re-rendering the review UI
2
+
3
+ import { EXECUTION_STATUS } from './planTypes';
4
+
5
+ /**
6
+ * Creates the initial execution state.
7
+ * @returns {object}
8
+ */
9
+ export function createInitialExecutionState() {
10
+ return {
11
+ status: 'idle',
12
+ progress: {},
13
+ error: null,
14
+ };
15
+ }
16
+
17
+ /**
18
+ * Execution reducer for useReducer. Manages per-font upload progress.
19
+ * Mounted in UploadStep3Execute — isolated from the plan/review UI.
20
+ *
21
+ * @param {object} state - Current ExecutionState
22
+ * @param {object} action - Dispatched action
23
+ * @returns {object} New ExecutionState
24
+ */
25
+ export function executionReducer(state, action) {
26
+ switch (action.type) {
27
+ case 'SET_EXECUTION_STATUS': {
28
+ return { ...state, status: action.status };
29
+ }
30
+
31
+ case 'SET_FONT_EXECUTION_PROGRESS': {
32
+ const { tempId, progress } = action;
33
+ return {
34
+ ...state,
35
+ progress: {
36
+ ...state.progress,
37
+ [tempId]: {
38
+ ...(state.progress[tempId] || {}),
39
+ ...progress,
40
+ },
41
+ },
42
+ };
43
+ }
44
+
45
+ case 'SET_EXECUTION_ERROR': {
46
+ return {
47
+ ...state,
48
+ status: 'error',
49
+ error: action.error,
50
+ };
51
+ }
52
+
53
+ default:
54
+ return state;
55
+ }
56
+ }
@@ -1,281 +1,281 @@
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
- const raw = cmap.getSupportedCharCodes(3, 1);
84
- if (!Array.isArray(raw) || raw.length === 0) return [];
85
- // getSupportedCharCodes may return range objects { start, end } — expand to individual codepoints
86
- if (typeof raw[0] === 'object' && raw[0].start !== undefined) {
87
- const codes = [];
88
- for (const range of raw) {
89
- for (let i = range.start; i <= range.end; i++) {
90
- codes.push(i);
91
- }
92
- }
93
- return codes;
94
- }
95
- return raw;
96
- } catch {
97
- return [];
98
- }
99
- }
100
-
101
- /**
102
- * Build a variation axis map from fvar table.
103
- * Filters out degenerate axes (min === max). Returns null if not a variable font.
104
- *
105
- * @param {object} font - lib-font Font instance
106
- * @returns {{ [tag: string]: { min: number, max: number, default: number, name: string } } | null}
107
- */
108
- export function getVariationAxes(font) {
109
- const fvar = font.opentype?.tables?.fvar;
110
- if (!fvar?.axes) return null;
111
-
112
- const axes = {};
113
- for (const axis of fvar.axes) {
114
- if (axis.minValue === axis.maxValue) continue;
115
- axes[axis.tag] = {
116
- min: axis.minValue,
117
- max: axis.maxValue,
118
- default: axis.defaultValue,
119
- name: getNameString(font, axis.axisNameID) || axis.tag,
120
- };
121
- }
122
- return Object.keys(axes).length > 0 ? axes : null;
123
- }
124
-
125
- /**
126
- * Get named instances from fvar table.
127
- * Resolves subfamilyNameID and postScriptNameID via the name table.
128
- *
129
- * @param {object} font - lib-font Font instance
130
- * @returns {Array<{ name: string, coordinates: number[], postScriptName: string }>}
131
- */
132
- export function getNamedInstances(font) {
133
- const fvar = font.opentype?.tables?.fvar;
134
- if (!fvar?.instances) return [];
135
- return fvar.instances.map(inst => ({
136
- name: getNameString(font, inst.subfamilyNameID),
137
- coordinates: inst.coordinates,
138
- postScriptName: getNameString(font, inst.postScriptNameID || 0),
139
- }));
140
- }
141
-
142
- /**
143
- * Build font metrics object matching the Sanity document shape.
144
- * Uses OS/2 typo metrics when USE_TYPO_METRICS bit is set, otherwise hhea.
145
- *
146
- * @param {object} font - lib-font Font instance
147
- * @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 } }}
148
- */
149
- export function getFontMetrics(font) {
150
- const tables = font.opentype?.tables;
151
- const os2 = tables?.['OS/2'];
152
- const head = tables?.head;
153
- const post = tables?.post;
154
- const hhea = tables?.hhea;
155
-
156
- // USE_TYPO_METRICS flag (fsSelection bit 7) — when set, use OS/2 typo metrics
157
- const useTypo = os2 ? (os2.fsSelection & 0x80) !== 0 : false;
158
-
159
- return {
160
- unitsPerEm: head?.unitsPerEm || 1000,
161
- ascender: useTypo ? (os2?.sTypoAscender || 0) : (hhea?.ascender ?? os2?.sTypoAscender ?? 0),
162
- descender: useTypo ? (os2?.sTypoDescender || 0) : (hhea?.descender ?? os2?.sTypoDescender ?? 0),
163
- lineGap: useTypo ? (os2?.sTypoLineGap || 0) : (hhea?.lineGap ?? os2?.sTypoLineGap ?? 0),
164
- underlinePosition: post?.underlinePosition || 0,
165
- underlineThickness: post?.underlineThickness || 0,
166
- italicAngle: post?.italicAngle || 0,
167
- capHeight: (os2?.version >= 2) ? (os2?.sCapHeight || 0) : 0,
168
- xHeight: (os2?.version >= 2) ? (os2?.sxHeight || 0) : 0,
169
- boundingBox: {
170
- xMin: head?.xMin || 0,
171
- yMin: head?.yMin || 0,
172
- xMax: head?.xMax || 0,
173
- yMax: head?.yMax || 0,
174
- },
175
- };
176
- }
177
-
178
- /**
179
- * Build font metadata object matching the Sanity document shape.
180
- *
181
- * @param {object} font - lib-font Font instance
182
- * @returns {{ postscriptName: string, fullName: string, familyName: string, subfamilyName: string, copyright: string, version: string, genDate: string }}
183
- */
184
- export function getFontMetadata(font) {
185
- return {
186
- postscriptName: getNameString(font, 6),
187
- fullName: getNameString(font, 4),
188
- familyName: getNameString(font, 1),
189
- subfamilyName: getNameString(font, 2),
190
- preferredFamily: getNameString(font, 16),
191
- preferredSubfamily: getNameString(font, 17),
192
- copyright: getNameString(font, 0),
193
- version: getNameString(font, 5),
194
- genDate: new Date().toISOString(),
195
- };
196
- }
197
-
198
- /**
199
- * Get the OS/2 usWeightClass value.
200
- *
201
- * @param {object} font - lib-font Font instance
202
- * @returns {number|null} Weight class (1-1000) or null if OS/2 table is missing
203
- */
204
- export function getWeightClass(font) {
205
- return font.opentype?.tables?.['OS/2']?.usWeightClass || null;
206
- }
207
-
208
- /**
209
- * Get the OS/2 fsSelection flags as a raw uint16.
210
- *
211
- * @param {object} font - lib-font Font instance
212
- * @returns {number} fsSelection bitmask (0 if OS/2 table is missing)
213
- */
214
- export function getFsSelection(font) {
215
- return font.opentype?.tables?.['OS/2']?.fsSelection || 0;
216
- }
217
-
218
- /**
219
- * Get the head macStyle flags as a uint16 bitmask.
220
- * lib-font returns macStyle as a bit array (big-endian order: index 15 = bit 0).
221
- * This helper converts it back to a standard uint16 for bitwise testing.
222
- *
223
- * @param {object} font - lib-font Font instance
224
- * @returns {number} macStyle bitmask (0 if head table is missing)
225
- */
226
- export function getMacStyle(font) {
227
- const macStyle = font.opentype?.tables?.head?.macStyle;
228
- if (!macStyle) return 0;
229
- // lib-font returns a bit array or a number depending on version
230
- if (typeof macStyle === 'number') return macStyle;
231
- // Convert bit array (big-endian) to uint16: index 15 = bit 0, index 14 = bit 1, etc.
232
- if (typeof macStyle === 'object') {
233
- let value = 0;
234
- for (let i = 0; i < 16; i++) {
235
- if (macStyle[i]) value |= (1 << (15 - i));
236
- }
237
- return value;
238
- }
239
- return 0;
240
- }
241
-
242
- /**
243
- * Get the post table italic angle.
244
- *
245
- * @param {object} font - lib-font Font instance
246
- * @returns {number} Italic angle in degrees (0 for upright fonts)
247
- */
248
- export function getItalicAngle(font) {
249
- return font.opentype?.tables?.post?.italicAngle || 0;
250
- }
251
-
252
- /**
253
- * Get glyph count from maxp table.
254
- *
255
- * @param {object} font - lib-font Font instance
256
- * @returns {number} Number of glyphs
257
- */
258
- export function getGlyphCount(font) {
259
- return font.opentype?.tables?.maxp?.numGlyphs || 0;
260
- }
261
-
262
- /**
263
- * Get the OS/2 sFamilyClass value for font category detection.
264
- *
265
- * @param {object} font - lib-font Font instance
266
- * @returns {number} sFamilyClass value (0 if missing)
267
- */
268
- export function getFamilyClass(font) {
269
- return font.opentype?.tables?.['OS/2']?.sFamilyClass || 0;
270
- }
271
-
272
- /**
273
- * Escape a font name for safe interpolation into CSS font-family declarations.
274
- * Prevents CSS injection via crafted name table strings.
275
- *
276
- * @param {string} name - Raw font name from the name table
277
- * @returns {string} Escaped name safe for CSS string context
278
- */
279
- export function escapeCssFontName(name) {
280
- return name.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/;/g, '');
281
- }
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
+ const raw = cmap.getSupportedCharCodes(3, 1);
84
+ if (!Array.isArray(raw) || raw.length === 0) return [];
85
+ // getSupportedCharCodes may return range objects { start, end } — expand to individual codepoints
86
+ if (typeof raw[0] === 'object' && raw[0].start !== undefined) {
87
+ const codes = [];
88
+ for (const range of raw) {
89
+ for (let i = range.start; i <= range.end; i++) {
90
+ codes.push(i);
91
+ }
92
+ }
93
+ return codes;
94
+ }
95
+ return raw;
96
+ } catch {
97
+ return [];
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Build a variation axis map from fvar table.
103
+ * Filters out degenerate axes (min === max). Returns null if not a variable font.
104
+ *
105
+ * @param {object} font - lib-font Font instance
106
+ * @returns {{ [tag: string]: { min: number, max: number, default: number, name: string } } | null}
107
+ */
108
+ export function getVariationAxes(font) {
109
+ const fvar = font.opentype?.tables?.fvar;
110
+ if (!fvar?.axes) return null;
111
+
112
+ const axes = {};
113
+ for (const axis of fvar.axes) {
114
+ if (axis.minValue === axis.maxValue) continue;
115
+ axes[axis.tag] = {
116
+ min: axis.minValue,
117
+ max: axis.maxValue,
118
+ default: axis.defaultValue,
119
+ name: getNameString(font, axis.axisNameID) || axis.tag,
120
+ };
121
+ }
122
+ return Object.keys(axes).length > 0 ? axes : null;
123
+ }
124
+
125
+ /**
126
+ * Get named instances from fvar table.
127
+ * Resolves subfamilyNameID and postScriptNameID via the name table.
128
+ *
129
+ * @param {object} font - lib-font Font instance
130
+ * @returns {Array<{ name: string, coordinates: number[], postScriptName: string }>}
131
+ */
132
+ export function getNamedInstances(font) {
133
+ const fvar = font.opentype?.tables?.fvar;
134
+ if (!fvar?.instances) return [];
135
+ return fvar.instances.map(inst => ({
136
+ name: getNameString(font, inst.subfamilyNameID),
137
+ coordinates: inst.coordinates,
138
+ postScriptName: getNameString(font, inst.postScriptNameID || 0),
139
+ }));
140
+ }
141
+
142
+ /**
143
+ * Build font metrics object matching the Sanity document shape.
144
+ * Uses OS/2 typo metrics when USE_TYPO_METRICS bit is set, otherwise hhea.
145
+ *
146
+ * @param {object} font - lib-font Font instance
147
+ * @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 } }}
148
+ */
149
+ export function getFontMetrics(font) {
150
+ const tables = font.opentype?.tables;
151
+ const os2 = tables?.['OS/2'];
152
+ const head = tables?.head;
153
+ const post = tables?.post;
154
+ const hhea = tables?.hhea;
155
+
156
+ // USE_TYPO_METRICS flag (fsSelection bit 7) — when set, use OS/2 typo metrics
157
+ const useTypo = os2 ? (os2.fsSelection & 0x80) !== 0 : false;
158
+
159
+ return {
160
+ unitsPerEm: head?.unitsPerEm || 1000,
161
+ ascender: useTypo ? (os2?.sTypoAscender || 0) : (hhea?.ascender ?? os2?.sTypoAscender ?? 0),
162
+ descender: useTypo ? (os2?.sTypoDescender || 0) : (hhea?.descender ?? os2?.sTypoDescender ?? 0),
163
+ lineGap: useTypo ? (os2?.sTypoLineGap || 0) : (hhea?.lineGap ?? os2?.sTypoLineGap ?? 0),
164
+ underlinePosition: post?.underlinePosition || 0,
165
+ underlineThickness: post?.underlineThickness || 0,
166
+ italicAngle: post?.italicAngle || 0,
167
+ capHeight: (os2?.version >= 2) ? (os2?.sCapHeight || 0) : 0,
168
+ xHeight: (os2?.version >= 2) ? (os2?.sxHeight || 0) : 0,
169
+ boundingBox: {
170
+ xMin: head?.xMin || 0,
171
+ yMin: head?.yMin || 0,
172
+ xMax: head?.xMax || 0,
173
+ yMax: head?.yMax || 0,
174
+ },
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Build font metadata object matching the Sanity document shape.
180
+ *
181
+ * @param {object} font - lib-font Font instance
182
+ * @returns {{ postscriptName: string, fullName: string, familyName: string, subfamilyName: string, copyright: string, version: string, genDate: string }}
183
+ */
184
+ export function getFontMetadata(font) {
185
+ return {
186
+ postscriptName: getNameString(font, 6),
187
+ fullName: getNameString(font, 4),
188
+ familyName: getNameString(font, 1),
189
+ subfamilyName: getNameString(font, 2),
190
+ preferredFamily: getNameString(font, 16),
191
+ preferredSubfamily: getNameString(font, 17),
192
+ copyright: getNameString(font, 0),
193
+ version: getNameString(font, 5),
194
+ genDate: new Date().toISOString(),
195
+ };
196
+ }
197
+
198
+ /**
199
+ * Get the OS/2 usWeightClass value.
200
+ *
201
+ * @param {object} font - lib-font Font instance
202
+ * @returns {number|null} Weight class (1-1000) or null if OS/2 table is missing
203
+ */
204
+ export function getWeightClass(font) {
205
+ return font.opentype?.tables?.['OS/2']?.usWeightClass || null;
206
+ }
207
+
208
+ /**
209
+ * Get the OS/2 fsSelection flags as a raw uint16.
210
+ *
211
+ * @param {object} font - lib-font Font instance
212
+ * @returns {number} fsSelection bitmask (0 if OS/2 table is missing)
213
+ */
214
+ export function getFsSelection(font) {
215
+ return font.opentype?.tables?.['OS/2']?.fsSelection || 0;
216
+ }
217
+
218
+ /**
219
+ * Get the head macStyle flags as a uint16 bitmask.
220
+ * lib-font returns macStyle as a bit array (big-endian order: index 15 = bit 0).
221
+ * This helper converts it back to a standard uint16 for bitwise testing.
222
+ *
223
+ * @param {object} font - lib-font Font instance
224
+ * @returns {number} macStyle bitmask (0 if head table is missing)
225
+ */
226
+ export function getMacStyle(font) {
227
+ const macStyle = font.opentype?.tables?.head?.macStyle;
228
+ if (!macStyle) return 0;
229
+ // lib-font returns a bit array or a number depending on version
230
+ if (typeof macStyle === 'number') return macStyle;
231
+ // Convert bit array (big-endian) to uint16: index 15 = bit 0, index 14 = bit 1, etc.
232
+ if (typeof macStyle === 'object') {
233
+ let value = 0;
234
+ for (let i = 0; i < 16; i++) {
235
+ if (macStyle[i]) value |= (1 << (15 - i));
236
+ }
237
+ return value;
238
+ }
239
+ return 0;
240
+ }
241
+
242
+ /**
243
+ * Get the post table italic angle.
244
+ *
245
+ * @param {object} font - lib-font Font instance
246
+ * @returns {number} Italic angle in degrees (0 for upright fonts)
247
+ */
248
+ export function getItalicAngle(font) {
249
+ return font.opentype?.tables?.post?.italicAngle || 0;
250
+ }
251
+
252
+ /**
253
+ * Get glyph count from maxp table.
254
+ *
255
+ * @param {object} font - lib-font Font instance
256
+ * @returns {number} Number of glyphs
257
+ */
258
+ export function getGlyphCount(font) {
259
+ return font.opentype?.tables?.maxp?.numGlyphs || 0;
260
+ }
261
+
262
+ /**
263
+ * Get the OS/2 sFamilyClass value for font category detection.
264
+ *
265
+ * @param {object} font - lib-font Font instance
266
+ * @returns {number} sFamilyClass value (0 if missing)
267
+ */
268
+ export function getFamilyClass(font) {
269
+ return font.opentype?.tables?.['OS/2']?.sFamilyClass || 0;
270
+ }
271
+
272
+ /**
273
+ * Escape a font name for safe interpolation into CSS font-family declarations.
274
+ * Prevents CSS injection via crafted name table strings.
275
+ *
276
+ * @param {string} name - Raw font name from the name table
277
+ * @returns {string} Escaped name safe for CSS string context
278
+ */
279
+ export function escapeCssFontName(name) {
280
+ return name.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/;/g, '');
281
+ }