@gakim-digital/dexter-bridge 0.11.24 → 0.12.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.
@@ -1,196 +0,0 @@
1
- // Framer's style-based font search ("font-search" with a `query`) is matched
2
- // by Framer's AI and billed to the workspace's AI credit; when that fails, the
3
- // run still needs typography candidates. Google Fonts publishes its full
4
- // catalog (category, weights, italics, variable axes, popularity) without a
5
- // key, and every Google Fonts family is in Framer's font library, so the
6
- // bridge answers those searches from it in Framer's own result shape.
7
-
8
- const GOOGLE_FONTS_METADATA_URL = 'https://fonts.google.com/metadata/fonts';
9
- const CATALOG_TTL_MS = 24 * 60 * 60_000;
10
- const CATALOG_TIMEOUT_MS = 20_000;
11
- const MAX_RESULTS = 20;
12
-
13
- const WEIGHT_DESCRIPTORS = {
14
- thin: 100,
15
- 'extra-light': 200,
16
- light: 300,
17
- regular: 400,
18
- medium: 500,
19
- semibold: 600,
20
- bold: 700,
21
- 'extra-bold': 800,
22
- black: 900,
23
- };
24
-
25
- // Words in a style query that point at a category.
26
- const CATEGORY_WORDS = {
27
- serif: 'serif',
28
- serifs: 'serif',
29
- sans: 'sans-serif',
30
- 'sans-serif': 'sans-serif',
31
- grotesque: 'sans-serif',
32
- grotesk: 'sans-serif',
33
- geometric: 'sans-serif',
34
- slab: 'slab',
35
- mono: 'monospace',
36
- monospace: 'monospace',
37
- monospaced: 'monospace',
38
- code: 'monospace',
39
- script: 'handwriting',
40
- handwriting: 'handwriting',
41
- handwritten: 'handwriting',
42
- calligraphy: 'handwriting',
43
- display: 'display',
44
- poster: 'display',
45
- };
46
- const NARROW_WORDS = new Set(['condensed', 'narrow', 'compressed']);
47
-
48
- let cachedCatalog = null;
49
- let cachedAt = 0;
50
- let pendingCatalog = null;
51
-
52
- export async function loadGoogleFontsCatalog(fetchImpl = fetch) {
53
- if (cachedCatalog && Date.now() - cachedAt < CATALOG_TTL_MS) return cachedCatalog;
54
- pendingCatalog ||= (async () => {
55
- const response = await fetchImpl(GOOGLE_FONTS_METADATA_URL, {
56
- signal: AbortSignal.timeout(CATALOG_TIMEOUT_MS),
57
- });
58
- if (!response.ok) throw new Error(`Google Fonts catalog returned HTTP ${response.status}.`);
59
- const text = await response.text();
60
- // The feed starts with an anti-JSON-hijacking prefix before the object.
61
- const families = JSON.parse(text.slice(text.indexOf('{'))).familyMetadataList;
62
- if (!Array.isArray(families) || families.length === 0) {
63
- throw new Error('Google Fonts catalog is empty.');
64
- }
65
- cachedCatalog = families;
66
- cachedAt = Date.now();
67
- return families;
68
- })().finally(() => {
69
- pendingCatalog = null;
70
- });
71
- return pendingCatalog;
72
- }
73
-
74
- function categoryOf(family) {
75
- if (family.category === 'Monospace') return 'monospace';
76
- if (family.category === 'Handwriting') return 'handwriting';
77
- if (family.category === 'Display') return 'display';
78
- if (family.category === 'Serif') return 'serif';
79
- return 'sans-serif';
80
- }
81
-
82
- function fontFacts(family) {
83
- const keys = Object.keys(family.fonts || {});
84
- const weights = [...new Set(keys.map((key) => Number.parseInt(key, 10)))]
85
- .filter(Number.isFinite)
86
- .sort((a, b) => a - b);
87
- const styles = [
88
- ...(keys.some((key) => !key.endsWith('i')) ? ['normal'] : []),
89
- ...(keys.some((key) => key.endsWith('i')) ? ['italic'] : []),
90
- ];
91
- const axes = (family.axes || []).map((axis) => ({
92
- tag: axis.tag,
93
- minValue: axis.min,
94
- maxValue: axis.max,
95
- defaultValue: axis.defaultValue,
96
- }));
97
- return { weights, styles, axes };
98
- }
99
-
100
- function hasDescriptor(family, facts, descriptor) {
101
- const classifications = family.classifications || [];
102
- if (descriptor in WEIGHT_DESCRIPTORS) {
103
- const weight = WEIGHT_DESCRIPTORS[descriptor];
104
- const wght = facts.axes.find((axis) => axis.tag === 'wght');
105
- return facts.weights.includes(weight)
106
- || Boolean(wght && wght.minValue <= weight && wght.maxValue >= weight);
107
- }
108
- if (descriptor.startsWith('variation-axis/')) {
109
- const tag = descriptor.slice('variation-axis/'.length);
110
- return facts.axes.some((axis) => axis.tag === tag);
111
- }
112
- switch (descriptor) {
113
- case 'serif':
114
- return family.category === 'Serif' || family.stroke === 'Serif';
115
- case 'sans-serif':
116
- return family.category === 'Sans Serif' || family.stroke === 'Sans Serif';
117
- case 'slab':
118
- return family.stroke === 'Slab Serif';
119
- case 'monospace':
120
- return family.category === 'Monospace' || classifications.includes('Monospace');
121
- case 'display':
122
- return family.category === 'Display' || classifications.includes('Display');
123
- case 'handwriting':
124
- return family.category === 'Handwriting' || classifications.includes('Handwriting');
125
- case 'normal':
126
- case 'italic':
127
- return facts.styles.includes(descriptor);
128
- default:
129
- // Descriptors the catalog cannot judge do not exclude a family.
130
- return true;
131
- }
132
- }
133
-
134
- function queryScore(family, facts, words) {
135
- const nameWords = family.family.toLowerCase().split(/\s+/);
136
- let score = 0;
137
- for (const word of words) {
138
- const category = CATEGORY_WORDS[word];
139
- if (category && hasDescriptor(family, facts, category)) score += 2;
140
- if (NARROW_WORDS.has(word)) {
141
- const width = facts.axes.find((axis) => axis.tag === 'wdth');
142
- if (nameWords.some((part) => NARROW_WORDS.has(part)) || (width && width.minValue < 100)) {
143
- score += 2;
144
- }
145
- }
146
- if (
147
- !category
148
- && word.length >= 4
149
- && nameWords.some((part) => part.slice(0, 5) === word.slice(0, 5))
150
- ) {
151
- score += 3;
152
- }
153
- }
154
- return score;
155
- }
156
-
157
- // Ranks catalog families for one font-search query: every `mustHave`
158
- // descriptor is required, query words raise matching families, and Google
159
- // Fonts popularity breaks ties. More candidates than `limit` are returned
160
- // because keyword matching is coarse; the model chooses by how they look.
161
- export function searchGoogleFonts(catalog, query = {}) {
162
- const mustHave = (Array.isArray(query.mustHave) ? query.mustHave : [])
163
- .map((descriptor) => String(descriptor).trim().toLowerCase())
164
- .filter(Boolean);
165
- const words = String(query.query || '')
166
- .toLowerCase()
167
- .split(/[^a-z0-9-]+/)
168
- .filter(Boolean);
169
- const limit = Math.min(
170
- MAX_RESULTS,
171
- Math.max(10, (Number.parseInt(query.limit, 10) || 5) * 2),
172
- );
173
- return catalog
174
- .filter((family) =>
175
- typeof family?.family === 'string'
176
- && !family.isNoto
177
- && !family.isBrandFont
178
- && (family.subsets || []).includes('latin'))
179
- .map((family) => ({ family, facts: fontFacts(family) }))
180
- .filter(({ family, facts }) =>
181
- facts.weights.length > 0
182
- && mustHave.every((descriptor) => hasDescriptor(family, facts, descriptor)))
183
- .map((entry) => ({ ...entry, score: queryScore(entry.family, entry.facts, words) }))
184
- .sort((a, b) =>
185
- b.score - a.score
186
- || (a.family.popularity ?? Infinity) - (b.family.popularity ?? Infinity))
187
- .slice(0, limit)
188
- .map(({ family, facts }) => ({
189
- name: family.family,
190
- weights: facts.weights,
191
- styles: facts.styles,
192
- category: categoryOf(family),
193
- variationAxes: facts.axes,
194
- reason: 'Google Fonts catalog match',
195
- }));
196
- }