@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,9 @@
|
|
|
1
|
+
// Returns the Sanity client instance from the studio context
|
|
2
|
+
import {useMemo} from 'react'
|
|
3
|
+
import {useClient} from 'sanity'
|
|
4
|
+
|
|
5
|
+
/** Returns a memoized Sanity client pinned to api version 2021-10-23 */
|
|
6
|
+
export function useSanityClient() {
|
|
7
|
+
const client = useClient({apiVersion: '2021-10-23'})
|
|
8
|
+
return useMemo(() => client, [client])
|
|
9
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Entry point for @liiift-studio/sanity-font-manager — exports all font manager components, hooks, and utilities
|
|
2
|
+
|
|
3
|
+
// Components
|
|
4
|
+
export { BatchUploadFonts } from './components/BatchUploadFonts.jsx';
|
|
5
|
+
export { GenerateCollectionsPairsComponent } from './components/GenerateCollectionsPairsComponent.jsx';
|
|
6
|
+
export { UpdateScriptsComponent } from './components/UpdateScriptsComponent.jsx';
|
|
7
|
+
export { SingleUploaderTool } from './components/SingleUploaderTool.jsx';
|
|
8
|
+
export { RegenerateSubfamiliesComponent } from './components/RegenerateSubfamiliesComponent.jsx';
|
|
9
|
+
export { UploadScriptsComponent } from './components/UploadScriptsComponent.jsx';
|
|
10
|
+
export { FontScriptUploaderComponent } from './components/FontScriptUploaderComponent.jsx';
|
|
11
|
+
export { default as StatusDisplay } from './components/StatusDisplay.jsx';
|
|
12
|
+
export { default as PriceInput } from './components/PriceInput.jsx';
|
|
13
|
+
export { default as UploadButton } from './components/UploadButton.jsx';
|
|
14
|
+
|
|
15
|
+
// Hooks
|
|
16
|
+
export { useSanityClient } from './hooks/useSanityClient.js';
|
|
17
|
+
|
|
18
|
+
// Core utilities
|
|
19
|
+
export { default as generateCssFile } from './utils/generateCssFile.js';
|
|
20
|
+
export { default as generateFontData } from './utils/generateFontData.js';
|
|
21
|
+
export { default as generateFontFile } from './utils/generateFontFile.js';
|
|
22
|
+
export { default as generateSubset } from './utils/generateSubset.js';
|
|
23
|
+
export { default as parseVariableFontInstances } from './utils/parseVariableFontInstances.js';
|
|
24
|
+
export { getEmptyFontKit } from './utils/getEmptyFontKit.js';
|
|
25
|
+
export { SCRIPTS, SCRIPTS_OBJECT, HtmlDescription } from './utils/utils.js';
|
|
26
|
+
|
|
27
|
+
// Font processing utilities
|
|
28
|
+
export {
|
|
29
|
+
processFontFiles,
|
|
30
|
+
readFontFile,
|
|
31
|
+
extractFontMetadata,
|
|
32
|
+
extractWeightName,
|
|
33
|
+
extractWeightFromFullName,
|
|
34
|
+
processSubfamilyName,
|
|
35
|
+
processItalicKeywords,
|
|
36
|
+
formatFontTitle,
|
|
37
|
+
addItalicToFontTitle,
|
|
38
|
+
createFontObject,
|
|
39
|
+
determineWeight,
|
|
40
|
+
sortFontObjects,
|
|
41
|
+
logFontInfo,
|
|
42
|
+
} from './utils/processFontFiles.js';
|
|
43
|
+
|
|
44
|
+
export { uploadFontFiles } from './utils/uploadFontFiles.js';
|
|
45
|
+
export { updateTypefaceDocument } from './utils/updateTypefaceDocument.js';
|
|
46
|
+
export { renameFontDocuments } from './utils/regenerateFontData.js';
|
|
47
|
+
export { updateFontPrices } from './utils/updateFontPrices.js';
|
|
48
|
+
export { sanitizeForSanityId } from './utils/sanitizeForSanityId.js';
|
|
49
|
+
|
|
50
|
+
// Keyword utilities
|
|
51
|
+
export {
|
|
52
|
+
generateStyleKeywords,
|
|
53
|
+
reverseSpellingLookup,
|
|
54
|
+
expandAbbreviations,
|
|
55
|
+
removeWeightNames,
|
|
56
|
+
} from './utils/generateKeywords.js';
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// Builds a @font-face CSS file from a WOFF2 blob — URL or base64 src, variable font axis descriptors, metric-tuned fallback @font-face for CLS reduction
|
|
2
|
+
import base64 from 'base-64';
|
|
3
|
+
import { Buffer } from 'buffer';
|
|
4
|
+
import * as fontkit from 'fontkit';
|
|
5
|
+
|
|
6
|
+
function _arrayBufferToBase64(buffer) {
|
|
7
|
+
var binary = '';
|
|
8
|
+
var bytes = new Uint8Array(buffer);
|
|
9
|
+
var len = bytes.byteLength;
|
|
10
|
+
for (var i = 0; i < len; i++) {
|
|
11
|
+
binary += String.fromCharCode(bytes[i]);
|
|
12
|
+
}
|
|
13
|
+
return base64.encode(binary);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Reads variable axes from a fontkit font object and returns:
|
|
18
|
+
* { descriptors, skipped }
|
|
19
|
+
* where `descriptors` is the CSS string for the @font-face block and
|
|
20
|
+
* `skipped` lists axis tags that have no CSS descriptor (opsz, custom axes, etc.)
|
|
21
|
+
* so callers can surface them in comments.
|
|
22
|
+
*
|
|
23
|
+
* Edge cases handled:
|
|
24
|
+
* - Degenerate axes (min === max): skipped — no actual variation range
|
|
25
|
+
* - ital with max === 0: skipped — axis exists but font has no italic
|
|
26
|
+
* - slnt ordering: sorted ascending as CSS spec requires
|
|
27
|
+
* - ital + slnt coexistence: slnt takes priority (more expressive)
|
|
28
|
+
* - min > max from corrupt font data: clamped with Math.min/Math.max
|
|
29
|
+
* @param {Object} font - fontkit font instance
|
|
30
|
+
* @returns {{ descriptors: string, skipped: string[] }}
|
|
31
|
+
*/
|
|
32
|
+
export function buildVFDescriptors(font) {
|
|
33
|
+
const cssAxes = {}
|
|
34
|
+
const skipped = []
|
|
35
|
+
try {
|
|
36
|
+
const va = font.variationAxes
|
|
37
|
+
if (!va) return { descriptors: '', skipped: [] }
|
|
38
|
+
|
|
39
|
+
for (const [tag, axis] of Object.entries(va)) {
|
|
40
|
+
const lo = Math.min(axis.min, axis.max)
|
|
41
|
+
const hi = Math.max(axis.min, axis.max)
|
|
42
|
+
|
|
43
|
+
// Skip degenerate axes — no actual range
|
|
44
|
+
if (lo === hi) { skipped.push(tag); continue }
|
|
45
|
+
|
|
46
|
+
if (tag === 'wght') {
|
|
47
|
+
cssAxes['font-weight'] = `${lo} ${hi}`
|
|
48
|
+
} else if (tag === 'wdth') {
|
|
49
|
+
cssAxes['font-stretch'] = `${lo}% ${hi}%`
|
|
50
|
+
} else if (tag === 'slnt') {
|
|
51
|
+
// slnt: ascending order required by CSS Fonts Level 4
|
|
52
|
+
cssAxes['font-style'] = `oblique ${lo}deg ${hi}deg`
|
|
53
|
+
} else if (tag === 'ital' && !cssAxes['font-style']) {
|
|
54
|
+
// ital: only emit if font actually has italic range; slnt takes priority
|
|
55
|
+
if (hi > 0) cssAxes['font-style'] = 'italic'
|
|
56
|
+
else skipped.push(tag)
|
|
57
|
+
} else {
|
|
58
|
+
// opsz, GRAD, XTRA, XHGT and all other custom axes have no CSS @font-face descriptor
|
|
59
|
+
skipped.push(tag)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch (_) {
|
|
63
|
+
// axes unreadable — no descriptors
|
|
64
|
+
}
|
|
65
|
+
const descriptors = Object.entries(cssAxes).map(([k, v]) => `${k}:${v}`).join(';') + (Object.keys(cssAxes).length ? ';' : '')
|
|
66
|
+
return { descriptors, skipped }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Cross-platform fallback font stacks by category.
|
|
70
|
+
// Multiple local() sources ensure the first available system font is used.
|
|
71
|
+
// Liberation Sans covers Linux; Roboto covers Android; Georgia is universal for serifs.
|
|
72
|
+
const FALLBACK_STACKS = {
|
|
73
|
+
'sans-serif': "local('Arial'), local('Helvetica Neue'), local('Roboto'), local('Liberation Sans')",
|
|
74
|
+
'serif': "local('Georgia'), local('Times New Roman'), local('Times')",
|
|
75
|
+
'monospace': "local('Courier New'), local('Courier'), local('Menlo'), local('Monaco')",
|
|
76
|
+
// Display and script fonts have no universally suitable system fallback; default to sans-serif
|
|
77
|
+
'default': "local('Arial'), local('Helvetica Neue'), local('Roboto'), local('Liberation Sans')",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// OS/2 sFamilyClass high byte → FALLBACK_STACKS key.
|
|
81
|
+
// Classes 1–5,7 = serif variants; 8 = sans-serif; 9 = ornamental; 10 = script; 12 = symbolic.
|
|
82
|
+
const FAMILY_CLASS_MAP = {
|
|
83
|
+
1: 'serif', 2: 'serif', 3: 'serif', 4: 'serif', 5: 'serif', 7: 'serif',
|
|
84
|
+
8: 'sans-serif',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** Detects font category from the OS/2 sFamilyClass field; returns a key into FALLBACK_STACKS */
|
|
88
|
+
function detectFontCategory(font) {
|
|
89
|
+
try {
|
|
90
|
+
const familyClass = font._tables?.['OS/2']?.sFamilyClass ?? 0;
|
|
91
|
+
const highByte = (familyClass >> 8) & 0xFF;
|
|
92
|
+
return FAMILY_CLASS_MAP[highByte] ?? 'default';
|
|
93
|
+
} catch {
|
|
94
|
+
return 'default';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Extracts metric override percentages and detects the category fallback stack from a font ArrayBuffer */
|
|
99
|
+
function calcFallbackData(arrayBuffer) {
|
|
100
|
+
try {
|
|
101
|
+
let font = fontkit.create(Buffer.from(arrayBuffer));
|
|
102
|
+
let upm = font.unitsPerEm;
|
|
103
|
+
let category = detectFontCategory(font);
|
|
104
|
+
return {
|
|
105
|
+
fallbackSrc: FALLBACK_STACKS[category],
|
|
106
|
+
ascentOverride: `${(font.ascent / upm * 100).toFixed(2)}%`,
|
|
107
|
+
descentOverride: `${(Math.abs(font.descent) / upm * 100).toFixed(2)}%`,
|
|
108
|
+
lineGapOverride: `${(font.lineGap / upm * 100).toFixed(2)}%`,
|
|
109
|
+
};
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.error('Failed to extract fallback font data:', err);
|
|
112
|
+
return {
|
|
113
|
+
fallbackSrc: FALLBACK_STACKS['default'],
|
|
114
|
+
ascentOverride: '100%',
|
|
115
|
+
descentOverride: '0%',
|
|
116
|
+
lineGapOverride: '0%',
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export default async function generateCssFile({
|
|
122
|
+
woff2File,
|
|
123
|
+
fileInput,
|
|
124
|
+
language = null,
|
|
125
|
+
fileName,
|
|
126
|
+
fontName,
|
|
127
|
+
variableFont,
|
|
128
|
+
weight,
|
|
129
|
+
style = 'Normal',
|
|
130
|
+
client,
|
|
131
|
+
}) {
|
|
132
|
+
try {
|
|
133
|
+
// Read the file once; reuse the same buffer for base64 encoding and fontkit analysis
|
|
134
|
+
let arrayBuffer = await woff2File.arrayBuffer();
|
|
135
|
+
let b64 = _arrayBufferToBase64(arrayBuffer);
|
|
136
|
+
let fontkitFont = fontkit.create(Buffer.from(arrayBuffer));
|
|
137
|
+
let { fallbackSrc, ascentOverride, descentOverride, lineGapOverride } = calcFallbackData(arrayBuffer);
|
|
138
|
+
|
|
139
|
+
let cssString;
|
|
140
|
+
if (variableFont) {
|
|
141
|
+
let { descriptors, skipped } = buildVFDescriptors(fontkitFont);
|
|
142
|
+
// Axes with no CSS @font-face descriptor (opsz needs font-optical-sizing:auto on elements;
|
|
143
|
+
// custom axes like GRAD require CSS @font-face syntax not yet standardised).
|
|
144
|
+
let skipComment = skipped.length
|
|
145
|
+
? `/* axes present but have no @font-face descriptor: ${skipped.join(', ')}` +
|
|
146
|
+
(skipped.includes('opsz') ? ' — add font-optical-sizing:auto to your element CSS' : '') +
|
|
147
|
+
' */'
|
|
148
|
+
: ''
|
|
149
|
+
cssString = `${skipComment}@font-face{font-family:'${fontName}';src:url(data:application/font-woff2;charset=utf-8;base64,${b64})format('woff2-variations');${descriptors}font-display:swap;}`;
|
|
150
|
+
} else {
|
|
151
|
+
let fontStyle = style === 'Italic' ? 'italic' : 'normal';
|
|
152
|
+
cssString = `@font-face{font-family:'${fontName}';src:url(data:application/font-woff2;charset=utf-8;base64,${b64})format('woff2');font-weight:${weight};font-style:${fontStyle};font-display:swap;}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Fallback @font-face: tunes a category-appropriate system font to match the custom font's
|
|
156
|
+
// line metrics (ascent/descent/lineGap), reducing layout shift while the custom font loads.
|
|
157
|
+
// Customers who reference fontSrc in their own projects get this fallback automatically.
|
|
158
|
+
let fallbackCssString = `@font-face{font-family:'${fontName} Fallback';src:${fallbackSrc};ascent-override:${ascentOverride};descent-override:${descentOverride};line-gap-override:${lineGapOverride};}`;
|
|
159
|
+
|
|
160
|
+
let uploadBuffer = Buffer.from(cssString + fallbackCssString, 'utf-8');
|
|
161
|
+
let doc = await client.assets.upload('file', uploadBuffer, { filename: fileName + '.css' });
|
|
162
|
+
|
|
163
|
+
let newFileInput = language == null ?
|
|
164
|
+
{
|
|
165
|
+
...fileInput,
|
|
166
|
+
css: {
|
|
167
|
+
_type: 'file',
|
|
168
|
+
asset: {
|
|
169
|
+
_type: 'reference',
|
|
170
|
+
_ref: doc._id
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
:
|
|
175
|
+
{
|
|
176
|
+
...fileInput,
|
|
177
|
+
[language]: {
|
|
178
|
+
...fileInput[language],
|
|
179
|
+
css: {
|
|
180
|
+
_type: 'file',
|
|
181
|
+
asset: {
|
|
182
|
+
_type: 'reference',
|
|
183
|
+
_ref: doc._id
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return newFileInput;
|
|
190
|
+
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
console.error(err);
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
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';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extracts metadata and metrics from a fontkit font object into plain objects.
|
|
8
|
+
* @param {Object} font - fontkit font instance
|
|
9
|
+
* @returns {{ metaData: Object, metrics: Object }}
|
|
10
|
+
*/
|
|
11
|
+
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
|
+
};
|
|
33
|
+
return { metaData, metrics };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default async function generateFontData({ fileInput, url, fontKit, fontId, client, commit = true }) {
|
|
37
|
+
if (fontId.startsWith('drafts.')) {
|
|
38
|
+
fontId = fontId.replace('drafts.', '');
|
|
39
|
+
}
|
|
40
|
+
console.log('generate-font-data ', fontId, commit);
|
|
41
|
+
|
|
42
|
+
let srcUrl;
|
|
43
|
+
if (!url || url == null) {
|
|
44
|
+
srcUrl = await client.fetch(`*[_id == $id]{url}`, { id: fileInput.ttf.asset._ref });
|
|
45
|
+
console.log('src url ', srcUrl);
|
|
46
|
+
srcUrl = srcUrl[0].url;
|
|
47
|
+
} else {
|
|
48
|
+
srcUrl = url;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let font = fontKit;
|
|
52
|
+
if (!fontKit || fontKit == null) {
|
|
53
|
+
let buffer = await fetch(srcUrl);
|
|
54
|
+
buffer = await buffer.arrayBuffer();
|
|
55
|
+
buffer = Buffer.from(buffer);
|
|
56
|
+
font = fontkit.create(buffer);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
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
|
+
|
|
79
|
+
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];
|
|
91
|
+
});
|
|
92
|
+
variableInstances[key] = coord;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
}
|
|
96
|
+
console.log('font : ', font);
|
|
97
|
+
console.log('variable instances : ', variableInstances);
|
|
98
|
+
console.log('variable axes : ', variableAxes);
|
|
99
|
+
|
|
100
|
+
let opentypeFeatures = font.availableFeatures;
|
|
101
|
+
let glyphCount = font.numGlyphs;
|
|
102
|
+
let characterSet = font.characterSet;
|
|
103
|
+
|
|
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
|
+
};
|
|
113
|
+
|
|
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
|
+
};
|
|
126
|
+
|
|
127
|
+
let variableFont = false;
|
|
128
|
+
if (variableAxes && variableAxes != null && Object.keys(variableAxes).length > 0 && variableInstances && variableInstances != null && Object.keys(variableInstances).length > 0) {
|
|
129
|
+
variableFont = true;
|
|
130
|
+
}
|
|
131
|
+
let patch = {
|
|
132
|
+
metrics: metrics,
|
|
133
|
+
metaData: metaData,
|
|
134
|
+
variableFont: variableFont,
|
|
135
|
+
variableAxes: JSON.stringify(variableAxes),
|
|
136
|
+
variableInstances: JSON.stringify(variableInstances),
|
|
137
|
+
glyphCount: glyphCount,
|
|
138
|
+
opentypeFeatures: { chars: opentypeFeatures },
|
|
139
|
+
characterSet: { chars: characterSet }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log('data : ', patch);
|
|
143
|
+
if (commit) patch = await client.patch(fontId).set(patch).commit({ autoGenerateArrayKeys: true });
|
|
144
|
+
return patch;
|
|
145
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Triggers server-side font format conversion via the consuming site's fontWorker API endpoint
|
|
2
|
+
|
|
3
|
+
export default async function generateFontFile({
|
|
4
|
+
srcUrl,
|
|
5
|
+
language = null,
|
|
6
|
+
filename,
|
|
7
|
+
codes,
|
|
8
|
+
documentId,
|
|
9
|
+
documentTitle,
|
|
10
|
+
documentVariableFont,
|
|
11
|
+
documentStyle,
|
|
12
|
+
documentWeight,
|
|
13
|
+
fileInput
|
|
14
|
+
}) {
|
|
15
|
+
await fetch(`${process.env.SANITY_STUDIO_SITE_URL}/api/sanity/fontWorker`, {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
mode: 'no-cors',
|
|
18
|
+
headers: { 'Content-Type': 'application/json' },
|
|
19
|
+
body: JSON.stringify({
|
|
20
|
+
code: 'generate-fonts',
|
|
21
|
+
language: language,
|
|
22
|
+
srcUrl: srcUrl,
|
|
23
|
+
filename: filename,
|
|
24
|
+
documentId: documentId,
|
|
25
|
+
documentTitle: documentTitle,
|
|
26
|
+
documentVariableFont: documentVariableFont,
|
|
27
|
+
documentStyle: documentStyle,
|
|
28
|
+
documentWeight: documentWeight,
|
|
29
|
+
fileInput: fileInput,
|
|
30
|
+
codes: codes
|
|
31
|
+
})
|
|
32
|
+
}).catch(e => {
|
|
33
|
+
console.error(e.message);
|
|
34
|
+
return -1;
|
|
35
|
+
});
|
|
36
|
+
return 1;
|
|
37
|
+
|
|
38
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// Builds weight and italic keyword lists with abbreviation expansions for parsing font subfamily names
|
|
2
|
+
|
|
3
|
+
const coreWeights = ["Hairline", "ExtraThin", "Thin", "Mager", "Maigre", "ExtraLight", "Light", "Chiaro", "Lite", "Leicht", "Demi", "Book", "Buch", "Regular", "Normal", "Medium", "Stark", "Thick", "Kräftig", "Viertelfett", "Halbfett", "Dreiviertelfett", "Dark", "Bold", "Neretto", "Gras", "Fett", "Extrafett", "Black", "Nero", "Heavy", "Nerissimo", "Ultra", "Fat", "Poster"];
|
|
4
|
+
const modifiers = ["Demi", "Semi", "Extra", "Ultra", "Super", "Plus"];
|
|
5
|
+
|
|
6
|
+
const coreItalics = ["Italic", "Slant", "Oblique", "Cursive", "Rotalic", "Reverse", "Crab Claw", "Crabclaw", "South Paw", "Southpaw", "Backwards", "Backslant", "Backslanted", "Back Slant"];
|
|
7
|
+
|
|
8
|
+
/** All known abbreviation-to-canonical-name mappings, sorted alphabetically */
|
|
9
|
+
const alternativeSpelling = {
|
|
10
|
+
Backslant: ["Bsl"],
|
|
11
|
+
Backwards: ["Bck"],
|
|
12
|
+
Black: ["Blak", "Blk"],
|
|
13
|
+
Bold: ["Bd", "Bld"], // B omitted — too ambiguous
|
|
14
|
+
Condensed: ["Cond", "Cnd"],
|
|
15
|
+
Crabclaw: ["Crab", "Claw"],
|
|
16
|
+
Cursive: ["Cur"],
|
|
17
|
+
Dark: ["Drk"],
|
|
18
|
+
Expanded: ["Exp"],
|
|
19
|
+
Extra: ["Xt", "Xtra", "Xtr", "X"], // X omitted as standalone — too ambiguous
|
|
20
|
+
ExtraBlack: ["Xblk", "XBlk", "Xblck", "XBlck"],
|
|
21
|
+
ExtraBold: ["Xbd", "XBd", "Xbld", "XBld", "Xbold", "XBold", "ExBold", "Exbold", "Exbd", "ExBd", "Exbld", "ExBld"],
|
|
22
|
+
ExtraCondensed: ["XCond", "Xcnd"],
|
|
23
|
+
ExtraExpanded: ["XExp"],
|
|
24
|
+
ExtraLight: ["Xlight", "XLight", "Xlt", "XLt", "Xlgt", "XLgt", "Xl", "XL", "Xlght", "XLght"],
|
|
25
|
+
ExtraThin: ["Xthin", "Xthn", "Xth", "XThin", "XThn", "XTh", "XT"],
|
|
26
|
+
Extended: ["Ext"],
|
|
27
|
+
Hairline: ["Hl", "Hln", "Hlnn", "Hlnne", "Hlnnne"],
|
|
28
|
+
Italic: ["Ital", "It"],
|
|
29
|
+
Light: ["Lt", "Lght"],
|
|
30
|
+
Medium: ["Med", "Md", "md", "med"],
|
|
31
|
+
Oblique: ["Obl"],
|
|
32
|
+
Plus: ["Pls"],
|
|
33
|
+
Regular: ["Reg", "Rg"],
|
|
34
|
+
Reverse: ["Rev"],
|
|
35
|
+
Rotalic: ["Rot"],
|
|
36
|
+
SemiBold: ["SmBd", "Sb", "Sbd", "Sbld", "Sbold", "Semibd", "SemiBd", "Semibld", "SemiBld", "semiBd", "semiBld"],
|
|
37
|
+
Slant: ["Sl"],
|
|
38
|
+
Southpaw: ["South", "Paw"],
|
|
39
|
+
Super: ["Supr"],
|
|
40
|
+
Thin: ["Thn"],
|
|
41
|
+
Ultra: ["Ult", "Ultre", "Ul", "Ulta"],
|
|
42
|
+
XX: ["XXt", "XXtra", "XXtr", "XX"],
|
|
43
|
+
XXBlack: ["XXblk", "XXBlk", "XXblck", "XXBlck"],
|
|
44
|
+
XXLight: ["XXlight", "XXLight", "XXlt", "XXLt", "XXlgt", "XXLgt", "XXl", "XXL", "XXlght", "XXLght"],
|
|
45
|
+
XXX: ["XXXt", "XXXtra", "XXXtr", "XXX"],
|
|
46
|
+
XXXLight: ["XXXlight", "XXXLight", "XXXlt", "XXXLt", "XXXlgt", "XXXLgt", "XXXl", "XXXL", "XXXlght", "XXXLght"],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Maps an abbreviated font name word back to its canonical weight/style name */
|
|
50
|
+
export function reverseSpellingLookup(str) {
|
|
51
|
+
// Exact match first to avoid partial collisions
|
|
52
|
+
let exactMatch = "";
|
|
53
|
+
Object.keys(alternativeSpelling).forEach(function (key) {
|
|
54
|
+
alternativeSpelling[key].forEach(function (alternative) {
|
|
55
|
+
if (str === alternative) {
|
|
56
|
+
exactMatch = key;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
if (exactMatch) return exactMatch;
|
|
61
|
+
|
|
62
|
+
// Fall back to longest word-boundary match
|
|
63
|
+
let result = "";
|
|
64
|
+
let longestMatch = 0;
|
|
65
|
+
Object.keys(alternativeSpelling).forEach(function (key) {
|
|
66
|
+
alternativeSpelling[key].forEach(function (alternative) {
|
|
67
|
+
const regex = new RegExp(`\\b${alternative}\\b`);
|
|
68
|
+
if (regex.test(str) && alternative.length > longestMatch) {
|
|
69
|
+
result = key;
|
|
70
|
+
longestMatch = alternative.length;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Expands each word in a string from abbreviation to its canonical weight/style name */
|
|
78
|
+
export function expandAbbreviations(str) {
|
|
79
|
+
if (!str) return str;
|
|
80
|
+
return str.split(' ')
|
|
81
|
+
.map(word => {
|
|
82
|
+
const expanded = reverseSpellingLookup(word);
|
|
83
|
+
return expanded || word;
|
|
84
|
+
})
|
|
85
|
+
.join(' ');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Removes weight and style keywords from a string, returning only the remainder */
|
|
89
|
+
export function removeWeightNames(str) {
|
|
90
|
+
if (!str) return str;
|
|
91
|
+
return str.split(' ')
|
|
92
|
+
.map(word => {
|
|
93
|
+
coreWeights.forEach((weight) => {
|
|
94
|
+
if (word === weight) word = "";
|
|
95
|
+
modifiers.forEach((modifier) => {
|
|
96
|
+
if (word === modifier || modifier + weight === word) word = "";
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
const expanded = reverseSpellingLookup(word);
|
|
100
|
+
if (expanded) return "";
|
|
101
|
+
return word;
|
|
102
|
+
})
|
|
103
|
+
.join(' ')
|
|
104
|
+
.trim();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Generates comprehensive weight and italic keyword lists including all alternative spellings */
|
|
108
|
+
export function generateStyleKeywords() {
|
|
109
|
+
let weightKeywordList = [];
|
|
110
|
+
let italicKeywordList = [];
|
|
111
|
+
|
|
112
|
+
// Start with all core weights
|
|
113
|
+
weightKeywordList = [...coreWeights];
|
|
114
|
+
|
|
115
|
+
// Add all modifier + weight combinations
|
|
116
|
+
modifiers.forEach(modifier => {
|
|
117
|
+
coreWeights.forEach(weight => {
|
|
118
|
+
weightKeywordList.push(modifier + weight);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Add standalone modifiers
|
|
123
|
+
weightKeywordList = [...weightKeywordList, ...modifiers];
|
|
124
|
+
|
|
125
|
+
// Set up italic keywords
|
|
126
|
+
italicKeywordList = [...coreItalics];
|
|
127
|
+
|
|
128
|
+
// Expand weight list with alternative spellings
|
|
129
|
+
weightKeywordList = weightKeywordList.map(function (el) {
|
|
130
|
+
var newEls = [];
|
|
131
|
+
Object.keys(alternativeSpelling).forEach(function (key) {
|
|
132
|
+
if (el.indexOf(key) !== -1) {
|
|
133
|
+
alternativeSpelling[key].forEach(function (alternative) {
|
|
134
|
+
let newSpelling = el.replace(key, alternative);
|
|
135
|
+
newEls.push(newSpelling);
|
|
136
|
+
Object.keys(alternativeSpelling).forEach(function (key2) {
|
|
137
|
+
if (newSpelling.indexOf(key2) !== -1) {
|
|
138
|
+
alternativeSpelling[key2].forEach(function (alternative2) {
|
|
139
|
+
let newSpelling2 = newSpelling.replace(key2, alternative2);
|
|
140
|
+
newEls.push(newSpelling2);
|
|
141
|
+
Object.keys(alternativeSpelling).forEach(function (key3) {
|
|
142
|
+
if (newSpelling2.indexOf(key3) !== -1) {
|
|
143
|
+
alternativeSpelling[key3].forEach(function (alternative3) {
|
|
144
|
+
newEls.push(newSpelling2.replace(key3, alternative3));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
newEls.push(el);
|
|
155
|
+
return newEls;
|
|
156
|
+
}).reduce(function (a, b) {
|
|
157
|
+
return a.concat(b);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Expand italic list with alternative spellings
|
|
161
|
+
italicKeywordList = italicKeywordList.map(function (el) {
|
|
162
|
+
var newEls = [];
|
|
163
|
+
Object.keys(alternativeSpelling).forEach(function (key) {
|
|
164
|
+
if (el.indexOf(key) !== -1) {
|
|
165
|
+
alternativeSpelling[key].forEach(function (alternative) {
|
|
166
|
+
newEls.push(el.replace(key, alternative));
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
newEls.push(el);
|
|
171
|
+
return newEls;
|
|
172
|
+
}).reduce(function (a, b) {
|
|
173
|
+
return a.concat(b);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Sort longest to shortest so more specific matches win
|
|
177
|
+
weightKeywordList = weightKeywordList.sort((a, b) => b.length - a.length);
|
|
178
|
+
italicKeywordList = italicKeywordList.sort((a, b) => b.length - a.length);
|
|
179
|
+
|
|
180
|
+
// Deduplicate
|
|
181
|
+
weightKeywordList = weightKeywordList.filter((item, pos) => weightKeywordList.indexOf(item) === pos);
|
|
182
|
+
italicKeywordList = italicKeywordList.filter((item, pos) => italicKeywordList.indexOf(item) === pos);
|
|
183
|
+
|
|
184
|
+
return { weightKeywordList, italicKeywordList };
|
|
185
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Requests DS-WEB fingerprinted WOFF2 and display subset generation from an existing WOFF2 via fontWorker
|
|
2
|
+
// The server subsets the WOFF2 directly — no TTF conversion involved.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Calls fontWorker to generate a display subset WOFF2 and subset CSS from an existing WOFF2 URL.
|
|
6
|
+
* Patches fileInput.woff2_subset and fileInput.css_subset on the Sanity document directly.
|
|
7
|
+
* @param {object} params
|
|
8
|
+
* @param {string} params.woff2Url - CDN URL of the existing WOFF2 to subset
|
|
9
|
+
* @param {string} params.filename - base filename (no extension) for the generated assets
|
|
10
|
+
* @param {string} params.documentId
|
|
11
|
+
* @param {string} params.documentTitle
|
|
12
|
+
* @param {boolean} params.documentVariableFont
|
|
13
|
+
* @param {string} params.documentStyle
|
|
14
|
+
* @param {string} params.documentWeight
|
|
15
|
+
* @returns {Promise<number>} 1 on success, -1 on network error
|
|
16
|
+
*/
|
|
17
|
+
export default async function generateSubset({
|
|
18
|
+
woff2Url,
|
|
19
|
+
filename,
|
|
20
|
+
documentId,
|
|
21
|
+
documentTitle,
|
|
22
|
+
documentVariableFont,
|
|
23
|
+
documentStyle,
|
|
24
|
+
documentWeight,
|
|
25
|
+
}) {
|
|
26
|
+
await fetch(`${process.env.SANITY_STUDIO_SITE_URL}/api/sanity/fontWorker`, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
mode: 'no-cors',
|
|
29
|
+
headers: { 'Content-Type': 'application/json' },
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
code: 'generate-subset',
|
|
32
|
+
woff2Url,
|
|
33
|
+
filename,
|
|
34
|
+
documentId,
|
|
35
|
+
documentTitle,
|
|
36
|
+
documentVariableFont,
|
|
37
|
+
documentStyle,
|
|
38
|
+
documentWeight,
|
|
39
|
+
})
|
|
40
|
+
}).catch(e => {
|
|
41
|
+
console.warn('Subset generation call failed:', e.message);
|
|
42
|
+
return -1;
|
|
43
|
+
});
|
|
44
|
+
return 1;
|
|
45
|
+
}
|