@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.
Files changed (36) hide show
  1. package/dist/UploadModal-ADNRGQUI.mjs +6 -0
  2. package/dist/UploadModal-WPK2CXLR.js +6 -0
  3. package/dist/chunk-JCDZ7SWZ.js +7711 -0
  4. package/dist/chunk-TMDE4A54.mjs +7711 -0
  5. package/dist/index.js +666 -1647
  6. package/dist/index.mjs +319 -1209
  7. package/package.json +5 -5
  8. package/src/components/BatchUploadFonts.jsx +57 -44
  9. package/src/components/BulkActions.jsx +99 -0
  10. package/src/components/ExistingDocumentResolver.jsx +152 -0
  11. package/src/components/FontReviewCard.jsx +455 -0
  12. package/src/components/SingleUploaderTool.jsx +3 -4
  13. package/src/components/UploadModal.jsx +304 -0
  14. package/src/components/UploadScriptsComponent.jsx +23 -21
  15. package/src/components/UploadStep1Settings.jsx +272 -0
  16. package/src/components/UploadStep2Review.jsx +474 -0
  17. package/src/components/UploadStep3Execute.jsx +234 -0
  18. package/src/components/UploadStep3bInstances.jsx +396 -0
  19. package/src/components/UploadSummary.jsx +196 -0
  20. package/src/index.js +46 -0
  21. package/src/utils/buildUploadPlan.js +326 -0
  22. package/src/utils/executeUploadPlan.js +430 -0
  23. package/src/utils/executionReducer.js +56 -0
  24. package/src/utils/fontHelpers.js +267 -0
  25. package/src/utils/generateCssFile.js +79 -77
  26. package/src/utils/generateFontData.js +47 -94
  27. package/src/utils/getEmptyFontKit.js +19 -17
  28. package/src/utils/parseFont.js +55 -0
  29. package/src/utils/parseVariableFontInstances.js +237 -147
  30. package/src/utils/planReducer.js +517 -0
  31. package/src/utils/planTypes.js +183 -0
  32. package/src/utils/processFontFiles.js +121 -78
  33. package/src/utils/regenerateFontData.js +2 -2
  34. package/src/utils/resolveExistingFont.js +87 -0
  35. package/src/utils/updateTypefaceDocument.js +15 -2
  36. package/src/utils/uploadFontFiles.js +405 -405
@@ -1,35 +1,30 @@
1
- // Extracts metadata, metrics, glyph count, OpenType features, and variable axes from a TTF and optionally patches the Sanity font document
2
-
3
- import { Buffer } from 'buffer';
4
- import * as fontkit from 'fontkit';
1
+ // Extracts metadata, metrics, glyph count, OpenType features, and variable axes from a font and optionally patches the Sanity font document
2
+
3
+ import { parseFont } from './parseFont';
4
+ import {
5
+ getFontMetadata,
6
+ getFontMetrics,
7
+ getVariationAxes,
8
+ getNamedInstances,
9
+ getAllFeatureTags,
10
+ getGlyphCount,
11
+ getCharacterSet,
12
+ getNameString,
13
+ } from './fontHelpers';
5
14
 
6
15
  /**
7
- * Extracts metadata and metrics from a fontkit font object into plain objects.
8
- * @param {Object} font - fontkit font instance
16
+ * Extracts metadata and metrics from a lib-font parsed font into plain objects.
17
+ * Uses fontHelpers for all table access — no direct font.opentype.tables usage here.
18
+ * @param {object} font - lib-font Font instance
9
19
  * @returns {{ metaData: Object, metrics: Object }}
10
20
  */
