@liiift-studio/sanity-font-manager 2.6.0 → 2.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +695 -437
- package/dist/index.js +1628 -17701
- package/dist/index.mjs +1537 -17599
- package/package.json +83 -83
- package/src/components/BatchUploadFonts.jsx +655 -653
- package/src/components/BulkActions.jsx +99 -99
- package/src/components/ExistingDocumentResolver.jsx +152 -152
- package/src/components/FontReviewCard.jsx +455 -455
- package/src/components/FontScriptUploaderComponent.jsx +463 -463
- package/src/components/GenerateCollectionsPairsComponent.jsx +259 -259
- package/src/components/KeyValueInput.jsx +95 -95
- package/src/components/KeyValueReferenceInput.jsx +267 -267
- package/src/components/NestedObjectArraySelector.jsx +146 -146
- package/src/components/PriceInput.jsx +26 -26
- package/src/components/PrimaryCollectionGeneratorTypeface.jsx +116 -116
- package/src/components/RegenerateSubfamiliesComponent.jsx +185 -185
- package/src/components/SetOTF.jsx +87 -87
- package/src/components/SingleUploaderTool.jsx +674 -674
- package/src/components/StatusDisplay.jsx +26 -26
- package/src/components/StyleCountInput.jsx +16 -16
- package/src/components/UpdateScriptsComponent.jsx +76 -76
- package/src/components/UploadButton.jsx +43 -43
- package/src/components/UploadModal.jsx +309 -304
- package/src/components/UploadScriptsComponent.jsx +539 -539
- package/src/components/UploadStep1Settings.jsx +272 -272
- package/src/components/UploadStep2Review.jsx +478 -474
- package/src/components/UploadStep3Execute.jsx +234 -234
- package/src/components/UploadStep3bInstances.jsx +396 -396
- package/src/components/UploadSummary.jsx +196 -196
- package/src/components/VariableInstanceReferencesInput.jsx +190 -190
- package/src/hooks/useNestedObjects.js +92 -92
- package/src/hooks/useSanityClient.js +9 -9
- package/src/index.js +120 -120
- package/src/schema/openTypeField.js +1995 -1995
- package/src/schema/styleCountField.js +12 -12
- package/src/schema/stylesField.js +302 -302
- package/src/schema/stylisticSetField.js +301 -301
- package/src/utils/buildUploadPlan.js +326 -326
- package/src/utils/executeUploadPlan.js +430 -430
- package/src/utils/executionReducer.js +56 -56
- package/src/utils/fontHelpers.js +281 -267
- package/src/utils/generateCssFile.js +207 -207
- package/src/utils/generateFontData.js +98 -98
- package/src/utils/generateFontFile.js +38 -38
- package/src/utils/generateKeywords.js +185 -185
- package/src/utils/generateSubset.js +45 -45
- package/src/utils/getEmptyFontKit.js +101 -101
- package/src/utils/parseFont.js +56 -56
- package/src/utils/parseVariableFontInstances.js +301 -301
- package/src/utils/planReducer.js +531 -517
- package/src/utils/planTypes.js +183 -183
- package/src/utils/processFontFiles.js +530 -530
- package/src/utils/regenerateFontData.js +146 -146
- package/src/utils/resolveExistingFont.js +87 -87
- package/src/utils/retitleFontEntries.js +154 -0
- package/src/utils/sanitizeForSanityId.js +65 -65
- package/src/utils/setupDecompressors.js +27 -27
- package/src/utils/updateFontPrices.js +94 -94
- package/src/utils/updateTypefaceDocument.js +162 -162
- package/src/utils/uploadFontFiles.js +405 -405
- package/src/utils/utils.js +24 -24
|
@@ -1,146 +1,146 @@
|
|
|
1
|
-
// Renames font document IDs across a typeface when a typeface slug changes
|
|
2
|
-
|
|
3
|
-
import { parseFont } from './parseFont';
|
|
4
|
-
import {
|
|
5
|
-
readFontFile,
|
|
6
|
-
extractFontMetadata,
|
|
7
|
-
} from './processFontFiles';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Re-downloads TTF/OTF files for all fonts in a typeface and re-extracts their metadata,
|
|
11
|
-
* patching each font document's title, slug, weightName, and subfamily.
|
|
12
|
-
*
|
|
13
|
-
* @param {Object} params
|
|
14
|
-
* @param {Object} params.client - Sanity client
|
|
15
|
-
* @param {string} params.typefaceName
|
|
16
|
-
* @param {Object} params.slug - Typeface slug object
|
|
17
|
-
* @param {string[]} params.weightKeywordList
|
|
18
|
-
* @param {string[]} params.italicKeywordList
|
|
19
|
-
* @param {boolean} params.preserveShortenedNames
|
|
20
|
-
* @param {Function} params.setStatus
|
|
21
|
-
* @param {Function} params.setError
|
|
22
|
-
* @returns {Promise<Object>}
|
|
23
|
-
*/
|
|
24
|
-
export const renameFontDocuments = async ({
|
|
25
|
-
client,
|
|
26
|
-
typefaceName,
|
|
27
|
-
slug,
|
|
28
|
-
weightKeywordList,
|
|
29
|
-
italicKeywordList,
|
|
30
|
-
preserveShortenedNames = false,
|
|
31
|
-
setStatus,
|
|
32
|
-
setError,
|
|
33
|
-
}) => {
|
|
34
|
-
try {
|
|
35
|
-
if (!typefaceName) {
|
|
36
|
-
setStatus('Typeface needs a title');
|
|
37
|
-
setError(true);
|
|
38
|
-
console.error('Typeface needs title');
|
|
39
|
-
return { success: false, message: 'Typeface needs title' };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
setStatus('Fetching font documents...');
|
|
43
|
-
const slugCurrent = slug?.current || typefaceName;
|
|
44
|
-
|
|
45
|
-
const query = await client.fetch(
|
|
46
|
-
`*[_type == "typeface" && slug.current == $slugCurrent][0]{
|
|
47
|
-
"fonts": styles.fonts[]->{ _id, title, subfamily, fileInput, style, weight, _key }
|
|
48
|
-
}`,
|
|
49
|
-
{ slugCurrent }
|
|
50
|
-
);
|
|
51
|
-
const fontDocuments = query?.fonts || [];
|
|
52
|
-
|
|
53
|
-
if (fontDocuments.length === 0) {
|
|
54
|
-
setStatus('No font documents found for this typeface');
|
|
55
|
-
setError(true);
|
|
56
|
-
return { success: false, message: 'No font documents found for this typeface' };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
setStatus(`Found ${fontDocuments.length} font documents to process...`);
|
|
60
|
-
|
|
61
|
-
let updatedCount = 0;
|
|
62
|
-
let skippedCount = 0;
|
|
63
|
-
let errorCount = 0;
|
|
64
|
-
|
|
65
|
-
for (let i = 0; i < fontDocuments.length; i++) {
|
|
66
|
-
const fontDoc = fontDocuments[i];
|
|
67
|
-
setStatus(`Processing font ${i + 1}/${fontDocuments.length}: ${fontDoc.title}`);
|
|
68
|
-
|
|
69
|
-
if (!fontDoc.fileInput?.ttf?.asset?._ref && !fontDoc.fileInput?.otf?.asset?._ref) {
|
|
70
|
-
console.log(`Skipping ${fontDoc.title} — no TTF or OTF file found`);
|
|
71
|
-
skippedCount++;
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
let file;
|
|
77
|
-
let ttfAsset;
|
|
78
|
-
let otfAsset;
|
|
79
|
-
|
|
80
|
-
if (fontDoc.fileInput?.ttf?.asset?._ref) {
|
|
81
|
-
ttfAsset = await client.getDocument(fontDoc.fileInput.ttf.asset._ref);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (!ttfAsset?.url) {
|
|
85
|
-
if (fontDoc.fileInput?.otf?.asset?._ref) {
|
|
86
|
-
otfAsset = await client.getDocument(fontDoc.fileInput.otf.asset._ref);
|
|
87
|
-
}
|
|
88
|
-
if (!otfAsset?.url) {
|
|
89
|
-
console.log(`Skipping ${fontDoc.title} — no TTF or OTF URL`);
|
|
90
|
-
skippedCount++;
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
setStatus(`Fetching OTF file for ${fontDoc.title}...`);
|
|
94
|
-
const res = await fetch(otfAsset.url);
|
|
95
|
-
file = await res.blob();
|
|
96
|
-
} else {
|
|
97
|
-
setStatus(`Fetching TTF file for ${fontDoc.title}...`);
|
|
98
|
-
const res = await fetch(ttfAsset.url);
|
|
99
|
-
file = await res.blob();
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const fontBuffer = await readFontFile(file);
|
|
103
|
-
const font = await parseFont(fontBuffer, `${fontDoc._id}.ttf`);
|
|
104
|
-
|
|
105
|
-
const { weightName, subfamilyName, fontTitle } = extractFontMetadata(
|
|
106
|
-
font,
|
|
107
|
-
typefaceName,
|
|
108
|
-
weightKeywordList,
|
|
109
|
-
italicKeywordList,
|
|
110
|
-
preserveShortenedNames,
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
const slugValue = fontTitle.toLowerCase().replace(/\s+/g, '-');
|
|
114
|
-
|
|
115
|
-
setStatus(`Updating font ${i + 1}/${fontDocuments.length}: ${fontTitle}`);
|
|
116
|
-
await client.patch(fontDoc._id)
|
|
117
|
-
.set({
|
|
118
|
-
title: fontTitle,
|
|
119
|
-
slug: { _type: 'slug', current: slugValue },
|
|
120
|
-
weightName: weightName,
|
|
121
|
-
subfamily: subfamilyName,
|
|
122
|
-
})
|
|
123
|
-
.commit();
|
|
124
|
-
|
|
125
|
-
updatedCount++;
|
|
126
|
-
} catch (err) {
|
|
127
|
-
console.error(`Error processing ${fontDoc.title}:`, err);
|
|
128
|
-
errorCount++;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const resultMessage = `Renamed ${updatedCount} of ${fontDocuments.length} font documents (${skippedCount} skipped, ${errorCount} errors)`;
|
|
133
|
-
setStatus(resultMessage);
|
|
134
|
-
|
|
135
|
-
return {
|
|
136
|
-
success: true,
|
|
137
|
-
message: resultMessage,
|
|
138
|
-
stats: { total: fontDocuments.length, updated: updatedCount, skipped: skippedCount, errors: errorCount },
|
|
139
|
-
};
|
|
140
|
-
} catch (err) {
|
|
141
|
-
console.error('Error renaming font documents:', err);
|
|
142
|
-
setError(true);
|
|
143
|
-
setStatus(`Error: ${err.message}`);
|
|
144
|
-
return { success: false, message: `Error: ${err.message}` };
|
|
145
|
-
}
|
|
146
|
-
};
|
|
1
|
+
// Renames font document IDs across a typeface when a typeface slug changes
|
|
2
|
+
|
|
3
|
+
import { parseFont } from './parseFont';
|
|
4
|
+
import {
|
|
5
|
+
readFontFile,
|
|
6
|
+
extractFontMetadata,
|
|
7
|
+
} from './processFontFiles';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Re-downloads TTF/OTF files for all fonts in a typeface and re-extracts their metadata,
|
|
11
|
+
* patching each font document's title, slug, weightName, and subfamily.
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} params
|
|
14
|
+
* @param {Object} params.client - Sanity client
|
|
15
|
+
* @param {string} params.typefaceName
|
|
16
|
+
* @param {Object} params.slug - Typeface slug object
|
|
17
|
+
* @param {string[]} params.weightKeywordList
|
|
18
|
+
* @param {string[]} params.italicKeywordList
|
|
19
|
+
* @param {boolean} params.preserveShortenedNames
|
|
20
|
+
* @param {Function} params.setStatus
|
|
21
|
+
* @param {Function} params.setError
|
|
22
|
+
* @returns {Promise<Object>}
|
|
23
|
+
*/
|
|
24
|
+
export const renameFontDocuments = async ({
|
|
25
|
+
client,
|
|
26
|
+
typefaceName,
|
|
27
|
+
slug,
|
|
28
|
+
weightKeywordList,
|
|
29
|
+
italicKeywordList,
|
|
30
|
+
preserveShortenedNames = false,
|
|
31
|
+
setStatus,
|
|
32
|
+
setError,
|
|
33
|
+
}) => {
|
|
34
|
+
try {
|
|
35
|
+
if (!typefaceName) {
|
|
36
|
+
setStatus('Typeface needs a title');
|
|
37
|
+
setError(true);
|
|
38
|
+
console.error('Typeface needs title');
|
|
39
|
+
return { success: false, message: 'Typeface needs title' };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setStatus('Fetching font documents...');
|
|
43
|
+
const slugCurrent = slug?.current || typefaceName;
|
|
44
|
+
|
|
45
|
+
const query = await client.fetch(
|
|
46
|
+
`*[_type == "typeface" && slug.current == $slugCurrent][0]{
|
|
47
|
+
"fonts": styles.fonts[]->{ _id, title, subfamily, fileInput, style, weight, _key }
|
|
48
|
+
}`,
|
|
49
|
+
{ slugCurrent }
|
|
50
|
+
);
|
|
51
|
+
const fontDocuments = query?.fonts || [];
|
|
52
|
+
|
|
53
|
+
if (fontDocuments.length === 0) {
|
|
54
|
+
setStatus('No font documents found for this typeface');
|
|
55
|
+
setError(true);
|
|
56
|
+
return { success: false, message: 'No font documents found for this typeface' };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
setStatus(`Found ${fontDocuments.length} font documents to process...`);
|
|
60
|
+
|
|
61
|
+
let updatedCount = 0;
|
|
62
|
+
let skippedCount = 0;
|
|
63
|
+
let errorCount = 0;
|
|
64
|
+
|
|
65
|
+
for (let i = 0; i < fontDocuments.length; i++) {
|
|
66
|
+
const fontDoc = fontDocuments[i];
|
|
67
|
+
setStatus(`Processing font ${i + 1}/${fontDocuments.length}: ${fontDoc.title}`);
|
|
68
|
+
|
|
69
|
+
if (!fontDoc.fileInput?.ttf?.asset?._ref && !fontDoc.fileInput?.otf?.asset?._ref) {
|
|
70
|
+
console.log(`Skipping ${fontDoc.title} — no TTF or OTF file found`);
|
|
71
|
+
skippedCount++;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
let file;
|
|
77
|
+
let ttfAsset;
|
|
78
|
+
let otfAsset;
|
|
79
|
+
|
|
80
|
+
if (fontDoc.fileInput?.ttf?.asset?._ref) {
|
|
81
|
+
ttfAsset = await client.getDocument(fontDoc.fileInput.ttf.asset._ref);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!ttfAsset?.url) {
|
|
85
|
+
if (fontDoc.fileInput?.otf?.asset?._ref) {
|
|
86
|
+
otfAsset = await client.getDocument(fontDoc.fileInput.otf.asset._ref);
|
|
87
|
+
}
|
|
88
|
+
if (!otfAsset?.url) {
|
|
89
|
+
console.log(`Skipping ${fontDoc.title} — no TTF or OTF URL`);
|
|
90
|
+
skippedCount++;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
setStatus(`Fetching OTF file for ${fontDoc.title}...`);
|
|
94
|
+
const res = await fetch(otfAsset.url);
|
|
95
|
+
file = await res.blob();
|
|
96
|
+
} else {
|
|
97
|
+
setStatus(`Fetching TTF file for ${fontDoc.title}...`);
|
|
98
|
+
const res = await fetch(ttfAsset.url);
|
|
99
|
+
file = await res.blob();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const fontBuffer = await readFontFile(file);
|
|
103
|
+
const font = await parseFont(fontBuffer, `${fontDoc._id}.ttf`);
|
|
104
|
+
|
|
105
|
+
const { weightName, subfamilyName, fontTitle } = extractFontMetadata(
|
|
106
|
+
font,
|
|
107
|
+
typefaceName,
|
|
108
|
+
weightKeywordList,
|
|
109
|
+
italicKeywordList,
|
|
110
|
+
preserveShortenedNames,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const slugValue = fontTitle.toLowerCase().replace(/\s+/g, '-');
|
|
114
|
+
|
|
115
|
+
setStatus(`Updating font ${i + 1}/${fontDocuments.length}: ${fontTitle}`);
|
|
116
|
+
await client.patch(fontDoc._id)
|
|
117
|
+
.set({
|
|
118
|
+
title: fontTitle,
|
|
119
|
+
slug: { _type: 'slug', current: slugValue },
|
|
120
|
+
weightName: weightName,
|
|
121
|
+
subfamily: subfamilyName,
|
|
122
|
+
})
|
|
123
|
+
.commit();
|
|
124
|
+
|
|
125
|
+
updatedCount++;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.error(`Error processing ${fontDoc.title}:`, err);
|
|
128
|
+
errorCount++;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const resultMessage = `Renamed ${updatedCount} of ${fontDocuments.length} font documents (${skippedCount} skipped, ${errorCount} errors)`;
|
|
133
|
+
setStatus(resultMessage);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
success: true,
|
|
137
|
+
message: resultMessage,
|
|
138
|
+
stats: { total: fontDocuments.length, updated: updatedCount, skipped: skippedCount, errors: errorCount },
|
|
139
|
+
};
|
|
140
|
+
} catch (err) {
|
|
141
|
+
console.error('Error renaming font documents:', err);
|
|
142
|
+
setError(true);
|
|
143
|
+
setStatus(`Error: ${err.message}`);
|
|
144
|
+
return { success: false, message: `Error: ${err.message}` };
|
|
145
|
+
}
|
|
146
|
+
};
|
|
@@ -1,87 +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
|
-
};
|
|
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
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Lightweight retitle pass — re-derives title, documentId, weightName, and
|
|
2
|
+
// subfamilyName from cached parsedMetadata when preserveShortenedNames changes.
|
|
3
|
+
// No fontkit parsing, no file I/O, no Sanity queries — pure string manipulation.
|
|
4
|
+
|
|
5
|
+
import { expandAbbreviations, generateStyleKeywords } from './generateKeywords.js';
|
|
6
|
+
import { formatFontTitle, addItalicToFontTitle, processSubfamilyName } from './processFontFiles.js';
|
|
7
|
+
import { sanitizeForSanityId } from './sanitizeForSanityId.js';
|
|
8
|
+
|
|
9
|
+
const { weightKeywordList: WEIGHT_KW, italicKeywordList: ITALIC_KW } = generateStyleKeywords();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Re-derive title, documentId, weightName, and subfamilyName for a single
|
|
13
|
+
* font entry using cached metadata and the new preserveShortenedNames setting.
|
|
14
|
+
* Skips entries with user overrides on title (user edits take precedence).
|
|
15
|
+
*
|
|
16
|
+
* @param {object} entry - Font plan entry with parsedMetadata and decisions
|
|
17
|
+
* @param {boolean} preserveShortenedNames - New setting value
|
|
18
|
+
* @param {string} typefaceTitle - Parent typeface title (for prefix stripping)
|
|
19
|
+
* @returns {object} Updated entry (shallow copy if changed, same ref if unchanged)
|
|
20
|
+
*/
|
|
21
|
+
export function retitleFontEntry(entry, preserveShortenedNames, typefaceTitle) {
|
|
22
|
+
if (!entry || !entry.parsedMetadata) return entry;
|
|
23
|
+
|
|
24
|
+
// Skip entries with user overrides on title — user edits take precedence
|
|
25
|
+
if (entry.decisions?.title?.userOverride) return entry;
|
|
26
|
+
|
|
27
|
+
const meta = entry.parsedMetadata;
|
|
28
|
+
const fullName = meta.fullName || '';
|
|
29
|
+
const familyName = meta.familyName || '';
|
|
30
|
+
const trimmedTitle = (typefaceTitle || '').trim();
|
|
31
|
+
|
|
32
|
+
// Re-derive weightName from the original detected value
|
|
33
|
+
let weightName = entry.decisions?.weightName?.detected || entry.weightName || '';
|
|
34
|
+
if (!preserveShortenedNames) {
|
|
35
|
+
weightName = expandAbbreviations(weightName);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Re-derive subfamilyName
|
|
39
|
+
const nameId4Remainder = fullName ? fullName.replace(trimmedTitle, '').trim() : '';
|
|
40
|
+
const nameId1Remainder = familyName ? familyName.replace(trimmedTitle, '').trim() : '';
|
|
41
|
+
let subfamilyName = nameId4Remainder || nameId1Remainder;
|
|
42
|
+
|
|
43
|
+
if (!preserveShortenedNames) {
|
|
44
|
+
subfamilyName = expandAbbreviations(subfamilyName);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
subfamilyName = processSubfamilyName(subfamilyName, WEIGHT_KW, ITALIC_KW, preserveShortenedNames);
|
|
48
|
+
|
|
49
|
+
// Strip style-only names from subfamily
|
|
50
|
+
subfamilyName = subfamilyName
|
|
51
|
+
.replace(/\b(Italic|Slant|Slanted|Oblique|Backslant|Roman|Upright)\b/gi, '')
|
|
52
|
+
.replace(/\s+/g, ' ')
|
|
53
|
+
.trim();
|
|
54
|
+
|
|
55
|
+
// Strip subfamily from weightName to avoid duplication
|
|
56
|
+
if (subfamilyName !== '') {
|
|
57
|
+
weightName = weightName
|
|
58
|
+
.replace(`${subfamilyName} `, '')
|
|
59
|
+
.replace(` ${subfamilyName}`, '')
|
|
60
|
+
.trim();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Re-derive title
|
|
64
|
+
let fontTitle = fullName.trim() || '';
|
|
65
|
+
fontTitle = formatFontTitle(fontTitle, preserveShortenedNames);
|
|
66
|
+
|
|
67
|
+
// Variable fonts get " VF" suffix
|
|
68
|
+
if (entry.variableFont) {
|
|
69
|
+
if (!fontTitle.toLowerCase().includes('vf')) {
|
|
70
|
+
fontTitle = fontTitle + ' VF';
|
|
71
|
+
}
|
|
72
|
+
subfamilyName = '';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Add italic suffix if needed
|
|
76
|
+
if (!(entry.variableFont && fontTitle.toLowerCase().includes('italic'))) {
|
|
77
|
+
// Build a minimal font-like object for addItalicToFontTitle
|
|
78
|
+
const italicKW = entry.parsedMetadata.italicAngle !== 0 ? 'Italic' : '';
|
|
79
|
+
fontTitle = addItalicToFontTitle(
|
|
80
|
+
{ opentype: { tables: { post: { italicAngle: entry.parsedMetadata.italicAngle || 0 } } } },
|
|
81
|
+
fontTitle,
|
|
82
|
+
italicKW,
|
|
83
|
+
entry.style,
|
|
84
|
+
preserveShortenedNames
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const documentId = sanitizeForSanityId(fontTitle);
|
|
89
|
+
|
|
90
|
+
// Only return a new object if something actually changed
|
|
91
|
+
if (
|
|
92
|
+
entry.title === fontTitle &&
|
|
93
|
+
entry.documentId === documentId &&
|
|
94
|
+
entry.weightName === weightName &&
|
|
95
|
+
entry.subfamily === (entry.variableFont ? '' : (subfamilyName || 'Regular'))
|
|
96
|
+
) {
|
|
97
|
+
return entry;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const finalSubfamily = entry.variableFont ? '' : (subfamilyName || 'Regular');
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
...entry,
|
|
104
|
+
title: fontTitle,
|
|
105
|
+
documentId,
|
|
106
|
+
weightName,
|
|
107
|
+
subfamily: finalSubfamily,
|
|
108
|
+
decisions: {
|
|
109
|
+
...entry.decisions,
|
|
110
|
+
title: { ...entry.decisions.title, processed: fontTitle },
|
|
111
|
+
documentId: { ...entry.decisions.documentId, generated: documentId },
|
|
112
|
+
weightName: { ...entry.decisions.weightName, detected: weightName },
|
|
113
|
+
subfamily: { ...entry.decisions.subfamily, detected: finalSubfamily },
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Retitle all font entries in a plan. Returns a new fonts map.
|
|
120
|
+
* Runs collision detection after retitling.
|
|
121
|
+
*
|
|
122
|
+
* @param {object} fonts - plan.fonts map
|
|
123
|
+
* @param {boolean} preserveShortenedNames
|
|
124
|
+
* @param {string} typefaceTitle
|
|
125
|
+
* @returns {object} New fonts map with updated titles and _idConflict flags
|
|
126
|
+
*/
|
|
127
|
+
export function retitleAllFonts(fonts, preserveShortenedNames, typefaceTitle) {
|
|
128
|
+
const updated = {};
|
|
129
|
+
for (const [tempId, entry] of Object.entries(fonts)) {
|
|
130
|
+
updated[tempId] = retitleFontEntry(entry, preserveShortenedNames, typefaceTitle);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Run collision detection
|
|
134
|
+
const idMap = {};
|
|
135
|
+
for (const [tempId, font] of Object.entries(updated)) {
|
|
136
|
+
updated[tempId] = { ...font, _idConflict: false };
|
|
137
|
+
const docId = font.documentId;
|
|
138
|
+
if (!idMap[docId]) {
|
|
139
|
+
idMap[docId] = [tempId];
|
|
140
|
+
} else {
|
|
141
|
+
idMap[docId].push(tempId);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const ids of Object.values(idMap)) {
|
|
146
|
+
if (ids.length > 1) {
|
|
147
|
+
for (const id of ids) {
|
|
148
|
+
updated[id] = { ...updated[id], _idConflict: true };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return updated;
|
|
154
|
+
}
|