@liiift-studio/sanity-font-manager 2.3.18 → 2.4.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.
Files changed (42) hide show
  1. package/README.md +437 -437
  2. package/dist/index.js +103 -48
  3. package/dist/index.mjs +103 -48
  4. package/package.json +85 -85
  5. package/src/components/BatchUploadFonts.jsx +640 -639
  6. package/src/components/FontScriptUploaderComponent.jsx +463 -463
  7. package/src/components/GenerateCollectionsPairsComponent.jsx +259 -259
  8. package/src/components/KeyValueInput.jsx +95 -95
  9. package/src/components/KeyValueReferenceInput.jsx +254 -254
  10. package/src/components/NestedObjectArraySelector.jsx +146 -146
  11. package/src/components/PriceInput.jsx +26 -26
  12. package/src/components/PrimaryCollectionGeneratorTypeface.jsx +116 -116
  13. package/src/components/RegenerateSubfamiliesComponent.jsx +185 -185
  14. package/src/components/SetOTF.jsx +87 -87
  15. package/src/components/SingleUploaderTool.jsx +673 -673
  16. package/src/components/StatusDisplay.jsx +26 -26
  17. package/src/components/StyleCountInput.jsx +16 -16
  18. package/src/components/UpdateScriptsComponent.jsx +76 -76
  19. package/src/components/UploadButton.jsx +43 -43
  20. package/src/components/UploadScriptsComponent.jsx +537 -537
  21. package/src/components/VariableInstanceReferencesInput.jsx +190 -190
  22. package/src/hooks/useNestedObjects.js +92 -92
  23. package/src/hooks/useSanityClient.js +9 -9
  24. package/src/index.js +70 -70
  25. package/src/schema/openTypeField.js +1945 -1945
  26. package/src/schema/styleCountField.js +12 -12
  27. package/src/schema/stylesField.js +268 -268
  28. package/src/schema/stylisticSetField.js +301 -301
  29. package/src/utils/generateCssFile.js +205 -205
  30. package/src/utils/generateFontData.js +145 -145
  31. package/src/utils/generateFontFile.js +38 -38
  32. package/src/utils/generateKeywords.js +185 -185
  33. package/src/utils/generateSubset.js +45 -45
  34. package/src/utils/getEmptyFontKit.js +99 -99
  35. package/src/utils/parseVariableFontInstances.js +211 -211
  36. package/src/utils/processFontFiles.js +487 -477
  37. package/src/utils/regenerateFontData.js +146 -146
  38. package/src/utils/sanitizeForSanityId.js +65 -65
  39. package/src/utils/updateFontPrices.js +94 -94
  40. package/src/utils/updateTypefaceDocument.js +149 -160
  41. package/src/utils/uploadFontFiles.js +115 -26
  42. package/src/utils/utils.js +24 -24
