@liiift-studio/sanity-font-manager 2.4.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/UploadModal-6LIX7XOK.js +6 -0
- package/dist/UploadModal-NME2W53V.mjs +6 -0
- package/dist/chunk-646WCBRR.mjs +7276 -0
- package/dist/chunk-FH4QKHOH.js +7276 -0
- package/dist/index.js +664 -1647
- package/dist/index.mjs +317 -1209
- package/package.json +5 -5
- package/src/components/BatchUploadFonts.jsx +57 -44
- package/src/components/BulkActions.jsx +99 -0
- package/src/components/ExistingDocumentResolver.jsx +152 -0
- package/src/components/FontReviewCard.jsx +415 -0
- package/src/components/SingleUploaderTool.jsx +3 -4
- package/src/components/UploadModal.jsx +268 -0
- package/src/components/UploadScriptsComponent.jsx +23 -21
- package/src/components/UploadStep1Settings.jsx +272 -0
- package/src/components/UploadStep2Review.jsx +472 -0
- package/src/components/UploadStep3Execute.jsx +234 -0
- package/src/components/UploadSummary.jsx +196 -0
- package/src/index.js +45 -0
- package/src/utils/buildUploadPlan.js +325 -0
- package/src/utils/executeUploadPlan.js +437 -0
- package/src/utils/executionReducer.js +56 -0
- package/src/utils/fontHelpers.js +267 -0
- package/src/utils/generateCssFile.js +79 -77
- package/src/utils/generateFontData.js +47 -94
- package/src/utils/getEmptyFontKit.js +19 -17
- package/src/utils/parseFont.js +55 -0
- package/src/utils/planReducer.js +517 -0
- package/src/utils/planTypes.js +183 -0
- package/src/utils/processFontFiles.js +120 -78
- package/src/utils/regenerateFontData.js +2 -2
- package/src/utils/resolveExistingFont.js +87 -0
- package/src/utils/uploadFontFiles.js +405 -405
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// Phase 1 orchestrator — parses font files, resolves existing documents, builds a complete upload plan for user review
|
|
2
|
+
|
|
3
|
+
import { nanoid } from 'nanoid';
|
|
4
|
+
import { parseFont } from './parseFont';
|
|
5
|
+
import { readFontFile, extractFontMetadata, determineWeight } from './processFontFiles';
|
|
6
|
+
import {
|
|
7
|
+
getNameString,
|
|
8
|
+
getFontMetadata,
|
|
9
|
+
getFontMetrics,
|
|
10
|
+
getVariationAxes,
|
|
11
|
+
getNamedInstances,
|
|
12
|
+
getAllFeatureTags,
|
|
13
|
+
getGlyphCount,
|
|
14
|
+
getCharacterSet,
|
|
15
|
+
getItalicAngle,
|
|
16
|
+
getWeightClass,
|
|
17
|
+
} from './fontHelpers';
|
|
18
|
+
import { resolveExistingFont } from './resolveExistingFont';
|
|
19
|
+
import { sanitizeForSanityId } from './sanitizeForSanityId';
|
|
20
|
+
import {
|
|
21
|
+
FONT_STATUS,
|
|
22
|
+
PLAN_PHASE,
|
|
23
|
+
RECOMMENDATION,
|
|
24
|
+
PLAN_VERSION,
|
|
25
|
+
createFontDecisions,
|
|
26
|
+
createEmptyPlan,
|
|
27
|
+
} from './planTypes';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Phase 1: Reads font files, parses metadata, resolves existing documents,
|
|
31
|
+
* returns a complete upload plan for user review.
|
|
32
|
+
*
|
|
33
|
+
* NOTE: This function performs Sanity READ operations (existing document lookups)
|
|
34
|
+
* but NO writes. The client parameter is used for reads only.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} params
|
|
37
|
+
* @param {File[]} params.files - Font files to process
|
|
38
|
+
* @param {string} params.typefaceTitle - Parent typeface title
|
|
39
|
+
* @param {string} params.docId - Typeface document _id (for scoping resolution queries)
|
|
40
|
+
* @param {object} params.settings - { price, preserveShortenedNames, preserveFileNames }
|
|
41
|
+
* @param {object} params.client - Sanity client (read-only usage)
|
|
42
|
+
* @param {object} params.stylesObject - Existing typeface styles
|
|
43
|
+
* @param {string[]} params.weightKeywordList - Weight keywords for extraction
|
|
44
|
+
* @param {string[]} params.italicKeywordList - Italic keywords for extraction
|
|
45
|
+
* @param {function} params.onProgress - Called after each font is processed
|
|
46
|
+
* @returns {Promise<object>} UploadPlan object
|
|
47
|
+
*/
|
|
48
|
+
export async function buildUploadPlan({
|
|
49
|
+
files,
|
|
50
|
+
typefaceTitle,
|
|
51
|
+
docId,
|
|
52
|
+
settings = {},
|
|
53
|
+
client,
|
|
54
|
+
stylesObject = {},
|
|
55
|
+
weightKeywordList = [],
|
|
56
|
+
italicKeywordList = [],
|
|
57
|
+
onProgress,
|
|
58
|
+
}) {
|
|
59
|
+
const plan = createEmptyPlan(settings);
|
|
60
|
+
plan.phase = PLAN_PHASE.PROCESSING;
|
|
61
|
+
plan.processingProgress.total = files.length;
|
|
62
|
+
|
|
63
|
+
for (let i = 0; i < files.length; i++) {
|
|
64
|
+
const file = files[i];
|
|
65
|
+
plan.processingProgress.currentFile = file.name;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const fontBuffer = await readFontFile(file);
|
|
69
|
+
const font = await parseFont(fontBuffer, file.name);
|
|
70
|
+
|
|
71
|
+
const entry = await buildFontPlanEntry({
|
|
72
|
+
file,
|
|
73
|
+
font,
|
|
74
|
+
typefaceTitle,
|
|
75
|
+
settings: plan.settings,
|
|
76
|
+
weightKeywordList,
|
|
77
|
+
italicKeywordList,
|
|
78
|
+
client,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Merge into plan — if same documentId, merge files
|
|
82
|
+
const existingKey = Object.keys(plan.fonts).find(k => plan.fonts[k].documentId === entry.documentId);
|
|
83
|
+
if (existingKey) {
|
|
84
|
+
plan.fonts[existingKey].files = [...plan.fonts[existingKey].files, ...entry.files];
|
|
85
|
+
} else {
|
|
86
|
+
plan.fonts[entry.tempId] = entry;
|
|
87
|
+
|
|
88
|
+
// Add to subfamily group
|
|
89
|
+
if (!entry.variableFont || entry.subfamily) {
|
|
90
|
+
const sfName = entry.subfamily;
|
|
91
|
+
if (!plan.subfamilyGroups[sfName]) {
|
|
92
|
+
plan.subfamilyGroups[sfName] = { title: sfName, fontIds: [] };
|
|
93
|
+
}
|
|
94
|
+
plan.subfamilyGroups[sfName].fontIds.push(entry.tempId);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
plan.processingProgress.completed++;
|
|
99
|
+
|
|
100
|
+
if (onProgress) {
|
|
101
|
+
onProgress({
|
|
102
|
+
type: 'font-processed',
|
|
103
|
+
tempId: entry.tempId,
|
|
104
|
+
fontEntry: entry,
|
|
105
|
+
progress: { ...plan.processingProgress },
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error(`Error processing ${file.name}:`, err.message);
|
|
110
|
+
plan.processingProgress.failed++;
|
|
111
|
+
|
|
112
|
+
const errorTempId = sanitizeForSanityId(file.name) + '-' + nanoid(6);
|
|
113
|
+
plan.fonts[errorTempId] = {
|
|
114
|
+
tempId: errorTempId,
|
|
115
|
+
files: [file],
|
|
116
|
+
sourceFileName: file.name,
|
|
117
|
+
title: file.name,
|
|
118
|
+
documentId: '',
|
|
119
|
+
weight: 400,
|
|
120
|
+
weightName: '',
|
|
121
|
+
style: 'Regular',
|
|
122
|
+
subfamily: '',
|
|
123
|
+
variableFont: false,
|
|
124
|
+
originalFilename: null,
|
|
125
|
+
decisions: createFontDecisions({}),
|
|
126
|
+
status: FONT_STATUS.ERROR,
|
|
127
|
+
error: err.message,
|
|
128
|
+
parsedMetadata: null,
|
|
129
|
+
glyphCount: 0,
|
|
130
|
+
opentypeFeatures: [],
|
|
131
|
+
variationAxes: null,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
if (onProgress) {
|
|
135
|
+
onProgress({
|
|
136
|
+
type: 'font-error',
|
|
137
|
+
tempId: errorTempId,
|
|
138
|
+
error: err.message,
|
|
139
|
+
progress: { ...plan.processingProgress },
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
plan.processingProgress.currentFile = null;
|
|
146
|
+
plan.phase = PLAN_PHASE.REVIEWING;
|
|
147
|
+
|
|
148
|
+
if (onProgress) {
|
|
149
|
+
onProgress({
|
|
150
|
+
type: 'processing-complete',
|
|
151
|
+
progress: { ...plan.processingProgress },
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return plan;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Builds a single FontPlanEntry from a parsed font file.
|
|
160
|
+
* @param {object} params
|
|
161
|
+
* @returns {Promise<object>} FontPlanEntry
|
|
162
|
+
*/
|
|
163
|
+
async function buildFontPlanEntry({
|
|
164
|
+
file,
|
|
165
|
+
font,
|
|
166
|
+
typefaceTitle,
|
|
167
|
+
settings,
|
|
168
|
+
weightKeywordList,
|
|
169
|
+
italicKeywordList,
|
|
170
|
+
client,
|
|
171
|
+
}) {
|
|
172
|
+
// Extract metadata using existing processFontFiles helpers
|
|
173
|
+
const { weightName, subfamilyName, fontTitle, style, italicKW, variableFont } = extractFontMetadata(
|
|
174
|
+
font,
|
|
175
|
+
typefaceTitle,
|
|
176
|
+
weightKeywordList,
|
|
177
|
+
italicKeywordList,
|
|
178
|
+
settings.preserveShortenedNames,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Determine title and ID based on preserveFileNames setting
|
|
182
|
+
let finalTitle = fontTitle;
|
|
183
|
+
let originalFilename = null;
|
|
184
|
+
|
|
185
|
+
if (settings.preserveFileNames) {
|
|
186
|
+
originalFilename = file.name.replace(/\.(ttf|otf|woff2?|eot|svg)$/i, '');
|
|
187
|
+
const normalizedName = originalFilename
|
|
188
|
+
.replace(/-/g, ' ')
|
|
189
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
190
|
+
.replace(/\s+/g, ' ')
|
|
191
|
+
.trim();
|
|
192
|
+
finalTitle = normalizedName;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const documentId = sanitizeForSanityId(finalTitle);
|
|
196
|
+
const tempId = documentId + '-' + nanoid(6);
|
|
197
|
+
const weight = Number(determineWeight(font, weightName));
|
|
198
|
+
|
|
199
|
+
// Build title alternatives from all name sources
|
|
200
|
+
const titleAlternatives = [
|
|
201
|
+
{ value: getNameString(font, 1), source: 'nameId1-familyName' },
|
|
202
|
+
{ value: getNameString(font, 4), source: 'nameId4-fullName' },
|
|
203
|
+
{ value: getNameString(font, 6), source: 'nameId6-postscriptName' },
|
|
204
|
+
{ value: getNameString(font, 16), source: 'nameId16-preferredFamily' },
|
|
205
|
+
{ value: getNameString(font, 17), source: 'nameId17-preferredSubfamily' },
|
|
206
|
+
{ value: file.name.replace(/\.(ttf|otf|woff2?|eot|svg)$/i, ''), source: 'filename' },
|
|
207
|
+
].filter(alt => alt.value);
|
|
208
|
+
|
|
209
|
+
// Determine title source
|
|
210
|
+
const titleSource = settings.preserveFileNames ? 'filename' : 'fontkit-fullName';
|
|
211
|
+
|
|
212
|
+
// Determine weight source
|
|
213
|
+
const usWeightClass = getWeightClass(font);
|
|
214
|
+
const weightSource = usWeightClass ? 'os2-usWeightClass' : 'keyword-match';
|
|
215
|
+
const matchedKeyword = usWeightClass ? null : weightName;
|
|
216
|
+
|
|
217
|
+
// Determine weight name source — nameId17 (preferredSubfamily) or nameId2 (fontSubfamily)
|
|
218
|
+
const nameId17 = getNameString(font, 17);
|
|
219
|
+
const weightNameSource = variableFont
|
|
220
|
+
? 'variable-font-empty'
|
|
221
|
+
: nameId17
|
|
222
|
+
? 'nameId17-preferredSubfamily'
|
|
223
|
+
: 'nameId2-fontSubfamily';
|
|
224
|
+
|
|
225
|
+
// Determine style source
|
|
226
|
+
const italicAngle = getItalicAngle(font);
|
|
227
|
+
const fullName = getNameString(font, 4);
|
|
228
|
+
let styleSource = 'default-regular';
|
|
229
|
+
let styleReason = '';
|
|
230
|
+
if (style === 'Italic') {
|
|
231
|
+
if (italicAngle !== 0) {
|
|
232
|
+
styleSource = 'italic-angle';
|
|
233
|
+
styleReason = `italicAngle = ${italicAngle}`;
|
|
234
|
+
} else if (fullName.toLowerCase().includes('italic')) {
|
|
235
|
+
styleSource = 'name-contains-italic';
|
|
236
|
+
styleReason = 'fullName contains "italic"';
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Determine subfamily source — mirrors the logic in extractFontMetadata
|
|
241
|
+
const familyNameRaw = getNameString(font, 1);
|
|
242
|
+
const nameId4Remainder = fullName ? fullName.replace(typefaceTitle.trim(), '').trim() : '';
|
|
243
|
+
const nameId1Remainder = familyNameRaw ? familyNameRaw.replace(typefaceTitle.trim(), '').trim() : '';
|
|
244
|
+
let subfamilySource = 'default-empty';
|
|
245
|
+
if (nameId4Remainder && subfamilyName) {
|
|
246
|
+
subfamilySource = 'nameId4-subtraction';
|
|
247
|
+
} else if (nameId1Remainder && subfamilyName) {
|
|
248
|
+
subfamilySource = 'nameId1-subtraction';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Build decisions audit trail
|
|
252
|
+
const decisions = createFontDecisions({
|
|
253
|
+
titleSource,
|
|
254
|
+
title: finalTitle,
|
|
255
|
+
titleOriginal: getNameString(font, 4),
|
|
256
|
+
documentId,
|
|
257
|
+
weight,
|
|
258
|
+
weightSource,
|
|
259
|
+
matchedKeyword,
|
|
260
|
+
weightName,
|
|
261
|
+
weightNameSource,
|
|
262
|
+
style,
|
|
263
|
+
styleSource,
|
|
264
|
+
styleReason,
|
|
265
|
+
subfamily: subfamilyName,
|
|
266
|
+
subfamilySource,
|
|
267
|
+
titleAlternatives,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Resolve existing document
|
|
271
|
+
const fontForResolution = {
|
|
272
|
+
_id: documentId,
|
|
273
|
+
typefaceName: typefaceTitle,
|
|
274
|
+
weightName,
|
|
275
|
+
style,
|
|
276
|
+
subfamily: subfamilyName,
|
|
277
|
+
variableFont,
|
|
278
|
+
title: finalTitle,
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const resolution = await resolveExistingFont(fontForResolution, client);
|
|
283
|
+
decisions.existingDocument = {
|
|
284
|
+
recommendation: resolution.recommendation,
|
|
285
|
+
exact: resolution.exact,
|
|
286
|
+
candidates: resolution.candidates,
|
|
287
|
+
userChoice: null,
|
|
288
|
+
selectedCandidate: null,
|
|
289
|
+
lookupFailed: resolution.lookupFailed || false,
|
|
290
|
+
};
|
|
291
|
+
} catch (err) {
|
|
292
|
+
console.warn('Document resolution failed for', documentId, err.message);
|
|
293
|
+
decisions.existingDocument.lookupFailed = true;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Extract parsed metadata snapshot (no raw fontkit/lib-font object retained)
|
|
297
|
+
const metadata = getFontMetadata(font);
|
|
298
|
+
const metrics = getFontMetrics(font);
|
|
299
|
+
const axes = getVariationAxes(font);
|
|
300
|
+
|
|
301
|
+
// Default empty subfamily to "Regular" — matches Regenerate Subfamilies behavior
|
|
302
|
+
const finalSubfamily = variableFont ? '' : (subfamilyName || 'Regular');
|
|
303
|
+
console.log(`[buildFontPlanEntry] "${finalTitle}" → subfamily: "${subfamilyName}" → final: "${finalSubfamily}" (variableFont: ${variableFont})`);
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
tempId,
|
|
307
|
+
files: [file],
|
|
308
|
+
sourceFileName: file.name,
|
|
309
|
+
title: finalTitle,
|
|
310
|
+
documentId,
|
|
311
|
+
weight,
|
|
312
|
+
weightName,
|
|
313
|
+
style,
|
|
314
|
+
subfamily: finalSubfamily,
|
|
315
|
+
variableFont,
|
|
316
|
+
originalFilename,
|
|
317
|
+
decisions,
|
|
318
|
+
status: FONT_STATUS.PROCESSED,
|
|
319
|
+
error: null,
|
|
320
|
+
parsedMetadata: { ...metadata, ...metrics },
|
|
321
|
+
glyphCount: getGlyphCount(font),
|
|
322
|
+
opentypeFeatures: getAllFeatureTags(font),
|
|
323
|
+
variationAxes: axes,
|
|
324
|
+
};
|
|
325
|
+
}
|