@liiift-studio/sanity-font-manager 2.7.0 → 2.8.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/LICENSE +21 -0
- package/README.md +695 -437
- package/dist/index.js +2330 -7084
- package/dist/index.mjs +2331 -7085
- package/package.json +84 -83
- package/src/components/BatchUploadFonts.jsx +655 -655
- 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 -309
- package/src/components/UploadScriptsComponent.jsx +539 -539
- package/src/components/UploadStep1Settings.jsx +272 -272
- package/src/components/UploadStep2Review.jsx +478 -478
- 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 -281
- 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 -531
- 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 -154
- 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,154 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
// Converts arbitrary strings into valid Sanity document IDs (lowercase, hyphens, no special characters)
|
|
2
|
-
import slugify from 'slugify';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Sanitizes a string into a valid Sanity document ID.
|
|
6
|
-
*
|
|
7
|
-
* Sanity ID requirements:
|
|
8
|
-
* - Must start with a letter or underscore (not a number or hyphen)
|
|
9
|
-
* - Can only contain lowercase letters (a-z), numbers (0-9), hyphens (-), and underscores (_)
|
|
10
|
-
* - Must be between 1 and 128 characters
|
|
11
|
-
*
|
|
12
|
-
* @param {string} str - The raw string (e.g. font title or filename) to sanitize
|
|
13
|
-
* @returns {string} A valid Sanity document ID
|
|
14
|
-
*/
|
|
15
|
-
export function sanitizeForSanityId(str) {
|
|
16
|
-
if (!str || typeof str !== 'string') {
|
|
17
|
-
return 'font-' + Date.now();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
let sanitized = str.toLowerCase().trim();
|
|
21
|
-
|
|
22
|
-
// Replace common symbols before slugify
|
|
23
|
-
sanitized = sanitized.replace(/\+/g, 'plus');
|
|
24
|
-
sanitized = sanitized.replace(/&/g, 'and');
|
|
25
|
-
sanitized = sanitized.replace(/@/g, 'at');
|
|
26
|
-
|
|
27
|
-
sanitized = slugify(sanitized, {
|
|
28
|
-
replacement: '-',
|
|
29
|
-
remove: /[^\w\s-]/g,
|
|
30
|
-
lower: true,
|
|
31
|
-
strict: true,
|
|
32
|
-
locale: 'en',
|
|
33
|
-
trim: true,
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// Strip any characters that still aren't lowercase-alphanumeric, hyphens, or underscores
|
|
37
|
-
sanitized = sanitized.replace(/[^a-z0-9\-_]/g, '-');
|
|
38
|
-
|
|
39
|
-
// Collapse repeated hyphens and strip leading/trailing hyphens or underscores
|
|
40
|
-
sanitized = sanitized.replace(/-+/g, '-');
|
|
41
|
-
sanitized = sanitized.replace(/^[-_]+|[-_]+$/g, '');
|
|
42
|
-
|
|
43
|
-
// IDs must not start with a number or hyphen
|
|
44
|
-
if (sanitized && !/^[a-z_]/.test(sanitized)) {
|
|
45
|
-
sanitized = 'font_' + sanitized;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (!sanitized) {
|
|
49
|
-
sanitized = 'font_' + Date.now();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Sanity hard-caps IDs at 128 characters
|
|
53
|
-
if (sanitized.length > 128) {
|
|
54
|
-
const hash = Math.random().toString(36).substring(2, 8);
|
|
55
|
-
sanitized = sanitized.substring(0, 120) + '_' + hash;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Paranoid final validation
|
|
59
|
-
if (!/^[a-z_][a-z0-9\-_]*$/.test(sanitized)) {
|
|
60
|
-
console.warn(`ID sanitization produced invalid result: "${sanitized}", using fallback`);
|
|
61
|
-
sanitized = 'font_' + Date.now();
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return sanitized;
|
|
65
|
-
}
|
|
1
|
+
// Converts arbitrary strings into valid Sanity document IDs (lowercase, hyphens, no special characters)
|
|
2
|
+
import slugify from 'slugify';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sanitizes a string into a valid Sanity document ID.
|
|
6
|
+
*
|
|
7
|
+
* Sanity ID requirements:
|
|
8
|
+
* - Must start with a letter or underscore (not a number or hyphen)
|
|
9
|
+
* - Can only contain lowercase letters (a-z), numbers (0-9), hyphens (-), and underscores (_)
|
|
10
|
+
* - Must be between 1 and 128 characters
|
|
11
|
+
*
|
|
12
|
+
* @param {string} str - The raw string (e.g. font title or filename) to sanitize
|
|
13
|
+
* @returns {string} A valid Sanity document ID
|
|
14
|
+
*/
|
|
15
|
+
export function sanitizeForSanityId(str) {
|
|
16
|
+
if (!str || typeof str !== 'string') {
|
|
17
|
+
return 'font-' + Date.now();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let sanitized = str.toLowerCase().trim();
|
|
21
|
+
|
|
22
|
+
// Replace common symbols before slugify
|
|
23
|
+
sanitized = sanitized.replace(/\+/g, 'plus');
|
|
24
|
+
sanitized = sanitized.replace(/&/g, 'and');
|
|
25
|
+
sanitized = sanitized.replace(/@/g, 'at');
|
|
26
|
+
|
|
27
|
+
sanitized = slugify(sanitized, {
|
|
28
|
+
replacement: '-',
|
|
29
|
+
remove: /[^\w\s-]/g,
|
|
30
|
+
lower: true,
|
|
31
|
+
strict: true,
|
|
32
|
+
locale: 'en',
|
|
33
|
+
trim: true,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Strip any characters that still aren't lowercase-alphanumeric, hyphens, or underscores
|
|
37
|
+
sanitized = sanitized.replace(/[^a-z0-9\-_]/g, '-');
|
|
38
|
+
|
|
39
|
+
// Collapse repeated hyphens and strip leading/trailing hyphens or underscores
|
|
40
|
+
sanitized = sanitized.replace(/-+/g, '-');
|
|
41
|
+
sanitized = sanitized.replace(/^[-_]+|[-_]+$/g, '');
|
|
42
|
+
|
|
43
|
+
// IDs must not start with a number or hyphen
|
|
44
|
+
if (sanitized && !/^[a-z_]/.test(sanitized)) {
|
|
45
|
+
sanitized = 'font_' + sanitized;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!sanitized) {
|
|
49
|
+
sanitized = 'font_' + Date.now();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Sanity hard-caps IDs at 128 characters
|
|
53
|
+
if (sanitized.length > 128) {
|
|
54
|
+
const hash = Math.random().toString(36).substring(2, 8);
|
|
55
|
+
sanitized = sanitized.substring(0, 120) + '_' + hash;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Paranoid final validation
|
|
59
|
+
if (!/^[a-z_][a-z0-9\-_]*$/.test(sanitized)) {
|
|
60
|
+
console.warn(`ID sanitization produced invalid result: "${sanitized}", using fallback`);
|
|
61
|
+
sanitized = 'font_' + Date.now();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return sanitized;
|
|
65
|
+
}
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
// Sets up globalThis.pako and globalThis.unbrotli for lib-font WOFF/WOFF2 decompression.
|
|
2
|
-
// Must be imported before lib-font.
|
|
3
|
-
|
|
4
|
-
import pako from 'pako';
|
|
5
|
-
|
|
6
|
-
// Set pako for WOFF (zlib) decompression
|
|
7
|
-
globalThis.pako = pako;
|
|
8
|
-
|
|
9
|
-
// Set unbrotli for WOFF2 (brotli) decompression
|
|
10
|
-
// The vendor unbrotli.js UMD sets globalThis.unbrotli in browser contexts.
|
|
11
|
-
// In Node/bundler contexts it exports via module.exports instead.
|
|
12
|
-
// We use a side-effect import and then check if the global was set.
|
|
13
|
-
import '../vendor/unbrotli.js';
|
|
14
|
-
|
|
15
|
-
// If the UMD didn't set the global (CJS path in Node/bundler), try to require it
|
|
16
|
-
if (!globalThis.unbrotli) {
|
|
17
|
-
try {
|
|
18
|
-
// In bundler context, the UMD file's module.exports is available
|
|
19
|
-
// tsup will resolve this at build time
|
|
20
|
-
const brotli = require('../vendor/unbrotli.js');
|
|
21
|
-
if (typeof brotli === 'function') {
|
|
22
|
-
globalThis.unbrotli = brotli;
|
|
23
|
-
}
|
|
24
|
-
} catch {
|
|
25
|
-
// Silently fail — WOFF2 parsing will error gracefully
|
|
26
|
-
}
|
|
27
|
-
}
|
|
1
|
+
// Sets up globalThis.pako and globalThis.unbrotli for lib-font WOFF/WOFF2 decompression.
|
|
2
|
+
// Must be imported before lib-font.
|
|
3
|
+
|
|
4
|
+
import pako from 'pako';
|
|
5
|
+
|
|
6
|
+
// Set pako for WOFF (zlib) decompression
|
|
7
|
+
globalThis.pako = pako;
|
|
8
|
+
|
|
9
|
+
// Set unbrotli for WOFF2 (brotli) decompression
|
|
10
|
+
// The vendor unbrotli.js UMD sets globalThis.unbrotli in browser contexts.
|
|
11
|
+
// In Node/bundler contexts it exports via module.exports instead.
|
|
12
|
+
// We use a side-effect import and then check if the global was set.
|
|
13
|
+
import '../vendor/unbrotli.js';
|
|
14
|
+
|
|
15
|
+
// If the UMD didn't set the global (CJS path in Node/bundler), try to require it
|
|
16
|
+
if (!globalThis.unbrotli) {
|
|
17
|
+
try {
|
|
18
|
+
// In bundler context, the UMD file's module.exports is available
|
|
19
|
+
// tsup will resolve this at build time
|
|
20
|
+
const brotli = require('../vendor/unbrotli.js');
|
|
21
|
+
if (typeof brotli === 'function') {
|
|
22
|
+
globalThis.unbrotli = brotli;
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
// Silently fail — WOFF2 parsing will error gracefully
|
|
26
|
+
}
|
|
27
|
+
}
|