dsh-fonttune 0.1.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/CHANGELOG.md +41 -0
- package/LICENSE +21 -0
- package/README.en.md +78 -0
- package/README.md +108 -0
- package/cordis.patch.yml +20 -0
- package/lib/client.js +2455 -0
- package/lib/index.js +124 -0
- package/lib/shared.cjs +663 -0
- package/package.json +66 -0
package/lib/shared.cjs
ADDED
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-fonttune shared core.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions and constants only — no document, no cordis, no React. The
|
|
5
|
+
* host half (`lib/index.js`, ESM) and the browser half (`lib/client.js`, the
|
|
6
|
+
* lazy-CJS loader format) both load this file, so it is written as a CJS module
|
|
7
|
+
* that also works when a browser bundle inlines it verbatim.
|
|
8
|
+
*
|
|
9
|
+
* The single source of truth for: the durable field names, the CSS a
|
|
10
|
+
* configuration produces, and the sanitizing that keeps user-typed font names
|
|
11
|
+
* from breaking out of the injected stylesheet.
|
|
12
|
+
*
|
|
13
|
+
* @module dsh-fonttune/shared
|
|
14
|
+
*/
|
|
15
|
+
"use strict";
|
|
16
|
+
|
|
17
|
+
/** Settings namespace registered by the host half. */
|
|
18
|
+
var NAMESPACE = "dsh-fonttune";
|
|
19
|
+
|
|
20
|
+
/** Field carrying the body/UI CSS font-family stack (empty = leave DSH alone). */
|
|
21
|
+
var SANS_FIELD = "sans";
|
|
22
|
+
|
|
23
|
+
/** Field carrying the code CSS font-family stack (empty = leave DSH alone). */
|
|
24
|
+
var MONO_FIELD = "mono";
|
|
25
|
+
|
|
26
|
+
/** Field carrying the global font-size offset in px (0 = leave DSH alone). */
|
|
27
|
+
var SIZE_FIELD = "sizeOffset";
|
|
28
|
+
|
|
29
|
+
/** Field carrying the global font weight (0 = leave DSH alone). */
|
|
30
|
+
var WEIGHT_FIELD = "weight";
|
|
31
|
+
|
|
32
|
+
/** Allowed font-size offset range. The upper bound stays under a 2x scale. */
|
|
33
|
+
var SIZE_MIN = -3;
|
|
34
|
+
var SIZE_MAX = 6;
|
|
35
|
+
|
|
36
|
+
/** Allowed font weight range, and the value meaning "do not touch". */
|
|
37
|
+
var WEIGHT_MIN = 300;
|
|
38
|
+
var WEIGHT_MAX = 600;
|
|
39
|
+
var WEIGHT_UNSET = 0;
|
|
40
|
+
|
|
41
|
+
/** Marker on both injected style tags, used for scoping every rule we write. */
|
|
42
|
+
var MARKER = "dfp";
|
|
43
|
+
|
|
44
|
+
/** `data-plugin-css` value of the tag applying families/offset/weight. */
|
|
45
|
+
var STYLE_TAG = "dsh-fonttune";
|
|
46
|
+
|
|
47
|
+
/** `data-plugin-css` value of the card's own chrome stylesheet. */
|
|
48
|
+
var CARD_STYLE_TAG = "dsh-fonttune-card";
|
|
49
|
+
|
|
50
|
+
/** Composition defaults: every axis dormant, so installing changes nothing. */
|
|
51
|
+
var DEFAULTS = {
|
|
52
|
+
sans: "",
|
|
53
|
+
mono: "",
|
|
54
|
+
sizeOffset: 0,
|
|
55
|
+
weight: WEIGHT_UNSET,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Longest accepted font stack, in characters (mirrored by the host schema).
|
|
60
|
+
*/
|
|
61
|
+
var MAX_STACK_LENGTH = 200;
|
|
62
|
+
|
|
63
|
+
/** Longest accepted single family name, in characters. */
|
|
64
|
+
var MAX_FAMILY_LENGTH = 64;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* What may survive in a family name.
|
|
68
|
+
*
|
|
69
|
+
* This is an allow-list, not a deny-list: a family name is letters (any
|
|
70
|
+
* script, so CJK and accented Latin pass), digits, spaces and the handful of
|
|
71
|
+
* punctuation marks real families use. Everything else — quotes, braces,
|
|
72
|
+
* semicolons, colons, commas, parentheses, slashes, angle brackets, comment
|
|
73
|
+
* markers — is dropped, which is what makes it impossible for a typed name to
|
|
74
|
+
* end a declaration, open a rule, or reach `url(...)`.
|
|
75
|
+
*/
|
|
76
|
+
var UNSAFE_CHARS = /[^\p{L}\p{N} .,_-]/gu;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Strip everything a family name cannot contain, and collapse whitespace.
|
|
80
|
+
* @param {unknown} value - candidate text.
|
|
81
|
+
* @returns {string} the safe text (possibly empty).
|
|
82
|
+
*/
|
|
83
|
+
function sanitize(value) {
|
|
84
|
+
if (typeof value !== "string") return "";
|
|
85
|
+
return value.replace(UNSAFE_CHARS, "").replace(/\s+/g, " ").trim();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Sanitize one family name and drop what cannot be one.
|
|
90
|
+
* @param {unknown} value - candidate family name.
|
|
91
|
+
* @returns {string} the safe name, or "" when nothing usable is left.
|
|
92
|
+
*/
|
|
93
|
+
function sanitizeFamily(value) {
|
|
94
|
+
var name = sanitize(value).slice(0, MAX_FAMILY_LENGTH);
|
|
95
|
+
// A lone comma would split into an empty entry; a lone quote cannot pair.
|
|
96
|
+
if (name === "" || name === ",") return "";
|
|
97
|
+
return name;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Render one family name the way it must appear inside a CSS list.
|
|
102
|
+
*
|
|
103
|
+
* Generic keywords are passed through; anything else is double-quoted, because
|
|
104
|
+
* unquoted multi-word names are invalid CSS unless every word is an identifier.
|
|
105
|
+
* @param {string} name - a sanitized family name.
|
|
106
|
+
* @returns {string} one CSS list entry.
|
|
107
|
+
*/
|
|
108
|
+
function quoteFamily(name) {
|
|
109
|
+
var text = sanitizeFamily(name);
|
|
110
|
+
if (text === "") return "";
|
|
111
|
+
if (GENERIC_FAMILIES.indexOf(text.toLowerCase()) >= 0) return text;
|
|
112
|
+
return '"' + text + '"';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* CSS-wide generic family keywords and the two system shorthands.
|
|
117
|
+
*/
|
|
118
|
+
var GENERIC_FAMILIES = [
|
|
119
|
+
"serif",
|
|
120
|
+
"sans-serif",
|
|
121
|
+
"monospace",
|
|
122
|
+
"cursive",
|
|
123
|
+
"fantasy",
|
|
124
|
+
"system-ui",
|
|
125
|
+
"ui-serif",
|
|
126
|
+
"ui-sans-serif",
|
|
127
|
+
"ui-monospace",
|
|
128
|
+
"ui-rounded",
|
|
129
|
+
"math",
|
|
130
|
+
"emoji",
|
|
131
|
+
"fangsong",
|
|
132
|
+
"-apple-system",
|
|
133
|
+
"blinkmacsystemfont",
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Normalize a configuration-shaped object coming from the settings document,
|
|
138
|
+
* a config layer, or a test fixture.
|
|
139
|
+
* @param {unknown} value - candidate configuration.
|
|
140
|
+
* @returns {{sans: string, mono: string, sizeOffset: number, weight: number}} the normalized config.
|
|
141
|
+
*/
|
|
142
|
+
function normalizeConfig(value) {
|
|
143
|
+
var source = value !== null && typeof value === "object" ? value : {};
|
|
144
|
+
var size = Number(source[SIZE_FIELD]);
|
|
145
|
+
if (!isFinite(size)) size = 0;
|
|
146
|
+
size = Math.round(Math.min(SIZE_MAX, Math.max(SIZE_MIN, size)));
|
|
147
|
+
var weight = Number(source[WEIGHT_FIELD]);
|
|
148
|
+
if (!isFinite(weight)) weight = WEIGHT_UNSET;
|
|
149
|
+
weight = Math.round(weight);
|
|
150
|
+
if (weight !== WEIGHT_UNSET) {
|
|
151
|
+
weight = Math.min(WEIGHT_MAX, Math.max(WEIGHT_MIN, weight));
|
|
152
|
+
}
|
|
153
|
+
var config = {};
|
|
154
|
+
config[SANS_FIELD] = sanitize(source[SANS_FIELD]).slice(0, MAX_STACK_LENGTH);
|
|
155
|
+
config[MONO_FIELD] = sanitize(source[MONO_FIELD]).slice(0, MAX_STACK_LENGTH);
|
|
156
|
+
config[SIZE_FIELD] = size;
|
|
157
|
+
config[WEIGHT_FIELD] = weight;
|
|
158
|
+
return config;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Format a family list as a CSS font-family value.
|
|
163
|
+
* @param {readonly string[]} families - family names in precedence order.
|
|
164
|
+
* @returns {string} the CSS list, or "" when nothing usable remains.
|
|
165
|
+
*/
|
|
166
|
+
function formatStack(families) {
|
|
167
|
+
var parts = [];
|
|
168
|
+
for (var index = 0; index < families.length; index += 1) {
|
|
169
|
+
var entry = quoteFamily(families[index]);
|
|
170
|
+
if (entry !== "") parts.push(entry);
|
|
171
|
+
}
|
|
172
|
+
return parts.join(", ");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Split a CSS font-family value back into family names.
|
|
177
|
+
*
|
|
178
|
+
* Tolerates both quoting styles, missing spaces after commas, and stray
|
|
179
|
+
* whitespace — the value may have been typed by hand or written by an earlier
|
|
180
|
+
* version of the picker.
|
|
181
|
+
* @param {unknown} value - a CSS font-family value.
|
|
182
|
+
* @returns {string[]} the family names, in order, without quotes.
|
|
183
|
+
*/
|
|
184
|
+
function parseStack(value) {
|
|
185
|
+
if (typeof value !== "string" || value.trim() === "") return [];
|
|
186
|
+
var out = [];
|
|
187
|
+
var current = "";
|
|
188
|
+
var quote = "";
|
|
189
|
+
for (var index = 0; index < value.length; index += 1) {
|
|
190
|
+
var char = value.charAt(index);
|
|
191
|
+
if (quote !== "") {
|
|
192
|
+
if (char === quote) quote = "";
|
|
193
|
+
else current += char;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (char === '"' || char === "'") {
|
|
197
|
+
quote = char;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (char === ",") {
|
|
201
|
+
out.push(current);
|
|
202
|
+
current = "";
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
current += char;
|
|
206
|
+
}
|
|
207
|
+
out.push(current);
|
|
208
|
+
var families = [];
|
|
209
|
+
for (var index2 = 0; index2 < out.length; index2 += 1) {
|
|
210
|
+
var name = sanitizeFamily(out[index2]);
|
|
211
|
+
if (name === "") continue;
|
|
212
|
+
// A generic keyword quoted by hand ("monospace") is normalized back.
|
|
213
|
+
families.push(name);
|
|
214
|
+
}
|
|
215
|
+
return families;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Keyword-only family names: they match every script, so they are neither a
|
|
220
|
+
* western nor a CJK slot value.
|
|
221
|
+
* @param {string} name - a family name.
|
|
222
|
+
* @returns {boolean} true for generic keywords.
|
|
223
|
+
*/
|
|
224
|
+
function isGenericFamilyName(name) {
|
|
225
|
+
var lower = name.toLowerCase();
|
|
226
|
+
return (
|
|
227
|
+
lower === "system-ui" ||
|
|
228
|
+
lower === "sans-serif" ||
|
|
229
|
+
lower === "serif" ||
|
|
230
|
+
lower === "monospace" ||
|
|
231
|
+
lower === "cursive" ||
|
|
232
|
+
lower === "fantasy" ||
|
|
233
|
+
lower === "math" ||
|
|
234
|
+
lower === "emoji" ||
|
|
235
|
+
lower === "fangsong" ||
|
|
236
|
+
lower === "ui-monospace" ||
|
|
237
|
+
lower === "ui-sans-serif" ||
|
|
238
|
+
lower === "ui-serif" ||
|
|
239
|
+
lower === "ui-rounded"
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Name-shaped CJK recognition, used only when the browser cannot measure a
|
|
245
|
+
* family's actual glyph coverage. Deliberately generous: a false "east" on a
|
|
246
|
+
* western font merely means the simple mode shows it in the CJK slot, while a
|
|
247
|
+
* miss would put a CJK family into the western slot where it hurts.
|
|
248
|
+
* @param {string} name - a family name.
|
|
249
|
+
* @returns {boolean} true when the name looks like a CJK family.
|
|
250
|
+
*/
|
|
251
|
+
function isCJKFamilyName(name) {
|
|
252
|
+
var lower = name.toLowerCase();
|
|
253
|
+
if (
|
|
254
|
+
/(?:yahei|jhenghei|pingfang|hiragino|simsun|simhei|nsimsun|kaiti|fangsong|meiryo|yu ?goth|yu ?minch|ms ?gothic|ms ?mincho|noto (?:sans|serif) (?:sc|tc|cjk|jp|kr|hk)|source han|sourcehansc|sourcehanserifc|sarasa|misans|harmonyos|wenquanyi|lxgw|unifont|dengxian)/.test(
|
|
255
|
+
lower
|
|
256
|
+
)
|
|
257
|
+
) {
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
return /[\u4e00-\u9fff]/.test(name);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Set the western slot of a stack: the first entry a classifier counts as
|
|
265
|
+
* non-east. Simple mode binds this to the front of the stack, so a tuned
|
|
266
|
+
* order beyond the two slots is never rearranged.
|
|
267
|
+
* @param {readonly string[]} families - the current stack, in precedence order.
|
|
268
|
+
* @param {string} family - the family to place in the slot.
|
|
269
|
+
* @param {(name: string) => boolean} isEast - CJK classifier.
|
|
270
|
+
* @returns {string[]} the new stack.
|
|
271
|
+
*/
|
|
272
|
+
function setWestEntry(families, family, isEast) {
|
|
273
|
+
var next = [];
|
|
274
|
+
for (var index = 0; index < families.length; index += 1) {
|
|
275
|
+
if (families[index].toLowerCase() !== family.toLowerCase()) next.push(families[index]);
|
|
276
|
+
}
|
|
277
|
+
for (var index2 = 0; index2 < next.length; index2 += 1) {
|
|
278
|
+
if (!isEast(next[index2])) {
|
|
279
|
+
next[index2] = family;
|
|
280
|
+
return next;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
next.unshift(family);
|
|
284
|
+
return next;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Set the CJK slot of a stack: the first entry the classifier counts as east,
|
|
289
|
+
* or a new entry right after the western slot — the position CSS semantics
|
|
290
|
+
* need for `western, cjk, ...` to actually route glyphs.
|
|
291
|
+
* @param {readonly string[]} families - the current stack, in precedence order.
|
|
292
|
+
* @param {string} family - the family to place in the slot.
|
|
293
|
+
* @param {(name: string) => boolean} isEast - CJK classifier.
|
|
294
|
+
* @returns {string[]} the new stack.
|
|
295
|
+
*/
|
|
296
|
+
function setEastEntry(families, family, isEast) {
|
|
297
|
+
var next = [];
|
|
298
|
+
for (var index = 0; index < families.length; index += 1) {
|
|
299
|
+
if (families[index].toLowerCase() !== family.toLowerCase()) next.push(families[index]);
|
|
300
|
+
}
|
|
301
|
+
for (var index2 = 0; index2 < next.length; index2 += 1) {
|
|
302
|
+
if (isEast(next[index2])) {
|
|
303
|
+
next[index2] = family;
|
|
304
|
+
return next;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
var insertAt = 0;
|
|
308
|
+
for (var index3 = 0; index3 < next.length; index3 += 1) {
|
|
309
|
+
if (!isEast(next[index3])) {
|
|
310
|
+
// Right after the western slot: a generic catch-all later in the stack
|
|
311
|
+
// must not shadow the CJK entry.
|
|
312
|
+
insertAt = index3 + 1;
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
next.splice(insertAt, 0, family);
|
|
317
|
+
return next;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Remove one family wherever it sits in the stack.
|
|
322
|
+
* @param {readonly string[]} families - the current stack.
|
|
323
|
+
* @param {string} family - the family to remove.
|
|
324
|
+
* @returns {string[]} the new stack.
|
|
325
|
+
*/
|
|
326
|
+
function removeStackEntry(families, family) {
|
|
327
|
+
var next = [];
|
|
328
|
+
for (var index = 0; index < families.length; index += 1) {
|
|
329
|
+
if (families[index].toLowerCase() !== family.toLowerCase()) next.push(families[index]);
|
|
330
|
+
}
|
|
331
|
+
return next;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Compute the uniform scale a pixel offset produces over DSH's 16px base.
|
|
336
|
+
*
|
|
337
|
+
* Scaling by one ratio is what keeps a size change proportional: every design
|
|
338
|
+
* token that carries a size or a line height rides the same factor, so nothing
|
|
339
|
+
* compounds through nesting and no element ends up with a size its own line
|
|
340
|
+
* height does not expect.
|
|
341
|
+
* @param {number} sizeOffset - offset in px.
|
|
342
|
+
* @param {number} [base=16] - the px size the ratio is taken against.
|
|
343
|
+
* @returns {number} the scale factor (1 when the offset is 0).
|
|
344
|
+
*/
|
|
345
|
+
function scaleFor(sizeOffset, base) {
|
|
346
|
+
var offset = Number(sizeOffset);
|
|
347
|
+
if (!isFinite(offset) || offset === 0) return 1;
|
|
348
|
+
var reference = typeof base === "number" && base > 0 ? base : 16;
|
|
349
|
+
var scale = (reference + offset) / reference;
|
|
350
|
+
return scale < 0.5 ? 0.5 : scale > 2 ? 2 : scale;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Whether a configuration asks for any change at all.
|
|
355
|
+
* @param {{sans: string, mono: string, sizeOffset: number, weight: number}} config - normalized config.
|
|
356
|
+
* @returns {boolean} true when nothing should be injected.
|
|
357
|
+
*/
|
|
358
|
+
function isDormant(config) {
|
|
359
|
+
return (
|
|
360
|
+
config[SANS_FIELD] === "" &&
|
|
361
|
+
config[MONO_FIELD] === "" &&
|
|
362
|
+
config[SIZE_FIELD] === 0 &&
|
|
363
|
+
config[WEIGHT_FIELD] === WEIGHT_UNSET
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Build the stylesheet one configuration applies.
|
|
369
|
+
*
|
|
370
|
+
* Families are declared at their SOURCE: DSH's design tokens chain to two
|
|
371
|
+
* variables (`--dsw-font-family` for every sans token, `--ds-font-family-code`
|
|
372
|
+
* for every code token — verified against the shipped theme), so overriding
|
|
373
|
+
* those variables is what reaches the conversation markdown and the sidebar,
|
|
374
|
+
* whose elements declare `font-family: var(--dsw-font-family)` themselves and
|
|
375
|
+
* would never inherit a plain `body` rule. The explicit `body` / `pre,code`
|
|
376
|
+
* rules stay as a second path for elements that hardcode a family.
|
|
377
|
+
*
|
|
378
|
+
* The size offset rewrites DSH's own design tokens rather than every element:
|
|
379
|
+
* a token whose name ends in `-font-size` or `-line-height` is re-declared as
|
|
380
|
+
* itself multiplied by one scale factor. `baseTokens` supplies the untouched
|
|
381
|
+
* values (read from the live document, falling back to the embedded map), so
|
|
382
|
+
* the ratio composes with DSH's own font-size slider instead of replacing it.
|
|
383
|
+
*
|
|
384
|
+
* @param {{sans: string, mono: string, sizeOffset: number, weight: number}} config - normalized config.
|
|
385
|
+
* @param {Record<string, string>} [baseTokens] - token name to untouched value.
|
|
386
|
+
* @returns {string} declarations for one `<style>` element ("" when dormant).
|
|
387
|
+
*/
|
|
388
|
+
function buildFontCss(config, baseTokens) {
|
|
389
|
+
var normalized = normalizeConfig(config);
|
|
390
|
+
var sans = formatStack(parseStack(normalized[SANS_FIELD]));
|
|
391
|
+
var mono = formatStack(parseStack(normalized[MONO_FIELD]));
|
|
392
|
+
var offset = normalized[SIZE_FIELD];
|
|
393
|
+
var weight = normalized[WEIGHT_FIELD];
|
|
394
|
+
if (isDormant(normalized)) return "";
|
|
395
|
+
var declarations = [];
|
|
396
|
+
|
|
397
|
+
if (sans !== "") {
|
|
398
|
+
declarations.push(":root,body{--dsw-font-family:" + sans + " !important}");
|
|
399
|
+
declarations.push("body{font-family:" + sans + " !important}");
|
|
400
|
+
}
|
|
401
|
+
if (mono !== "") {
|
|
402
|
+
// The theme's code tokens chain to `--ds-font-family-code`; the other name
|
|
403
|
+
// is what dsh-ui-font historically wrote and costs nothing to cover.
|
|
404
|
+
declarations.push(
|
|
405
|
+
":root,body{--dsw-font-mono:" + mono + " !important;--ds-font-family-code:" + mono + " !important}"
|
|
406
|
+
);
|
|
407
|
+
// Written after the body rule: equal specificity on a code element means
|
|
408
|
+
// the later declaration wins, so code keeps its own family inside the UI.
|
|
409
|
+
declarations.push(
|
|
410
|
+
"pre,code,kbd,samp,var,tt,textarea,.cm-editor .cm-content{font-family:" +
|
|
411
|
+
mono +
|
|
412
|
+
" !important}"
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
if (offset !== 0) {
|
|
416
|
+
var scale = round(scaleFor(offset), 6);
|
|
417
|
+
var names = tokenNames(baseTokens);
|
|
418
|
+
var scaled = [];
|
|
419
|
+
for (var index = 0; index < names.length; index += 1) {
|
|
420
|
+
var name = names[index];
|
|
421
|
+
var base = baseTokens[name];
|
|
422
|
+
if (typeof base !== "string" || base === "") continue;
|
|
423
|
+
// A base that itself references another token via var() derives from it:
|
|
424
|
+
// scaling the var target already scales this one, so re-scaling here
|
|
425
|
+
// would compound (markdown-base = var(--dsh-content-font-size) would
|
|
426
|
+
// take the ratio twice). DSH's chain does the work instead.
|
|
427
|
+
if (base.indexOf("var(") !== -1) continue;
|
|
428
|
+
// !important: the theme writes `--dsh-content-font-size` INLINE on body,
|
|
429
|
+
// and an inline declaration outranks a plain stylesheet one — without
|
|
430
|
+
// the flag body itself would keep DSH's own size while every descendant
|
|
431
|
+
// scales, splitting the page in two.
|
|
432
|
+
scaled.push(name + ":calc((" + base + ") * " + scale + ") !important");
|
|
433
|
+
}
|
|
434
|
+
if (scaled.length > 0) {
|
|
435
|
+
declarations.push(bodyAndDescendants(scaled.join(";")));
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (weight !== WEIGHT_UNSET) {
|
|
439
|
+
// Written as the user chose it: a variable font honors every integer, and
|
|
440
|
+
// a static one rounds to its own nearest step by itself.
|
|
441
|
+
declarations.push(bodyAndDescendants("font-weight:" + weight + " !important"));
|
|
442
|
+
}
|
|
443
|
+
return declarations.join("\n");
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Match DSH's typography tokens: the design system's sizes plus the content
|
|
448
|
+
* size the theme plugin writes on `body` (whose secondary variant carries a
|
|
449
|
+
* suffix of its own). Line heights scale with sizes so a token's shorthand
|
|
450
|
+
* never disagrees with its parts.
|
|
451
|
+
*
|
|
452
|
+
* Deliberately a shape test rather than an enumeration: DSH generates these
|
|
453
|
+
* tokens at runtime, so whatever a future release names them, a token that
|
|
454
|
+
* ends in a size or a line height is one that must scale.
|
|
455
|
+
*/
|
|
456
|
+
var TOKEN_PATTERN = /^--(?:dsw-font-[a-z0-9-]*|dsh-content-font)-(?:font-size|line-height)(?:-secondary)?$|^--dsh-content-font-(?:size|line-height)(?:-secondary)?$/;
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* A `body, body *` selector list, kept in one place because every global
|
|
460
|
+
* declaration needs the same reach (DSH writes some tokens on descendants).
|
|
461
|
+
* @param {string} declarations - CSS declarations without braces.
|
|
462
|
+
* @returns {string} the rule.
|
|
463
|
+
*/
|
|
464
|
+
function bodyAndDescendants(declarations) {
|
|
465
|
+
return "body,body *{" + declarations + "}";
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Whether one custom-property name is a token this plugin scales.
|
|
470
|
+
* @param {string} name - custom property name including the leading dashes.
|
|
471
|
+
* @returns {boolean} true when the token carries a size or a line height.
|
|
472
|
+
*/
|
|
473
|
+
function isFontToken(name) {
|
|
474
|
+
return typeof name === "string" && TOKEN_PATTERN.test(name);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Ordered token names present in a base map, sizes before line heights so a
|
|
479
|
+
* reader of the stylesheet sees each size next to its height.
|
|
480
|
+
* @param {Record<string, string>} [baseTokens] - token name to value.
|
|
481
|
+
* @returns {string[]} the names to scale.
|
|
482
|
+
*/
|
|
483
|
+
function tokenNames(baseTokens) {
|
|
484
|
+
if (baseTokens === null || typeof baseTokens !== "object") return [];
|
|
485
|
+
var names = Object.keys(baseTokens).filter(isFontToken);
|
|
486
|
+
names.sort(function (left, right) {
|
|
487
|
+
if (left.length !== right.length) return left.length - right.length;
|
|
488
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
489
|
+
});
|
|
490
|
+
return names;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Embedded fallback for the tokens the theme plugin defines at runtime.
|
|
495
|
+
*
|
|
496
|
+
* The live document is always preferred; this map only covers the window
|
|
497
|
+
* before those declarations exist (and any token a future release renames
|
|
498
|
+
* away from the pattern). Values mirror the size/line-height pairs shipped in
|
|
499
|
+
* DSH 0.1.5-rc.2.
|
|
500
|
+
*/
|
|
501
|
+
var FALLBACK_TOKENS = {
|
|
502
|
+
"--dsw-font-xxxs-11-font-size": "11px",
|
|
503
|
+
"--dsw-font-xxxs-11-line-height": "18px",
|
|
504
|
+
"--dsw-font-xxxs-strong-11-font-size": "11px",
|
|
505
|
+
"--dsw-font-xxxs-strong-11-line-height": "18px",
|
|
506
|
+
"--dsw-font-xxs-12-font-size": "12px",
|
|
507
|
+
"--dsw-font-xxs-12-line-height": "20px",
|
|
508
|
+
"--dsw-font-xxs-strong-12-font-size": "12px",
|
|
509
|
+
"--dsw-font-xxs-strong-12-line-height": "20px",
|
|
510
|
+
"--dsw-font-xs-13-font-size": "13px",
|
|
511
|
+
"--dsw-font-xs-13-line-height": "22px",
|
|
512
|
+
"--dsw-font-xs-strong-13-font-size": "13px",
|
|
513
|
+
"--dsw-font-xs-strong-13-line-height": "22px",
|
|
514
|
+
"--dsw-font-s-14-font-size": "14px",
|
|
515
|
+
"--dsw-font-s-14-line-height": "24px",
|
|
516
|
+
"--dsw-font-s-strong-14-font-size": "14px",
|
|
517
|
+
"--dsw-font-s-strong-14-line-height": "24px",
|
|
518
|
+
"--dsw-font-base-16-font-size": "16px",
|
|
519
|
+
"--dsw-font-base-16-line-height": "26px",
|
|
520
|
+
"--dsw-font-base-strong-16-font-size": "16px",
|
|
521
|
+
"--dsw-font-base-strong-16-line-height": "26px",
|
|
522
|
+
"--dsw-font-m-18-font-size": "18px",
|
|
523
|
+
"--dsw-font-m-18-line-height": "28px",
|
|
524
|
+
"--dsw-font-l-20-font-size": "20px",
|
|
525
|
+
"--dsw-font-l-20-line-height": "30px",
|
|
526
|
+
"--dsw-font-xl-24-font-size": "24px",
|
|
527
|
+
"--dsw-font-xl-24-line-height": "34px",
|
|
528
|
+
"--dsw-font-markdown-base-font-size": "14px",
|
|
529
|
+
"--dsw-font-markdown-base-line-height": "24px",
|
|
530
|
+
"--dsw-font-markdown-small-font-size": "13px",
|
|
531
|
+
"--dsw-font-markdown-small-line-height": "22px",
|
|
532
|
+
"--dsw-font-markdown-h1-font-size": "21px",
|
|
533
|
+
"--dsw-font-markdown-h1-line-height": "30px",
|
|
534
|
+
"--dsw-font-markdown-h2-font-size": "19px",
|
|
535
|
+
"--dsw-font-markdown-h2-line-height": "28px",
|
|
536
|
+
"--dsw-font-markdown-h3-font-size": "17px",
|
|
537
|
+
"--dsw-font-markdown-h3-line-height": "26px",
|
|
538
|
+
"--dsw-font-markdown-h4-font-size": "15px",
|
|
539
|
+
"--dsw-font-markdown-h4-line-height": "24px",
|
|
540
|
+
"--dsw-font-markdown-code-font-size": "13px",
|
|
541
|
+
"--dsw-font-markdown-code-line-height": "20px",
|
|
542
|
+
"--dsw-font-markdown-code-block-font-size": "13px",
|
|
543
|
+
"--dsw-font-markdown-code-block-line-height": "20px",
|
|
544
|
+
"--dsw-font-markdown-code-block-small-font-size": "12px",
|
|
545
|
+
"--dsw-font-markdown-code-block-small-line-height": "18px",
|
|
546
|
+
"--dsw-font-markdown-table-font-size": "13px",
|
|
547
|
+
"--dsw-font-markdown-table-line-height": "22px",
|
|
548
|
+
"--dsh-content-font-size": "14px",
|
|
549
|
+
"--dsh-content-font-size-secondary": "13px",
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Round to a fixed number of decimals so generated CSS stays readable.
|
|
554
|
+
* @param {number} value - the number.
|
|
555
|
+
* @param {number} digits - decimals to keep.
|
|
556
|
+
* @returns {number} the rounded number.
|
|
557
|
+
*/
|
|
558
|
+
function round(value, digits) {
|
|
559
|
+
var factor = Math.pow(10, digits);
|
|
560
|
+
return Math.round(value * factor) / factor;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Curated families offered when the browser cannot enumerate local fonts —
|
|
565
|
+
* and offered first even when it can, because a working CJK stack is the
|
|
566
|
+
* common case and scrolling a thousand families is not.
|
|
567
|
+
*/
|
|
568
|
+
var PRESETS = {
|
|
569
|
+
mono: [
|
|
570
|
+
"JetBrains Mono",
|
|
571
|
+
"Cascadia Code",
|
|
572
|
+
"Cascadia Mono",
|
|
573
|
+
"Fira Code",
|
|
574
|
+
"Source Code Pro",
|
|
575
|
+
"IBM Plex Mono",
|
|
576
|
+
"Roboto Mono",
|
|
577
|
+
"SF Mono",
|
|
578
|
+
"Menlo",
|
|
579
|
+
"Consolas",
|
|
580
|
+
"DejaVu Sans Mono",
|
|
581
|
+
"Sarasa Mono SC",
|
|
582
|
+
"Sarasa Mono HC",
|
|
583
|
+
"Noto Sans Mono CJK SC",
|
|
584
|
+
"Microsoft YaHei Mono",
|
|
585
|
+
"monospace",
|
|
586
|
+
],
|
|
587
|
+
cjk: [
|
|
588
|
+
"Microsoft YaHei",
|
|
589
|
+
"Microsoft YaHei UI",
|
|
590
|
+
"微软雅黑",
|
|
591
|
+
"PingFang SC",
|
|
592
|
+
"Hiragino Sans GB",
|
|
593
|
+
"Source Han Sans SC",
|
|
594
|
+
"Noto Sans SC",
|
|
595
|
+
"Noto Sans CJK SC",
|
|
596
|
+
"Sarasa Gothic SC",
|
|
597
|
+
"SimSun",
|
|
598
|
+
"宋体",
|
|
599
|
+
"NSimSun",
|
|
600
|
+
"SimHei",
|
|
601
|
+
"黑体",
|
|
602
|
+
"KaiTi",
|
|
603
|
+
"楷体",
|
|
604
|
+
"FangSong",
|
|
605
|
+
"仿宋",
|
|
606
|
+
"Microsoft JhengHei",
|
|
607
|
+
"DengXian",
|
|
608
|
+
"HarmonyOS Sans SC",
|
|
609
|
+
"Alibaba PuHuiTi 3",
|
|
610
|
+
],
|
|
611
|
+
latin: [
|
|
612
|
+
"Inter",
|
|
613
|
+
"Segoe UI",
|
|
614
|
+
"Segoe UI Variable",
|
|
615
|
+
"Helvetica Neue",
|
|
616
|
+
"Arial",
|
|
617
|
+
"Calibri",
|
|
618
|
+
"Tahoma",
|
|
619
|
+
"Verdana",
|
|
620
|
+
"Georgia",
|
|
621
|
+
"Times New Roman",
|
|
622
|
+
"Cambria",
|
|
623
|
+
],
|
|
624
|
+
generic: ["system-ui", "sans-serif", "serif"],
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
var shared = {
|
|
628
|
+
NAMESPACE: NAMESPACE,
|
|
629
|
+
SANS_FIELD: SANS_FIELD,
|
|
630
|
+
MONO_FIELD: MONO_FIELD,
|
|
631
|
+
SIZE_FIELD: SIZE_FIELD,
|
|
632
|
+
WEIGHT_FIELD: WEIGHT_FIELD,
|
|
633
|
+
SIZE_MIN: SIZE_MIN,
|
|
634
|
+
SIZE_MAX: SIZE_MAX,
|
|
635
|
+
WEIGHT_MIN: WEIGHT_MIN,
|
|
636
|
+
WEIGHT_MAX: WEIGHT_MAX,
|
|
637
|
+
WEIGHT_UNSET: WEIGHT_UNSET,
|
|
638
|
+
MARKER: MARKER,
|
|
639
|
+
STYLE_TAG: STYLE_TAG,
|
|
640
|
+
CARD_STYLE_TAG: CARD_STYLE_TAG,
|
|
641
|
+
DEFAULTS: DEFAULTS,
|
|
642
|
+
MAX_STACK_LENGTH: MAX_STACK_LENGTH,
|
|
643
|
+
MAX_FAMILY_LENGTH: MAX_FAMILY_LENGTH,
|
|
644
|
+
FALLBACK_TOKENS: FALLBACK_TOKENS,
|
|
645
|
+
PRESETS: PRESETS,
|
|
646
|
+
sanitize: sanitize,
|
|
647
|
+
sanitizeFamily: sanitizeFamily,
|
|
648
|
+
quoteFamily: quoteFamily,
|
|
649
|
+
normalizeConfig: normalizeConfig,
|
|
650
|
+
formatStack: formatStack,
|
|
651
|
+
parseStack: parseStack,
|
|
652
|
+
scaleFor: scaleFor,
|
|
653
|
+
isDormant: isDormant,
|
|
654
|
+
buildFontCss: buildFontCss,
|
|
655
|
+
isFontToken: isFontToken,
|
|
656
|
+
isGenericFamilyName: isGenericFamilyName,
|
|
657
|
+
isCJKFamilyName: isCJKFamilyName,
|
|
658
|
+
setWestEntry: setWestEntry,
|
|
659
|
+
setEastEntry: setEastEntry,
|
|
660
|
+
removeStackEntry: removeStackEntry,
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
if (typeof module !== "undefined" && module.exports) module.exports = shared;
|