@@ -1,537 +1,537 @@
1
- // Batch uploader for script-specific font variants across multiple fonts at once
2
-
3
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
4
- import { Button, Flex, Grid, Stack, Text, TextInput, MenuButton, Menu, MenuItem, Select } from '@sanity/ui';
5
- import * as fontkit from 'fontkit';
6
- import slugify from 'slugify';
7
- import { useSanityClient } from '../hooks/useSanityClient';
8
- import { useFormValue } from 'sanity';
9
- import { nanoid } from 'nanoid';
10
- import generateCssFile from '../utils/generateCssFile';
11
- import { generateStyleKeywords, reverseSpellingLookup } from '../utils/generateKeywords';
12
- import { SCRIPTS } from '../utils/utils';
13
-
14
- /**
15
- * Component for uploading and managing script variants of fonts
16
- * @param {Object} props - Component props
17
- * @param {Object} props.elementProps - Element properties including ref
18
- * @returns {JSX.Element} Upload interface for script variants
19
- */
20
- export const UploadScriptsComponent = (props) => {
21
-
22
- // Props and client initialization
23
- const {elementProps: {ref}} = props;
24
- const client = useSanityClient();
25
-
26
- // Component state
27
- const [selectedScript, setSelectedScript] = useState(""); // Currently selected script
28
- const [status, setStatus] = React.useState(''); // Upload status message
29
- const [ready, setReady] = React.useState(true); // Component ready state
30
-
31
- // Form values from Sanity
32
- let doc_id = useFormValue(['_id']); // Document ID
33
- const title = useFormValue(['title']); // Typeface title
34
- const slug = useFormValue(['slug']); // URL slug
35
- const scripts = useFormValue(['scripts']) || []; // Supported scripts
36
- const stylesObject = useFormValue(['styles']); // Font styles data
37
- let subfamiliesArray = stylesObject?.subfamilies || []; // Font subfamilies
38
-
39
- // Memoized style keywords for font processing
40
- const {weightKeywordList, italicKeywordList} = useMemo(() =>
41
- generateStyleKeywords()
42
- , []);
43
-
44
- /**
45
- * Reads a font file and returns its content as a Uint8Array
46
- * @param {File} file - The font file to read
47
- * @returns {Promise<Uint8Array>} Font file content
48
- */
49
- const readFontFile = (file) => {
50
- return new Promise((resolve, reject) => {
51
- const reader = new FileReader();
52
-
53
- reader.onload = (event) => {
54
- resolve(new Uint8Array(event.target.result));
55
- };
56
-
57
- reader.onerror = (error) => { reject(error); };
58
- reader.readAsArrayBuffer(file);
59
- });
60
- };
61
-
62
-
63
- /**
64
- * Handles the upload and processing of font files for a specific script
65
- * @param {Event} event - The file input change event
66
- * @param {string} script - The selected script variant (e.g., 'cyrillic', 'greek')
67
- */
68
- const handleUpload = useCallback(async(event, script) => {
69
- setReady(false);
70
- try{
71
- let failedFiles = [];
72
-
73
- console.log('handle upload ', title, script );
74
- setStatus('uploading fonts files.. ');
75
-
76
- if(!title) {
77
- console.error('typeface needs title');
78
- return;
79
- }
80
-
81
- let fontRefs = [];
82
- let variableRefs = [];
83
- let subfamilies = {};
84
- let fontsObjects = {};
85
-
86
- // read font files ,
87
- // create if doesnt exist - create sanity fontObjects template
88
- // add font file to sanity font
89
- // create subfamily list
90
- for(var i = 0 ; i < event.target.files.length ; i++ ){
91
-
92
- const file = event.target.files[i];
93
- const fontBuffer = await readFontFile(file);
94
- const font = fontkit.create(fontBuffer);
95
-
96
- console.log('reading font : ', font.fullName +' '+file.name, font.name.records);
97
-
98
- let weightName = font?.name?.records?.preferredSubfamily ? font?.name?.records?.preferredSubfamily: font?.name?.records?.fontSubfamily;
99
- weightName = weightName?.en ? weightName.en: weightName.constructor == Object ? weightName[Object.keys(weightName)[0]] : weightName;
100
- weightName = weightName?.replace("Italic", "").replace("It", "").trim();
101
-
102
- if ((weightName == '' || weightName.toLowerCase() == 'roman') && font?.name?.records?.fullName) {
103
- weightName = font?.name?.records?.fullName;
104
- weightName = weightName?.en ? weightName.en: weightName.constructor == Object ? weightName[Object.keys(weightName)[0]] : weightName;
105
- weightName = weightName?.replace(title + " ", "").replace(title, "").trim();
106
- weightName = weightName?.replace("Italic", "").replace("It", "").trim();
107
- }
108
-
109
- let variableFont = font?.variationAxes && Object.keys(font.variationAxes).length > 0 ? true: false;
110
- let subfamilyName = font.familyName.toLowerCase().trim().replace(title.toLowerCase().trim(),'').trim();
111
- let fontTitle = font?.fullName;
112
- let style = (font?.italicAngle !== 0 || font?.fullName.toLowerCase().includes('italic')) ? 'Italic' : 'Regular';
113
-
114
- if(fontTitle.toLowerCase().trim().includes(script)){
115
- fontTitle = fontTitle.toLowerCase().trim().replace(script, '').trim();
116
- fontTitle = fontTitle.split(' ').map( word => {
117
- if( word == '') return
118
- return word;
119
- })
120
- .filter( word => word != undefined)
121
- .join(' ');
122
- }
123
-
124
- // remove weight and italic keywords from subfamily name
125
- weightKeywordList.forEach( keyword => {
126
- const kw = keyword.trim();
127
- if(subfamilyName.includes(kw)) subfamilyName = subfamilyName.replace(kw, '').trim();
128
-
129
- // if(fontTitle.includes(kw)){
130
- // fontTitle = fontTitle.replace(kw, '');
131
- // }
132
- });
133
-
134
- let italicKW = [];
135
- italicKeywordList.forEach( keyword => {
136
- const kw = keyword.toLowerCase().trim();
137
- if(subfamilyName.includes(kw)){
138
- subfamilyName = subfamilyName.replace(kw, '');
139
- }
140
-
141
- if(fontTitle.includes(kw)){
142
- fontTitle = fontTitle.replace(kw, '');
143
- italicKW.push(kw.charAt(0).toUpperCase() + kw.slice(1));
144
-
145
- }
146
- });
147
-
148
- fontTitle = fontTitle.replace(/-/g, ' ');
149
- fontTitle = fontTitle.trim().split(' ').map( word => word[0].toUpperCase() + word.slice(1)).join(' ');
150
-
151
- if(subfamilyName.trim().includes(script)){
152
- subfamilyName = subfamilyName.trim().replace(script, '').trim();
153
- }
154
-
155
- subfamilyName = subfamilyName.trim();
156
- subfamilyName = (subfamilyName == '' ) ? 'Regular' : subfamilyName.split(' ').map( word => word[0].toUpperCase() + word.slice(1)).join(' ');
157
-
158
- // remove subfamily from weight name
159
- if (subfamilyName !== '' ) {
160
- weightName = weightName
161
- .replace(`${subfamilyName} `, '')
162
- .replace(` ${subfamilyName}`, '')
163
- .trim();
164
- }
165
-
166
- if(variableFont && !fontTitle.toLowerCase().trim().endsWith(' vf')) fontTitle = fontTitle + ' VF';
167
-
168
- if(italicKW.length > 0){
169
- italicKW = italicKW.map( item => reverseSpellingLookup(item)); // replace each item in the italicKW list with the value in reverseSpellingLookup
170
- fontTitle = fontTitle + italicKW.join(' ');
171
- style = 'Italic';
172
- }
173
-
174
- let id = slugify(fontTitle.toLowerCase().trim());
175
-
176
- console.log('=== Font Info ====');
177
- console.log(' ')
178
- console.log('font id : ', id);
179
- console.log('font title : ', fontTitle);
180
- console.log('fontkit fullName : ', font.fullName );
181
- console.log('fontkit family name: ', font.familyName);
182
- console.log('file name : ', file.name);
183
- console.log('subfamily : ', subfamilyName);
184
- console.log('style : ', style);
185
- console.log('weight : ', weightName);
186
- console.log('variable : ', variableFont);
187
- console.log('italicKW ', italicKW);
188
- console.log(' ')
189
- console.log('=======');
190
-
191
- subfamilies[id] = subfamilyName; // add subfamily to list
192
-
193
- if( fontsObjects[id]){
194
- fontsObjects[id].files = [...fontsObjects[id].files, file];
195
- } else {
196
- let fontObject = {
197
- _key: nanoid(),
198
- _id: id,
199
- title: fontTitle,
200
- slug: {_type:'slug', current:id},
201
- typefaceName: title, // Change to match Typeface Document
202
- style: (font?.italicAngle !== 0 || font?.fullName.toLowerCase().includes('italic')) ? 'Italic' : 'Regular',
203
- variableFont: variableFont,
204
- weightName: weightName,
205
- normalWeight:true, // TODO : check if weight is normal ??
206
- weight: font['OS/2']?.usWeightClass ? Number(font['OS/2']?.usWeightClass) :
207
- /hairline|extra thin|extrathin/.test(weightName?.toLowerCase()) ? 100 :
208
- /thin|extra light|extralight/.test(weightName?.toLowerCase()) ? 200 :
209
- /light|book/.test(weightName?.toLowerCase()) ? 300 :
210
- /regular|normal/.test(weightName?.toLowerCase()) ? 400 :
211
- /medium/.test(weightName?.toLowerCase()) ? 500 :
212
- /semi bold|semibold/.test(weightName?.toLowerCase()) ? 600 :
213
- /bold/.test(weightName?.toLowerCase()) ? 700 :
214
- /extra bold|extrabold/.test(weightName?.toLowerCase()) ? 800 :
215
- /black|ultra/.test(weightName?.toLowerCase()) ? 900 :
216
- 400,
217
- files : [file],
218
- fontKit: font,
219
- scriptFileInput: {[script]:{}},
220
- };
221
- fontsObjects[id] = fontObject;
222
- }
223
- }
224
-
225
- // Extract unique subfamily names and prepare for processing
226
- let uniqueSubfamiles = [...new Set(Object.values(subfamilies))];
227
-
228
- console.log('Subfamilies : ', subfamilies, uniqueSubfamiles, uniqueSubfamiles.length);
229
- console.log('fontsObjects : ', fontsObjects);
230
-
231
- // Process each font object:
232
- // 1. Upload font files as Sanity assets
233
- // 2. Create file references linking fonts to assets
234
- // 3. Generate CSS for web fonts
235
- for(var i = 0 ; i < Object.keys(fontsObjects).length ; i++ ){
236
-
237
- let id = Object.keys(fontsObjects)[i];
238
- let fontObject = fontsObjects[id];
239
- let files = fontObject.files;
240
- let newFileInput = fontObject.scriptFileInput[script];
241
-
242
- console.log(fontObject.title , ' : subfamily : ', subfamilies[id]);
243
-
244
- // add subfamily to font object if more than one exists
245
- if(uniqueSubfamiles.length > 1) fontObject.subfamily = subfamilies[id];
246
- else fontObject.subfamily = '';
247
-
248
- // add price to font object - set sell = true if there is a price > 0
249
- fontObject.price = process.env.SANITY_STUDIO_DEFAULT_STYLE_PRICE || 40;
250
- if(fontObject.price > 0) fontObject.sell = true;
251
-
252
- // upload files
253
- for(var j = 0 ; j < files.length ; j++ ){
254
- let file = files[j];
255
- let fileType = "";
256
- if ( file.name.endsWith('.otf') ) fileType = "otf"
257
- else if ( file.name.endsWith('.ttf') ) fileType = "ttf"
258
- else if ( file.name.endsWith('.woff') ) fileType = "woff"
259
- else if ( file.name.endsWith('.woff2') ) fileType = "woff2"
260
- else if ( file.name.endsWith('.eot') ) fileType = "eot"
261
- else if ( file.name.endsWith('.svg') ) fileType = "svg"
262
-
263
- console.log('uploading font file : ', fontObject._id+'.'+fileType);
264
- const filename = fontObject._id+'-'+script;
265
- let fontTitle = fontObject.title+' '+script;
266
- fontTitle = fontTitle.split(' ').map( word => word[0].toUpperCase() + word.slice(1)).join(' ');
267
-
268
- let baseAsset = await client.assets.upload('file', file, { filename: filename+'.'+fileType })
269
- .catch( err => {
270
- console.error('error uploading font: ', fontObject.title);
271
- setStatus('error uploading font ' + err.message);
272
- });
273
-
274
- // create file ref from font
275
- newFileInput[fileType] = {
276
- _type: 'file',
277
- asset: {
278
- _ref: baseAsset._id,
279
- _type: 'reference'
280
- }
281
- }
282
-
283
- console.log('newFileInput', newFileInput);
284
-
285
- // generate css
286
- if(file.name.endsWith('.woff2')){
287
- console.log('generating css file for: ', fontObject.title);
288
- setStatus('generating css file for: ' + fontObject.title);
289
- newFileInput = await generateCssFile({
290
- woff2File: file,
291
- fileInput: newFileInput,
292
- // script: script,
293
- fontName: fontTitle,
294
- fileName: filename,
295
- variableFont: fontObject.variableFont,
296
- weight: fontObject.weight,
297
- client: client,
298
- });
299
- }
300
-
301
- fontObject.scriptFileInput[script] = newFileInput;
302
- fontsObjects[id] = fontObject;
303
-
304
- }
305
- }
306
-
307
- console.log('creating sanity fonts', fontsObjects);
308
-
309
-
310
- // create (with existing data if exists ) fonts and refs (for typeface)
311
- for(var i = 0 ; i < Object.keys(fontsObjects).length ; i++ ){
312
- let fontId = Object.keys(fontsObjects)[i];
313
- let font = fontsObjects[fontId];
314
-
315
- // add existing file refs to new file input
316
- let existingFont = await client.fetch(
317
- `*[_type == 'font' && _id == $fontId]{
318
- fileInput,
319
- description,
320
- metaData,
321
- metrics,
322
- opentypeFeatures,
323
- characterSet,
324
- subfamily,
325
- scriptFileInput,
326
- }`,
327
- { fontId: font._id }
328
- );
329
-
330
- existingFont = existingFont[0];
331
-
332
- let fontResponse;
333
- let files = font.files;
334
- let fontKit = font.fontKit;
335
- delete font.files;
336
- delete font.fontKit;
337
-
338
- console.log('creating font : ', font);
339
-
340
- try{
341
- if(existingFont && existingFont != null){
342
-
343
- if(existingFont.scriptFileInput && existingFont.scriptFileInput != null){
344
- let newFileInput = {...font.scriptFileInput};
345
-
346
- Object.keys(existingFont.scriptFileInput).forEach( key => {
347
- if(!newFileInput[key]){
348
- newFileInput[key] = existingFont.scriptFileInput[key];
349
- }
350
- });
351
- font.scriptFileInput = newFileInput;
352
- }
353
-
354
- fontResponse = await client.patch(font._id).set({ scriptFileInput: font.scriptFileInput }).commit()
355
-
356
- } else{
357
- fontResponse = await client.createOrReplace({
358
- _key: nanoid(),
359
- _id: font._id,
360
- _type: 'font',
361
- ...font,
362
- });
363
- }
364
- }
365
- catch(e){
366
- console.error('error creating font: ', font.title, font.subfamily);
367
- failedFiles = [...failedFiles, ...(files.map(file=>{return{name:file.name, fk: fontKit}}))];
368
- continue;
369
- }
370
-
371
-
372
- // Create font refs for typeface
373
- // add to fontRef array or variableRef array
374
-
375
- const fontRef = {_key: nanoid(), _type:'reference', _ref: fontResponse._id, _weak: true };
376
-
377
- console.log('font response : ', fontResponse);
378
- console.log('existing styles object : ', stylesObject);
379
-
380
- // add new font refs for typeface
381
- if(!font.variableFont){
382
- if(stylesObject.fonts && stylesObject.fonts.length > 0){
383
- let fontExists = stylesObject.fonts.findIndex( font => font._ref == fontResponse._id);
384
- let inFontRefs = fontRefs.findIndex( font => font._ref == fontResponse._id);
385
- if(fontExists == -1 && inFontRefs == -1){
386
- fontRefs.push(fontRef);
387
- }
388
- } else {
389
- fontRefs.push(fontRef);
390
- }
391
- }
392
-
393
- // add new font refs for typeface (variable)
394
- if(font.variableFont){
395
- if(stylesObject.variableFont && stylesObject.variableFont.length > 0){
396
- let vfExists = stylesObject.variableFont.findIndex( font => font._ref == fontResponse._id);
397
- let inVariableRefs = variableRefs.findIndex( font => font._ref == fontResponse._id);
398
- if( vfExists == -1 && inVariableRefs == -1 && font.variableFont){
399
- variableRefs.push(fontRef);
400
- }
401
- } else {
402
- variableRefs.push(fontRef);
403
- }
404
- }
405
-
406
- console.log(fontResponse._id, ' created!');
407
- }
408
-
409
- // Update Sanity typeface document with new font references
410
- console.log('updating styles refs (fonts, variable fonts, subfamilies) ', fontRefs, variableRefs, subfamilies, uniqueSubfamiles)
411
- setStatus('Updating font references...');
412
-
413
- let newStylesObject = stylesObject.fonts ?
414
- { ...stylesObject, fonts : [...stylesObject.fonts, ...fontRefs] }
415
- :
416
- { ...stylesObject, fonts : [...fontRefs] };
417
-
418
- if(uniqueSubfamiles.length > 1){
419
- newStylesObject.subfamilies = uniqueSubfamiles;
420
- }
421
- else{
422
- newStylesObject.subfamilies = [];
423
- }
424
-
425
- newStylesObject.variableFont = stylesObject?.variableFont ? [...stylesObject?.variableFont, ...variableRefs] : [...variableRefs];
426
-
427
- let patch = {styles:newStylesObject};
428
-
429
- subfamiliesArray = subfamiliesArray ? subfamiliesArray : [];
430
-
431
- console.log('new styles obj : ', newStylesObject);
432
- console.log('existing subfamily list : ', subfamiliesArray);
433
- console.log('unique subfamilies ', uniqueSubfamiles);
434
-
435
- subfamiliesArray = [...subfamiliesArray, ...uniqueSubfamiles].filter((sf, index, self) => {
436
- return self.indexOf(sf) === index;
437
- });
438
-
439
- patch.styles.subfamilies = subfamiliesArray;
440
-
441
- console.log('doc_id : ',doc_id);
442
- console.log('typeface patch : ',patch);
443
-
444
- let includedScripts = [ script, ...scripts].filter((lang, index, self) => {
445
- return self.indexOf(lang) === index;
446
- });
447
-
448
- patch.scripts = includedScripts;
449
-
450
- console.log('included scripts : ', includedScripts);
451
-
452
-
453
- if( doc_id.startsWith('drafts.')){
454
- await client.patch(doc_id).set(patch).commit()
455
- .catch(err => {
456
- console.error('error patching styles: ', err.message);
457
- setStatus('error patching styles '+ err.message);
458
- });
459
- doc_id = doc_id.replace('drafts.','');
460
- }
461
-
462
- await client.patch(doc_id).set(patch).commit()
463
- .catch(err => {
464
- console.error('error patching styles: ', err.message);
465
- setStatus('error patching styles');
466
- });
467
-
468
- console.log('success');
469
-
470
- if(failedFiles.length > 0){
471
- console.log('failed files : ', failedFiles);
472
- const names = failedFiles.map( file => file.name);
473
- console.log('names : ', failedFiles.map( file => file?.fk?.name?.records));
474
- setStatus('fonts uploaded with errors. Failed files : '+ names.join(', '));
475
- } else {
476
- setStatus('fonts uploaded!');
477
- }
478
- setStatus('fonts uploaded!');
479
- } catch(e){
480
- console.error(e);
481
- setStatus('error uploading font '+e.message);
482
- }
483
- setReady(true);
484
-
485
- },[title, slug, doc_id]);
486
-
487
- // Render component UI
488
- return (
489
- <Stack>
490
- {/* Display status message when processing */}
491
- {!ready &&
492
- <Text><br/>{status}<br/><br/></Text>
493
- }
494
-
495
- {/* Display upload interface when ready */}
496
- {ready &&
497
- <Stack>
498
- <Grid columns={!!(selectedScript && selectedScript !== "") ? 2 : 1} gap={2}>
499
- {/* Script selection dropdown */}
500
- <Select
501
- id="menu-button-example"
502
- onChange={(e)=>setSelectedScript(e.target.value)}
503
- >
504
- <option key={'script-none'} value={""}> </option>
505
- {SCRIPTS.map((script,i) =>
506
- <option key={'script-'+i} value={script}>
507
- {script[0]?.toUpperCase()+script.slice(1)}
508
- </option>
509
- )}
510
- </Select>
511
-
512
- {/* File upload button - only shown when script is selected */}
513
- {!!(selectedScript && selectedScript !== "") &&
514
- <>
515
- <label htmlFor="upload-scripts-file">
516
- <Button
517
- style={{pointerEvents: "none"}}
518
- text="Upload (ttf/otf/woff/woff2/etc..)"
519
- />
520
- </label>
521
- <input
522
- ref={ref}
523
- name="upload-scripts-file"
524
- id="upload-scripts-file"
525
- type="file"
526
- multiple
527
- hidden
528
- onChange={(event) => handleUpload(event, selectedScript)}
529
- />
530
- </>
531
- }
532
- </Grid>
533
- </Stack>
534
- }
535
- </Stack>
536
- )
537
- };
1
+ // Batch uploader for script-specific font variants across multiple fonts at once
2
+
3
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
4
+ import { Button, Flex, Grid, Stack, Text, TextInput, MenuButton, Menu, MenuItem, Select } from '@sanity/ui';
5
+ import * as fontkit from 'fontkit';
6
+ import slugify from 'slugify';
7
+ import { useSanityClient } from '../hooks/useSanityClient';
8
+ import { useFormValue } from 'sanity';
9
+ import { nanoid } from 'nanoid';
10
+ import generateCssFile from '../utils/generateCssFile';
11
+ import { generateStyleKeywords, reverseSpellingLookup } from '../utils/generateKeywords';
12
+ import { SCRIPTS } from '../utils/utils';
13
+
14
+ /**
15
+ * Component for uploading and managing script variants of fonts
16
+ * @param {Object} props - Component props
17
+ * @param {Object} props.elementProps - Element properties including ref
18
+ * @returns {JSX.Element} Upload interface for script variants
19
+ */
20
+ export const UploadScriptsComponent = (props) => {
21
+
22
+ // Props and client initialization
23
+ const {elementProps: {ref}} = props;
24
+ const client = useSanityClient();
25
+
26
+ // Component state
27
+ const [selectedScript, setSelectedScript] = useState(""); // Currently selected script
28
+ const [status, setStatus] = React.useState(''); // Upload status message
29
+ const [ready, setReady] = React.useState(true); // Component ready state
30
+
31
+ // Form values from Sanity
32
+ let doc_id = useFormValue(['_id']); // Document ID
33
+ const title = useFormValue(['title']); // Typeface title
34
+ const slug = useFormValue(['slug']); // URL slug
35
+ const scripts = useFormValue(['scripts']) || []; // Supported scripts
36
+ const stylesObject = useFormValue(['styles']); // Font styles data
37
+ let subfamiliesArray = stylesObject?.subfamilies || []; // Font subfamilies
38
+
39
+ // Memoized style keywords for font processing
40
+ const {weightKeywordList, italicKeywordList} = useMemo(() =>
41
+ generateStyleKeywords()
42
+ , []);
43
+
44
+ /**
45
+ * Reads a font file and returns its content as a Uint8Array
46
+ * @param {File} file - The font file to read
47
+ * @returns {Promise<Uint8Array>} Font file content
48
+ */
49
+ const readFontFile = (file) => {
50
+ return new Promise((resolve, reject) => {
51
+ const reader = new FileReader();
52
+
53
+ reader.onload = (event) => {
54
+ resolve(new Uint8Array(event.target.result));
55
+ };
56
+
57
+ reader.onerror = (error) => { reject(error); };
58
+ reader.readAsArrayBuffer(file);
59
+ });
60
+ };
61
+
62
+
63
+ /**
64
+ * Handles the upload and processing of font files for a specific script
65
+ * @param {Event} event - The file input change event
66
+ * @param {string} script - The selected script variant (e.g., 'cyrillic', 'greek')
67
+ */
68
+ const handleUpload = useCallback(async(event, script) => {
69
+ setReady(false);
70
+ try{
71
+ let failedFiles = [];
72
+
73
+ console.log('handle upload ', title, script );
74
+ setStatus('uploading fonts files.. ');
75
+
76
+ if(!title) {
77
+ console.error('typeface needs title');
78
+ return;
79
+ }
80
+
81
+ let fontRefs = [];
82
+ let variableRefs = [];
83
+ let subfamilies = {};
84
+ let fontsObjects = {};
85
+
86
+ // read font files ,
87
+ // create if doesnt exist - create sanity fontObjects template
88
+ // add font file to sanity font
89
+ // create subfamily list
90
+ for(var i = 0 ; i < event.target.files.length ; i++ ){
91
+
92
+ const file = event.target.files[i];
93
+ const fontBuffer = await readFontFile(file);
94
+ const font = fontkit.create(fontBuffer);
95
+
96
+ console.log('reading font : ', font.fullName +' '+file.name, font.name.records);
97
+
98
+ let weightName = font?.name?.records?.preferredSubfamily ? font?.name?.records?.preferredSubfamily: font?.name?.records?.fontSubfamily;
99
+ weightName = weightName?.en ? weightName.en: weightName.constructor == Object ? weightName[Object.keys(weightName)[0]] : weightName;
100
+ weightName = weightName?.replace("Italic", "").replace("It", "").trim();
101
+
102
+ if ((weightName == '' || weightName.toLowerCase() == 'roman') && font?.name?.records?.fullName) {
103
+ weightName = font?.name?.records?.fullName;
104
+ weightName = weightName?.en ? weightName.en: weightName.constructor == Object ? weightName[Object.keys(weightName)[0]] : weightName;
105
+ weightName = weightName?.replace(title + " ", "").replace(title, "").trim();
106
+ weightName = weightName?.replace("Italic", "").replace("It", "").trim();
107
+ }
108
+
109
+ let variableFont = font?.variationAxes && Object.keys(font.variationAxes).length > 0 ? true: false;
110
+ let subfamilyName = font.familyName.toLowerCase().trim().replace(title.toLowerCase().trim(),'').trim();
111
+ let fontTitle = font?.fullName;
112
+ let style = (font?.italicAngle !== 0 || font?.fullName.toLowerCase().includes('italic')) ? 'Italic' : 'Regular';
113
+
114
+ if(fontTitle.toLowerCase().trim().includes(script)){
115
+ fontTitle = fontTitle.toLowerCase().trim().replace(script, '').trim();
116
+ fontTitle = fontTitle.split(' ').map( word => {
117
+ if( word == '') return
118
+ return word;
119
+ })
120
+ .filter( word => word != undefined)
121
+ .join(' ');
122
+ }
123
+
124
+ // remove weight and italic keywords from subfamily name
125
+ weightKeywordList.forEach( keyword => {
126
+ const kw = keyword.trim();
127
+ if(subfamilyName.includes(kw)) subfamilyName = subfamilyName.replace(kw, '').trim();
128
+
129
+ // if(fontTitle.includes(kw)){
130
+ // fontTitle = fontTitle.replace(kw, '');
131
+ // }
132
+ });
133
+
134
+ let italicKW = [];
135
+ italicKeywordList.forEach( keyword => {
136
+ const kw = keyword.toLowerCase().trim();
137
+ if(subfamilyName.includes(kw)){
138
+ subfamilyName = subfamilyName.replace(kw, '');
139
+ }
140
+
141
+ if(fontTitle.includes(kw)){
142
+ fontTitle = fontTitle.replace(kw, '');
143
+ italicKW.push(kw.charAt(0).toUpperCase() + kw.slice(1));
144
+
145
+ }
146
+ });
147
+
148
+ fontTitle = fontTitle.replace(/-/g, ' ');
149
+ fontTitle = fontTitle.trim().split(' ').map( word => word[0].toUpperCase() + word.slice(1)).join(' ');
150
+
151
+ if(subfamilyName.trim().includes(script)){
152
+ subfamilyName = subfamilyName.trim().replace(script, '').trim();
153
+ }
154
+
155
+ subfamilyName = subfamilyName.trim();
156
+ subfamilyName = (subfamilyName == '' ) ? 'Regular' : subfamilyName.split(' ').map( word => word[0].toUpperCase() + word.slice(1)).join(' ');
157
+
158
+ // remove subfamily from weight name
159
+ if (subfamilyName !== '' ) {
160
+ weightName = weightName
161
+ .replace(`${subfamilyName} `, '')
162
+ .replace(` ${subfamilyName}`, '')
163
+ .trim();
164
+ }
165
+
166
+ if(variableFont && !fontTitle.toLowerCase().trim().endsWith(' vf')) fontTitle = fontTitle + ' VF';
167
+
168
+ if(italicKW.length > 0){
169
+ italicKW = italicKW.map( item => reverseSpellingLookup(item)); // replace each item in the italicKW list with the value in reverseSpellingLookup
170
+ fontTitle = fontTitle + italicKW.join(' ');
171
+ style = 'Italic';
172
+ }
173
+
174
+ let id = slugify(fontTitle.toLowerCase().trim());
175
+
176
+ console.log('=== Font Info ====');
177
+ console.log(' ')
178
+ console.log('font id : ', id);
179
+ console.log('font title : ', fontTitle);
180
+ console.log('fontkit fullName : ', font.fullName );
181
+ console.log('fontkit family name: ', font.familyName);
182
+ console.log('file name : ', file.name);
183
+ console.log('subfamily : ', subfamilyName);
184
+ console.log('style : ', style);
185
+ console.log('weight : ', weightName);
186
+ console.log('variable : ', variableFont);
187
+ console.log('italicKW ', italicKW);
188
+ console.log(' ')
189
+ console.log('=======');
190
+
191
+ subfamilies[id] = subfamilyName; // add subfamily to list
192
+
193
+ if( fontsObjects[id]){
194
+ fontsObjects[id].files = [...fontsObjects[id].files, file];
195
+ } else {
196
+ let fontObject = {
197
+ _key: nanoid(),
198
+ _id: id,
199
+ title: fontTitle,
200
+ slug: {_type:'slug', current:id},
201
+ typefaceName: title, // Change to match Typeface Document
202
+ style: (font?.italicAngle !== 0 || font?.fullName.toLowerCase().includes('italic')) ? 'Italic' : 'Regular',
203
+ variableFont: variableFont,
204
+ weightName: weightName,
205
+ normalWeight:true, // TODO : check if weight is normal ??
206
+ weight: font['OS/2']?.usWeightClass ? Number(font['OS/2']?.usWeightClass) :
207
+ /hairline|extra thin|extrathin/.test(weightName?.toLowerCase()) ? 100 :
208
+ /thin|extra light|extralight/.test(weightName?.toLowerCase()) ? 200 :
209
+ /light|book/.test(weightName?.toLowerCase()) ? 300 :
210
+ /regular|normal/.test(weightName?.toLowerCase()) ? 400 :
211
+ /medium/.test(weightName?.toLowerCase()) ? 500 :
212
+ /semi bold|semibold/.test(weightName?.toLowerCase()) ? 600 :
213
+ /bold/.test(weightName?.toLowerCase()) ? 700 :
214
+ /extra bold|extrabold/.test(weightName?.toLowerCase()) ? 800 :
215
+ /black|ultra/.test(weightName?.toLowerCase()) ? 900 :
216
+ 400,
217
+ files : [file],
218
+ fontKit: font,
219
+ scriptFileInput: {[script]:{}},
220
+ };
221
+ fontsObjects[id] = fontObject;
222
+ }
223
+ }
224
+
225
+ // Extract unique subfamily names and prepare for processing
226
+ let uniqueSubfamiles = [...new Set(Object.values(subfamilies))];
227
+
228
+ console.log('Subfamilies : ', subfamilies, uniqueSubfamiles, uniqueSubfamiles.length);
229
+ console.log('fontsObjects : ', fontsObjects);
230
+
231
+ // Process each font object:
232
+ // 1. Upload font files as Sanity assets
233
+ // 2. Create file references linking fonts to assets
234
+ // 3. Generate CSS for web fonts
235
+ for(var i = 0 ; i < Object.keys(fontsObjects).length ; i++ ){
236
+
237
+ let id = Object.keys(fontsObjects)[i];
238
+ let fontObject = fontsObjects[id];
239
+ let files = fontObject.files;
240
+ let newFileInput = fontObject.scriptFileInput[script];
241
+
242
+ console.log(fontObject.title , ' : subfamily : ', subfamilies[id]);
243
+
244
+ // add subfamily to font object if more than one exists
245
+ if(uniqueSubfamiles.length > 1) fontObject.subfamily = subfamilies[id];
246
+ else fontObject.subfamily = '';
247
+
248
+ // add price to font object - set sell = true if there is a price > 0
249
+ fontObject.price = process.env.SANITY_STUDIO_DEFAULT_STYLE_PRICE || 40;
250
+ if(fontObject.price > 0) fontObject.sell = true;
251
+
252
+ // upload files
253
+ for(var j = 0 ; j < files.length ; j++ ){
254
+ let file = files[j];
255
+ let fileType = "";
256
+ if ( file.name.endsWith('.otf') ) fileType = "otf"
257
+ else if ( file.name.endsWith('.ttf') ) fileType = "ttf"
258
+ else if ( file.name.endsWith('.woff') ) fileType = "woff"
259
+ else if ( file.name.endsWith('.woff2') ) fileType = "woff2"
260
+ else if ( file.name.endsWith('.eot') ) fileType = "eot"
261
+ else if ( file.name.endsWith('.svg') ) fileType = "svg"
262
+
263
+ console.log('uploading font file : ', fontObject._id+'.'+fileType);
264
+ const filename = fontObject._id+'-'+script;
265
+ let fontTitle = fontObject.title+' '+script;
266
+ fontTitle = fontTitle.split(' ').map( word => word[0].toUpperCase() + word.slice(1)).join(' ');
267
+
268
+ let baseAsset = await client.assets.upload('file', file, { filename: filename+'.'+fileType })
269
+ .catch( err => {
270
+ console.error('error uploading font: ', fontObject.title);
271
+ setStatus('error uploading font ' + err.message);
272
+ });
273
+
274
+ // create file ref from font
275
+ newFileInput[fileType] = {
276
+ _type: 'file',
277
+ asset: {
278
+ _ref: baseAsset._id,
279
+ _type: 'reference'
280
+ }
281
+ }
282
+
283
+ console.log('newFileInput', newFileInput);
284
+
285
+ // generate css
286
+ if(file.name.endsWith('.woff2')){
287
+ console.log('generating css file for: ', fontObject.title);
288
+ setStatus('generating css file for: ' + fontObject.title);
289
+ newFileInput = await generateCssFile({
290
+ woff2File: file,
291
+ fileInput: newFileInput,
292
+ // script: script,
293
+ fontName: fontTitle,
294
+ fileName: filename,
295
+ variableFont: fontObject.variableFont,
296
+ weight: fontObject.weight,
297
+ client: client,
298
+ });
299
+ }
300
+
301
+ fontObject.scriptFileInput[script] = newFileInput;
302
+ fontsObjects[id] = fontObject;
303
+
304
+ }
305
+ }
306
+
307
+ console.log('creating sanity fonts', fontsObjects);
308
+
309
+
310
+ // create (with existing data if exists ) fonts and refs (for typeface)
311
+ for(var i = 0 ; i < Object.keys(fontsObjects).length ; i++ ){
312
+ let fontId = Object.keys(fontsObjects)[i];
313
+ let font = fontsObjects[fontId];
314
+
315
+ // add existing file refs to new file input
316
+ let existingFont = await client.fetch(
317
+ `*[_type == 'font' && _id == $fontId]{
318
+ fileInput,
319
+ description,
320
+ metaData,
321
+ metrics,
322
+ opentypeFeatures,
323
+ characterSet,
324
+ subfamily,
325
+ scriptFileInput,
326
+ }`,
327
+ { fontId: font._id }
328
+ );
329
+
330
+ existingFont = existingFont[0];
331
+
332
+ let fontResponse;
333
+ let files = font.files;
334
+ let fontKit = font.fontKit;
335
+ delete font.files;
336
+ delete font.fontKit;
337
+
338
+ console.log('creating font : ', font);
339
+
340
+ try{
341
+ if(existingFont && existingFont != null){
342
+
343
+ if(existingFont.scriptFileInput && existingFont.scriptFileInput != null){
344
+ let newFileInput = {...font.scriptFileInput};
345
+
346
+ Object.keys(existingFont.scriptFileInput).forEach( key => {
347
+ if(!newFileInput[key]){
348
+ newFileInput[key] = existingFont.scriptFileInput[key];
349
+ }
350
+ });
351
+ font.scriptFileInput = newFileInput;
352
+ }
353
+
354
+ fontResponse = await client.patch(font._id).set({ scriptFileInput: font.scriptFileInput }).commit()
355
+
356
+ } else{
357
+ fontResponse = await client.createOrReplace({
358
+ _key: nanoid(),
359
+ _id: font._id,
360
+ _type: 'font',
361
+ ...font,
362
+ });
363
+ }
364
+ }
365
+ catch(e){
366
+ console.error('error creating font: ', font.title, font.subfamily);
367
+ failedFiles = [...failedFiles, ...(files.map(file=>{return{name:file.name, fk: fontKit}}))];
368
+ continue;
369
+ }
370
+
371
+
372
+ // Create font refs for typeface
373
+ // add to fontRef array or variableRef array
374
+
375
+ const fontRef = {_key: nanoid(), _type:'reference', _ref: fontResponse._id, _weak: true };
376
+
377
+ console.log('font response : ', fontResponse);
378
+ console.log('existing styles object : ', stylesObject);
379
+
380
+ // add new font refs for typeface
381
+ if(!font.variableFont){
382
+ if(stylesObject.fonts && stylesObject.fonts.length > 0){
383
+ let fontExists = stylesObject.fonts.findIndex( font => font._ref == fontResponse._id);
384
+ let inFontRefs = fontRefs.findIndex( font => font._ref == fontResponse._id);
385
+ if(fontExists == -1 && inFontRefs == -1){
386
+ fontRefs.push(fontRef);
387
+ }
388
+ } else {
389
+ fontRefs.push(fontRef);
390
+ }
391
+ }
392
+
393
+ // add new font refs for typeface (variable)
394
+ if(font.variableFont){
395
+ if(stylesObject.variableFont && stylesObject.variableFont.length > 0){
396
+ let vfExists = stylesObject.variableFont.findIndex( font => font._ref == fontResponse._id);
397
+ let inVariableRefs = variableRefs.findIndex( font => font._ref == fontResponse._id);
398
+ if( vfExists == -1 && inVariableRefs == -1 && font.variableFont){
399
+ variableRefs.push(fontRef);
400
+ }
401
+ } else {
402
+ variableRefs.push(fontRef);
403
+ }
404
+ }
405
+
406
+ console.log(fontResponse._id, ' created!');
407
+ }
408
+
409
+ // Update Sanity typeface document with new font references
410
+ console.log('updating styles refs (fonts, variable fonts, subfamilies) ', fontRefs, variableRefs, subfamilies, uniqueSubfamiles)
411
+ setStatus('Updating font references...');
412
+
413
+ let newStylesObject = stylesObject.fonts ?
414
+ { ...stylesObject, fonts : [...stylesObject.fonts, ...fontRefs] }
415
+ :
416
+ { ...stylesObject, fonts : [...fontRefs] };
417
+
418
+ if(uniqueSubfamiles.length > 1){
419
+ newStylesObject.subfamilies = uniqueSubfamiles;
420
+ }
421
+ else{
422
+ newStylesObject.subfamilies = [];
423
+ }
424
+
425
+ newStylesObject.variableFont = stylesObject?.variableFont ? [...stylesObject?.variableFont, ...variableRefs] : [...variableRefs];
426
+
427
+ let patch = {styles:newStylesObject};
428
+
429
+ subfamiliesArray = subfamiliesArray ? subfamiliesArray : [];
430
+
431
+ console.log('new styles obj : ', newStylesObject);
432
+ console.log('existing subfamily list : ', subfamiliesArray);
433
+ console.log('unique subfamilies ', uniqueSubfamiles);
434
+
435
+ subfamiliesArray = [...subfamiliesArray, ...uniqueSubfamiles].filter((sf, index, self) => {
436
+ return self.indexOf(sf) === index;
437
+ });
438
+
439
+ patch.styles.subfamilies = subfamiliesArray;
440
+
441
+ console.log('doc_id : ',doc_id);
442
+ console.log('typeface patch : ',patch);
443
+
444
+ let includedScripts = [ script, ...scripts].filter((lang, index, self) => {
445
+ return self.indexOf(lang) === index;
446
+ });
447
+
448
+ patch.scripts = includedScripts;
449
+
450
+ console.log('included scripts : ', includedScripts);
451
+
452
+
453
+ if( doc_id.startsWith('drafts.')){
454
+ await client.patch(doc_id).set(patch).commit()
455
+ .catch(err => {
456
+ console.error('error patching styles: ', err.message);
457
+ setStatus('error patching styles '+ err.message);
458
+ });
459
+ doc_id = doc_id.replace('drafts.','');
460
+ }
461
+
462
+ await client.patch(doc_id).set(patch).commit()
463
+ .catch(err => {
464
+ console.error('error patching styles: ', err.message);
465
+ setStatus('error patching styles');
466
+ });
467
+
468
+ console.log('success');
469
+
470
+ if(failedFiles.length > 0){
471
+ console.log('failed files : ', failedFiles);
472
+ const names = failedFiles.map( file => file.name);
473
+ console.log('names : ', failedFiles.map( file => file?.fk?.name?.records));
474
+ setStatus('fonts uploaded with errors. Failed files : '+ names.join(', '));
475
+ } else {
476
+ setStatus('fonts uploaded!');
477
+ }
478
+ setStatus('fonts uploaded!');
479
+ } catch(e){
480
+ console.error(e);
481
+ setStatus('error uploading font '+e.message);
482
+ }
483
+ setReady(true);
484
+
485
+ },[title, slug, doc_id]);
486
+
487
+ // Render component UI
488
+ return (
489
+ <Stack>
490
+ {/* Display status message when processing */}
491
+ {!ready &&
492
+ <Text><br/>{status}<br/><br/></Text>
493
+ }
494
+
495
+ {/* Display upload interface when ready */}
496
+ {ready &&
497
+ <Stack>
498
+ <Grid columns={!!(selectedScript && selectedScript !== "") ? 2 : 1} gap={2}>
499
+ {/* Script selection dropdown */}
500
+ <Select
501
+ id="menu-button-example"
502
+ onChange={(e)=>setSelectedScript(e.target.value)}
503
+ >
504
+ <option key={'script-none'} value={""}> </option>
505
+ {SCRIPTS.map((script,i) =>
506
+ <option key={'script-'+i} value={script}>
507
+ {script[0]?.toUpperCase()+script.slice(1)}
508
+ </option>
509
+ )}
510
+ </Select>
511
+
512
+ {/* File upload button - only shown when script is selected */}
513
+ {!!(selectedScript && selectedScript !== "") &&
514
+ <>
515
+ <label htmlFor="upload-scripts-file">
516
+ <Button
517
+ style={{pointerEvents: "none"}}
518
+ text="Upload (ttf/otf/woff/woff2/etc..)"
519
+ />
520
+ </label>
521
+ <input
522
+ ref={ref}
523
+ name="upload-scripts-file"
524
+ id="upload-scripts-file"
525
+ type="file"
526
+ multiple
527
+ hidden
528
+ onChange={(event) => handleUpload(event, selectedScript)}
529
+ />
530
+ </>
531
+ }
532
+ </Grid>
533
+ </Stack>
534
+ }
535
+ </Stack>
536
+ )
537
+ };