@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.
- package/dist/UploadModal-ADNRGQUI.mjs +6 -0
- package/dist/UploadModal-WPK2CXLR.js +6 -0
- package/dist/chunk-JCDZ7SWZ.js +7711 -0
- package/dist/chunk-TMDE4A54.mjs +7711 -0
- package/dist/index.js +666 -1647
- package/dist/index.mjs +319 -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 +455 -0
- package/src/components/SingleUploaderTool.jsx +3 -4
- package/src/components/UploadModal.jsx +304 -0
- package/src/components/UploadScriptsComponent.jsx +23 -21
- package/src/components/UploadStep1Settings.jsx +272 -0
- package/src/components/UploadStep2Review.jsx +474 -0
- package/src/components/UploadStep3Execute.jsx +234 -0
- package/src/components/UploadStep3bInstances.jsx +396 -0
- package/src/components/UploadSummary.jsx +196 -0
- package/src/index.js +46 -0
- package/src/utils/buildUploadPlan.js +326 -0
- package/src/utils/executeUploadPlan.js +430 -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/parseVariableFontInstances.js +237 -147
- package/src/utils/planReducer.js +517 -0
- package/src/utils/planTypes.js +183 -0
- package/src/utils/processFontFiles.js +121 -78
- package/src/utils/regenerateFontData.js +2 -2
- package/src/utils/resolveExistingFont.js +87 -0
- package/src/utils/updateTypefaceDocument.js +15 -2
- package/src/utils/uploadFontFiles.js +405 -405
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// Post-upload summary — results display with retry for failed fonts and typeface patch
|
|
2
|
+
|
|
3
|
+
import React, { useState, useCallback } from 'react';
|
|
4
|
+
import { Stack, Flex, Text, Card, Badge, Button, Box, Spinner } from '@sanity/ui';
|
|
5
|
+
import { CheckmarkCircleIcon, WarningOutlineIcon, ResetIcon } from '@sanity/icons';
|
|
6
|
+
import { updateTypefaceDocument } from '../utils/updateTypefaceDocument';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Post-upload summary — shows execution results with retry options.
|
|
10
|
+
*/
|
|
11
|
+
export default function UploadSummary({
|
|
12
|
+
plan,
|
|
13
|
+
result,
|
|
14
|
+
onClose,
|
|
15
|
+
onRetry,
|
|
16
|
+
client,
|
|
17
|
+
docId,
|
|
18
|
+
stylesObject,
|
|
19
|
+
preferredStyleRef,
|
|
20
|
+
}) {
|
|
21
|
+
const [retryingPatch, setRetryingPatch] = useState(false);
|
|
22
|
+
const [patchRetryResult, setPatchRetryResult] = useState(null);
|
|
23
|
+
|
|
24
|
+
const hasFailedFonts = result?.failedFonts?.length > 0;
|
|
25
|
+
const hasTypefacePatchError = result?.typefacePatchError && !patchRetryResult?.success;
|
|
26
|
+
const allSuccess = result?.success && !hasTypefacePatchError;
|
|
27
|
+
|
|
28
|
+
/** Retry the typeface document patch only */
|
|
29
|
+
const handleRetryTypefacePatch = useCallback(async () => {
|
|
30
|
+
if (!result || !client || !docId) return;
|
|
31
|
+
setRetryingPatch(true);
|
|
32
|
+
setPatchRetryResult(null);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const subfamilies = {};
|
|
36
|
+
const uniqueSubfamilies = new Set();
|
|
37
|
+
for (const entry of Object.values(plan.fonts)) {
|
|
38
|
+
if (entry.status === 'error') continue;
|
|
39
|
+
subfamilies[entry.documentId] = entry.subfamily;
|
|
40
|
+
if (entry.subfamily) uniqueSubfamilies.add(entry.subfamily);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await updateTypefaceDocument(
|
|
44
|
+
docId,
|
|
45
|
+
result.fontRefs || [],
|
|
46
|
+
result.variableRefs || [],
|
|
47
|
+
subfamilies,
|
|
48
|
+
[...uniqueSubfamilies],
|
|
49
|
+
stylesObject?.subfamilies || [],
|
|
50
|
+
preferredStyleRef || {},
|
|
51
|
+
{ weight: -100, style: 'Italic', _ref: result.fontRefs?.[0]?._ref || '' },
|
|
52
|
+
stylesObject || {},
|
|
53
|
+
client,
|
|
54
|
+
() => {},
|
|
55
|
+
() => {},
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
setPatchRetryResult({ success: true });
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error('Typeface patch retry failed:', err);
|
|
61
|
+
setPatchRetryResult({ success: false, error: err.message });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setRetryingPatch(false);
|
|
65
|
+
}, [result, client, docId, plan, stylesObject, preferredStyleRef]);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Stack space={4}>
|
|
69
|
+
{/* Header */}
|
|
70
|
+
<Flex align="center" gap={3} ref={(el) => el?.focus?.()} tabIndex={-1}>
|
|
71
|
+
{allSuccess ? (
|
|
72
|
+
<CheckmarkCircleIcon style={{ color: '#43b649', fontSize: 28 }} />
|
|
73
|
+
) : (
|
|
74
|
+
<WarningOutlineIcon style={{ color: '#f03e2f', fontSize: 28 }} />
|
|
75
|
+
)}
|
|
76
|
+
<Text size={2} weight="semibold">
|
|
77
|
+
{allSuccess ? 'Upload Complete' : 'Upload Completed with Issues'}
|
|
78
|
+
</Text>
|
|
79
|
+
</Flex>
|
|
80
|
+
|
|
81
|
+
{/* Stats */}
|
|
82
|
+
{result && (
|
|
83
|
+
<Card border padding={4} radius={2}>
|
|
84
|
+
<Stack space={3}>
|
|
85
|
+
<Flex gap={2} wrap="wrap">
|
|
86
|
+
{result.created > 0 && (
|
|
87
|
+
<Badge tone="positive" fontSize={1}>
|
|
88
|
+
{result.created} created
|
|
89
|
+
</Badge>
|
|
90
|
+
)}
|
|
91
|
+
{result.updated > 0 && (
|
|
92
|
+
<Badge tone="caution" fontSize={1}>
|
|
93
|
+
{result.updated} updated
|
|
94
|
+
</Badge>
|
|
95
|
+
)}
|
|
96
|
+
{result.failed > 0 && (
|
|
97
|
+
<Badge tone="critical" fontSize={1}>
|
|
98
|
+
{result.failed} failed
|
|
99
|
+
</Badge>
|
|
100
|
+
)}
|
|
101
|
+
{result.skipped > 0 && (
|
|
102
|
+
<Badge mode="outline" fontSize={1}>
|
|
103
|
+
{result.skipped} skipped
|
|
104
|
+
</Badge>
|
|
105
|
+
)}
|
|
106
|
+
</Flex>
|
|
107
|
+
</Stack>
|
|
108
|
+
</Card>
|
|
109
|
+
)}
|
|
110
|
+
|
|
111
|
+
{/* Failed fonts */}
|
|
112
|
+
{hasFailedFonts && (
|
|
113
|
+
<Stack space={3}>
|
|
114
|
+
<Flex align="center" justify="space-between">
|
|
115
|
+
<Flex align="center" gap={2}>
|
|
116
|
+
<Text size={1} weight="semibold">Failed Fonts</Text>
|
|
117
|
+
<Badge tone="critical" fontSize={0}>{result.failedFonts.length}</Badge>
|
|
118
|
+
</Flex>
|
|
119
|
+
<Button
|
|
120
|
+
mode="ghost"
|
|
121
|
+
tone="primary"
|
|
122
|
+
icon={ResetIcon}
|
|
123
|
+
text="Retry Failed"
|
|
124
|
+
fontSize={1}
|
|
125
|
+
padding={2}
|
|
126
|
+
onClick={() => onRetry(result.failedFonts.map(f => f.tempId).filter(Boolean))}
|
|
127
|
+
/>
|
|
128
|
+
</Flex>
|
|
129
|
+
<Stack space={2}>
|
|
130
|
+
{result.failedFonts.map((f, i) => (
|
|
131
|
+
<Card key={i} tone="critical" border padding={3} radius={2}>
|
|
132
|
+
<Stack space={2}>
|
|
133
|
+
<Flex align="center" gap={2}>
|
|
134
|
+
<Text size={1} weight="semibold">{f.title}</Text>
|
|
135
|
+
{f.failedAt && f.failedAt !== 'unknown' && (
|
|
136
|
+
<Badge tone="critical" fontSize={0} mode="outline">Failed at: {f.failedAt}</Badge>
|
|
137
|
+
)}
|
|
138
|
+
</Flex>
|
|
139
|
+
<Text size={1}>{f.error}</Text>
|
|
140
|
+
</Stack>
|
|
141
|
+
</Card>
|
|
142
|
+
))}
|
|
143
|
+
</Stack>
|
|
144
|
+
</Stack>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
{/* Typeface patch error */}
|
|
148
|
+
{hasTypefacePatchError && (
|
|
149
|
+
<Card tone="caution" border padding={4} radius={2}>
|
|
150
|
+
<Stack space={3}>
|
|
151
|
+
<Text size={1} weight="semibold">Typeface Document Not Updated</Text>
|
|
152
|
+
<Text size={1}>
|
|
153
|
+
{result.created + result.updated} font document{result.created + result.updated === 1 ? '' : 's'} created/updated successfully, but the typeface document could not be patched to reference them.
|
|
154
|
+
</Text>
|
|
155
|
+
<Text size={1} muted>{result.typefacePatchError}</Text>
|
|
156
|
+
<Flex gap={2}>
|
|
157
|
+
<Button
|
|
158
|
+
mode="default"
|
|
159
|
+
tone="primary"
|
|
160
|
+
icon={retryingPatch ? undefined : ResetIcon}
|
|
161
|
+
text={retryingPatch ? 'Retrying...' : 'Retry Typeface Patch'}
|
|
162
|
+
disabled={retryingPatch}
|
|
163
|
+
onClick={handleRetryTypefacePatch}
|
|
164
|
+
/>
|
|
165
|
+
{retryingPatch && <Spinner />}
|
|
166
|
+
</Flex>
|
|
167
|
+
</Stack>
|
|
168
|
+
</Card>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
{/* Successful typeface patch retry */}
|
|
172
|
+
{patchRetryResult?.success && (
|
|
173
|
+
<Card tone="positive" border padding={3} radius={2}>
|
|
174
|
+
<Text size={1}>Typeface document updated successfully on retry.</Text>
|
|
175
|
+
</Card>
|
|
176
|
+
)}
|
|
177
|
+
|
|
178
|
+
{/* Failed typeface patch retry */}
|
|
179
|
+
{patchRetryResult && !patchRetryResult.success && (
|
|
180
|
+
<Card tone="critical" border padding={3} radius={2}>
|
|
181
|
+
<Text size={1}>Retry failed: {patchRetryResult.error}</Text>
|
|
182
|
+
</Card>
|
|
183
|
+
)}
|
|
184
|
+
|
|
185
|
+
{/* Close */}
|
|
186
|
+
<Flex justify="flex-end">
|
|
187
|
+
<Button
|
|
188
|
+
mode="default"
|
|
189
|
+
tone="primary"
|
|
190
|
+
text="Close"
|
|
191
|
+
onClick={onClose}
|
|
192
|
+
/>
|
|
193
|
+
</Flex>
|
|
194
|
+
</Stack>
|
|
195
|
+
);
|
|
196
|
+
}
|
package/src/index.js
CHANGED
|
@@ -55,6 +55,52 @@ export { renameFontDocuments } from './utils/regenerateFontData.js';
|
|
|
55
55
|
export { updateFontPrices } from './utils/updateFontPrices.js';
|
|
56
56
|
export { sanitizeForSanityId } from './utils/sanitizeForSanityId.js';
|
|
57
57
|
|
|
58
|
+
// Plan/Execute split — v3.0 upload modal architecture
|
|
59
|
+
export { buildUploadPlan } from './utils/buildUploadPlan.js';
|
|
60
|
+
export { executeUploadPlan } from './utils/executeUploadPlan.js';
|
|
61
|
+
export { resolveExistingFont } from './utils/resolveExistingFont.js';
|
|
62
|
+
export {
|
|
63
|
+
FONT_STATUS,
|
|
64
|
+
PLAN_PHASE,
|
|
65
|
+
RECOMMENDATION,
|
|
66
|
+
EXECUTION_STATUS,
|
|
67
|
+
PLAN_VERSION,
|
|
68
|
+
createFontDecisions,
|
|
69
|
+
createEmptyPlan,
|
|
70
|
+
} from './utils/planTypes.js';
|
|
71
|
+
export { planReducer } from './utils/planReducer.js';
|
|
72
|
+
export { executionReducer, createInitialExecutionState } from './utils/executionReducer.js';
|
|
73
|
+
|
|
74
|
+
// Upload modal components
|
|
75
|
+
export { default as UploadModal } from './components/UploadModal.jsx';
|
|
76
|
+
export { default as UploadStep1Settings } from './components/UploadStep1Settings.jsx';
|
|
77
|
+
export { default as UploadStep2Review } from './components/UploadStep2Review.jsx';
|
|
78
|
+
export { default as UploadStep3Execute } from './components/UploadStep3Execute.jsx';
|
|
79
|
+
export { default as UploadStep3bInstances } from './components/UploadStep3bInstances.jsx';
|
|
80
|
+
export { default as UploadSummary } from './components/UploadSummary.jsx';
|
|
81
|
+
export { default as FontReviewCard } from './components/FontReviewCard.jsx';
|
|
82
|
+
export { default as ExistingDocumentResolver } from './components/ExistingDocumentResolver.jsx';
|
|
83
|
+
export { default as BulkActions } from './components/BulkActions.jsx';
|
|
84
|
+
|
|
85
|
+
// Font parsing (lib-font wrappers)
|
|
86
|
+
export { parseFont } from './utils/parseFont.js';
|
|
87
|
+
export {
|
|
88
|
+
getNameString,
|
|
89
|
+
getAllFeatureTags,
|
|
90
|
+
getCharacterSet,
|
|
91
|
+
getVariationAxes,
|
|
92
|
+
getNamedInstances,
|
|
93
|
+
getFontMetrics,
|
|
94
|
+
getFontMetadata,
|
|
95
|
+
getWeightClass,
|
|
96
|
+
getFsSelection,
|
|
97
|
+
getMacStyle,
|
|
98
|
+
getItalicAngle,
|
|
99
|
+
getGlyphCount,
|
|
100
|
+
getFamilyClass,
|
|
101
|
+
escapeCssFontName,
|
|
102
|
+
} from './utils/fontHelpers.js';
|
|
103
|
+
|
|
58
104
|
// Schema field definitions
|
|
59
105
|
export { openTypeField } from './schema/openTypeField.js';
|
|
60
106
|
export { styleCountField } from './schema/styleCountField.js';
|
|
@@ -0,0 +1,326 @@
|
|
|
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.settings.typefaceTitle = typefaceTitle;
|
|
61
|
+
plan.phase = PLAN_PHASE.PROCESSING;
|
|
62
|
+
plan.processingProgress.total = files.length;
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < files.length; i++) {
|
|
65
|
+
const file = files[i];
|
|
66
|
+
plan.processingProgress.currentFile = file.name;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const fontBuffer = await readFontFile(file);
|
|
70
|
+
const font = await parseFont(fontBuffer, file.name);
|
|
71
|
+
|
|
72
|
+
const entry = await buildFontPlanEntry({
|
|
73
|
+
file,
|
|
74
|
+
font,
|
|
75
|
+
typefaceTitle,
|
|
76
|
+
settings: plan.settings,
|
|
77
|
+
weightKeywordList,
|
|
78
|
+
italicKeywordList,
|
|
79
|
+
client,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Merge into plan — if same documentId, merge files
|
|
83
|
+
const existingKey = Object.keys(plan.fonts).find(k => plan.fonts[k].documentId === entry.documentId);
|
|
84
|
+
if (existingKey) {
|
|
85
|
+
plan.fonts[existingKey].files = [...plan.fonts[existingKey].files, ...entry.files];
|
|
86
|
+
} else {
|
|
87
|
+
plan.fonts[entry.tempId] = entry;
|
|
88
|
+
|
|
89
|
+
// Add to subfamily group
|
|
90
|
+
if (!entry.variableFont || entry.subfamily) {
|
|
91
|
+
const sfName = entry.subfamily;
|
|
92
|
+
if (!plan.subfamilyGroups[sfName]) {
|
|
93
|
+
plan.subfamilyGroups[sfName] = { title: sfName, fontIds: [] };
|
|
94
|
+
}
|
|
95
|
+
plan.subfamilyGroups[sfName].fontIds.push(entry.tempId);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
plan.processingProgress.completed++;
|
|
100
|
+
|
|
101
|
+
if (onProgress) {
|
|
102
|
+
onProgress({
|
|
103
|
+
type: 'font-processed',
|
|
104
|
+
tempId: entry.tempId,
|
|
105
|
+
fontEntry: entry,
|
|
106
|
+
progress: { ...plan.processingProgress },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.error(`Error processing ${file.name}:`, err.message);
|
|
111
|
+
plan.processingProgress.failed++;
|
|
112
|
+
|
|
113
|
+
const errorTempId = sanitizeForSanityId(file.name) + '-' + nanoid(6);
|
|
114
|
+
plan.fonts[errorTempId] = {
|
|
115
|
+
tempId: errorTempId,
|
|
116
|
+
files: [file],
|
|
117
|
+
sourceFileName: file.name,
|
|
118
|
+
title: file.name,
|
|
119
|
+
documentId: '',
|
|
120
|
+
weight: 400,
|
|
121
|
+
weightName: '',
|
|
122
|
+
style: 'Regular',
|
|
123
|
+
subfamily: '',
|
|
124
|
+
variableFont: false,
|
|
125
|
+
originalFilename: null,
|
|
126
|
+
decisions: createFontDecisions({}),
|
|
127
|
+
status: FONT_STATUS.ERROR,
|
|
128
|
+
error: err.message,
|
|
129
|
+
parsedMetadata: null,
|
|
130
|
+
glyphCount: 0,
|
|
131
|
+
opentypeFeatures: [],
|
|
132
|
+
variationAxes: null,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
if (onProgress) {
|
|
136
|
+
onProgress({
|
|
137
|
+
type: 'font-error',
|
|
138
|
+
tempId: errorTempId,
|
|
139
|
+
error: err.message,
|
|
140
|
+
progress: { ...plan.processingProgress },
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
plan.processingProgress.currentFile = null;
|
|
147
|
+
plan.phase = PLAN_PHASE.REVIEWING;
|
|
148
|
+
|
|
149
|
+
if (onProgress) {
|
|
150
|
+
onProgress({
|
|
151
|
+
type: 'processing-complete',
|
|
152
|
+
progress: { ...plan.processingProgress },
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return plan;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Builds a single FontPlanEntry from a parsed font file.
|
|
161
|
+
* @param {object} params
|
|
162
|
+
* @returns {Promise<object>} FontPlanEntry
|
|
163
|
+
*/
|
|
164
|
+
async function buildFontPlanEntry({
|
|
165
|
+
file,
|
|
166
|
+
font,
|
|
167
|
+
typefaceTitle,
|
|
168
|
+
settings,
|
|
169
|
+
weightKeywordList,
|
|
170
|
+
italicKeywordList,
|
|
171
|
+
client,
|
|
172
|
+
}) {
|
|
173
|
+
// Extract metadata using existing processFontFiles helpers
|
|
174
|
+
const { weightName, subfamilyName, fontTitle, style, italicKW, variableFont } = extractFontMetadata(
|
|
175
|
+
font,
|
|
176
|
+
typefaceTitle,
|
|
177
|
+
weightKeywordList,
|
|
178
|
+
italicKeywordList,
|
|
179
|
+
settings.preserveShortenedNames,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// Determine title and ID based on preserveFileNames setting
|
|
183
|
+
let finalTitle = fontTitle;
|
|
184
|
+
let originalFilename = null;
|
|
185
|
+
|
|
186
|
+
if (settings.preserveFileNames) {
|
|
187
|
+
originalFilename = file.name.replace(/\.(ttf|otf|woff2?|eot|svg)$/i, '');
|
|
188
|
+
const normalizedName = originalFilename
|
|
189
|
+
.replace(/-/g, ' ')
|
|
190
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
191
|
+
.replace(/\s+/g, ' ')
|
|
192
|
+
.trim();
|
|
193
|
+
finalTitle = normalizedName;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const documentId = sanitizeForSanityId(finalTitle);
|
|
197
|
+
const tempId = documentId + '-' + nanoid(6);
|
|
198
|
+
const weight = Number(determineWeight(font, weightName));
|
|
199
|
+
|
|
200
|
+
// Build title alternatives from all name sources
|
|
201
|
+
const titleAlternatives = [
|
|
202
|
+
{ value: getNameString(font, 1), source: 'nameId1-familyName' },
|
|
203
|
+
{ value: getNameString(font, 4), source: 'nameId4-fullName' },
|
|
204
|
+
{ value: getNameString(font, 6), source: 'nameId6-postscriptName' },
|
|
205
|
+
{ value: getNameString(font, 16), source: 'nameId16-preferredFamily' },
|
|
206
|
+
{ value: getNameString(font, 17), source: 'nameId17-preferredSubfamily' },
|
|
207
|
+
{ value: file.name.replace(/\.(ttf|otf|woff2?|eot|svg)$/i, ''), source: 'filename' },
|
|
208
|
+
].filter(alt => alt.value);
|
|
209
|
+
|
|
210
|
+
// Determine title source
|
|
211
|
+
const titleSource = settings.preserveFileNames ? 'filename' : 'fontkit-fullName';
|
|
212
|
+
|
|
213
|
+
// Determine weight source
|
|
214
|
+
const usWeightClass = getWeightClass(font);
|
|
215
|
+
const weightSource = usWeightClass ? 'os2-usWeightClass' : 'keyword-match';
|
|
216
|
+
const matchedKeyword = usWeightClass ? null : weightName;
|
|
217
|
+
|
|
218
|
+
// Determine weight name source — nameId17 (preferredSubfamily) or nameId2 (fontSubfamily)
|
|
219
|
+
const nameId17 = getNameString(font, 17);
|
|
220
|
+
const weightNameSource = variableFont
|
|
221
|
+
? 'variable-font-empty'
|
|
222
|
+
: nameId17
|
|
223
|
+
? 'nameId17-preferredSubfamily'
|
|
224
|
+
: 'nameId2-fontSubfamily';
|
|
225
|
+
|
|
226
|
+
// Determine style source
|
|
227
|
+
const italicAngle = getItalicAngle(font);
|
|
228
|
+
const fullName = getNameString(font, 4);
|
|
229
|
+
let styleSource = 'default-regular';
|
|
230
|
+
let styleReason = '';
|
|
231
|
+
if (style === 'Italic') {
|
|
232
|
+
if (italicAngle !== 0) {
|
|
233
|
+
styleSource = 'italic-angle';
|
|
234
|
+
styleReason = `italicAngle = ${italicAngle}`;
|
|
235
|
+
} else if (fullName.toLowerCase().includes('italic')) {
|
|
236
|
+
styleSource = 'name-contains-italic';
|
|
237
|
+
styleReason = 'fullName contains "italic"';
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Determine subfamily source — mirrors the logic in extractFontMetadata
|
|
242
|
+
const familyNameRaw = getNameString(font, 1);
|
|
243
|
+
const nameId4Remainder = fullName ? fullName.replace(typefaceTitle.trim(), '').trim() : '';
|
|
244
|
+
const nameId1Remainder = familyNameRaw ? familyNameRaw.replace(typefaceTitle.trim(), '').trim() : '';
|
|
245
|
+
let subfamilySource = 'default-empty';
|
|
246
|
+
if (nameId4Remainder && subfamilyName) {
|
|
247
|
+
subfamilySource = 'nameId4-subtraction';
|
|
248
|
+
} else if (nameId1Remainder && subfamilyName) {
|
|
249
|
+
subfamilySource = 'nameId1-subtraction';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Build decisions audit trail
|
|
253
|
+
const decisions = createFontDecisions({
|
|
254
|
+
titleSource,
|
|
255
|
+
title: finalTitle,
|
|
256
|
+
titleOriginal: getNameString(font, 4),
|
|
257
|
+
documentId,
|
|
258
|
+
weight,
|
|
259
|
+
weightSource,
|
|
260
|
+
matchedKeyword,
|
|
261
|
+
weightName,
|
|
262
|
+
weightNameSource,
|
|
263
|
+
style,
|
|
264
|
+
styleSource,
|
|
265
|
+
styleReason,
|
|
266
|
+
subfamily: subfamilyName,
|
|
267
|
+
subfamilySource,
|
|
268
|
+
titleAlternatives,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Resolve existing document
|
|
272
|
+
const fontForResolution = {
|
|
273
|
+
_id: documentId,
|
|
274
|
+
typefaceName: typefaceTitle,
|
|
275
|
+
weightName,
|
|
276
|
+
style,
|
|
277
|
+
subfamily: subfamilyName,
|
|
278
|
+
variableFont,
|
|
279
|
+
title: finalTitle,
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
const resolution = await resolveExistingFont(fontForResolution, client);
|
|
284
|
+
decisions.existingDocument = {
|
|
285
|
+
recommendation: resolution.recommendation,
|
|
286
|
+
exact: resolution.exact,
|
|
287
|
+
candidates: resolution.candidates,
|
|
288
|
+
userChoice: null,
|
|
289
|
+
selectedCandidate: null,
|
|
290
|
+
lookupFailed: resolution.lookupFailed || false,
|
|
291
|
+
};
|
|
292
|
+
} catch (err) {
|
|
293
|
+
console.warn('Document resolution failed for', documentId, err.message);
|
|
294
|
+
decisions.existingDocument.lookupFailed = true;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Extract parsed metadata snapshot (no raw fontkit/lib-font object retained)
|
|
298
|
+
const metadata = getFontMetadata(font);
|
|
299
|
+
const metrics = getFontMetrics(font);
|
|
300
|
+
const axes = getVariationAxes(font);
|
|
301
|
+
|
|
302
|
+
// Default empty subfamily to "Regular" — matches Regenerate Subfamilies behavior
|
|
303
|
+
const finalSubfamily = variableFont ? '' : (subfamilyName || 'Regular');
|
|
304
|
+
console.log(`[buildFontPlanEntry] "${finalTitle}" → subfamily: "${subfamilyName}" → final: "${finalSubfamily}" (variableFont: ${variableFont})`);
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
tempId,
|
|
308
|
+
files: [file],
|
|
309
|
+
sourceFileName: file.name,
|
|
310
|
+
title: finalTitle,
|
|
311
|
+
documentId,
|
|
312
|
+
weight,
|
|
313
|
+
weightName,
|
|
314
|
+
style,
|
|
315
|
+
subfamily: finalSubfamily,
|
|
316
|
+
variableFont,
|
|
317
|
+
originalFilename,
|
|
318
|
+
decisions,
|
|
319
|
+
status: FONT_STATUS.PROCESSED,
|
|
320
|
+
error: null,
|
|
321
|
+
parsedMetadata: { ...metadata, ...metrics },
|
|
322
|
+
glyphCount: getGlyphCount(font),
|
|
323
|
+
opentypeFeatures: getAllFeatureTags(font),
|
|
324
|
+
variationAxes: axes,
|
|
325
|
+
};
|
|
326
|
+
}
|