@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
@@ -1,405 +1,405 @@
1
- // Core batch upload orchestrator — uploads each format to Sanity, generates CSS and metadata, resolves existing documents, then creates or updates font documents
2
-
3
- import { nanoid } from 'nanoid';
4
- import generateCssFile from './generateCssFile';
5
- import generateFontData from './generateFontData';
6
- import { parseVariableFontInstances } from './parseVariableFontInstances';
7
-
8
- /**
9
- * Uploads all font files to Sanity and creates/updates font documents.
10
- * @param {Object} fontsObjects
11
- * @param {Object} subfamilies
12
- * @param {Object} client - Sanity client
13
- * @param {string} inputPrice
14
- * @param {Object} stylesObject - Existing typeface styles object
15
- * @param {Function} setStatus
16
- * @param {Function} setError
17
- * @param {boolean} preserveFileNames - Use original filenames for asset naming
18
- * @returns {Promise<Object>} fontRefs, variableRefs, failedFiles
19
- */
20
- export const uploadFontFiles = async (
21
- fontsObjects,
22
- subfamilies,
23
- client,
24
- inputPrice,
25
- stylesObject,
26
- setStatus,
27
- setError,
28
- preserveFileNames = false,
29
- ) => {
30
- let fontRefs = [];
31
- let variableRefs = [];
32
- let failedFiles = [];
33
-
34
- const fontObjectKeys = Object.keys(fontsObjects);
35
-
36
- // Upload files for each font
37
- for (let i = 0; i < fontObjectKeys.length; i++) {
38
- const id = fontObjectKeys[i];
39
- const fontObject = fontsObjects[id];
40
- const files = fontObject.files;
41
- const fontKit = fontObject.fontKit;
42
- let newFileInput = fontObject.fileInput;
43
-
44
- fontObject.subfamily = subfamilies[id] ? subfamilies[id] : '';
45
- fontObject.price = Number(inputPrice) ? Number(inputPrice) : 0;
46
- if (fontObject.price > 0) fontObject.sell = true;
47
-
48
- for (let j = 0; j < files.length; j++) {
49
- const file = files[j];
50
- const fileType = determineFileType(file);
51
-
52
- // Use original filename for asset naming when preserveFileNames is enabled
53
- const assetFilename = preserveFileNames && fontObject.originalFilename
54
- ? `${fontObject.originalFilename}.${fileType}`
55
- : `${fontObject._id}.${fileType}`;
56
-
57
- console.log(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Uploading font file: ${assetFilename}`);
58
- setStatus(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Uploading font file: ${assetFilename}`);
59
-
60
- try {
61
- const baseAsset = await client.assets.upload('file', file, { filename: assetFilename });
62
-
63
- // Sanity deduplicates assets by SHA1 hash and reuses the existing asset doc,
64
- // which means originalFilename may reflect a previous upload. Patch it to
65
- // match the intended filename so downstream consumers see the correct name.
66
- if (preserveFileNames && baseAsset.originalFilename !== assetFilename) {
67
- try {
68
- await client.patch(baseAsset._id).set({ originalFilename: assetFilename }).commit();
69
- } catch (renameErr) {
70
- console.warn('Could not rename asset — permissions may be restricted:', renameErr.message);
71
- }
72
- }
73
-
74
- newFileInput[fileType] = {
75
- _type: 'file',
76
- asset: { _ref: baseAsset._id, _type: 'reference' },
77
- };
78
-
79
- if (fileType === 'woff2') {
80
- console.log(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Generating CSS for: ${fontObject.title}`);
81
- setStatus(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Generating CSS for: ${fontObject.title}`);
82
- newFileInput = await generateCssFile({
83
- woff2File: file,
84
- fileInput: newFileInput,
85
- fontName: fontObject.title,
86
- fileName: fontObject._id,
87
- variableFont: fontObject.variableFont,
88
- weight: fontObject.weight,
89
- client: client,
90
- style: fontObject.style,
91
- });
92
- }
93
-
94
- if (fileType === 'ttf') {
95
- console.log(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Generating font data for: ${fontObject.title}`);
96
- setStatus(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Generating font data for: ${fontObject.title}`);
97
- const metadata = await generateFontData({
98
- fontId: fontObject._id,
99
- url: baseAsset.url,
100
- fontKit: fontKit,
101
- client: client,
102
- commit: false,
103
- });
104
- Object.assign(fontObject, metadata);
105
- }
106
- } catch (err) {
107
- console.error('Error uploading font:', fontObject.title, err.message);
108
- setStatus('Error uploading font: ' + err.message);
109
- setError(true);
110
- failedFiles.push({ name: file.name, fk: fontKit });
111
- continue;
112
- }
113
-
114
- fontObject.fileInput = newFileInput;
115
- fontsObjects[id] = fontObject;
116
- }
117
- }
118
-
119
- // Create or update Sanity documents
120
- console.log('Creating/updating Sanity font documents:', fontsObjects);
121
-
122
- for (let i = 0; i < fontObjectKeys.length; i++) {
123
- const fontId = fontObjectKeys[i];
124
- const font = fontsObjects[fontId];
125
-
126
- const fontRef = await createOrUpdateFontDocument(font, client, setError);
127
-
128
- if (fontRef) {
129
- if (!font.variableFont) {
130
- addToFontRefs(fontRef, font, fontRefs, stylesObject);
131
- } else {
132
- addToVariableRefs(fontRef, font, variableRefs, stylesObject);
133
- }
134
- console.log(`[${i + 1}/${fontObjectKeys.length}] ${fontRef._ref} created`);
135
- setStatus(`[${i + 1}/${fontObjectKeys.length}] ${fontRef._ref} created`);
136
- }
137
- }
138
-
139
- return { fontRefs, variableRefs, failedFiles };
140
- };
141
-
142
- /**
143
- * Returns the file extension for a font file.
144
- * @param {File} file
145
- * @returns {string}
146
- */
147
- const determineFileType = (file) => {
148
- if (file.name.endsWith('.otf')) return 'otf';
149
- if (file.name.endsWith('.ttf')) return 'ttf';
150
- if (file.name.endsWith('.woff')) return 'woff';
151
- if (file.name.endsWith('.woff2')) return 'woff2';
152
- if (file.name.endsWith('.eot')) return 'eot';
153
- if (file.name.endsWith('.svg')) return 'svg';
154
- return '';
155
- };
156
-
157
- /**
158
- * Resolves whether a font document already exists in Sanity, returning match details
159
- * and a recommendation for how to proceed.
160
- *
161
- * Resolution strategies (in priority order):
162
- * 1. Exact _id match or draft _id match or slug.current match
163
- * 2. Content match by typefaceName + weightName + style + subfamily + variableFont
164
- *
165
- * @param {Object} font - The font object with _id, typefaceName, weightName, style, subfamily, variableFont
166
- * @param {Object} client - Sanity client (parameterized queries only)
167
- * @returns {Promise<{ exact: Object|null, candidates: Object[], recommendation: string }>}
168
- */
169
- export const resolveExistingFont = async (font, client) => {
170
- const result = { exact: null, candidates: [], recommendation: 'create' };
171
-
172
- try {
173
- // Strategy 1: ID / slug match
174
- const idMatches = await client.fetch(
175
- `*[_type == 'font' && (_id == $id || _id == $draftId || slug.current == $id)]{
176
- _id, title, weight, style, weightName, typefaceName, subfamily, variableFont,
177
- fileInput, description, metaData, metrics, opentypeFeatures, characterSet,
178
- scriptFileInput, variableInstanceReferences
179
- }`,
180
- { id: font._id, draftId: `drafts.${font._id}` }
181
- );
182
-
183
- if (idMatches.length > 0) {
184
- result.exact = idMatches[0];
185
- result.recommendation = 'use-exact';
186
- return result;
187
- }
188
-
189
- // Strategy 2: Content match (only when ID query returns nothing)
190
- const subfamily = font.subfamily || '';
191
- const contentMatches = await client.fetch(
192
- `*[_type == 'font'
193
- && lower(typefaceName) == lower($typefaceName)
194
- && lower(weightName) == lower($weightName)
195
- && lower(style) == lower($style)
196
- && (variableFont == $variableFont || (!defined(variableFont) && $variableFont == false))
197
- && (
198
- lower(coalesce(subfamily, '')) == lower($subfamily)
199
- || (lower(coalesce(subfamily, '')) in ['', 'regular'] && lower($subfamily) in ['', 'regular'])
200
- )
201
- ]{
202
- _id, title, weight, style, weightName, typefaceName, subfamily, variableFont,
203
- fileInput, description, metaData, metrics, opentypeFeatures, characterSet,
204
- scriptFileInput, variableInstanceReferences
205
- }`,
206
- {
207
- typefaceName: font.typefaceName,
208
- weightName: font.weightName || '',
209
- style: font.style || 'Regular',
210
- variableFont: font.variableFont || false,
211
- subfamily: subfamily === '' ? 'regular' : subfamily,
212
- }
213
- );
214
-
215
- if (contentMatches.length === 1) {
216
- result.candidates = contentMatches;
217
- result.recommendation = 'use-candidate';
218
- return result;
219
- }
220
-
221
- if (contentMatches.length > 1) {
222
- result.candidates = contentMatches;
223
- result.recommendation = 'ambiguous';
224
- console.warn(`Ambiguous font match for "${font.title}" — ${contentMatches.length} candidates found:`,
225
- contentMatches.map(c => c._id));
226
- return result;
227
- }
228
- } catch (err) {
229
- console.error('Error resolving existing font:', font._id, err.message);
230
- }
231
-
232
- return result;
233
- };
234
-
235
- /**
236
- * Creates a new font document or updates an existing one, returning its reference.
237
- * Uses resolveExistingFont to determine whether to create or update.
238
- * @param {Object} font
239
- * @param {Object} client
240
- * @param {Function} setError
241
- * @returns {Promise<Object|null>}
242
- */
243
- const createOrUpdateFontDocument = async (font, client, setError) => {
244
- try {
245
- const { exact, candidates, recommendation } = await resolveExistingFont(font, client);
246
-
247
- const { files, fontKit } = font;
248
- delete font.files;
249
- delete font.fontKit;
250
- // Remove temp field used by preserveFileNames — must not be saved to Sanity
251
- delete font.originalFilename;
252
-
253
- if (font.variableFont && font.variableInstances) {
254
- const instanceMappings = await parseVariableFontInstances(font, client);
255
- if (instanceMappings.length > 0) {
256
- font.variableInstanceReferences = instanceMappings;
257
- }
258
- }
259
-
260
- let fontResponse;
261
- if (recommendation === 'use-exact' && exact) {
262
- fontResponse = await updateExistingFont(font, exact, client);
263
- } else if (recommendation === 'use-candidate' && candidates.length === 1) {
264
- // Reassign font._id to match the existing document so inbound references resolve
265
- console.log(`Content-match: reassigning "${font._id}" → "${candidates[0]._id}"`);
266
- font._id = candidates[0]._id;
267
- fontResponse = await updateExistingFont(font, candidates[0], client);
268
- } else {
269
- // 'ambiguous' or 'create' — create a new document
270
- fontResponse = await createNewFont(font, client);
271
- }
272
-
273
- return {
274
- _key: nanoid(),
275
- _type: 'reference',
276
- _ref: fontResponse._id,
277
- _weak: true,
278
- };
279
- } catch (e) {
280
- console.error('Error creating font:', font.title, font.subfamily, e);
281
- setError(true);
282
- return null;
283
- }
284
- };
285
-
286
- /**
287
- * Patches an existing font document, merging file references and preserving stored metadata.
288
- * @param {Object} font
289
- * @param {Object} existingFont
290
- * @param {Object} client
291
- * @returns {Promise<Object>}
292
- */
293
- const updateExistingFont = async (font, existingFont, client) => {
294
- if (existingFont.fileInput) {
295
- const newFileInput = { ...font.fileInput };
296
- Object.keys(existingFont.fileInput).forEach(key => {
297
- if (!newFileInput[key]) newFileInput[key] = existingFont.fileInput[key];
298
- });
299
- font.fileInput = newFileInput;
300
- }
301
-
302
- font.metaData = !font?.metaData || Object.keys(font?.metaData || {}).length === 0
303
- ? existingFont?.metaData || {}
304
- : font.metaData;
305
- font.metrics = !font?.metrics || Object.keys(font?.metrics || {}).length === 0
306
- ? existingFont?.metrics || {}
307
- : font.metrics;
308
- font.opentypeFeatures = !font?.opentypeFeatures || Object.keys(font?.opentypeFeatures || {}).length === 0
309
- ? existingFont?.opentypeFeatures || {}
310
- : font.opentypeFeatures;
311
- font.characterSet = !font?.characterSet || Object.keys(font?.characterSet || {}).length === 0
312
- ? existingFont?.characterSet || {}
313
- : font.characterSet;
314
- font.scriptFileInput = existingFont?.scriptFileInput || {};
315
-
316
- cleanMetadataValues(font);
317
-
318
- if (font.variableFont && existingFont?.variableInstanceReferences &&
319
- (!font.variableInstanceReferences || font.variableInstanceReferences.length === 0)) {
320
- font.variableInstanceReferences = existingFont.variableInstanceReferences;
321
- }
322
-
323
- console.log('Updating existing font:', font._id, font.title);
324
-
325
- const patchObject = {
326
- fileInput: font.fileInput,
327
- subfamily: font.subfamily,
328
- weight: font.weight,
329
- };
330
-
331
- if (font.variableInstanceReferences) {
332
- patchObject.variableInstanceReferences = font.variableInstanceReferences;
333
- }
334
-
335
- return await client.patch(font._id).set(patchObject).commit();
336
- };
337
-
338
- /**
339
- * Creates a new font document in Sanity.
340
- * @param {Object} font
341
- * @param {Object} client
342
- * @returns {Promise<Object>}
343
- */
344
- const createNewFont = async (font, client) => {
345
- console.log('Creating new font:', font._id, font.title);
346
- if (font.metaData) cleanMetadataValues(font);
347
-
348
- const newDocument = {
349
- _key: nanoid(),
350
- _id: font._id,
351
- _type: 'font',
352
- ...font,
353
- };
354
-
355
- return await client.createOrReplace(newDocument);
356
- };
357
-
358
- /**
359
- * Removes null metadata values and strips control characters from string values.
360
- * @param {Object} font
361
- */
362
- const cleanMetadataValues = (font) => {
363
- if (!font.metaData) return;
364
- Object.keys(font.metaData).forEach(key => {
365
- if (font.metaData[key] == null) {
366
- font.metaData[key] = '';
367
- } else {
368
- font.metaData[key] = font.metaData[key].replace(/[\x00-\x1f]/g, '');
369
- }
370
- });
371
- };
372
-
373
- /**
374
- * Adds a font reference to fontRefs if it's not already in the existing styles.
375
- * @param {Object} fontRef
376
- * @param {Object} font
377
- * @param {Array} fontRefs
378
- * @param {Object} stylesObject
379
- */
380
- const addToFontRefs = (fontRef, font, fontRefs, stylesObject) => {
381
- if (stylesObject.fonts && stylesObject.fonts.length > 0) {
382
- const fontExists = stylesObject.fonts.findIndex(f => f._ref === fontRef._ref);
383
- const inFontRefs = fontRefs.findIndex(f => f._ref === fontRef._ref);
384
- if (fontExists === -1 && inFontRefs === -1) fontRefs.push(fontRef);
385
- } else {
386
- fontRefs.push(fontRef);
387
- }
388
- };
389
-
390
- /**
391
- * Adds a variable font reference to variableRefs if it's not already in the existing styles.
392
- * @param {Object} fontRef
393
- * @param {Object} font
394
- * @param {Array} variableRefs
395
- * @param {Object} stylesObject
396
- */
397
- const addToVariableRefs = (fontRef, font, variableRefs, stylesObject) => {
398
- if (stylesObject?.variableFont?.length) {
399
- const vfExists = stylesObject.variableFont.findIndex(f => f._ref === fontRef._ref);
400
- const inVariableRefs = variableRefs.findIndex(f => f._ref === fontRef._ref);
401
- if (vfExists === -1 && inVariableRefs === -1 && font.variableFont) variableRefs.push(fontRef);
402
- } else {
403
- variableRefs.push(fontRef);
404
- }
405
- };
1
+ // Core batch upload orchestrator — uploads each format to Sanity, generates CSS and metadata, resolves existing documents, then creates or updates font documents
2
+
3
+ import { nanoid } from 'nanoid';
4
+ import generateCssFile from './generateCssFile';
5
+ import generateFontData from './generateFontData';
6
+ import { parseVariableFontInstances } from './parseVariableFontInstances';
7
+
8
+ /**
9
+ * Uploads all font files to Sanity and creates/updates font documents.
10
+ * @param {Object} fontsObjects
11
+ * @param {Object} subfamilies
12
+ * @param {Object} client - Sanity client
13
+ * @param {string} inputPrice
14
+ * @param {Object} stylesObject - Existing typeface styles object
15
+ * @param {Function} setStatus
16
+ * @param {Function} setError
17
+ * @param {boolean} preserveFileNames - Use original filenames for asset naming
18
+ * @returns {Promise<Object>} fontRefs, variableRefs, failedFiles
19
+ */
20
+ export const uploadFontFiles = async (
21
+ fontsObjects,
22
+ subfamilies,
23
+ client,
24
+ inputPrice,
25
+ stylesObject,
26
+ setStatus,
27
+ setError,
28
+ preserveFileNames = false,
29
+ ) => {
30
+ let fontRefs = [];
31
+ let variableRefs = [];
32
+ let failedFiles = [];
33
+
34
+ const fontObjectKeys = Object.keys(fontsObjects);
35
+
36
+ // Upload files for each font
37
+ for (let i = 0; i < fontObjectKeys.length; i++) {
38
+ const id = fontObjectKeys[i];
39
+ const fontObject = fontsObjects[id];
40
+ const files = fontObject.files;
41
+ const fontKit = fontObject.fontKit;
42
+ let newFileInput = fontObject.fileInput;
43
+
44
+ fontObject.subfamily = subfamilies[id] ? subfamilies[id] : '';
45
+ fontObject.price = Number(inputPrice) ? Number(inputPrice) : 0;
46
+ if (fontObject.price > 0) fontObject.sell = true;
47
+
48
+ for (let j = 0; j < files.length; j++) {
49
+ const file = files[j];
50
+ const fileType = determineFileType(file);
51
+
52
+ // Use original filename for asset naming when preserveFileNames is enabled
53
+ const assetFilename = preserveFileNames && fontObject.originalFilename
54
+ ? `${fontObject.originalFilename}.${fileType}`
55
+ : `${fontObject._id}.${fileType}`;
56
+
57
+ console.log(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Uploading font file: ${assetFilename}`);
58
+ setStatus(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Uploading font file: ${assetFilename}`);
59
+
60
+ try {
61
+ const baseAsset = await client.assets.upload('file', file, { filename: assetFilename });
62
+
63
+ // Sanity deduplicates assets by SHA1 hash and reuses the existing asset doc,
64
+ // which means originalFilename may reflect a previous upload. Patch it to
65
+ // match the intended filename so downstream consumers see the correct name.
66
+ if (preserveFileNames && baseAsset.originalFilename !== assetFilename) {
67
+ try {
68
+ await client.patch(baseAsset._id).set({ originalFilename: assetFilename }).commit();
69
+ } catch (renameErr) {
70
+ console.warn('Could not rename asset — permissions may be restricted:', renameErr.message);
71
+ }
72
+ }
73
+
74
+ newFileInput[fileType] = {
75
+ _type: 'file',
76
+ asset: { _ref: baseAsset._id, _type: 'reference' },
77
+ };
78
+
79
+ if (fileType === 'woff2') {
80
+ console.log(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Generating CSS for: ${fontObject.title}`);
81
+ setStatus(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Generating CSS for: ${fontObject.title}`);
82
+ newFileInput = await generateCssFile({
83
+ woff2File: file,
84
+ fileInput: newFileInput,
85
+ fontName: fontObject.title,
86
+ fileName: fontObject._id,
87
+ variableFont: fontObject.variableFont,
88
+ weight: fontObject.weight,
89
+ client: client,
90
+ style: fontObject.style,
91
+ });
92
+ }
93
+
94
+ if (fileType === 'ttf') {
95
+ console.log(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Generating font data for: ${fontObject.title}`);
96
+ setStatus(`[${i + 1}/${fontObjectKeys.length}][${j + 1}/${files.length}] Generating font data for: ${fontObject.title}`);
97
+ const metadata = await generateFontData({
98
+ fontId: fontObject._id,
99
+ url: baseAsset.url,
100
+ fontKit: fontKit,
101
+ client: client,
102
+ commit: false,
103
+ });
104
+ Object.assign(fontObject, metadata);
105
+ }
106
+ } catch (err) {
107
+ console.error('Error uploading font:', fontObject.title, err.message);
108
+ setStatus('Error uploading font: ' + err.message);
109
+ setError(true);
110
+ failedFiles.push({ name: file.name, fk: fontKit });
111
+ continue;
112
+ }
113
+
114
+ fontObject.fileInput = newFileInput;
115
+ fontsObjects[id] = fontObject;
116
+ }
117
+ }
118
+
119
+ // Create or update Sanity documents
120
+ console.log('Creating/updating Sanity font documents:', fontsObjects);
121
+
122
+ for (let i = 0; i < fontObjectKeys.length; i++) {
123
+ const fontId = fontObjectKeys[i];
124
+ const font = fontsObjects[fontId];
125
+
126
+ const fontRef = await createOrUpdateFontDocument(font, client, setError);
127
+
128
+ if (fontRef) {
129
+ if (!font.variableFont) {
130
+ addToFontRefs(fontRef, font, fontRefs, stylesObject);
131
+ } else {
132
+ addToVariableRefs(fontRef, font, variableRefs, stylesObject);
133
+ }
134
+ console.log(`[${i + 1}/${fontObjectKeys.length}] ${fontRef._ref} created`);
135
+ setStatus(`[${i + 1}/${fontObjectKeys.length}] ${fontRef._ref} created`);
136
+ }
137
+ }
138
+
139
+ return { fontRefs, variableRefs, failedFiles };
140
+ };
141
+
142
+ /**
143
+ * Returns the file extension for a font file.
144
+ * @param {File} file
145
+ * @returns {string}
146
+ */
147
+ const determineFileType = (file) => {
148
+ if (file.name.endsWith('.otf')) return 'otf';
149
+ if (file.name.endsWith('.ttf')) return 'ttf';
150
+ if (file.name.endsWith('.woff')) return 'woff';
151
+ if (file.name.endsWith('.woff2')) return 'woff2';
152
+ if (file.name.endsWith('.eot')) return 'eot';
153
+ if (file.name.endsWith('.svg')) return 'svg';
154
+ return '';
155
+ };
156
+
157
+ /**
158
+ * Resolves whether a font document already exists in Sanity, returning match details
159
+ * and a recommendation for how to proceed.
160
+ *
161
+ * Resolution strategies (in priority order):
162
+ * 1. Exact _id match or draft _id match or slug.current match
163
+ * 2. Content match by typefaceName + weightName + style + subfamily + variableFont
164
+ *
165
+ * @param {Object} font - The font object with _id, typefaceName, weightName, style, subfamily, variableFont
166
+ * @param {Object} client - Sanity client (parameterized queries only)
167
+ * @returns {Promise<{ exact: Object|null, candidates: Object[], recommendation: string }>}
168
+ */
169
+ export const resolveExistingFont = async (font, client) => {
170
+ const result = { exact: null, candidates: [], recommendation: 'create' };
171
+
172
+ try {
173
+ // Strategy 1: ID / slug match
174
+ const idMatches = await client.fetch(
175
+ `*[_type == 'font' && (_id == $id || _id == $draftId || slug.current == $id)]{
176
+ _id, title, weight, style, weightName, typefaceName, subfamily, variableFont,
177
+ fileInput, description, metaData, metrics, opentypeFeatures, characterSet,
178
+ scriptFileInput, variableInstanceReferences
179
+ }`,
180
+ { id: font._id, draftId: `drafts.${font._id}` }
181
+ );
182
+
183
+ if (idMatches.length > 0) {
184
+ result.exact = idMatches[0];
185
+ result.recommendation = 'use-exact';
186
+ return result;
187
+ }
188
+
189
+ // Strategy 2: Content match (only when ID query returns nothing)
190
+ const subfamily = font.subfamily || '';
191
+ const contentMatches = await client.fetch(
192
+ `*[_type == 'font'
193
+ && lower(typefaceName) == lower($typefaceName)
194
+ && lower(weightName) == lower($weightName)
195
+ && lower(style) == lower($style)
196
+ && (variableFont == $variableFont || (!defined(variableFont) && $variableFont == false))
197
+ && (
198
+ lower(coalesce(subfamily, '')) == lower($subfamily)
199
+ || (lower(coalesce(subfamily, '')) in ['', 'regular'] && lower($subfamily) in ['', 'regular'])
200
+ )
201
+ ]{
202
+ _id, title, weight, style, weightName, typefaceName, subfamily, variableFont,
203
+ fileInput, description, metaData, metrics, opentypeFeatures, characterSet,
204
+ scriptFileInput, variableInstanceReferences
205
+ }`,
206
+ {
207
+ typefaceName: font.typefaceName,
208
+ weightName: font.weightName || '',
209
+ style: font.style || 'Regular',
210
+ variableFont: font.variableFont || false,
211
+ subfamily: subfamily === '' ? 'regular' : subfamily,
212
+ }
213
+ );
214
+
215
+ if (contentMatches.length === 1) {
216
+ result.candidates = contentMatches;
217
+ result.recommendation = 'use-candidate';
218
+ return result;
219
+ }
220
+
221
+ if (contentMatches.length > 1) {
222
+ result.candidates = contentMatches;
223
+ result.recommendation = 'ambiguous';
224
+ console.warn(`Ambiguous font match for "${font.title}" — ${contentMatches.length} candidates found:`,
225
+ contentMatches.map(c => c._id));
226
+ return result;
227
+ }
228
+ } catch (err) {
229
+ console.error('Error resolving existing font:', font._id, err.message);
230
+ }
231
+
232
+ return result;
233
+ };
234
+
235
+ /**
236
+ * Creates a new font document or updates an existing one, returning its reference.
237
+ * Uses resolveExistingFont to determine whether to create or update.
238
+ * @param {Object} font
239
+ * @param {Object} client
240
+ * @param {Function} setError
241
+ * @returns {Promise<Object|null>}
242
+ */
243
+ const createOrUpdateFontDocument = async (font, client, setError) => {
244
+ try {
245
+ const { exact, candidates, recommendation } = await resolveExistingFont(font, client);
246
+
247
+ const { files, fontKit } = font;
248
+ delete font.files;
249
+ delete font.fontKit;
250
+ // Remove temp field used by preserveFileNames — must not be saved to Sanity
251
+ delete font.originalFilename;
252
+
253
+ if (font.variableFont && font.variableInstances) {
254
+ const instanceMappings = await parseVariableFontInstances(font, client);
255
+ if (instanceMappings.length > 0) {
256
+ font.variableInstanceReferences = instanceMappings;
257
+ }
258
+ }
259
+
260
+ let fontResponse;
261
+ if (recommendation === 'use-exact' && exact) {
262
+ fontResponse = await updateExistingFont(font, exact, client);
263
+ } else if (recommendation === 'use-candidate' && candidates.length === 1) {
264
+ // Reassign font._id to match the existing document so inbound references resolve
265
+ console.log(`Content-match: reassigning "${font._id}" → "${candidates[0]._id}"`);
266
+ font._id = candidates[0]._id;
267
+ fontResponse = await updateExistingFont(font, candidates[0], client);
268
+ } else {
269
+ // 'ambiguous' or 'create' — create a new document
270
+ fontResponse = await createNewFont(font, client);
271
+ }
272
+
273
+ return {
274
+ _key: nanoid(),
275
+ _type: 'reference',
276
+ _ref: fontResponse._id,
277
+ _weak: true,
278
+ };
279
+ } catch (e) {
280
+ console.error('Error creating font:', font.title, font.subfamily, e);
281
+ setError(true);
282
+ return null;
283
+ }
284
+ };
285
+
286
+ /**
287
+ * Patches an existing font document, merging file references and preserving stored metadata.
288
+ * @param {Object} font
289
+ * @param {Object} existingFont
290
+ * @param {Object} client
291
+ * @returns {Promise<Object>}
292
+ */
293
+ const updateExistingFont = async (font, existingFont, client) => {
294
+ if (existingFont.fileInput) {
295
+ const newFileInput = { ...font.fileInput };
296
+ Object.keys(existingFont.fileInput).forEach(key => {
297
+ if (!newFileInput[key]) newFileInput[key] = existingFont.fileInput[key];
298
+ });
299
+ font.fileInput = newFileInput;
300
+ }
301
+
302
+ font.metaData = !font?.metaData || Object.keys(font?.metaData || {}).length === 0
303
+ ? existingFont?.metaData || {}
304
+ : font.metaData;
305
+ font.metrics = !font?.metrics || Object.keys(font?.metrics || {}).length === 0
306
+ ? existingFont?.metrics || {}
307
+ : font.metrics;
308
+ font.opentypeFeatures = !font?.opentypeFeatures || Object.keys(font?.opentypeFeatures || {}).length === 0
309
+ ? existingFont?.opentypeFeatures || {}
310
+ : font.opentypeFeatures;
311
+ font.characterSet = !font?.characterSet || Object.keys(font?.characterSet || {}).length === 0
312
+ ? existingFont?.characterSet || {}
313
+ : font.characterSet;
314
+ font.scriptFileInput = existingFont?.scriptFileInput || {};
315
+
316
+ cleanMetadataValues(font);
317
+
318
+ if (font.variableFont && existingFont?.variableInstanceReferences &&
319
+ (!font.variableInstanceReferences || font.variableInstanceReferences.length === 0)) {
320
+ font.variableInstanceReferences = existingFont.variableInstanceReferences;
321
+ }
322
+
323
+ console.log('Updating existing font:', font._id, font.title);
324
+
325
+ const patchObject = {
326
+ fileInput: font.fileInput,
327
+ subfamily: font.subfamily,
328
+ weight: font.weight,
329
+ };
330
+
331
+ if (font.variableInstanceReferences) {
332
+ patchObject.variableInstanceReferences = font.variableInstanceReferences;
333
+ }
334
+
335
+ return await client.patch(font._id).set(patchObject).commit();
336
+ };
337
+
338
+ /**
339
+ * Creates a new font document in Sanity.
340
+ * @param {Object} font
341
+ * @param {Object} client
342
+ * @returns {Promise<Object>}
343
+ */
344
+ const createNewFont = async (font, client) => {
345
+ console.log('Creating new font:', font._id, font.title);
346
+ if (font.metaData) cleanMetadataValues(font);
347
+
348
+ const newDocument = {
349
+ _key: nanoid(),
350
+ _id: font._id,
351
+ _type: 'font',
352
+ ...font,
353
+ };
354
+
355
+ return await client.createOrReplace(newDocument);
356
+ };
357
+
358
+ /**
359
+ * Removes null metadata values and strips control characters from string values.
360
+ * @param {Object} font
361
+ */
362
+ const cleanMetadataValues = (font) => {
363
+ if (!font.metaData) return;
364
+ Object.keys(font.metaData).forEach(key => {
365
+ if (font.metaData[key] == null) {
366
+ font.metaData[key] = '';
367
+ } else {
368
+ font.metaData[key] = font.metaData[key].replace(/[\x00-\x1f]/g, '');
369
+ }
370
+ });
371
+ };
372
+
373
+ /**
374
+ * Adds a font reference to fontRefs if it's not already in the existing styles.
375
+ * @param {Object} fontRef
376
+ * @param {Object} font
377
+ * @param {Array} fontRefs
378
+ * @param {Object} stylesObject
379
+ */
380
+ const addToFontRefs = (fontRef, font, fontRefs, stylesObject) => {
381
+ if (stylesObject.fonts && stylesObject.fonts.length > 0) {
382
+ const fontExists = stylesObject.fonts.findIndex(f => f._ref === fontRef._ref);
383
+ const inFontRefs = fontRefs.findIndex(f => f._ref === fontRef._ref);
384
+ if (fontExists === -1 && inFontRefs === -1) fontRefs.push(fontRef);
385
+ } else {
386
+ fontRefs.push(fontRef);
387
+ }
388
+ };
389
+
390
+ /**
391
+ * Adds a variable font reference to variableRefs if it's not already in the existing styles.
392
+ * @param {Object} fontRef
393
+ * @param {Object} font
394
+ * @param {Array} variableRefs
395
+ * @param {Object} stylesObject
396
+ */
397
+ const addToVariableRefs = (fontRef, font, variableRefs, stylesObject) => {
398
+ if (stylesObject?.variableFont?.length) {
399
+ const vfExists = stylesObject.variableFont.findIndex(f => f._ref === fontRef._ref);
400
+ const inVariableRefs = variableRefs.findIndex(f => f._ref === fontRef._ref);
401
+ if (vfExists === -1 && inVariableRefs === -1 && font.variableFont) variableRefs.push(fontRef);
402
+ } else {
403
+ variableRefs.push(fontRef);
404
+ }
405
+ };