@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.
Files changed (33) hide show
  1. package/dist/UploadModal-6LIX7XOK.js +6 -0
  2. package/dist/UploadModal-NME2W53V.mjs +6 -0
  3. package/dist/chunk-646WCBRR.mjs +7276 -0
  4. package/dist/chunk-FH4QKHOH.js +7276 -0
  5. package/dist/index.js +664 -1647
  6. package/dist/index.mjs +317 -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 +415 -0
  12. package/src/components/SingleUploaderTool.jsx +3 -4
  13. package/src/components/UploadModal.jsx +268 -0
  14. package/src/components/UploadScriptsComponent.jsx +23 -21
  15. package/src/components/UploadStep1Settings.jsx +272 -0
  16. package/src/components/UploadStep2Review.jsx +472 -0
  17. package/src/components/UploadStep3Execute.jsx +234 -0
  18. package/src/components/UploadSummary.jsx +196 -0
  19. package/src/index.js +45 -0
  20. package/src/utils/buildUploadPlan.js +325 -0
  21. package/src/utils/executeUploadPlan.js +437 -0
  22. package/src/utils/executionReducer.js +56 -0
  23. package/src/utils/fontHelpers.js +267 -0
  24. package/src/utils/generateCssFile.js +79 -77
  25. package/src/utils/generateFontData.js +47 -94
  26. package/src/utils/getEmptyFontKit.js +19 -17
  27. package/src/utils/parseFont.js +55 -0
  28. package/src/utils/planReducer.js +517 -0
  29. package/src/utils/planTypes.js +183 -0
  30. package/src/utils/processFontFiles.js +120 -78
  31. package/src/utils/regenerateFontData.js +2 -2
  32. package/src/utils/resolveExistingFont.js +87 -0
  33. package/src/utils/uploadFontFiles.js +405 -405
@@ -1,19 +1,20 @@
1
- // Reads font files via FileReader, parses with fontkit, and builds the fontsObjects map — exports individual weight/style extraction helpers
1
+ // Reads font files via FileReader, parses with lib-font, and builds the fontsObjects map — exports individual weight/style extraction helpers
2
2
 
3
- import * as fontkit from 'fontkit';
3
+ import { parseFont } from './parseFont';
4
+ import { getNameString, getVariationAxes, getItalicAngle, getWeightClass } from './fontHelpers';
4
5
  import { nanoid } from 'nanoid';
5
6
  import { expandAbbreviations, removeWeightNames, reverseSpellingLookup } from './generateKeywords';
6
7
  import { sanitizeForSanityId } from './sanitizeForSanityId';
7
8
 
8
9
  /**
9
- * Reads a font file and returns its content as a Uint8Array.
10
+ * Reads a font file and returns its content as an ArrayBuffer.
10
11
  * @param {File} file
11
- * @returns {Promise<Uint8Array>}
12
+ * @returns {Promise<ArrayBuffer>}
12
13
  */