11
21
  export function buildFontMetadata(font) {
12
- const metaData = {
13
- postscriptName: font.postscriptName,
14
- fullName: font.fullName,
15
- familyName: font.familyName,
16
- subfamilyName: font.subfamilyName,
17
- copyright: font.copyright,
18
- version: font.version ? String(font.version).replaceAll('Version ', '') : '',
19
- genDate: new Date().toISOString(),
20
- };
21
- const metrics = {
22
- unitsPerEm: font.unitsPerEm,
23
- ascender: font.ascent,
24
- descender: font.descent,
25
- lineGap: font.lineGap,
26
- underlinePosition: font.underlinePosition,
27
- underlineThickness: font.underlineThickness,
28
- italicAngle: font.italicAngle,
29
- capHeight: font.capHeight,
30
- xHeight: font.xHeight,
31
- boundingBox: font.bbox,
32
- };
22
+ const metaData = getFontMetadata(font);
23
+ // Strip "Version " prefix for consistency with existing Sanity documents
24
+ if (metaData.version) {
25
+ metaData.version = String(metaData.version).replaceAll('Version ', '');
26
+ }
27
+ const metrics = getFontMetrics(font);
33
28
  return { metaData, metrics };
34
29
  }
35
30
 
@@ -37,12 +32,11 @@ export default async function generateFontData({ fileInput, url, fontKit, fontId
37
32
  if (fontId.startsWith('drafts.')) {
38
33
  fontId = fontId.replace('drafts.', '');
39
34
  }
40
- console.log('generate-font-data ', fontId, commit);
35
+ console.log('Generate font data:', fontId, commit);
41
36
 
42
37
  let srcUrl;
43
38
  if (!url || url == null) {
44
39
  srcUrl = await client.fetch(`*[_id == $id]{url}`, { id: fileInput.ttf.asset._ref });
45
- console.log('src url ', srcUrl);
46
40
  srcUrl = srcUrl[0].url;
47
41
  } else {
48
42
  srcUrl = url;
@@ -52,82 +46,41 @@ export default async function generateFontData({ fileInput, url, fontKit, fontId
52
46
  if (!fontKit || fontKit == null) {
53
47
  let buffer = await fetch(srcUrl);
54
48
  buffer = await buffer.arrayBuffer();
55
- buffer = Buffer.from(buffer);
56
- font = fontkit.create(buffer);
49
+ font = await parseFont(buffer, `${fontId}.ttf`);
57
50
  }
58
51
 
52
+ const variableAxes = getVariationAxes(font);
53
+ const namedInstances = getNamedInstances(font);
59
54
 
60
- let variableAxes;
61
- try {
62
- variableAxes = font.variationAxes;
63
- } catch (err) {
64
- console.error('err: ', err);
65
- }
66
-
67
- let variableInstances;
68
- try {
69
- variableInstances = font.namedVariations;
70
- } catch (e) {
71
- console.log('variable instances 2 error : ', e.message);
72
- let fvar = font?.fvar?.instance;
73
-
74
- fvar?.forEach(fv => {
75
- if (fv?.nameID === 2) fv.name = font?._tables?.name?.records?.fontSubfamily
76
- if (fv?.nameID === 17) fv.name = font?._tables?.name?.records?.preferredSubfamily
77
- })
78
-
55
+ // Build variableInstances as a keyed object matching existing Sanity document shape
56
+ let variableInstances = null;
57
+ if (namedInstances.length > 0 && variableAxes) {
79
58
  variableInstances = {};
80
- fvar.forEach(v => {
81
- let key = v.name;
82
- if (typeof key === 'object') {
83
- key = Object.values(key)[0];
84
- }
85
-
86
- let coordKeys = Object.keys(variableAxes);
87
- let coord = {};
88
-
89
- coordKeys.forEach((ck, ckIndex) => {
90
- coord[ck] = v.coord[ckIndex];
59
+ const axisTags = Object.keys(variableAxes);
60
+ for (const inst of namedInstances) {
61
+ const key = inst.name || inst.postScriptName || 'Unknown';
62
+ const coord = {};
63
+ axisTags.forEach((tag, index) => {
64
+ coord[tag] = inst.coordinates[index];
91
65
  });
92
66
  variableInstances[key] = coord;
93
- });
94
-
67
+ }
95
68
  }
96
- console.log('font : ', font);
97
- console.log('variable instances : ', variableInstances);
98
- console.log('variable axes : ', variableAxes);
99
69
 
100
- let opentypeFeatures = font.availableFeatures;
101
- let glyphCount = font.numGlyphs;
102
- let characterSet = font.characterSet;
70
+ console.log('Variable instances:', variableInstances);
71
+ console.log('Variable axes:', variableAxes);
103
72
 
104
- let metaData = {
105
- postscriptName: font.postscriptName,
106
- fullName: font.fullName,
107
- familyName: font.familyName,
108
- subfamilyName: font.subfamilyName,
109
- copyright: font.copyright,
110
- version: font.version.replaceAll("Version ", ""),
111
- genDate: new Date().toISOString(),
112
- };
73
+ const opentypeFeatures = getAllFeatureTags(font);
74
+ const glyphCount = getGlyphCount(font);
75
+ const characterSet = getCharacterSet(font);
113
76
 
114
- let metrics = {
115
- unitsPerEm: font.unitsPerEm,
116
- ascender: font.ascent,
117
- descender: font.descent,
118
- lineGap: font.lineGap,
119
- underlinePosition: font.underlinePosition,
120
- underlineThickness: font.underlineThickness,
121
- italicAngle: font.italicAngle,
122
- capHeight: font.capHeight,
123
- xHeight: font.xHeight,
124
- boundingBox: font.bbox
125
- };
77
+ const { metaData, metrics } = buildFontMetadata(font);
126
78
 
127
79
  let variableFont = false;
128
- if (variableAxes && variableAxes != null && Object.keys(variableAxes).length > 0 && variableInstances && variableInstances != null && Object.keys(variableInstances).length > 0) {
80
+ if (variableAxes && variableInstances && Object.keys(variableInstances).length > 0) {
129
81
  variableFont = true;
130
82
  }
83
+
131
84
  let patch = {
132
85
  metrics: metrics,
133
86
  metaData: metaData,
@@ -136,10 +89,10 @@ export default async function generateFontData({ fileInput, url, fontKit, fontId
136
89
  variableInstances: JSON.stringify(variableInstances),
137
90
  glyphCount: glyphCount,
138
91
  opentypeFeatures: { chars: opentypeFeatures },
139
- characterSet: { chars: characterSet }
140
- }
92
+ characterSet: { chars: characterSet },
93
+ };
141
94
 
142
- console.log('data : ', patch);
95
+ console.log('Font data patch:', Object.keys(patch));
143
96
  if (commit) patch = await client.patch(fontId).set(patch).commit({ autoGenerateArrayKeys: true });
144
97
  return patch;
145
98
  }
@@ -1,5 +1,7 @@
1
- // Returns a zeroed-out fontkit-shaped placeholder object used when no font binary is available
2
- import * as fontkit from 'fontkit';
1
+ // Parses font files and returns name/subfamily metadata groupings without writing to Sanity
2
+
3
+ import { parseFont } from './parseFont';
4
+ import { getNameString, getVariationAxes } from './fontHelpers';
3
5
  import slugify from 'slugify';
4
6
 
5
7
  /** Reads font files and returns name/subfamily metadata without writing to Sanity */
@@ -12,14 +14,16 @@ export async function getEmptyFontKit({ title, files, weightKeywordList, italicK
12
14
 
13
15
  const file = files[i];
14
16
  const fontBuffer = await readFontFile(file);
15
- const font = fontkit.create(fontBuffer);
17
+ const font = await parseFont(fontBuffer, file.name);
16
18
 
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
+ let weightName = getNameString(font, 17) || getNameString(font, 2) || '';
19
20
 
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();
21
+ const axes = getVariationAxes(font);
22
+ let variableFont = axes !== null;
23
+ const familyName = getNameString(font, 1);
24
+ const fullName = getNameString(font, 4);
25
+ let subfamilyName = familyName.toLowerCase().trim().replace(title.toLowerCase().trim(), '').trim();
26
+ let fontTitle = fullName.toLowerCase().trim();
23
27
 
24
28
  weightKeywordList.forEach(keyword => {
25
29
  const kw = keyword.toLowerCase().trim();
@@ -65,32 +69,30 @@ export async function getEmptyFontKit({ title, files, weightKeywordList, italicK
65
69
  if (!fontNames[id]) {
66
70
  fontNames[id] = [{
67
71
  file: file.name,
68
- fullName: font.fullName,
69
- familyName: font.familyName,
72
+ fullName: fullName,
73
+ familyName: familyName,
70
74
  subFamily: subfamilyName,
71
75
  }];
72
76
  } else if (fontNames[id].indexOf(file.name) == -1) {
73
77
  fontNames[id].push({
74
78
  file: file.name,
75
- fullName: font.fullName,
76
- familyName: font.familyName,
79
+ fullName: fullName,
80
+ familyName: familyName,
77
81
  subFamily: subfamilyName,
78
82
  })
79
83
  }
80
-
81
-
82
84
  }
83
85
 
84
- console.log('font names : ', fontNames);
86
+ console.log('Font names:', fontNames);
85
87
  }
86
88
 
87
- /** Reads a font file and returns its content as a Uint8Array */
89
+ /** Reads a font file and returns its content as an ArrayBuffer */
88
90
  const readFontFile = (file) => {
89
91
  return new Promise((resolve, reject) => {
90
92
  const reader = new FileReader();
91
93
 
92
94
  reader.onload = (event) => {
93
- resolve(new Uint8Array(event.target.result));
95
+ resolve(event.target.result);
94
96
  };
95
97
 
96
98
  reader.onerror = (error) => { reject(error); };
@@ -0,0 +1,55 @@
1
+ // Async font parser — wraps lib-font event model in a Promise with decompressor bootstrap
2
+
3
+ import pako from 'pako';
4
+
5
+ // Set decompressor globals BEFORE lib-font is imported.
6
+ // lib-font reads globalThis.pako and globalThis.unbrotli at module evaluation time
7
+ // (top of woff.js / woff2.js), not at parse time. These must be set before lib-font
8
+ // is first evaluated, so we use dynamic import() below to guarantee ordering.
9
+ globalThis.pako = pako;
10
+
11
+ // unbrotli is a UMD that sets globalThis.unbrotli as a side effect on evaluation.
12
+ // We vendor it from lib-font/lib/unbrotli.js because the subpath is not in
13
+ // lib-font's exports map (ERR_PACKAGE_PATH_NOT_EXPORTED).
14
+ import '../vendor/unbrotli.js';
15
+
16
+ // Lazy-loaded lib-font Font constructor — resolved on first parseFont() call.
17
+ // Using dynamic import() guarantees globalThis.pako and globalThis.unbrotli are
18
+ // set before lib-font evaluates, which static imports cannot guarantee in ESM.
19
+ let _Font = null;
20
+
21
+ /** Returns the lib-font Font constructor, loading it on first call */
22
+ async function getFont() {
23
+ if (!_Font) {
24
+ const mod = await import('lib-font');
25
+ _Font = mod.Font;
26
+ }
27
+ return _Font;
28
+ }
29
+
30
+ /** Maximum font file size accepted for parsing (50 MB) */
31
+ const MAX_FONT_FILE_SIZE = 50 * 1024 * 1024;
32
+
33
+ /**
34
+ * Parse a font file from an ArrayBuffer.
35
+ * Returns a lib-font Font object with all tables accessible via font.opentype.tables.*.
36
+ *
37
+ * @param {ArrayBuffer} buffer - Raw font file bytes
38
+ * @param {string} filename - Original filename (used for format detection by lib-font)
39
+ * @returns {Promise<import('lib-font').Font>} Parsed lib-font Font object
40
+ * @throws {Error} If the file exceeds MAX_FONT_FILE_SIZE or parsing fails
41
+ */
42
+ export async function parseFont(buffer, filename) {
43
+ if (buffer.byteLength > MAX_FONT_FILE_SIZE) {
44
+ throw new Error(`Font file exceeds ${MAX_FONT_FILE_SIZE / 1024 / 1024}MB limit: ${filename} (${(buffer.byteLength / 1024 / 1024).toFixed(1)}MB)`);
45
+ }
46
+
47
+ const Font = await getFont();
48
+
49
+ return new Promise((resolve, reject) => {
50
+ const font = new Font('font', { skipStyleSheet: true });
51
+ font.onload = (evt) => resolve(evt.detail.font);
52
+ font.onerror = (evt) => reject(new Error(evt.detail?.message || `Failed to parse ${filename}`));
53
+ font.fromDataBuffer(buffer, filename);
54
+ });
55
+ }