@liiift-studio/sanity-font-manager 2.2.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/README.md +263 -0
- package/dist/index.js +3699 -0
- package/dist/index.mjs +3622 -0
- package/package.json +77 -0
- package/src/components/BatchUploadFonts.jsx +639 -0
- package/src/components/FontScriptUploaderComponent.jsx +463 -0
- package/src/components/GenerateCollectionsPairsComponent.jsx +259 -0
- package/src/components/PriceInput.jsx +26 -0
- package/src/components/RegenerateSubfamiliesComponent.jsx +185 -0
- package/src/components/SingleUploaderTool.jsx +673 -0
- package/src/components/StatusDisplay.jsx +26 -0
- package/src/components/UpdateScriptsComponent.jsx +76 -0
- package/src/components/UploadButton.jsx +43 -0
- package/src/components/UploadScriptsComponent.jsx +537 -0
- package/src/hooks/useSanityClient.js +9 -0
- package/src/index.js +56 -0
- package/src/utils/generateCssFile.js +197 -0
- package/src/utils/generateFontData.js +145 -0
- package/src/utils/generateFontFile.js +38 -0
- package/src/utils/generateKeywords.js +185 -0
- package/src/utils/generateSubset.js +45 -0
- package/src/utils/getEmptyFontKit.js +99 -0
- package/src/utils/parseVariableFontInstances.js +211 -0
- package/src/utils/processFontFiles.js +477 -0
- package/src/utils/regenerateFontData.js +146 -0
- package/src/utils/sanitizeForSanityId.js +65 -0
- package/src/utils/updateFontPrices.js +94 -0
- package/src/utils/updateTypefaceDocument.js +160 -0
- package/src/utils/uploadFontFiles.js +316 -0
- package/src/utils/utils.js +16 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Returns a zeroed-out fontkit-shaped placeholder object used when no font binary is available
|
|
2
|
+
import * as fontkit from 'fontkit';
|
|
3
|
+
import slugify from 'slugify';
|
|
4
|
+
|
|
5
|
+
/** Reads font files and returns name/subfamily metadata without writing to Sanity */
|
|
6
|
+
export async function getEmptyFontKit({ title, files, weightKeywordList, italicKeywordList }) {
|
|
7
|
+
|
|
8
|
+
let fontNames = {};
|
|
9
|
+
let subfamilies = {};
|
|
10
|
+
|
|
11
|
+
for (var i = 0; i < files.length; i++) {
|
|
12
|
+
|
|
13
|
+
const file = files[i];
|
|
14
|
+
const fontBuffer = await readFontFile(file);
|
|
15
|
+
const font = fontkit.create(fontBuffer);
|
|
16
|
+
|
|
17
|
+
let weightName = font?.name?.records?.preferredSubfamily ? font?.name?.records?.preferredSubfamily : font?.name?.records?.fontSubfamily;
|
|
18
|
+
weightName = weightName?.en ? weightName.en : weightName.constructor == Object ? weightName[Object.keys(weightName)[0]] : weightName;
|
|
19
|
+
|
|
20
|
+
let variableFont = font?.variationAxes && Object.keys(font.variationAxes).length > 0 ? true : false;
|
|
21
|
+
let subfamilyName = font.familyName.toLowerCase().trim().replace(title.toLowerCase().trim(), '').trim();
|
|
22
|
+
let fontTitle = font?.fullName.toLowerCase().trim();
|
|
23
|
+
|
|
24
|
+
weightKeywordList.forEach(keyword => {
|
|
25
|
+
const kw = keyword.toLowerCase().trim();
|
|
26
|
+
|
|
27
|
+
if (fontTitle.includes(kw)) {
|
|
28
|
+
fontTitle = fontTitle.replace(kw, '');
|
|
29
|
+
}
|
|
30
|
+
if (subfamilyName.includes(kw)) {
|
|
31
|
+
subfamilyName = subfamilyName.replace(kw, '');
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
italicKeywordList.forEach(keyword => {
|
|
36
|
+
const kw = keyword.toLowerCase().trim();
|
|
37
|
+
|
|
38
|
+
if (subfamilyName.includes(kw)) {
|
|
39
|
+
subfamilyName = subfamilyName.replace(kw, '');
|
|
40
|
+
}
|
|
41
|
+
if (fontTitle.includes(kw)) {
|
|
42
|
+
fontTitle = fontTitle.replace(kw, '');
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
fontTitle = fontTitle.trim().split(' ').map(word => word[0].toUpperCase() + word.slice(1)).join(' ');
|
|
47
|
+
|
|
48
|
+
subfamilyName = subfamilyName.trim();
|
|
49
|
+
subfamilyName = (subfamilyName == '') ? 'Regular' : subfamilyName.split(' ').map(word => word[0].toUpperCase() + word.slice(1)).join(' ');
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
let id = slugify(fontTitle.toLowerCase().trim());
|
|
53
|
+
if (variableFont && !id.endsWith('-vf')) {
|
|
54
|
+
id = id + '-vf';
|
|
55
|
+
fontTitle = fontTitle + ' VF';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// add subfamily to list
|
|
59
|
+
if (!subfamilies[id]) {
|
|
60
|
+
subfamilies[id] = [subfamilyName];
|
|
61
|
+
} else if (subfamilies[id].indexOf(subfamilyName) == -1) {
|
|
62
|
+
subfamilies[id] = [...subfamilies[id], subfamilyName];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!fontNames[id]) {
|
|
66
|
+
fontNames[id] = [{
|
|
67
|
+
file: file.name,
|
|
68
|
+
fullName: font.fullName,
|
|
69
|
+
familyName: font.familyName,
|
|
70
|
+
subFamily: subfamilyName,
|
|
71
|
+
}];
|
|
72
|
+
} else if (fontNames[id].indexOf(file.name) == -1) {
|
|
73
|
+
fontNames[id].push({
|
|
74
|
+
file: file.name,
|
|
75
|
+
fullName: font.fullName,
|
|
76
|
+
familyName: font.familyName,
|
|
77
|
+
subFamily: subfamilyName,
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log('font names : ', fontNames);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Reads a font file and returns its content as a Uint8Array */
|
|
88
|
+
const readFontFile = (file) => {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
const reader = new FileReader();
|
|
91
|
+
|
|
92
|
+
reader.onload = (event) => {
|
|
93
|
+
resolve(new Uint8Array(event.target.result));
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
reader.onerror = (error) => { reject(error); };
|
|
97
|
+
reader.readAsArrayBuffer(file);
|
|
98
|
+
});
|
|
99
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// Resolves named variable font instances into Sanity font document references, creating documents for missing instances
|
|
2
|
+
|
|
3
|
+
import { nanoid } from 'nanoid';
|
|
4
|
+
import { expandAbbreviations } from './generateKeywords';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parses a variable font's named instances and maps each to its corresponding static font document.
|
|
8
|
+
* Uses 6 matching strategies in priority order:
|
|
9
|
+
* 1. Exact title match
|
|
10
|
+
* 2. Title normalisation (strips VF/var/variable prefixes, handles Regular/Italic suffixes)
|
|
11
|
+
* 3. Abbreviation expansion
|
|
12
|
+
* 4. Weight + style matching
|
|
13
|
+
* 5. weightName comparison
|
|
14
|
+
* 6. metaData.fullName fallback
|
|
15
|
+
*
|
|
16
|
+
* @param {Object} font - The variable font object (must have typefaceName and variableInstances)
|
|
17
|
+
* @param {Object} client - Sanity client (parameterized queries only)
|
|
18
|
+
* @returns {Promise<Array>} Array of { key, value, _key } instance mappings
|
|
19
|
+
*/
|
|
20
|
+
export const parseVariableFontInstances = async (font, client) => {
|
|
21
|
+
if (!font.variableFont || !font.variableInstances) return [];
|
|
22
|
+
|
|
23
|
+
let variableInstances;
|
|
24
|
+
try {
|
|
25
|
+
variableInstances = JSON.parse(font.variableInstances);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.error('Error parsing variable instances:', err);
|
|
28
|
+
variableInstances = {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (Object.keys(variableInstances).length === 0) return [];
|
|
32
|
+
|
|
33
|
+
// Fetch the typeface's curated font list (parameterized — no injection risk)
|
|
34
|
+
let staticFonts;
|
|
35
|
+
const typeface = await client.fetch(
|
|
36
|
+
`*[_type == 'typeface' && title == $typefaceName][0]{
|
|
37
|
+
'fonts': styles.fonts[]-> {
|
|
38
|
+
_id,
|
|
39
|
+
title,
|
|
40
|
+
subfamily,
|
|
41
|
+
style,
|
|
42
|
+
weight,
|
|
43
|
+
weightName,
|
|
44
|
+
metaData,
|
|
45
|
+
variableFont
|
|
46
|
+
}
|
|
47
|
+
}`,
|
|
48
|
+
{ typefaceName: font.typefaceName }
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (typeface?.fonts && typeface.fonts.length > 0) {
|
|
52
|
+
staticFonts = typeface.fonts.filter(f => !f.variableFont);
|
|
53
|
+
console.log('Using curated typeface fonts list:', staticFonts.length, 'fonts');
|
|
54
|
+
} else {
|
|
55
|
+
console.warn('Typeface not found or no fonts in curated list, falling back to all fonts query');
|
|
56
|
+
staticFonts = await client.fetch(
|
|
57
|
+
`*[_type == 'font' && typefaceName == $typefaceName && variableFont != true]{
|
|
58
|
+
_id,
|
|
59
|
+
title,
|
|
60
|
+
subfamily,
|
|
61
|
+
style,
|
|
62
|
+
weight,
|
|
63
|
+
weightName,
|
|
64
|
+
metaData
|
|
65
|
+
}`,
|
|
66
|
+
{ typefaceName: font.typefaceName }
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log('Variable font instances:', Object.keys(variableInstances));
|
|
71
|
+
console.log('Available static fonts:', staticFonts.map(sf => sf.title));
|
|
72
|
+
|
|
73
|
+
const instanceMappings = [];
|
|
74
|
+
|
|
75
|
+
Object.keys(variableInstances).forEach(instanceName => {
|
|
76
|
+
let matchingFont = null;
|
|
77
|
+
|
|
78
|
+
// Strategy 1: Exact title match
|
|
79
|
+
matchingFont = staticFonts.find(sf => sf.title === instanceName);
|
|
80
|
+
|
|
81
|
+
// Strategy 2: Title normalisation — strip VF/var/variable prefix words, handle Regular/Italic
|
|
82
|
+
if (!matchingFont && staticFonts.some(sf => sf.metaData?.fullName)) {
|
|
83
|
+
matchingFont = staticFonts.find(sf => {
|
|
84
|
+
if (!sf.metaData?.fullName) return false;
|
|
85
|
+
|
|
86
|
+
let fullName = sf.metaData.fullName;
|
|
87
|
+
|
|
88
|
+
const WORDS_TO_REMOVE = ['VF', 'var', 'variable', 'VAR', 'vf'];
|
|
89
|
+
const variableName = font.metaData?.familyName
|
|
90
|
+
?.replace(new RegExp(`\\b(${WORDS_TO_REMOVE.join('|')})\\b`, 'gi'), '')
|
|
91
|
+
.replace(/\s{2,}/g, ' ')
|
|
92
|
+
.trim();
|
|
93
|
+
|
|
94
|
+
if (variableName && fullName.startsWith(variableName)) {
|
|
95
|
+
fullName = fullName.substring(variableName.length).trim();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (variableName) {
|
|
99
|
+
const words = variableName.split(/\s+/).map(w => w.trim()).filter(Boolean);
|
|
100
|
+
if (words.length > 0) {
|
|
101
|
+
const regex = new RegExp(`\\b(${words.join('|')})\\b`, 'gi');
|
|
102
|
+
const stripped = fullName.replace(regex, '').replace(/\s{2,}/g, ' ').trim();
|
|
103
|
+
if (stripped !== '') fullName = stripped;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (fullName.startsWith(font.typefaceName)) {
|
|
108
|
+
fullName = fullName.substring(font.typefaceName.length).trim();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (sf.style?.toLowerCase() === 'italic' &&
|
|
112
|
+
!fullName.toLowerCase().endsWith('italic') &&
|
|
113
|
+
!fullName.toLowerCase().endsWith('slanted')) {
|
|
114
|
+
fullName = fullName + ' Italic';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (fullName.trim().toLowerCase().endsWith('regular')) {
|
|
118
|
+
if (instanceName.trim().toLowerCase() + ' regular' === fullName.trim().toLowerCase()) return true;
|
|
119
|
+
}
|
|
120
|
+
if (fullName.trim().toLowerCase().startsWith('regular')) {
|
|
121
|
+
if ('regular ' + instanceName.trim().toLowerCase() === fullName.trim().toLowerCase()) return true;
|
|
122
|
+
}
|
|
123
|
+
if (fullName.trim().toLowerCase().endsWith('italic')) {
|
|
124
|
+
if (instanceName.trim().toLowerCase().endsWith('italic')) {
|
|
125
|
+
const k = instanceName.trim().toLowerCase().slice(0, -6).trim() + ' regular italic';
|
|
126
|
+
if (k === fullName.trim().toLowerCase()) return true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return fullName.trim().toLowerCase() === instanceName.trim().toLowerCase();
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Strategy 3: Abbreviation expansion
|
|
135
|
+
if (!matchingFont) {
|
|
136
|
+
const expandedName = instanceName.split(' ').map(word => expandAbbreviations(word)).join(' ');
|
|
137
|
+
matchingFont = staticFonts.find(sf => {
|
|
138
|
+
const nameWithoutTypeface = sf.title.replace(font.typefaceName, '').trim();
|
|
139
|
+
return nameWithoutTypeface === expandedName;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Strategy 4: Weight + style matching
|
|
144
|
+
if (!matchingFont) {
|
|
145
|
+
const isItalic = instanceName.toLowerCase().includes('italic');
|
|
146
|
+
const weightTerms = [
|
|
147
|
+
{ term: 'thin', weight: '100' },
|
|
148
|
+
{ term: 'extralight', weight: '200' },
|
|
149
|
+
{ term: 'extra light', weight: '200' },
|
|
150
|
+
{ term: 'light', weight: '300' },
|
|
151
|
+
{ term: 'regular', weight: '400' },
|
|
152
|
+
{ term: 'normal', weight: '400' },
|
|
153
|
+
{ term: 'medium', weight: '500' },
|
|
154
|
+
{ term: 'semibold', weight: '600' },
|
|
155
|
+
{ term: 'semi bold', weight: '600' },
|
|
156
|
+
{ term: 'bold', weight: '700' },
|
|
157
|
+
{ term: 'extrabold', weight: '800' },
|
|
158
|
+
{ term: 'extra bold', weight: '800' },
|
|
159
|
+
{ term: 'black', weight: '900' },
|
|
160
|
+
{ term: 'heavy', weight: '900' },
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
let instanceWeight = '400';
|
|
164
|
+
for (const { term, weight } of weightTerms) {
|
|
165
|
+
if (instanceName.toLowerCase().includes(term)) {
|
|
166
|
+
instanceWeight = weight;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
matchingFont = staticFonts.find(sf =>
|
|
172
|
+
sf.weight === instanceWeight &&
|
|
173
|
+
((isItalic && sf.style === 'Italic') || (!isItalic && sf.style === 'Regular'))
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Strategy 5: weightName comparison
|
|
178
|
+
if (!matchingFont) {
|
|
179
|
+
matchingFont = staticFonts.find(sf => {
|
|
180
|
+
if (!sf.weightName) return false;
|
|
181
|
+
const cleanInstance = instanceName.toLowerCase().replace(/italic/i, '').trim();
|
|
182
|
+
const cleanWeight = sf.weightName.toLowerCase().replace(/italic/i, '').trim();
|
|
183
|
+
return cleanInstance === cleanWeight;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Strategy 6: Legacy metaData.fullName fallback
|
|
188
|
+
if (!matchingFont && staticFonts.some(sf => sf.metaData?.fullName)) {
|
|
189
|
+
matchingFont = staticFonts.find(sf => {
|
|
190
|
+
if (!sf.metaData?.fullName) return false;
|
|
191
|
+
const typefacePattern = new RegExp(`^${font.typefaceName}\\s+`, 'i');
|
|
192
|
+
const stylePart = sf.metaData.fullName.replace(typefacePattern, '').trim();
|
|
193
|
+
return instanceName.toLowerCase() === stylePart.toLowerCase();
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
console.log(`Instance "${instanceName}" matched with:`, matchingFont ? matchingFont.title : 'No match found');
|
|
198
|
+
|
|
199
|
+
instanceMappings.push({
|
|
200
|
+
key: instanceName,
|
|
201
|
+
value: matchingFont
|
|
202
|
+
? { _type: 'reference', _ref: matchingFont._id, _weak: true }
|
|
203
|
+
: null,
|
|
204
|
+
_key: nanoid(),
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return instanceMappings;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export default parseVariableFontInstances;
|