13
14
  export const readFontFile = (file) => {
14
15
  return new Promise((resolve, reject) => {
15
16
  const reader = new FileReader();
16
- reader.onload = (event) => { resolve(new Uint8Array(event.target.result)); };
17
+ reader.onload = (event) => { resolve(event.target.result); };
17
18
  reader.onerror = (error) => { reject(error); };
18
19
  reader.readAsArrayBuffer(file);
19
20
  });
@@ -47,13 +48,12 @@ export const processFontFiles = async (
47
48
  for (let i = 0; i < files.length; i++) {
48
49
  const file = files[i];
49
50
  const fontBuffer = await readFontFile(file);
50
- const font = fontkit.create(fontBuffer);
51
+ const font = await parseFont(fontBuffer, file.name);
51
52
 
52
- console.log('File name: ', file.name);
53
+ console.log('File name:', file.name);
53
54
 
54
- if (file.name.endsWith('.woff2') || file.name.endsWith('.woff')) {
55
- await handleWebfontMetadata(file, font, files);
56
- }
55
+ // For webfonts with missing metadata, try to extract from TTF companion
56
+ const ttfFallbackMeta = await getWebfontFallbackMetadata(file, font, files);
57
57
 
58
58
  let { weightName, subfamilyName, fontTitle, style, italicKW, variableFont } = extractFontMetadata(
59
59
  font,
@@ -61,6 +61,7 @@ export const processFontFiles = async (
61
61
  weightKeywordList,
62
62
  italicKeywordList,
63
63
  preserveShortenedNames,
64
+ ttfFallbackMeta,
64
65
  );
65
66
 
66
67
  let id;
@@ -109,72 +110,106 @@ export const processFontFiles = async (
109
110
 
110
111
  console.log('Subfamilies:', subfamilies);
111
112
  console.log('Unique subfamilies:', uniqueSubfamilies, uniqueSubfamilies.length);
112
- console.log('Font objects:', fontsObjects);
113
+ console.log('Font objects:', Object.keys(fontsObjects));
113
114
 
114
115
  return { fontsObjects, subfamilies, uniqueSubfamilies, newPreferredStyle, failedFiles };
115
116
  };
116
117
 
117
118
  /**
118
- * Patches webfont name records from a matching TTF when woff/woff2 metadata is missing.
119
+ * Gets fallback metadata from a matching TTF when woff/woff2 metadata is missing.
120
+ * Returns null if no fallback is needed or no TTF companion exists.
121
+ * Unlike the old fontkit approach, this does NOT mutate the font object.
119
122
  * @param {File} file
120
- * @param {Object} font
123
+ * @param {object} font - lib-font parsed font
121
124
  * @param {File[]} files
125
+ * @returns {Promise<{ fullName: string, familyName: string, subfamilyName: string, preferredSubfamily: string }|null>}
122
126
  */
123
- const handleWebfontMetadata = async (file, font, files) => {
124
- if (
125
- !font?.name?.records?.fullName ||
126
- font?.name?.records?.fullName === '' ||
127
- !/^[A-Z0-9]+$/.test(font?.name?.records?.fullName)
128
- ) {
129
- const ttfFile = files.find(f => f.name === file.name.replace('.woff2', '.ttf').replace('.woff', '.ttf'));
130
- if (ttfFile) {
131
- const ttfFileBuffer = await readFontFile(ttfFile);
132
- const ttfFileData = fontkit.create(ttfFileBuffer);
133
- if (ttfFileData) font.name.records = ttfFileData?.name?.records;
134
- }
127
+ const getWebfontFallbackMetadata = async (file, font, files) => {
128
+ if (!file.name.endsWith('.woff2') && !file.name.endsWith('.woff')) return null;
129
+
130
+ const fullName = getNameString(font, 4);
131
+ // Check if name table is missing or corrupt (empty, or only uppercase hex-like garbage)
132
+ if (fullName && fullName !== '' && !/^[A-Z0-9]+$/.test(fullName)) return null;
133
+
134
+ const ttfFile = files.find(f => f.name === file.name.replace('.woff2', '.ttf').replace('.woff', '.ttf'));
135
+ if (!ttfFile) return null;
136
+
137
+ try {
138
+ const ttfBuffer = await readFontFile(ttfFile);
139
+ const ttfFont = await parseFont(ttfBuffer, ttfFile.name);
140
+ return {
141
+ fullName: getNameString(ttfFont, 4),
142
+ familyName: getNameString(ttfFont, 1),
143
+ subfamilyName: getNameString(ttfFont, 2),
144
+ preferredSubfamily: getNameString(ttfFont, 17),
145
+ preferredFamily: getNameString(ttfFont, 16),
146
+ };
147
+ } catch (err) {
148
+ console.warn('Could not parse TTF companion for webfont fallback:', err.message);
149
+ return null;
135
150
  }
136
151
  };
137
152
 
138
153
  /**
139
- * Extracts and normalises metadata from a fontkit font object.
140
- * @param {Object} font
141
- * @param {string} title
154
+ * Extracts and normalises metadata from a lib-font parsed font object.
155
+ * @param {object} font - lib-font parsed font
156
+ * @param {string} title - Typeface title
142
157
  * @param {string[]} weightKeywordList
143
158
  * @param {string[]} italicKeywordList
144
159
  * @param {boolean} preserveShortenedNames
160
+ * @param {object|null} ttfFallbackMeta - Fallback metadata from TTF companion (for webfonts with missing names)
145
161
  * @returns {Object}
146
162
  */
147
- export const extractFontMetadata = (font, title, weightKeywordList, italicKeywordList, preserveShortenedNames = false) => {
148
- let weightName = extractWeightName(font, italicKeywordList);
163
+ export const extractFontMetadata = (font, title, weightKeywordList, italicKeywordList, preserveShortenedNames = false, ttfFallbackMeta = null) => {
164
+ let weightName = extractWeightName(font, italicKeywordList, ttfFallbackMeta);
149
165
  if (!preserveShortenedNames) {
150
166
  weightName = expandAbbreviations(weightName);
151
167
  }
152
168
 
153
- if ((weightName === '' || weightName.toLowerCase() === 'roman') && font?.name?.records?.fullName) {
154
- weightName = extractWeightFromFullName(font, title);
169
+ const fullName = getNameString(font, 4) || ttfFallbackMeta?.fullName || '';
170
+
171
+ if ((weightName === '' || weightName.toLowerCase() === 'roman') && fullName) {
172
+ weightName = extractWeightFromFullName(font, title, ttfFallbackMeta);
155
173
  if (!preserveShortenedNames) {
156
174
  weightName = expandAbbreviations(weightName);
157
175
  }
158
176
  }
159
177
 
160
- const variableFont = font?.variationAxes && Object.keys(font.variationAxes).length > 0;
178
+ const axes = getVariationAxes(font);
179
+ const variableFont = axes !== null;
180
+
181
+ // Subfamily detection — extract width/optical variant from name table.
182
+ // Primary: nameId4 (fullName) minus typeface title — the most complete name record,
183
+ // always contains width + weight (e.g. "Gear XXNarrow Regular" → "XXNarrow Regular").
184
+ // Fallback: nameId1 (familyName) minus typeface title — contains width but not always weight.
185
+ // processSubfamilyName then strips weight/italic keywords, leaving just the width variant.
186
+ // This matches the production logic that has been reliable across all foundry sites.
187
+ const trimmedTitle = title.trim();
161
188
 
162
- let subfamilyName = font?.name?.records?.fullName?.en?.replace(title.trim(), '').trim() ||
163
- font.subfamilyName.trim().replace(title.trim(), '').trim();
189
+ const nameId4Remainder = fullName ? fullName.replace(trimmedTitle, '').trim() : '';
190
+ const nameId1 = getNameString(font, 1) || ttfFallbackMeta?.familyName || '';
191
+ const nameId1Remainder = nameId1 ? nameId1.replace(trimmedTitle, '').trim() : '';
192
+
193
+ let subfamilyName = nameId4Remainder || nameId1Remainder;
164
194
 
165
195
  if (!preserveShortenedNames) {
166
196
  subfamilyName = expandAbbreviations(subfamilyName);
167
197
  }
168
198
 
169
- let fontTitle = font?.fullName.trim();
170
- let style = (font?.italicAngle !== 0 || font?.fullName.toLowerCase().includes('italic')) ? 'Italic' : 'Regular';
199
+ let fontTitle = fullName.trim() || '';
200
+ const italicAngle = getItalicAngle(font);
201
+ let style = (italicAngle !== 0 || fullName.toLowerCase().includes('italic')) ? 'Italic' : 'Regular';
171
202
 
172
203
  const italicKW = processItalicKeywords(font, fontTitle, italicKeywordList);
173
204
 
174
205
  subfamilyName = processSubfamilyName(subfamilyName, weightKeywordList, italicKW, preserveShortenedNames);
175
206
  fontTitle = formatFontTitle(fontTitle, preserveShortenedNames);
176
207
 
177
- subfamilyName = subfamilyName === '' ? 'Regular' : subfamilyName.replace(/\s+/g, ' ').trim();
208
+ // Style-only names are not subfamilies strip them
209
+ subfamilyName = subfamilyName
210
+ .replace(/\b(Italic|Slant|Slanted|Oblique|Backslant|Roman|Upright)\b/gi, '')
211
+ .replace(/\s+/g, ' ')
212
+ .trim();
178
213
 
179
214
  if (subfamilyName !== '') {
180
215
  weightName = weightName
@@ -187,6 +222,7 @@ export const extractFontMetadata = (font, title, weightKeywordList, italicKeywor
187
222
  if (!fontTitle.toLowerCase().includes('vf')) {
188
223
  fontTitle = fontTitle + ' VF';
189
224
  }
225
+ // Variable fonts are not placed in subfamilies — they go in the separate variableFont array
190
226
  subfamilyName = '';
191
227
  }
192
228
 
@@ -200,20 +236,18 @@ export const extractFontMetadata = (font, title, weightKeywordList, italicKeywor
200
236
  /**
201
237
  * Extracts the weight name from a font's preferred subfamily or subfamily record.
202
238
  * Returns "Variable" for variable fonts.
203
- * @param {Object} font
239
+ * @param {object} font - lib-font parsed font
204
240
  * @param {string[]} italicKW
241
+ * @param {object|null} ttfFallbackMeta
205
242
  * @returns {string}
206
243
  */
207
- export const extractWeightName = (font, italicKW) => {
208
- let weightName = font?.name?.records?.preferredSubfamily || font?.name?.records?.fontSubfamily;
209
-
210
- if (typeof weightName === 'object') {
211
- weightName = weightName?.en ||
212
- (weightName.constructor === Object ? weightName[Object.keys(weightName)[0]] : null);
213
- }
244
+ export const extractWeightName = (font, italicKW, ttfFallbackMeta = null) => {
245
+ let weightName = getNameString(font, 17) || getNameString(font, 2) ||
246
+ ttfFallbackMeta?.preferredSubfamily || ttfFallbackMeta?.subfamilyName || '';
214
247
 
215
- if (font?.variationAxes && Object.keys(font.variationAxes).length > 0) {
216
- return 'Variable';
248
+ const axes = getVariationAxes(font);
249
+ if (axes !== null) {
250
+ return '';
217
251
  }
218
252
 
219
253
  if (italicKW) {
@@ -236,17 +270,15 @@ export const extractWeightName = (font, italicKW) => {
236
270
 
237
271
  /**
238
272
  * Extracts a weight name from the font's full name record when subfamily is empty or "Roman".
239
- * @param {Object} font
273
+ * @param {object} font - lib-font parsed font
240
274
  * @param {string} title
275
+ * @param {object|null} ttfFallbackMeta
241
276
  * @returns {string}
242
277
  */
243
- export const extractWeightFromFullName = (font, title) => {
244
- let weightName = font?.name?.records?.fullName;
245
- weightName = weightName?.en
246
- ? weightName.en
247
- : (weightName?.constructor === Object ? weightName[Object.keys(weightName)[0]] : weightName);
248
- weightName = weightName?.replace(title + ' ', '').replace(title, '').trim();
249
- weightName = weightName?.replace('Italic', '').replace('It', '').replace('Slanted', '').replace('Slant', '').trim();
278
+ export const extractWeightFromFullName = (font, title, ttfFallbackMeta = null) => {
279
+ let weightName = getNameString(font, 4) || ttfFallbackMeta?.fullName || '';
280
+ weightName = weightName.replace(title + ' ', '').replace(title, '').trim();
281
+ weightName = weightName.replace('Italic', '').replace('It', '').replace('Slanted', '').replace('Slant', '').trim();
250
282
  return weightName;
251
283
  };
252
284
 
@@ -282,13 +314,14 @@ export const processSubfamilyName = (subfamilyName, weightKeywordList, italicKey
282
314
 
283
315
  /**
284
316
  * Collects italic keywords present in a font's full name.
285
- * @param {Object} font
317
+ * @param {object} font - lib-font parsed font
286
318
  * @param {string} fontTitle
287
319
  * @param {string[]} italicKeywordList
288
320
  * @returns {string[]}
289
321
  */
290
322
  export const processItalicKeywords = (font, fontTitle, italicKeywordList) => {
291
323
  let italicKW = [];
324
+ const fullName = getNameString(font, 4);
292
325
 
293
326
  italicKeywordList.forEach(keyword => {
294
327
  const kw = keyword.trim();
@@ -297,7 +330,7 @@ export const processItalicKeywords = (font, fontTitle, italicKeywordList) => {
297
330
  fontTitle = fontTitle.replace(kwRegex, '').trim();
298
331
  italicKW.push(kw);
299
332
  }
300
- if (font?.fullName && typeof font.fullName === 'string' && font.fullName.toLowerCase().includes(kw.toLowerCase())) {
333
+ if (fullName && fullName.toLowerCase().includes(kw.toLowerCase())) {
301
334
  if (!italicKW.includes(kw)) italicKW.push(kw);
302
335
  }
303
336
  });
@@ -327,7 +360,7 @@ export const formatFontTitle = (fontTitle, preserveShortenedNames = false) => {
327
360
 
328
361
  /**
329
362
  * Appends any italic keywords to the font title that aren't already present.
330
- * @param {Object} font
363
+ * @param {object} font - lib-font parsed font
331
364
  * @param {string} fontTitle
332
365
  * @param {string[]} italicKW
333
366
  * @param {string} style
@@ -335,8 +368,9 @@ export const formatFontTitle = (fontTitle, preserveShortenedNames = false) => {
335
368
  * @returns {string}
336
369
  */
337
370
  export const addItalicToFontTitle = (font, fontTitle, italicKW, style, preserveShortenedNames = false) => {
338
- const hasItalicAngle = font?.italicAngle !== 0;
339
- const hasItalicInName = font?.fullName.toLowerCase().includes('italic');
371
+ const hasItalicAngle = getItalicAngle(font) !== 0;
372
+ const fullName = getNameString(font, 4);
373
+ const hasItalicInName = fullName.toLowerCase().includes('italic');
340
374
 
341
375
  if (italicKW.length > 0 || hasItalicAngle || hasItalicInName) {
342
376
  italicKW = [...new Set(italicKW)];
@@ -379,7 +413,7 @@ export const addItalicToFontTitle = (font, fontTitle, italicKW, style, preserveS
379
413
  * @param {string} id
380
414
  * @param {string} fontTitle
381
415
  * @param {string} title
382
- * @param {Object} font
416
+ * @param {object} font - lib-font parsed font
383
417
  * @param {boolean} variableFont
384
418
  * @param {string} weightName
385
419
  * @param {string} subfamilyName
@@ -388,13 +422,16 @@ export const addItalicToFontTitle = (font, fontTitle, italicKW, style, preserveS
388
422
  * @returns {Object}
389
423
  */
390
424
  export const createFontObject = (id, fontTitle, title, font, variableFont, weightName, subfamilyName, file, originalFilename = null) => {
425
+ const italicAngle = getItalicAngle(font);
426
+ const fullName = getNameString(font, 4);
427
+
391
428
  const fontObject = {
392
429
  _key: nanoid(),
393
430
  _id: id,
394
431
  title: fontTitle,
395
432
  slug: { _type: 'slug', current: id },
396
433
  typefaceName: title,
397
- style: (font?.italicAngle !== 0 || font?.fullName.toLowerCase().includes('italic')) ? 'Italic' : 'Regular',
434
+ style: (italicAngle !== 0 || fullName.toLowerCase().includes('italic')) ? 'Italic' : 'Regular',
398
435
  variableFont: variableFont,
399
436
  weightName: weightName,
400
437
  subfamily: subfamilyName,
@@ -414,13 +451,14 @@ export const createFontObject = (id, fontTitle, title, font, variableFont, weigh
414
451
 
415
452
  /**
416
453
  * Determines a numeric CSS weight value for a font.
417
- * @param {Object} font
454
+ * @param {object} font - lib-font parsed font
418
455
  * @param {string} weightName
419
456
  * @returns {number}
420
457
  */
421
458
  export const determineWeight = (font, weightName) => {
422
- if (font['OS/2']?.usWeightClass) {
423
- return Number(font['OS/2'].usWeightClass);
459
+ const usWeightClass = getWeightClass(font);
460
+ if (usWeightClass) {
461
+ return Number(usWeightClass);
424
462
  }
425
463
 
426
464
  const wn = weightName?.toLowerCase() || '';
@@ -462,7 +500,7 @@ export const sortFontObjects = (fontsObjects) => {
462
500
  * Logs font metadata to the console for debugging.
463
501
  * @param {string} id
464
502
  * @param {string} fontTitle
465
- * @param {Object} font
503
+ * @param {object} font - lib-font parsed font
466
504
  * @param {string} fileName
467
505
  * @param {string} subfamilyName
468
506
  * @param {string} style
@@ -471,17 +509,21 @@ export const sortFontObjects = (fontsObjects) => {
471
509
  * @param {string[]} italicKW
472
510
  */
473
511
  export const logFontInfo = (id, fontTitle, font, fileName, subfamilyName, style, weightName, variableFont, italicKW) => {
512
+ const fullName = getNameString(font, 4);
513
+ const familyName = getNameString(font, 1);
514
+ const italicAngle = getItalicAngle(font);
515
+
474
516
  console.log('=== Font Info ====');
475
- console.log('Font id: ', id);
476
- console.log('Font title: ', fontTitle);
477
- console.log('Fontkit fullName: ', font.fullName);
478
- console.log('Fontkit family name: ', font.familyName);
479
- console.log('File name: ', fileName);
480
- console.log('Subfamily: ', subfamilyName);
481
- console.log('Style: ', style);
482
- console.log('Weight: ', weightName);
483
- console.log('Variable: ', variableFont);
484
- console.log('italicKW: ', italicKW);
485
- console.log('Font italic angle: ', (font?.italicAngle !== 0 || font?.fullName.toLowerCase().includes('italic')) ? 'Italic' : 'Regular');
517
+ console.log('Font id:', id);
518
+ console.log('Font title:', fontTitle);
519
+ console.log('Full name:', fullName);
520
+ console.log('Family name:', familyName);
521
+ console.log('File name:', fileName);
522
+ console.log('Subfamily:', subfamilyName);
523
+ console.log('Style:', style);
524
+ console.log('Weight:', weightName);
525
+ console.log('Variable:', variableFont);
526
+ console.log('ItalicKW:', italicKW);
527
+ console.log('Italic detection:', (italicAngle !== 0 || fullName.toLowerCase().includes('italic')) ? 'Italic' : 'Regular');
486
528
  console.log('=======');
487
529
  };
@@ -1,6 +1,6 @@
1
1
  // Renames font document IDs across a typeface when a typeface slug changes
2
2
 
3
- import * as fontkit from 'fontkit';
3
+ import { parseFont } from './parseFont';
4
4
  import {
5
5
  readFontFile,
6
6
  extractFontMetadata,
@@ -100,7 +100,7 @@ export const renameFontDocuments = async ({
100
100
  }
101
101
 
102
102
  const fontBuffer = await readFontFile(file);
103
- const font = fontkit.create(fontBuffer);
103
+ const font = await parseFont(fontBuffer, `${fontDoc._id}.ttf`);
104
104
 
105
105
  const { weightName, subfamilyName, fontTitle } = extractFontMetadata(
106
106
  font,
@@ -0,0 +1,87 @@
1
+ // Standalone document resolution — determines if a font already exists in Sanity
2
+
3
+ import { RECOMMENDATION } from './planTypes';
4
+
5
+ /**
6
+ * Resolves whether a font document already exists in Sanity, returning match details
7
+ * and a recommendation for how to proceed.
8
+ *
9
+ * Resolution strategies (in priority order):
10
+ * 1. Exact _id match or draft _id match or slug.current match
11
+ * 2. Content match by typefaceName + weightName + style + subfamily + variableFont
12
+ *
13
+ * @param {object} font - { _id, typefaceName, weightName, style, subfamily, variableFont, title }
14
+ * @param {object} client - Sanity client (parameterized queries only)
15
+ * @returns {Promise<{ exact: object|null, candidates: object[], recommendation: string, lookupFailed: boolean }>}
16
+ */
17
+ export const resolveExistingFont = async (font, client) => {
18
+ const result = {
19
+ exact: null,
20
+ candidates: [],
21
+ recommendation: RECOMMENDATION.CREATE,
22
+ lookupFailed: false,
23
+ };
24
+
25
+ try {
26
+ // Strategy 1: ID / slug match
27
+ const idMatches = await client.fetch(
28
+ `*[_type == 'font' && (_id == $id || _id == $draftId || slug.current == $id)]{
29
+ _id, title, weight, style, weightName, typefaceName, subfamily, variableFont,
30
+ fileInput, description, metaData, metrics, opentypeFeatures, characterSet,
31
+ scriptFileInput, variableInstanceReferences
32
+ }`,
33
+ { id: font._id, draftId: `drafts.${font._id}` }
34
+ );
35
+
36
+ if (idMatches.length > 0) {
37
+ result.exact = idMatches[0];
38
+ result.recommendation = RECOMMENDATION.USE_EXACT;
39
+ return result;
40
+ }
41
+
42
+ // Strategy 2: Content match (only when ID query returns nothing)
43
+ const subfamily = font.subfamily || '';
44
+ const contentMatches = await client.fetch(
45
+ `*[_type == 'font'
46
+ && lower(typefaceName) == lower($typefaceName)
47
+ && lower(weightName) == lower($weightName)
48
+ && lower(style) == lower($style)
49
+ && (variableFont == $variableFont || (!defined(variableFont) && $variableFont == false))
50
+ && (
51
+ lower(coalesce(subfamily, '')) == lower($subfamily)
52
+ || (lower(coalesce(subfamily, '')) in ['', 'regular'] && lower($subfamily) in ['', 'regular'])
53
+ )
54
+ ]{
55
+ _id, title, weight, style, weightName, typefaceName, subfamily, variableFont,
56
+ fileInput, description, metaData, metrics, opentypeFeatures, characterSet,
57
+ scriptFileInput, variableInstanceReferences
58
+ }`,
59
+ {
60
+ typefaceName: font.typefaceName,
61
+ weightName: font.weightName || '',
62
+ style: font.style || 'Regular',
63
+ variableFont: font.variableFont || false,
64
+ subfamily: subfamily === '' ? 'regular' : subfamily,
65
+ }
66
+ );
67
+
68
+ if (contentMatches.length === 1) {
69
+ result.candidates = contentMatches;
70
+ result.recommendation = RECOMMENDATION.USE_CANDIDATE;
71
+ return result;
72
+ }
73
+
74
+ if (contentMatches.length > 1) {
75
+ result.candidates = contentMatches;
76
+ result.recommendation = RECOMMENDATION.AMBIGUOUS;
77
+ console.warn(`Ambiguous font match for "${font.title}" — ${contentMatches.length} candidates found:`,
78
+ contentMatches.map(c => c._id));
79
+ return result;
80
+ }
81
+ } catch (err) {
82
+ console.error('Error resolving existing font:', font._id, err.message);
83
+ result.lookupFailed = true;
84
+ }
85
+
86
+ return result;
87
+ };