dsh-fonttune 0.1.6 → 0.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/lib/shared.cjs CHANGED
@@ -10,6 +10,14 @@
10
10
  * configuration produces, and the sanitizing that keeps user-typed font names
11
11
  * from breaking out of the injected stylesheet.
12
12
  *
13
+ * 0.2.0 shape: the axes are grouped into three SURFACES — interface (the UI
14
+ * chrome), dialog (the conversation markdown) and code. Interface and dialog
15
+ * each carry a family, a size offset, a weight and a line-height ratio; code
16
+ * carries a family, a size offset, a weight, an additive line-height offset
17
+ * and the ligature/feature controls. A dialog field left at its neutral value
18
+ * (empty stack, 0, 100) FOLLOWS the interface value. `perTheme` enables a
19
+ * second value set for the dark theme, stored as a sparse override map.
20
+ *
13
21
  * @module dsh-fonttune/shared
14
22
  */
15
23
  "use strict";
@@ -17,24 +25,90 @@
17
25
  /** Settings namespace registered by the host half. */
18
26
  var NAMESPACE = "dsh-fonttune";
19
27
 
20
- /** Field carrying the body/UI CSS font-family stack (empty = leave DSH alone). */
28
+ /* ------------------------------------------------------------------ *
29
+ * durable field names
30
+ * ------------------------------------------------------------------ */
31
+
32
+ /** Field carrying the interface CSS font-family stack (empty = leave DSH). */
21
33
  var SANS_FIELD = "sans";
22
34
 
23
- /** Field carrying the code CSS font-family stack (empty = leave DSH alone). */
35
+ /** Field carrying the dialog stack; empty = follow the interface. */
36
+ var STACK_DIALOG_FIELD = "stackDialog";
37
+
38
+ /** Field carrying the code CSS font-family stack (empty = leave DSH). */
24
39
  var MONO_FIELD = "mono";
25
40
 
26
- /** Field carrying the body/UI font-size offset in px (0 = leave DSH alone). */
41
+ /** Field carrying the interface font-size offset in px (0 = leave DSH alone). */
27
42
  var SIZE_FIELD = "sizeOffset";
28
43
 
44
+ /** Field carrying the dialog font-size offset in px (0 = follow the interface). */
45
+ var SIZE_DIALOG_FIELD = "sizeOffsetDialog";
46
+
29
47
  /** Field carrying the code font-size offset in px (0 = leave DSH alone). */
30
48
  var CODE_SIZE_FIELD = "sizeOffsetCode";
31
49
 
32
- /** Field carrying the body/UI font weight (0 = leave DSH alone). */
50
+ /** Field carrying the interface font weight (0 = leave DSH alone). */
33
51
  var WEIGHT_FIELD = "weight";
34
52
 
53
+ /** Field carrying the dialog font weight (0 = follow the interface). */
54
+ var WEIGHT_DIALOG_FIELD = "weightDialog";
55
+
35
56
  /** Field carrying the code font weight (0 = leave DSH alone). */
36
57
  var CODE_WEIGHT_FIELD = "weightCode";
37
58
 
59
+ /**
60
+ * Field carrying the interface line-height ratio in percent (100 = untouched).
61
+ * A ratio rather than a px offset: interface line heights form a design ladder
62
+ * (14/18/20/22/24/26/28/32), and a ratio keeps every step proportional.
63
+ */
64
+ var LINE_HEIGHT_FIELD = "lineHeight";
65
+
66
+ /** Field carrying the dialog line-height ratio in percent (100 = follow). */
67
+ var LINE_HEIGHT_DIALOG_FIELD = "lineHeightDialog";
68
+
69
+ /** Field carrying the code line-height offset in px (0 = leave DSH alone). */
70
+ var CODE_LINE_HEIGHT_FIELD = "lineHeightCode";
71
+
72
+ /** Ligature mode: 0 = DSH default, 1 = forced on, 2 = forced off. */
73
+ var LIGATURES_FIELD = "codeLigatures";
74
+
75
+ /** Advanced `font-feature-settings` value for code (sanitized; empty = none). */
76
+ var FEATURES_FIELD = "codeFeatures";
77
+
78
+ /** Disable synthetic (faux) italic — the "tilted CJK" rendering. */
79
+ var NO_SYNTHETIC_ITALIC_FIELD = "noSyntheticItalic";
80
+
81
+ /** Disable synthetic (faux) bold, for faces without a real bold. */
82
+ var NO_SYNTHETIC_BOLD_FIELD = "noSyntheticBold";
83
+
84
+ /** Whether the dark theme keeps its own value set (sparse overrides). */
85
+ var PER_THEME_FIELD = "perTheme";
86
+
87
+ /**
88
+ * JSON string of the dark-theme sparse overrides: a map of axis field to
89
+ * value, applied over the single set while the dark theme is active.
90
+ */
91
+ var DARK_VALUES_FIELD = "darkValues";
92
+
93
+ /** JSON string of saved presets: [{name, values, savedAt}]. */
94
+ var PRESETS_FIELD = "presets";
95
+
96
+ /** Name of the preset the card's edits auto-save into. */
97
+ var ACTIVE_PRESET_FIELD = "activePreset";
98
+
99
+ /**
100
+ * Whether the INTERFACE follows the conversation. The conversation owns every
101
+ * axis; the interface only has a family and a weight to give (DSH exposes no
102
+ * interface size or line-height hook), so on — the default — the conversation's
103
+ * family and weight win and the interface's own values stay as the fallback for
104
+ * axes the conversation does not set.
105
+ */
106
+ var UI_FOLLOWS_FIELD = "uiFollowsDialog";
107
+
108
+ /* ------------------------------------------------------------------ *
109
+ * ranges and constants
110
+ * ------------------------------------------------------------------ */
111
+
38
112
  /** Allowed font-size offset range. The upper bound stays under a 2x scale. */
39
113
  var SIZE_MIN = -3;
40
114
  var SIZE_MAX = 6;
@@ -44,10 +118,23 @@ var WEIGHT_MIN = 300;
44
118
  var WEIGHT_MAX = 600;
45
119
  var WEIGHT_UNSET = 0;
46
120
 
121
+ /** Allowed line-height ratio range, in percent (100 = untouched). */
122
+ var LINE_HEIGHT_MIN = 100;
123
+ var LINE_HEIGHT_MAX = 160;
124
+
125
+ /** Allowed code line-height offset range, in px. */
126
+ var CODE_LINE_HEIGHT_MIN = -4;
127
+ var CODE_LINE_HEIGHT_MAX = 8;
128
+
129
+ /** Ligature modes. */
130
+ var LIGATURES_DEFAULT = 0;
131
+ var LIGATURES_ON = 1;
132
+ var LIGATURES_OFF = 2;
133
+
47
134
  /** Marker on both injected style tags, used for scoping every rule we write. */
48
135
  var MARKER = "dfp";
49
136
 
50
- /** `data-plugin-css` value of the tag applying families/offset/weight. */
137
+ /** `data-plugin-css` value of the tag applying the configuration. */
51
138
  var STYLE_TAG = "dsh-fonttune";
52
139
 
53
140
  /** `data-plugin-css` value of the card's own chrome stylesheet. */
@@ -56,13 +143,52 @@ var CARD_STYLE_TAG = "dsh-fonttune-card";
56
143
  /** Composition defaults: every axis dormant, so installing changes nothing. */
57
144
  var DEFAULTS = {
58
145
  sans: "",
146
+ stackDialog: "",
59
147
  mono: "",
60
148
  sizeOffset: 0,
149
+ sizeOffsetDialog: 0,
61
150
  sizeOffsetCode: 0,
62
151
  weight: WEIGHT_UNSET,
152
+ weightDialog: WEIGHT_UNSET,
63
153
  weightCode: WEIGHT_UNSET,
154
+ lineHeight: LINE_HEIGHT_MIN,
155
+ lineHeightDialog: LINE_HEIGHT_MIN,
156
+ lineHeightCode: 0,
157
+ codeLigatures: LIGATURES_DEFAULT,
158
+ codeFeatures: "",
159
+ noSyntheticItalic: false,
160
+ noSyntheticBold: false,
161
+ perTheme: false,
162
+ darkValues: "{}",
163
+ presets: "[]",
164
+ activePreset: "",
165
+ uiFollowsDialog: true,
64
166
  };
65
167
 
168
+ /**
169
+ * The axis fields a preset snapshots: everything the card edits, excluding
170
+ * the storage fields themselves.
171
+ */
172
+ var VALUE_FIELDS = [
173
+ SANS_FIELD,
174
+ STACK_DIALOG_FIELD,
175
+ MONO_FIELD,
176
+ SIZE_FIELD,
177
+ SIZE_DIALOG_FIELD,
178
+ CODE_SIZE_FIELD,
179
+ WEIGHT_FIELD,
180
+ WEIGHT_DIALOG_FIELD,
181
+ CODE_WEIGHT_FIELD,
182
+ LINE_HEIGHT_FIELD,
183
+ LINE_HEIGHT_DIALOG_FIELD,
184
+ CODE_LINE_HEIGHT_FIELD,
185
+ LIGATURES_FIELD,
186
+ FEATURES_FIELD,
187
+ NO_SYNTHETIC_ITALIC_FIELD,
188
+ NO_SYNTHETIC_BOLD_FIELD,
189
+ UI_FOLLOWS_FIELD,
190
+ ];
191
+
66
192
  /**
67
193
  * Longest accepted font stack, in characters (mirrored by the host schema).
68
194
  */
@@ -71,6 +197,19 @@ var MAX_STACK_LENGTH = 200;
71
197
  /** Longest accepted single family name, in characters. */
72
198
  var MAX_FAMILY_LENGTH = 64;
73
199
 
200
+ /** Longest accepted `font-feature-settings` value, in characters. */
201
+ var MAX_FEATURES_LENGTH = 120;
202
+
203
+ /** Most presets one configuration may hold. */
204
+ var MAX_PRESETS = 20;
205
+
206
+ /** Longest preset name, in characters. */
207
+ var MAX_PRESET_NAME = 32;
208
+
209
+ /* ------------------------------------------------------------------ *
210
+ * sanitizing
211
+ * ------------------------------------------------------------------ */
212
+
74
213
  /**
75
214
  * What may survive in a family name.
76
215
  *
@@ -141,28 +280,93 @@ var GENERIC_FAMILIES = [
141
280
  "blinkmacsystemfont",
142
281
  ];
143
282
 
283
+ /**
284
+ * Sanitize an advanced `font-feature-settings` value.
285
+ *
286
+ * Feature tags are letters and digits; entries may carry on/off/number and are
287
+ * comma separated. The allow-list (letters, digits, quotes, spaces, commas)
288
+ * keeps the value from closing the declaration or opening another one, and a
289
+ * coarse shape test rejects anything that is not a tag-entry list.
290
+ * @param {unknown} value - candidate features value.
291
+ * @returns {string} the sanitized value, or "".
292
+ */
293
+ function sanitizeFeatures(value) {
294
+ if (typeof value !== "string") return "";
295
+ var text = value.replace(FEATURES_UNSAFE, "").slice(0, MAX_FEATURES_LENGTH).trim();
296
+ if (text === "") return "";
297
+ if (!/^(?:"?[a-zA-Z0-9]{1,8}"?(?: (?:on|off|-?\d+))?(?:, ?)?)+$/.test(text)) return "";
298
+ return text;
299
+ }
300
+
301
+ /** Characters a `font-feature-settings` value cannot contain. */
302
+ var FEATURES_UNSAFE = /[^a-zA-Z0-9"' ,]/g;
303
+
304
+ /* ------------------------------------------------------------------ *
305
+ * configuration normalization
306
+ * ------------------------------------------------------------------ */
307
+
308
+ /** Clamp/validate one field by name (shared by config and value sets). */
309
+ function normalizeField(field, value) {
310
+ switch (field) {
311
+ case SANS_FIELD:
312
+ case STACK_DIALOG_FIELD:
313
+ case MONO_FIELD:
314
+ return sanitize(value).slice(0, MAX_STACK_LENGTH);
315
+ case SIZE_FIELD:
316
+ case SIZE_DIALOG_FIELD:
317
+ case CODE_SIZE_FIELD:
318
+ return clampOffset(value);
319
+ case WEIGHT_FIELD:
320
+ case WEIGHT_DIALOG_FIELD:
321
+ case CODE_WEIGHT_FIELD:
322
+ return clampWeight(value);
323
+ case LINE_HEIGHT_FIELD:
324
+ case LINE_HEIGHT_DIALOG_FIELD:
325
+ return clampRatio(value);
326
+ case CODE_LINE_HEIGHT_FIELD:
327
+ return clampLineOffset(value);
328
+ case LIGATURES_FIELD:
329
+ return clampLigatures(value);
330
+ case FEATURES_FIELD:
331
+ return sanitizeFeatures(value);
332
+ case NO_SYNTHETIC_ITALIC_FIELD:
333
+ case NO_SYNTHETIC_BOLD_FIELD:
334
+ return toBool(value);
335
+ case UI_FOLLOWS_FIELD:
336
+ // Absent means the default: the interface follows the conversation.
337
+ return value === undefined || value === null || value === "" ? true : toBool(value);
338
+ default:
339
+ return undefined;
340
+ }
341
+ }
342
+
144
343
  /**
145
344
  * Normalize a configuration-shaped object coming from the settings document,
146
- * a config layer, or a test fixture.
345
+ * a config layer, or a test fixture. Storage fields (`darkValues`,
346
+ * `presets`) are decoded and validated here too, so every consumer sees
347
+ * plain data.
147
348
  * @param {unknown} value - candidate configuration.
148
- * @returns {{sans: string, mono: string, sizeOffset: number, sizeOffsetCode: number, weight: number}} the normalized config.
349
+ * @returns {Record<string, string|number|boolean|object|array>} the normalized config.
149
350
  */
150
351
  function normalizeConfig(value) {
151
352
  var source = value !== null && typeof value === "object" ? value : {};
152
- var size = clampOffset(source[SIZE_FIELD]);
153
- var codeSize = clampOffset(source[CODE_SIZE_FIELD]);
154
- var weight = clampWeight(source[WEIGHT_FIELD]);
155
- var codeWeight = clampWeight(source[CODE_WEIGHT_FIELD]);
156
353
  var config = {};
157
- config[SANS_FIELD] = sanitize(source[SANS_FIELD]).slice(0, MAX_STACK_LENGTH);
158
- config[MONO_FIELD] = sanitize(source[MONO_FIELD]).slice(0, MAX_STACK_LENGTH);
159
- config[SIZE_FIELD] = size;
160
- config[CODE_SIZE_FIELD] = codeSize;
161
- config[WEIGHT_FIELD] = weight;
162
- config[CODE_WEIGHT_FIELD] = codeWeight;
354
+ for (var index = 0; index < VALUE_FIELDS.length; index += 1) {
355
+ var field = VALUE_FIELDS[index];
356
+ config[field] = normalizeField(field, source[field]);
357
+ }
358
+ config[PER_THEME_FIELD] = toBool(source[PER_THEME_FIELD]);
359
+ config[DARK_VALUES_FIELD] = normalizeDarkValues(source[DARK_VALUES_FIELD]);
360
+ config[PRESETS_FIELD] = normalizePresets(source[PRESETS_FIELD]);
361
+ config[ACTIVE_PRESET_FIELD] = sanitize(source[ACTIVE_PRESET_FIELD]).slice(0, MAX_PRESET_NAME);
163
362
  return config;
164
363
  }
165
364
 
365
+ /** Coerce a settings value to a strict boolean; anything unusable is false. */
366
+ function toBool(value) {
367
+ return value === true || value === 1 || value === "1" || value === "true";
368
+ }
369
+
166
370
  /**
167
371
  * Clamp one size offset to the allowed range, with 0 for anything unusable.
168
372
  * @param {unknown} value - candidate offset in px.
@@ -187,6 +391,109 @@ function clampWeight(value) {
187
391
  return Math.min(WEIGHT_MAX, Math.max(WEIGHT_MIN, weight));
188
392
  }
189
393
 
394
+ /**
395
+ * Clamp one line-height ratio to the allowed percent range.
396
+ * @param {unknown} value - candidate ratio in percent.
397
+ * @returns {number} the integer ratio (100 when unusable).
398
+ */
399
+ function clampRatio(value) {
400
+ var ratio = Math.round(Number(value));
401
+ if (!isFinite(ratio)) return LINE_HEIGHT_MIN;
402
+ return Math.min(LINE_HEIGHT_MAX, Math.max(LINE_HEIGHT_MIN, ratio));
403
+ }
404
+
405
+ /**
406
+ * Clamp one code line-height offset to the allowed px range.
407
+ * @param {unknown} value - candidate offset in px.
408
+ * @returns {number} the integer offset (0 when unusable).
409
+ */
410
+ function clampLineOffset(value) {
411
+ var offset = Math.round(Number(value));
412
+ if (!isFinite(offset) || offset === 0) return 0;
413
+ return Math.min(CODE_LINE_HEIGHT_MAX, Math.max(CODE_LINE_HEIGHT_MIN, offset));
414
+ }
415
+
416
+ /**
417
+ * Clamp one ligature mode to the three known values.
418
+ * @param {unknown} value - candidate mode.
419
+ * @returns {number} 0, 1 or 2.
420
+ */
421
+ function clampLigatures(value) {
422
+ var mode = Math.round(Number(value));
423
+ if (!isFinite(mode) || mode <= 0) return LIGATURES_DEFAULT;
424
+ return mode >= LIGATURES_OFF ? LIGATURES_OFF : LIGATURES_ON;
425
+ }
426
+
427
+ /**
428
+ * Validate and normalize one sparse axis-value map (dark overrides, presets).
429
+ * Only known axis fields survive, each clamped.
430
+ * @param {unknown} values - candidate map.
431
+ * @returns {Record<string, string|number|boolean>} the safe subset.
432
+ */
433
+ function normalizeValueSet(values) {
434
+ var out = {};
435
+ if (values === null || typeof values !== "object" || Array.isArray(values)) return out;
436
+ for (var index = 0; index < VALUE_FIELDS.length; index += 1) {
437
+ var field = VALUE_FIELDS[index];
438
+ if (!Object.prototype.hasOwnProperty.call(values, field)) continue;
439
+ out[field] = normalizeField(field, values[field]);
440
+ }
441
+ return out;
442
+ }
443
+
444
+ /**
445
+ * Parse and validate the durable `darkValues` JSON string.
446
+ * @param {unknown} value - candidate JSON text or object.
447
+ * @returns {Record<string, string|number|boolean>} the sparse override map.
448
+ */
449
+ function normalizeDarkValues(value) {
450
+ if (typeof value === "string") {
451
+ if (value.trim() === "") return {};
452
+ try {
453
+ value = JSON.parse(value);
454
+ } catch (error) {
455
+ return {};
456
+ }
457
+ }
458
+ return normalizeValueSet(value);
459
+ }
460
+
461
+ /**
462
+ * Parse and validate the durable `presets` JSON string.
463
+ * @param {unknown} value - candidate JSON text or array.
464
+ * @returns {{name: string, values: object, savedAt: number}[]}
465
+ */
466
+ function normalizePresets(value) {
467
+ var list = value;
468
+ if (typeof value === "string") {
469
+ if (value.trim() === "") return [];
470
+ try {
471
+ list = JSON.parse(value);
472
+ } catch (error) {
473
+ return [];
474
+ }
475
+ }
476
+ if (!Array.isArray(list)) return [];
477
+ var out = [];
478
+ for (var index = 0; index < list.length && out.length < MAX_PRESETS; index += 1) {
479
+ var entry = list[index];
480
+ if (entry === null || typeof entry !== "object") continue;
481
+ var name = sanitize(entry.name).slice(0, MAX_PRESET_NAME);
482
+ if (name === "") continue;
483
+ var stamp = Math.round(Number(entry.savedAt));
484
+ out.push({
485
+ name: name,
486
+ values: normalizeValueSet(entry.values),
487
+ savedAt: isFinite(stamp) && stamp > 0 ? stamp : 0,
488
+ });
489
+ }
490
+ return out;
491
+ }
492
+
493
+ /* ------------------------------------------------------------------ *
494
+ * stacks
495
+ * ------------------------------------------------------------------ */
496
+
190
497
  /**
191
498
  * Format a family list as a CSS font-family value.
192
499
  * @param {readonly string[]} families - family names in precedence order.
@@ -360,6 +667,10 @@ function removeStackEntry(families, family) {
360
667
  return next;
361
668
  }
362
669
 
670
+ /* ------------------------------------------------------------------ *
671
+ * scaling helpers
672
+ * ------------------------------------------------------------------ */
673
+
363
674
  /**
364
675
  * Compute the uniform scale a pixel offset produces over DSH's 16px base.
365
676
  *
@@ -380,152 +691,16 @@ function scaleFor(sizeOffset, base) {
380
691
  }
381
692
 
382
693
  /**
383
- * Whether a configuration asks for any change at all.
384
- * @param {{sans: string, mono: string, sizeOffset: number, sizeOffsetCode: number, weight: number}} config - normalized config.
385
- * @returns {boolean} true when nothing should be injected.
386
- */
387
- function isDormant(config) {
388
- return (
389
- config[SANS_FIELD] === "" &&
390
- config[MONO_FIELD] === "" &&
391
- config[SIZE_FIELD] === 0 &&
392
- config[CODE_SIZE_FIELD] === 0 &&
393
- config[WEIGHT_FIELD] === WEIGHT_UNSET &&
394
- config[CODE_WEIGHT_FIELD] === WEIGHT_UNSET
395
- );
396
- }
397
-
398
- /**
399
- * Build the stylesheet one configuration applies.
400
- *
401
- * Families are declared at their SOURCE: DSH's design tokens chain to two
402
- * variables (`--dsw-font-family` for every sans token, `--ds-font-family-code`
403
- * for every code token — verified against the shipped theme), so overriding
404
- * those variables is what reaches the conversation markdown and the sidebar,
405
- * whose elements declare `font-family: var(--dsw-font-family)` themselves and
406
- * would never inherit a plain `body` rule. The explicit `body` / `pre,code`
407
- * rules stay as a second path for elements that hardcode a family.
408
- *
409
- * The size offsets rewrite DSH's own design tokens rather than every element:
410
- * a token whose name ends in `-font-size` or `-line-height` is re-declared as
411
- * itself multiplied by one scale factor. `baseTokens` supplies the untouched
412
- * values (read from the live document, falling back to the embedded map), so
413
- * the ratio composes with DSH's own font-size slider instead of replacing it.
414
- *
415
- * Body and code are two independent axes. Body sizes hang off the content-size
416
- * chain (`--dsh-content-font-size` and the `--dsw-font-*` steps), while every
417
- * markdown code token is a bare literal (`--dsw-font-markdown-code-block:
418
- * 11px/19px …`) that the content chain never touches — so the code axis is the
419
- * only thing that moves it, and the body axis deliberately leaves those tokens
420
- * alone. Code sizing is consumed through the `font` SHORTHAND tokens (that is
421
- * what the shipped stylesheets use), which is why those are re-declared too,
422
- * not just their `-font-size` / `-line-height` parts.
423
- *
424
- * Weight has no token chain to ride: no shipped rule reads a `-font-weight`
425
- * token (measured — zero `font-weight: var(--dsw-font-…)` in rc.2), the theme
426
- * writes literal weights on its own class rules, and that is exactly why the
427
- * body axis has to be a blanket `!important` rule. The code axis is therefore a
428
- * selector-scoped `!important` rule placed after it, which wins on specificity
429
- * (`[class*=…]`) or on source order (`pre, …` ties with `body *`).
430
- *
431
- * @param {{sans: string, mono: string, sizeOffset: number, sizeOffsetCode: number, weight: number, weightCode: number}} config - normalized config.
432
- * @param {Record<string, string>} [baseTokens] - token name to untouched value.
433
- * @returns {string} declarations for one `<style>` element ("" when dormant).
694
+ * Round to a fixed number of decimals so generated CSS stays readable.
695
+ * @param {number} value - the number.
696
+ * @param {number} digits - decimals to keep.
697
+ * @returns {number} the rounded number.
434
698
  */
435
- function buildFontCss(config, baseTokens) {
436
- var normalized = normalizeConfig(config);
437
- var sans = formatStack(parseStack(normalized[SANS_FIELD]));
438
- var mono = formatStack(parseStack(normalized[MONO_FIELD]));
439
- var offset = normalized[SIZE_FIELD];
440
- var codeOffset = normalized[CODE_SIZE_FIELD];
441
- var weight = normalized[WEIGHT_FIELD];
442
- var codeWeight = normalized[CODE_WEIGHT_FIELD];
443
- if (isDormant(normalized)) return "";
444
- var declarations = [];
445
-
446
- if (sans !== "") {
447
- declarations.push(":root,body{--dsw-font-family:" + sans + " !important}");
448
- declarations.push("body{font-family:" + sans + " !important}");
449
- }
450
- if (mono !== "") {
451
- // The theme's code tokens chain to `--ds-font-family-code`; the other name
452
- // is what dsh-ui-font historically wrote and costs nothing to cover.
453
- declarations.push(
454
- ":root,body{--dsw-font-mono:" + mono + " !important;--ds-font-family-code:" + mono + " !important}"
455
- );
456
- // Written after the body rule: equal specificity on a code element means
457
- // the later declaration wins, so code keeps its own family inside the UI.
458
- declarations.push(
459
- "pre,code,kbd,samp,var,tt,textarea,.cm-editor .cm-content{font-family:" +
460
- mono +
461
- " !important}"
462
- );
463
- }
464
- if (offset !== 0) {
465
- var scaled = scaleTokens(bodyTokenNames(baseTokens), baseTokens, round(scaleFor(offset), 6));
466
- if (scaled.length > 0) {
467
- declarations.push(bodyAndDescendants(scaled.join(";")));
468
- }
469
- }
470
- if (codeOffset !== 0) {
471
- var scaledCode = scaleCodeTokens(baseTokens, round(scaleFor(codeOffset), 6));
472
- if (scaledCode.length > 0) {
473
- declarations.push(bodyAndDescendants(scaledCode.join(";")));
474
- }
475
- }
476
- if (weight !== WEIGHT_UNSET) {
477
- // Written as the user chose it: a variable font honors every integer, and
478
- // a static one rounds to its own nearest step by itself.
479
- declarations.push(bodyAndDescendants("font-weight:" + weight + " !important"));
480
- }
481
- if (codeWeight !== WEIGHT_UNSET) {
482
- declarations.push(CODE_SELECTOR + "{font-weight:" + codeWeight + " !important}");
483
- } else if (weight !== WEIGHT_UNSET) {
484
- // The body axis is a blanket `body, body *` rule, so code would inherit the
485
- // body weight unless it is explicitly taken back out. `normal` is what DSH
486
- // itself computes there (the code `font` shorthands carry no weight), so an
487
- // unset code weight means "code stays exactly as DSH shipped it".
488
- declarations.push(CODE_SELECTOR + "{font-weight:normal !important}");
489
- }
490
- return declarations.join("\n");
699
+ function round(value, digits) {
700
+ var factor = Math.pow(10, digits);
701
+ return Math.round(value * factor) / factor;
491
702
  }
492
703
 
493
- /**
494
- * What counts as code text for the weight axis.
495
- *
496
- * There is no token to hook here (see `buildFontCss`), so the code axis has to
497
- * name the elements. Two layers, both measured against rc.2:
498
- *
499
- * 1. The tags DSH's own stylesheets size with a code token: markdown `pre` /
500
- * inline `code`, the tool card's `:where(pre)` block, form controls, and the
501
- * CodeMirror editor. Their descendants are listed too, because the blanket
502
- * body rule matches EVERY element — a `<span>` inside a highlighted block
503
- * would otherwise be pulled back to the body weight.
504
- * 2. Containers that only a class name identifies: the terminal and tool-code
505
- * bodies are plain `div`s (`…_terminal`, `…_codeBody`, `md-code-block`).
506
- *
507
- * The `i` flag keeps a casing change in a future DSH from silently dropping the
508
- * rule, and an unmatched token/class simply means that surface follows the body
509
- * axis — the same degradation as an unknown token name on the size axes.
510
- */
511
- var CODE_SELECTOR = [
512
- "pre",
513
- "code",
514
- "kbd",
515
- "samp",
516
- "var",
517
- "tt",
518
- "textarea",
519
- ".cm-editor",
520
- ".dfp-previewCode",
521
- '[class*="code" i]',
522
- '[class*="terminal" i]',
523
- ]
524
- .map(function (selector) {
525
- return selector + "," + selector + " *";
526
- })
527
- .join(",");
528
-
529
704
  /**
530
705
  * Values a ratio may multiply: a number with a unit, a bare number, or an
531
706
  * expression. A keyword (`unset`, `inherit`) or an unrecognized shape is left
@@ -543,17 +718,17 @@ function isScalableValue(value) {
543
718
  }
544
719
 
545
720
  /**
546
- * Scale one set of tokens by one ratio.
721
+ * Scale one set of SIZE tokens by one ratio.
547
722
  *
548
723
  * A base that itself references another token via var() derives from it:
549
724
  * scaling the var target already scales this one, so re-scaling here would
550
725
  * compound (markdown-base = var(--dsh-content-font-size) would take the ratio
551
726
  * twice). DSH's chain does the work instead.
552
727
  *
553
- * `!important` is required: the theme writes `--dsh-content-font-size` INLINE on
554
- * body, and an inline declaration outranks a plain stylesheet one — without the
555
- * flag body itself would keep DSH's own size while every descendant scales,
556
- * splitting the page in two.
728
+ * `!important` is required: the theme writes `--dsh-content-font-size` INLINE
729
+ * on body, and an inline declaration outranks a plain stylesheet one —
730
+ * without the flag body itself would keep DSH's own size while every
731
+ * descendant scales, splitting the page in two.
557
732
  *
558
733
  * @param {string[]} names - token names to re-declare.
559
734
  * @param {Record<string, string>} baseTokens - token name to untouched value.
@@ -574,66 +749,143 @@ function scaleTokens(names, baseTokens, scale) {
574
749
  }
575
750
 
576
751
  /**
577
- * Re-declare the markdown code tokens at their own ratio.
578
- *
579
- * Two shapes exist and both must be covered, because the shipped stylesheets
580
- * consume the shorthand (`font: var(--dsw-font-markdown-code-block-small)`)
581
- * while the split tokens exist for anything that asks for a single part:
582
- * `11px/19px <family>` and a bare `11px`.
752
+ * Scale one set of LINE-HEIGHT tokens by one factor.
583
753
  *
754
+ * Unlike sizes, a line height never chains through another line height in
755
+ * DSH's theme (heights are literals or `calc(Npx + size-delta)`), so var()
756
+ * values are wrapped rather than skipped — the wrap keeps the var chain
757
+ * resolving at use time and cannot compound.
758
+ * @param {string[]} names - token names to re-declare.
584
759
  * @param {Record<string, string>} baseTokens - token name to untouched value.
585
- * @param {number} scale - the ratio to apply.
760
+ * @param {number} factor - the ratio to apply.
586
761
  * @returns {string[]} declarations.
587
762
  */
588
- function scaleCodeTokens(baseTokens, scale) {
589
- var names = codeTokenNames(baseTokens);
763
+ function scaleLineTokens(names, baseTokens, factor) {
590
764
  var out = [];
591
765
  for (var index = 0; index < names.length; index += 1) {
592
766
  var name = names[index];
593
767
  var base = baseTokens[name];
594
768
  if (typeof base !== "string" || base === "") continue;
595
- var shorthand = scaleFontShorthand(base, scale);
596
- if (shorthand !== null) {
597
- out.push(name + ":" + shorthand + " !important");
598
- continue;
599
- }
600
- // A part token that derives from another token follows that token's own
601
- // scaling; re-declaring it here would compound the ratio.
602
- if (base.indexOf("var(") !== -1) continue;
603
769
  if (!isScalableValue(base)) continue;
604
- out.push(name + ":calc((" + base + ") * " + scale + ") !important");
770
+ out.push(name + ":calc((" + base + ") * " + factor + ") !important");
605
771
  }
606
772
  return out;
607
773
  }
608
774
 
609
775
  /**
610
- * Multiply the size and the line height inside one `font` shorthand value,
611
- * leaving the family list (and anything else after it) untouched.
612
- * @param {string} value - a value shaped like `11px/19px var(--ds-font-family-code)`.
613
- * @param {number} scale - the ratio to apply.
614
- * @returns {string|null} the scaled shorthand, or null when the shape differs.
776
+ * Shift one set of tokens by an additive px amount.
777
+ * @param {string[]} names - token names to re-declare.
778
+ * @param {Record<string, string>} baseTokens - token name to untouched value.
779
+ * @param {number} offset - the px amount to add.
780
+ * @returns {string[]} declarations.
615
781
  */
616
- function scaleFontShorthand(value, scale) {
617
- var slash = value.indexOf("/");
782
+ function shiftTokens(names, baseTokens, offset) {
783
+ var out = [];
784
+ for (var index = 0; index < names.length; index += 1) {
785
+ var name = names[index];
786
+ var base = baseTokens[name];
787
+ if (typeof base !== "string" || base === "") continue;
788
+ if (!isScalableValue(base)) continue;
789
+ out.push(name + ":calc((" + base + ") + " + offset + "px) !important");
790
+ }
791
+ return out;
792
+ }
793
+
794
+ /* ------------------------------------------------------------------ *
795
+ * `font` shorthand parsing
796
+ * ------------------------------------------------------------------ */
797
+
798
+ /**
799
+ * The family reference every DSH `font` shorthand ends with. Markdown tokens
800
+ * chain to the sans variable, code tokens to the code variable.
801
+ */
802
+ var FAMILY_REFS = ["var(--dsw-font-family)", "var(--ds-font-family-code)"];
803
+
804
+ /**
805
+ * Parse one `font` shorthand into its parts.
806
+ *
807
+ * DSH's shorthands come in four shapes, all ending in a family var():
808
+ * `11px/19px var(--ds-font-family-code)` (code),
809
+ * `var(--dsh-content-font-size,14px) / calc(24px + var(--dsh-content-font-delta)) var(--dsw-font-family)` (base),
810
+ * `700 calc(21px + var(...)) / calc(30px + var(...)) var(--dsw-font-family)` (h1),
811
+ * `italic 600 var(...) / calc(...) var(--dsw-font-family)` (strong-italic).
812
+ *
813
+ * Because the size expression may itself contain `var(...)`, the split keys
814
+ * off the family reference (found from the right) and the `/`, never off
815
+ * whitespace inside `calc()`.
816
+ * @param {string} value - a `font` shorthand value.
817
+ * @returns {{lead: string, size: string, height: string, family: string}|null} the parts, or null when the shape differs.
818
+ */
819
+ function parseShorthand(value) {
820
+ if (typeof value !== "string") return null;
821
+ var familyAt = -1;
822
+ for (var index = 0; index < FAMILY_REFS.length; index += 1) {
823
+ var at = value.lastIndexOf(FAMILY_REFS[index]);
824
+ if (at > familyAt) familyAt = at;
825
+ }
826
+ if (familyAt <= 0) return null;
827
+ var family = value.slice(familyAt).trim();
828
+ if (family === "") return null;
829
+ var head = value.slice(0, familyAt).trim();
830
+ var slash = head.lastIndexOf("/");
618
831
  if (slash <= 0) return null;
619
- var size = value.slice(0, slash).trim();
620
- var rest = value.slice(slash + 1).trim();
621
- var gap = rest.search(/\s/);
622
- if (gap <= 0) return null;
623
- var height = rest.slice(0, gap).trim();
624
- var family = rest.slice(gap + 1).trim();
625
- if (size === "" || height === "" || family === "") return null;
832
+ var sizeChunk = head.slice(0, slash).trim();
833
+ var height = head.slice(slash + 1).trim();
834
+ if (sizeChunk === "" || height === "") return null;
835
+ var chunks = splitTopLevel(sizeChunk);
836
+ var size = chunks.pop();
837
+ var lead = chunks.join(" ");
626
838
  if (!isScalableValue(size) || !isScalableValue(height)) return null;
627
- return (
628
- "calc((" + size + ") * " + scale + ")/calc((" + height + ") * " + scale + ") " + family
629
- );
839
+ return { lead: lead, size: size, height: height, family: family };
630
840
  }
631
841
 
632
842
  /**
633
- * Match DSH's typography tokens: the design system's sizes plus the content
634
- * size the theme plugin writes on `body` (whose secondary variant carries a
635
- * suffix of its own). Line heights scale with sizes so a token's shorthand
636
- * never disagrees with its parts.
843
+ * Split a value on whitespace that sits OUTSIDE any parentheses, so a leading
844
+ * `700 calc(21px + var(…))` yields `["700", "calc(21px + var(...))"]`.
845
+ * @param {string} value - the chunk to split.
846
+ * @returns {string[]} the top-level chunks.
847
+ */
848
+ function splitTopLevel(value) {
849
+ var out = [];
850
+ var current = "";
851
+ var depth = 0;
852
+ for (var index = 0; index < value.length; index += 1) {
853
+ var char = value.charAt(index);
854
+ if (char === "(") depth += 1;
855
+ else if (char === ")") depth -= 1;
856
+ if (depth === 0 && /\s/.test(char)) {
857
+ if (current !== "") {
858
+ out.push(current);
859
+ current = "";
860
+ }
861
+ continue;
862
+ }
863
+ current += char;
864
+ }
865
+ if (current !== "") out.push(current);
866
+ return out;
867
+ }
868
+
869
+ /**
870
+ * Rebuild one `font` shorthand from parsed parts.
871
+ * @param {{lead: string, size: string, height: string, family: string}} parts - parsed parts.
872
+ * @returns {string} the shorthand value (without `!important`).
873
+ */
874
+ function rebuildShorthand(parts) {
875
+ var out = "";
876
+ if (parts.lead !== "") out += parts.lead + " ";
877
+ return out + parts.size + " / " + parts.height + " " + parts.family;
878
+ }
879
+
880
+ /* ------------------------------------------------------------------ *
881
+ * token classification
882
+ * ------------------------------------------------------------------ */
883
+
884
+ /**
885
+ * Match DSH's typography PART tokens: the design system's sizes plus the
886
+ * content size the theme plugin writes on `body` (whose secondary variant
887
+ * carries a suffix of its own). Line heights scale with sizes so a token's
888
+ * shorthand never disagrees with its parts.
637
889
  *
638
890
  * Deliberately a shape test rather than an enumeration: DSH generates these
639
891
  * tokens at runtime, so whatever a future release names them, a token that
@@ -641,6 +893,26 @@ function scaleFontShorthand(value, scale) {
641
893
  */
642
894
  var TOKEN_PATTERN = /^--(?:dsw-font-[a-z0-9-]*|dsh-content-font)-(?:font-size|line-height)(?:-secondary)?$|^--dsh-content-font-(?:size|line-height)(?:-secondary)?$/;
643
895
 
896
+ /**
897
+ * The content-size DELTA tokens the theme derives on `body`; the dialog size
898
+ * offset shifts these additively, which is what moves every delta-derived
899
+ * markdown size by exactly the offset.
900
+ */
901
+ var DELTA_PATTERN = /^--dsh-content-font-delta(?:-secondary)?$/;
902
+
903
+ /** The whole official content-size chain (size / line height / delta). */
904
+ var CONTENT_PATTERN = /^--dsh-content-font-(?:size|line-height|delta)(?:-secondary)?$/;
905
+
906
+ /**
907
+ * The PART-token suffixes DSH appends for single-axis consumers.
908
+ */
909
+ var PART_SUFFIXES = ["-font-size", "-line-height", "-font-family", "-font-weight", "-font-style"];
910
+
911
+ /**
912
+ * The bare family variables, which are NOT `font` shorthands.
913
+ */
914
+ var FAMILY_VARS = { "--dsw-font-family": true, "--dsw-font-mono": true, "--ds-font-family-code": true };
915
+
644
916
  /**
645
917
  * A `body, body *` selector list, kept in one place because every global
646
918
  * declaration needs the same reach (DSH writes some tokens on descendants).
@@ -652,7 +924,23 @@ function bodyAndDescendants(declarations) {
652
924
  }
653
925
 
654
926
  /**
655
- * Whether one custom-property name is a token this plugin scales.
927
+ * Whether one name belongs to the official CONTENT-size chain the conversation
928
+ * owns: `--dsh-content-font-size` / `-line-height` (the official "对话字号"
929
+ * setting writes the former) and the delta tokens derived from it.
930
+ *
931
+ * The interface axis must never scale these. Doing so was the real reason an
932
+ * interface size change moved the conversation: the markdown ladder consumes
933
+ * this chain directly, so scaling it is a conversation edit no matter which
934
+ * slider caused it.
935
+ * @param {string} name - custom property name including the leading dashes.
936
+ * @returns {boolean}
937
+ */
938
+ function isContentToken(name) {
939
+ return typeof name === "string" && CONTENT_PATTERN.test(name);
940
+ }
941
+
942
+ /**
943
+ * Whether one custom-property name is a PART token this plugin scales.
656
944
  * @param {string} name - custom property name including the leading dashes.
657
945
  * @returns {boolean} true when the token carries a size or a line height.
658
946
  */
@@ -661,13 +949,21 @@ function isFontToken(name) {
661
949
  }
662
950
 
663
951
  /**
664
- * Match DSH's markdown code tokens, which are their own sizing axis.
952
+ * Whether one name is a content-size delta token.
953
+ * @param {string} name - custom property name including the leading dashes.
954
+ * @returns {boolean}
955
+ */
956
+ function isDeltaToken(name) {
957
+ return typeof name === "string" && DELTA_PATTERN.test(name);
958
+ }
959
+
960
+ /**
961
+ * Whether one name is a markdown CODE token: its own sizing axis.
665
962
  *
666
963
  * The theme declares these as literals the content-size chain never reaches
667
964
  * (`--dsw-font-markdown-code:12px/19px …`, `-code-block:11px/19px …`,
668
965
  * `-code-block-small:11px/16px …`), and the shipped stylesheets consume them
669
- * through the `font` shorthand. Everything with this prefix therefore belongs
670
- * to the code offset and to nothing else.
966
+ * through the `font` shorthand.
671
967
  * @param {string} name - custom property name including the leading dashes.
672
968
  * @returns {boolean} true when the token sizes code.
673
969
  */
@@ -676,23 +972,63 @@ function isCodeToken(name) {
676
972
  }
677
973
 
678
974
  /**
679
- * Whether a token is one this plugin re-declares at all.
975
+ * Whether one name is a markdown (dialog) token that is NOT code: the token
976
+ * family the dialog axes own.
977
+ * @param {string} name - custom property name including the leading dashes.
978
+ * @returns {boolean}
979
+ */
980
+ function isMarkdownToken(name) {
981
+ return typeof name === "string" && name.indexOf("--dsw-font-markdown-") === 0 && !isCodeToken(name);
982
+ }
983
+
984
+ /**
985
+ * Whether one NAME looks like a `font` shorthand token (a bare
986
+ * `--dsw-font-…` name that is not a PART token or a family variable).
680
987
  * @param {string} name - custom property name including the leading dashes.
681
- * @returns {boolean} true for body sizes, body line heights and code tokens.
988
+ * @returns {boolean}
989
+ */
990
+ function isShorthandName(name) {
991
+ if (typeof name !== "string" || name.indexOf("--dsw-font-") !== 0) return false;
992
+ if (FAMILY_VARS[name] === true) return false;
993
+ for (var index = 0; index < PART_SUFFIXES.length; index += 1) {
994
+ var suffix = PART_SUFFIXES[index];
995
+ if (name.length > suffix.length && name.slice(-suffix.length) === suffix) return false;
996
+ }
997
+ return true;
998
+ }
999
+
1000
+ /**
1001
+ * Whether one token (name + value) is a `font` shorthand this plugin may
1002
+ * rewrite: a bare `--dsw-font-…` name whose value parses as a shorthand.
1003
+ * @param {string} name - custom property name.
1004
+ * @param {string} value - the token's untouched value.
1005
+ * @returns {boolean}
1006
+ */
1007
+ function isShorthandToken(name, value) {
1008
+ return isShorthandName(name) && parseShorthand(value) !== null;
1009
+ }
1010
+
1011
+ /**
1012
+ * Whether a token is one this plugin reads and re-declares at all.
1013
+ * @param {string} name - custom property name including the leading dashes.
1014
+ * @returns {boolean} true for part tokens, code tokens, shorthand names and
1015
+ * the content-size deltas.
682
1016
  */
683
1017
  function isScaledToken(name) {
684
- return isFontToken(name) || isCodeToken(name);
1018
+ return isFontToken(name) || isCodeToken(name) || isShorthandName(name) || isDeltaToken(name);
685
1019
  }
686
1020
 
687
1021
  /**
688
- * Ordered token names present in a base map, sizes before line heights so a
689
- * reader of the stylesheet sees each size next to its height.
1022
+ * Ordered PART-token names present in a base map, sizes before line heights
1023
+ * so a reader of the stylesheet sees each size next to its height.
690
1024
  * @param {Record<string, string>} [baseTokens] - token name to value.
691
- * @returns {string[]} the names to scale.
1025
+ * @returns {string[]} the names.
692
1026
  */
693
1027
  function tokenNames(baseTokens) {
694
1028
  if (baseTokens === null || typeof baseTokens !== "object") return [];
695
- var names = Object.keys(baseTokens).filter(isScaledToken);
1029
+ var names = Object.keys(baseTokens).filter(function (name) {
1030
+ return isFontToken(name) || isCodeToken(name) || isDeltaToken(name);
1031
+ });
696
1032
  names.sort(function (left, right) {
697
1033
  if (left.length !== right.length) return left.length - right.length;
698
1034
  return left < right ? -1 : left > right ? 1 : 0;
@@ -701,102 +1037,795 @@ function tokenNames(baseTokens) {
701
1037
  }
702
1038
 
703
1039
  /**
704
- * The body/UI tokens: everything this plugin scales except the code family,
705
- * which answers to its own offset.
1040
+ * The interface PART size tokens: everything scalable except the code and
1041
+ * markdown families and the delta tokens (which the dialog offset shifts
1042
+ * additively, never multiplicatively).
706
1043
  * @param {Record<string, string>} [baseTokens] - token name to value.
707
- * @returns {string[]} the names the body offset re-declares.
1044
+ * @returns {string[]} the names the interface offset re-declares.
708
1045
  */
709
1046
  function bodyTokenNames(baseTokens) {
710
1047
  return tokenNames(baseTokens).filter(function (name) {
711
- return !isCodeToken(name);
1048
+ return (
1049
+ !isCodeToken(name) &&
1050
+ !isMarkdownToken(name) &&
1051
+ !isDeltaToken(name) &&
1052
+ !isContentToken(name)
1053
+ );
1054
+ });
1055
+ }
1056
+
1057
+ /**
1058
+ * The interface line-height PART tokens (non-code, non-markdown).
1059
+ * @param {Record<string, string>} [baseTokens] - token name to value.
1060
+ * @returns {string[]}
1061
+ */
1062
+ function uiLineTokenNames(baseTokens) {
1063
+ return bodyTokenNames(baseTokens).filter(function (name) {
1064
+ return name.indexOf("-line-height") >= 0;
712
1065
  });
713
1066
  }
714
1067
 
715
1068
  /**
716
- * The code tokens: the `font` shorthands plus their `-font-size` /
717
- * `-line-height` parts, when the live stylesheets declare them.
1069
+ * The markdown (dialog) PART tokens: sizes and line heights of the markdown
1070
+ * family, excluding code.
718
1071
  * @param {Record<string, string>} [baseTokens] - token name to value.
719
- * @returns {string[]} the names the code offset re-declares.
1072
+ * @returns {string[]}
1073
+ */
1074
+ function markdownTokenNames(baseTokens) {
1075
+ return tokenNames(baseTokens).filter(isMarkdownToken);
1076
+ }
1077
+
1078
+ /**
1079
+ * The markdown content-size DELTA tokens present in a base map.
1080
+ * @param {Record<string, string>} [baseTokens] - token name to value.
1081
+ * @returns {string[]}
1082
+ */
1083
+ function deltaTokenNames(baseTokens) {
1084
+ if (baseTokens === null || typeof baseTokens !== "object") return [];
1085
+ return Object.keys(baseTokens).filter(isDeltaToken);
1086
+ }
1087
+
1088
+ /**
1089
+ * The code PART tokens: the `font` shorthands' size/height parts, when the
1090
+ * live stylesheets declare them. The shorthands themselves are handled by
1091
+ * `codeShorthandTokens`, so they are excluded here to avoid double
1092
+ * declarations.
1093
+ * @param {Record<string, string>} [baseTokens] - token name to value.
1094
+ * @returns {string[]}
720
1095
  */
721
1096
  function codeTokenNames(baseTokens) {
722
- return tokenNames(baseTokens).filter(isCodeToken);
1097
+ return tokenNames(baseTokens).filter(function (name) {
1098
+ return isCodeToken(name) && !isShorthandName(name);
1099
+ });
1100
+ }
1101
+
1102
+ /**
1103
+ * Every `font` shorthand token in a base map that parses, sorted by name
1104
+ * length (families before variants, mirroring the part-token order).
1105
+ * @param {Record<string, string>} [baseTokens] - token name to value.
1106
+ * @returns {{name: string, parts: object}[]}
1107
+ */
1108
+ function shorthandTokens(baseTokens) {
1109
+ if (baseTokens === null || typeof baseTokens !== "object") return [];
1110
+ var out = [];
1111
+ var names = Object.keys(baseTokens).filter(isShorthandName);
1112
+ names.sort(function (left, right) {
1113
+ if (left.length !== right.length) return left.length - right.length;
1114
+ return left < right ? -1 : left > right ? 1 : 0;
1115
+ });
1116
+ for (var index = 0; index < names.length; index += 1) {
1117
+ var parts = parseShorthand(baseTokens[names[index]]);
1118
+ if (parts !== null) out.push({ name: names[index], parts: parts });
1119
+ }
1120
+ return out;
1121
+ }
1122
+
1123
+ /**
1124
+ * The markdown (dialog) `font` shorthand tokens, excluding code.
1125
+ * @param {Record<string, string>} [baseTokens] - token name to value.
1126
+ * @returns {{name: string, parts: object}[]}
1127
+ */
1128
+ function markdownShorthandTokens(baseTokens) {
1129
+ return shorthandTokens(baseTokens).filter(function (entry) {
1130
+ return isMarkdownToken(entry.name);
1131
+ });
1132
+ }
1133
+
1134
+ /**
1135
+ * The interface (non-markdown, non-code) `font` shorthand tokens.
1136
+ * @param {Record<string, string>} [baseTokens] - token name to value.
1137
+ * @returns {{name: string, parts: object}[]}
1138
+ */
1139
+ function uiShorthandTokens(baseTokens) {
1140
+ return shorthandTokens(baseTokens).filter(function (entry) {
1141
+ return !isMarkdownToken(entry.name) && !isCodeToken(entry.name);
1142
+ });
1143
+ }
1144
+
1145
+ /**
1146
+ * The code `font` shorthand tokens.
1147
+ * @param {Record<string, string>} [baseTokens] - token name to value.
1148
+ * @returns {{name: string, parts: object}[]}
1149
+ */
1150
+ function codeShorthandTokens(baseTokens) {
1151
+ return shorthandTokens(baseTokens).filter(function (entry) {
1152
+ return isCodeToken(entry.name);
1153
+ });
1154
+ }
1155
+
1156
+ /* ------------------------------------------------------------------ *
1157
+ * selectors
1158
+ * ------------------------------------------------------------------ */
1159
+
1160
+ /**
1161
+ * What counts as code text for the code axes.
1162
+ *
1163
+ * There is no token to hook here (no shipped rule reads a `-font-weight`
1164
+ * token — measured, zero `font-weight: var(--dsw-font-…)` in rc.2), so the
1165
+ * code axes have to name the elements. Two layers, both measured against
1166
+ * rc.2:
1167
+ *
1168
+ * 1. The tags DSH's own stylesheets size with a code token: markdown `pre` /
1169
+ * inline `code`, the tool card's `:where(pre)` block, form controls, and
1170
+ * CodeMirror-like editors. Their descendants are listed too, because the
1171
+ * blanket body rule matches EVERY element — a `<span>` inside a
1172
+ * highlighted block would otherwise be pulled back to the body weight.
1173
+ * 2. Containers that only a class name identifies: the terminal and tool-code
1174
+ * bodies are plain `div`s (`…_terminal`, `…_codeBody`, `md-code-block`).
1175
+ *
1176
+ * The `i` flag keeps a casing change in a future DSH from silently dropping
1177
+ * the rule, and an unmatched token/class simply means that surface follows
1178
+ * the interface axis — the same degradation as an unknown token name.
1179
+ */
1180
+ var CODE_SELECTOR = [
1181
+ "pre",
1182
+ "code",
1183
+ "kbd",
1184
+ "samp",
1185
+ "var",
1186
+ "tt",
1187
+ "textarea",
1188
+ ".cm-editor",
1189
+ ".dfp-previewCode",
1190
+ '[class*="code" i]',
1191
+ '[class*="terminal" i]',
1192
+ ]
1193
+ .map(function (selector) {
1194
+ return selector + "," + selector + " *";
1195
+ })
1196
+ .join(",");
1197
+
1198
+ /**
1199
+ * Everything a code surface must be excluded from a DIALOG-scoped rule:
1200
+ * the code elements themselves AND their descendants (a highlight `<span>`
1201
+ * inside a `<pre>` is not a `pre`, so the exclusion has to reach through
1202
+ * ancestry with complex `:not()` arguments — supported by the Chromium line
1203
+ * DSH targets).
1204
+ */
1205
+ var CODE_EXCLUDES = [
1206
+ "pre",
1207
+ "pre *",
1208
+ "code",
1209
+ "code *",
1210
+ "kbd",
1211
+ "kbd *",
1212
+ "samp",
1213
+ "samp *",
1214
+ "var",
1215
+ "var *",
1216
+ "tt",
1217
+ "tt *",
1218
+ "textarea",
1219
+ "textarea *",
1220
+ '[class*="code" i]',
1221
+ '[class*="code" i] *',
1222
+ '[class*="terminal" i]',
1223
+ '[class*="terminal" i] *',
1224
+ ]
1225
+ .map(function (selector) {
1226
+ return ":not(" + selector + ")";
1227
+ })
1228
+ .join("");
1229
+
1230
+ /**
1231
+ * The dialog surface: DSH renders the conversation markdown in a CSS-module
1232
+ * container whose class hashes per release (`_markdown_<hash>_…`). A
1233
+ * class-substring match degrades safely — no match means the dialog follows
1234
+ * the interface axis, the same behavior as an unknown token name.
1235
+ */
1236
+ var MARKDOWN_SELECTOR =
1237
+ '[class*="_markdown_" i]' +
1238
+ CODE_EXCLUDES +
1239
+ ',[class*="_markdown_" i] *' +
1240
+ CODE_EXCLUDES;
1241
+
1242
+ /** The dark-theme attribute DSH's ThemePresenter writes on body. */
1243
+ var DARK_ATTR = "body[data-ds-dark-theme]";
1244
+
1245
+ /**
1246
+ * Prefix every selector in a comma list, for the per-theme rules.
1247
+ * @param {string} selectorList - a comma-separated selector list.
1248
+ * @param {string} prefix - the prefix to prepend to each selector.
1249
+ * @returns {string} the prefixed list.
1250
+ */
1251
+ function prefixSelector(selectorList, prefix) {
1252
+ var parts = selectorList.split(",");
1253
+ var out = [];
1254
+ for (var index = 0; index < parts.length; index += 1) {
1255
+ out.push(prefix + " " + parts[index]);
1256
+ }
1257
+ return out.join(",");
1258
+ }
1259
+
1260
+ /* ------------------------------------------------------------------ *
1261
+ * per-theme resolution
1262
+ * ------------------------------------------------------------------ */
1263
+
1264
+ /**
1265
+ * Expand the FOLLOW semantics over one normalized configuration: the interface
1266
+ * borrows the conversation's family (and an unset conversation family falls back
1267
+ * to the interface's own). The interface's size, line-height and weight axes are
1268
+ * retired and render nothing, so no other axis follows.
1269
+ * @param {Record<string, unknown>} raw - a normalized configuration.
1270
+ * @returns {Record<string, unknown>} the set with the follow rule applied.
1271
+ */
1272
+ function expandFollow(raw) {
1273
+ var out = {};
1274
+ for (var field in raw) {
1275
+ if (Object.prototype.hasOwnProperty.call(raw, field)) out[field] = raw[field];
1276
+ }
1277
+ // The conversation inherits the interface's FAMILY for as long as it does not
1278
+ // set one itself. That is the whole of the old "follow" behaviour that is
1279
+ // left: the interface's size, line height and weight axes are retired (DSH
1280
+ // exposes no interface hook for the first two, and a body-wide weight rule
1281
+ // cannot help but reach the conversation), so the conversation's own values
1282
+ // are the only ones that render for them.
1283
+ out[STACK_DIALOG_FIELD] = raw[STACK_DIALOG_FIELD] !== "" ? raw[STACK_DIALOG_FIELD] : raw[SANS_FIELD];
1284
+ out[WEIGHT_DIALOG_FIELD] = raw[WEIGHT_DIALOG_FIELD];
1285
+ // The interface follows the conversation: the conversation's family wins
1286
+ // whenever the conversation sets one, and the interface's own family stands as
1287
+ // the fallback otherwise.
1288
+ if (raw[UI_FOLLOWS_FIELD] !== false) {
1289
+ out[SANS_FIELD] = out[STACK_DIALOG_FIELD];
1290
+ }
1291
+ return out;
1292
+ }
1293
+
1294
+ /**
1295
+ * Resolve the effective light and dark axis sets of one configuration.
1296
+ *
1297
+ * With `perTheme` off the dark set IS the light set; with it on, the sparse
1298
+ * `darkValues` map is applied over the single set first and follow-expansion
1299
+ * runs on the result, so a dark dialog value still tracks the dark interface
1300
+ * value when the interface itself is overridden per theme.
1301
+ * @param {unknown} config - candidate configuration.
1302
+ * @returns {{light: object, dark: object}} the two axis sets.
1303
+ */
1304
+ function resolveAxes(config) {
1305
+ var raw = normalizeConfig(config);
1306
+ var light = expandFollow(raw);
1307
+ if (raw[PER_THEME_FIELD] !== true) return { light: light, dark: light };
1308
+ // The dark overrides are applied to the RAW values and only then expanded: an
1309
+ // already-expanded set has the follow-borrowed values baked in, so expanding
1310
+ // it again would let a borrowed value shadow the dark override.
1311
+ var merged = {};
1312
+ for (var field in raw) {
1313
+ if (Object.prototype.hasOwnProperty.call(raw, field)) merged[field] = raw[field];
1314
+ }
1315
+ var overrides = raw[DARK_VALUES_FIELD];
1316
+ for (var key in overrides) {
1317
+ if (Object.prototype.hasOwnProperty.call(overrides, key)) merged[key] = overrides[key];
1318
+ }
1319
+ return { light: light, dark: expandFollow(merged) };
723
1320
  }
724
1321
 
1322
+ /* ------------------------------------------------------------------ *
1323
+ * the stylesheet builder
1324
+ * ------------------------------------------------------------------ */
1325
+
1326
+ /**
1327
+ * Whether a normalized configuration asks for any change at all.
1328
+ * @param {unknown} config - candidate configuration.
1329
+ * @returns {boolean} true when nothing should be injected.
1330
+ */
1331
+ function isDormant(config) {
1332
+ var axis = config !== null && typeof config === "object" ? config : {};
1333
+ if (axis[PER_THEME_FIELD] === true) {
1334
+ var dark = axis[DARK_VALUES_FIELD];
1335
+ if (dark !== null && typeof dark === "object") {
1336
+ for (var field in dark) {
1337
+ if (Object.prototype.hasOwnProperty.call(dark, field)) return false;
1338
+ }
1339
+ }
1340
+ }
1341
+ return (
1342
+ // The retired interface axes (size, line height, weight) are deliberately
1343
+ // absent: a configuration that only sets them renders nothing, so it is
1344
+ // dormant like an empty one.
1345
+ sanitize(axis[SANS_FIELD]) === "" &&
1346
+ sanitize(axis[STACK_DIALOG_FIELD]) === "" &&
1347
+ sanitize(axis[MONO_FIELD]) === "" &&
1348
+ clampOffset(axis[SIZE_DIALOG_FIELD]) === 0 &&
1349
+ clampOffset(axis[CODE_SIZE_FIELD]) === 0 &&
1350
+ clampWeight(axis[WEIGHT_DIALOG_FIELD]) === WEIGHT_UNSET &&
1351
+ clampWeight(axis[CODE_WEIGHT_FIELD]) === WEIGHT_UNSET &&
1352
+ clampRatio(axis[LINE_HEIGHT_DIALOG_FIELD]) === LINE_HEIGHT_MIN &&
1353
+ clampLineOffset(axis[CODE_LINE_HEIGHT_FIELD]) === 0 &&
1354
+ clampLigatures(axis[LIGATURES_FIELD]) === LIGATURES_DEFAULT &&
1355
+ sanitizeFeatures(axis[FEATURES_FIELD]) === "" &&
1356
+ toBool(axis[NO_SYNTHETIC_ITALIC_FIELD]) === false &&
1357
+ toBool(axis[NO_SYNTHETIC_BOLD_FIELD]) === false
1358
+ );
1359
+ }
1360
+
1361
+ /**
1362
+ * Build the stylesheet one THEME VALUE SET applies.
1363
+ *
1364
+ * The dialog axes reach the conversation markdown through a scope rule keyed
1365
+ * on the markdown container (class-substring match) rather than through the
1366
+ * design tokens: the tokens' `font` shorthands inline their parts, so a
1367
+ * token-level dialog override would need a full shorthand rebuild, while a
1368
+ * scoped `!important` rule wins over every non-important declaration the
1369
+ * theme ships and degrades to "follows the interface" when nothing matches.
1370
+ * Code surfaces are excluded from the dialog scope by construction (see
1371
+ * `CODE_EXCLUDES`), so inline code inside a serif conversation stays mono.
1372
+ *
1373
+ * @param {Record<string, unknown>} axis - an expanded (follow-applied) value set.
1374
+ * @param {Record<string, string>} baseTokens - token name to untouched value.
1375
+ * @param {boolean} isDark - prefix every rule with the dark-theme attribute.
1376
+ * @returns {string} declarations for one `<style>` element.
1377
+ */
1378
+ function buildAxisCss(axis, baseTokens, isDark) {
1379
+ var sans = formatStack(parseStack(axis[SANS_FIELD]));
1380
+ var mono = formatStack(parseStack(axis[MONO_FIELD]));
1381
+ var declarations = [];
1382
+
1383
+ function blanket(declarations) {
1384
+ var selector = isDark ? DARK_ATTR + "," + DARK_ATTR + " *" : "body,body *";
1385
+ return selector + "{" + declarations + "}";
1386
+ }
1387
+ function codeRule(declarations) {
1388
+ var selector = isDark ? prefixSelector(CODE_SELECTOR, DARK_ATTR) : CODE_SELECTOR;
1389
+ return selector + "{" + declarations + "}";
1390
+ }
1391
+ function dialogRule(declarations) {
1392
+ var selector = isDark ? prefixSelector(MARKDOWN_SELECTOR, DARK_ATTR) : MARKDOWN_SELECTOR;
1393
+ return selector + "{" + declarations + "}";
1394
+ }
1395
+ /**
1396
+ * Re-assert the code family inside the dialog: scoped under the markdown
1397
+ * container, after the dialog rule and above it in specificity, so a
1398
+ * serif conversation keeps monospaced code.
1399
+ */
1400
+ function pushScopedCodeFamily() {
1401
+ var scopedCode = [];
1402
+ var surfaces = CODE_SELECTOR.split(",");
1403
+ for (var cIndex = 0; cIndex < surfaces.length; cIndex += 1) {
1404
+ scopedCode.push('[class*="_markdown_" i] ' + surfaces[cIndex]);
1405
+ }
1406
+ declarations.push(scopedCode.join(",") + "{font-family:" + mono + " !important}");
1407
+ }
1408
+
1409
+ // ---- families ----
1410
+ if (sans !== "") {
1411
+ declarations.push(
1412
+ (isDark ? DARK_ATTR : ":root,body") + "{--dsw-font-family:" + sans + " !important}"
1413
+ );
1414
+ declarations.push((isDark ? DARK_ATTR : "body") + "{font-family:" + sans + " !important}");
1415
+ }
1416
+ if (mono !== "") {
1417
+ // The theme's code tokens chain to `--ds-font-family-code`; the other name
1418
+ // is what dsh-ui-font historically wrote and costs nothing to cover.
1419
+ declarations.push(
1420
+ (isDark ? DARK_ATTR : ":root,body") +
1421
+ "{--dsw-font-mono:" +
1422
+ mono +
1423
+ " !important;--ds-font-family-code:" +
1424
+ mono +
1425
+ " !important}"
1426
+ );
1427
+ // Written after the interface family rule: equal specificity on a code
1428
+ // element means the later declaration wins, so code keeps its own family.
1429
+ declarations.push(codeRule("font-family:" + mono + " !important"));
1430
+ }
1431
+
1432
+ // ---- interface size / line height: RETIRED, and deliberately so ----
1433
+ // Verified against DSH 0.1.5-rc.2: the official settings expose exactly one
1434
+ // size hook, the CONVERSATION font size (`--dsh-content-font-size`). The
1435
+ // interface ladder tokens (`--dsw-font-xs-13`, `--dsw-font-s-14`, …) are
1436
+ // consumed only by conversation-area widgets (tool cards, trajectory,
1437
+ // attachment chips — 6 client-ui packages), while the settings sheet, the
1438
+ // left workspace/sidebar and the right sidebar declare their text sizes as
1439
+ // literals inside their own CSS modules. There is therefore no interface
1440
+ // size or line-height hook: scaling the ladder from these fields moved
1441
+ // nothing a user could recognise as "interface", and scaling the content
1442
+ // chain from them moved the conversation — the exact cross-talk this release
1443
+ // removes. The durable fields stay in the schema (old documents keep
1444
+ // parsing, presets keep round-tripping) but inject nothing at all.
1445
+ //
1446
+ // The interface axis that DOES work is kept above: the family (a page-wide
1447
+ // `font-family` on body plus the theme variable) and the weight (a
1448
+ // `body, body *` rule).
1449
+
1450
+ // ---- interface weight: RETIRED ----
1451
+ // A weight is only expressible as a `body, body *` rule, which cannot help
1452
+ // but reach the conversation's markdown (narrowing it off the markdown
1453
+ // subtree is possible, but it would then also strip the theme's own heading
1454
+ // weights). The conversation owns the weight axis instead: its rule is scoped
1455
+ // to the markdown subtree, so it moves the conversation and nothing else.
1456
+ // The durable field stays in the schema (old documents and presets keep
1457
+ // parsing) but injects nothing.
1458
+
1459
+ // ---- dialog family (scoped; skipped when it equals the interface) ----
1460
+ var dialog = formatStack(parseStack(axis[STACK_DIALOG_FIELD]));
1461
+ if (dialog !== "" && dialog !== sans) {
1462
+ declarations.push(dialogRule("font-family:" + dialog + " !important"));
1463
+ if (mono !== "") {
1464
+ // Keep the code family inside the dialog.
1465
+ pushScopedCodeFamily();
1466
+ }
1467
+ }
1468
+
1469
+ // ---- dialog weight (scoped; skipped when it equals the interface) ----
1470
+ var dialogWeight = axis[WEIGHT_DIALOG_FIELD];
1471
+ if (dialogWeight !== WEIGHT_UNSET) {
1472
+ declarations.push(dialogRule("font-weight:" + dialogWeight + " !important"));
1473
+ }
1474
+
1475
+ // ---- dialog size offset (additive, on the official content-size source) ----
1476
+ // Every markdown size derives from the official content-size chain, and that
1477
+ // chain is COMPUTED ON BODY: custom properties resolve where they are
1478
+ // declared, so overriding the size deeper down would leave the derived tokens
1479
+ // (secondary size, both deltas, delta-based heading sizes, heights) at their
1480
+ // body-computed values. The offset is therefore added once, at the source —
1481
+ // `--dsh-content-font-size` becomes "official base + offset" — and the whole
1482
+ // ladder follows by derivation, with nothing counted twice. The interface
1483
+ // axis never touches this chain (see `bodyTokenNames`), so the conversation
1484
+ // moves only when this axis says so, and a dialog that FOLLOWS the interface
1485
+ // follows it through this value.
1486
+ var dialogOffsetValue = axis[SIZE_DIALOG_FIELD];
1487
+ if (dialogOffsetValue !== 0) {
1488
+ var contentSources = tokenNames(baseTokens).filter(function (name) {
1489
+ if (!isContentToken(name)) return false;
1490
+ var base = baseTokens[name];
1491
+ // The source token only: the derived ones carry var()/calc() and would
1492
+ // add the offset a second time if they were shifted as well.
1493
+ return (
1494
+ typeof base === "string" &&
1495
+ base !== "" &&
1496
+ base.indexOf("var(") === -1 &&
1497
+ base.indexOf("calc(") === -1
1498
+ );
1499
+ });
1500
+ var contentShift = shiftTokens(contentSources, baseTokens, dialogOffsetValue);
1501
+ if (contentShift.length > 0) declarations.push(blanket(contentShift.join(";")));
1502
+ // The `small` series is the only markdown family with literal sizes: it
1503
+ // does not derive from the chain, so its parts and shorthands shift here.
1504
+ var literals = markdownTokenNames(baseTokens).filter(function (name) {
1505
+ var base = baseTokens[name];
1506
+ return name.indexOf("-font-size") >= 0 && typeof base === "string" && base.indexOf("var(") === -1;
1507
+ });
1508
+ var literalShift = shiftTokens(literals, baseTokens, dialogOffsetValue);
1509
+ if (literalShift.length > 0) declarations.push(blanket(literalShift.join(";")));
1510
+ // The shorthands whose size is a literal (the small series) must ride the
1511
+ // shift too; the delta-derived shorthands flow through the chain above.
1512
+ var sizeShifted = [];
1513
+ var markdownEntries = shorthandTokens(baseTokens).filter(function (entry) {
1514
+ return isMarkdownToken(entry.name) && entry.parts.size.indexOf("var(") === -1;
1515
+ });
1516
+ for (var mIndex = 0; mIndex < markdownEntries.length; mIndex += 1) {
1517
+ var mEntry = markdownEntries[mIndex];
1518
+ var mSize =
1519
+ "calc((" + mEntry.parts.size + ") + " + dialogOffsetValue + "px)";
1520
+ sizeShifted.push(
1521
+ mEntry.name + ":" + rebuildShorthand({ lead: mEntry.parts.lead, size: mSize, height: mEntry.parts.height, family: mEntry.parts.family }) + " !important"
1522
+ );
1523
+ }
1524
+ if (sizeShifted.length > 0) declarations.push(blanket(sizeShifted.join(";")));
1525
+ }
1526
+
1527
+ // ---- dialog line height (ratio, through the shorthand heights) ----
1528
+ var dialogLine = axis[LINE_HEIGHT_DIALOG_FIELD];
1529
+ if (dialogLine !== LINE_HEIGHT_MIN) {
1530
+ var dialogFactor = round(dialogLine / LINE_HEIGHT_MIN, 6);
1531
+ var markdownLines = markdownTokenNames(baseTokens).filter(function (name) {
1532
+ return name.indexOf("-line-height") >= 0;
1533
+ });
1534
+ var dialogLineDecls = scaleLineTokens(markdownLines, baseTokens, dialogFactor);
1535
+ if (dialogLineDecls.length > 0) declarations.push(blanket(dialogLineDecls.join(";")));
1536
+ var dialogShorthandList = markdownShorthandTokens(baseTokens);
1537
+ var dialogShorthandShifted = [];
1538
+ for (var dIndex = 0; dIndex < dialogShorthandList.length; dIndex += 1) {
1539
+ var dEntry = dialogShorthandList[dIndex];
1540
+ dialogShorthandShifted.push(
1541
+ dEntry.name +
1542
+ ":" +
1543
+ rebuildShorthand({
1544
+ lead: dEntry.parts.lead,
1545
+ size: dEntry.parts.size,
1546
+ height: "calc((" + dEntry.parts.height + ") * " + dialogFactor + ")",
1547
+ family: dEntry.parts.family,
1548
+ }) +
1549
+ " !important"
1550
+ );
1551
+ }
1552
+ if (dialogShorthandShifted.length > 0) {
1553
+ declarations.push(blanket(dialogShorthandShifted.join(";")));
1554
+ }
1555
+ }
1556
+
1557
+ // ---- code axes ----
1558
+ var codeSize = axis[CODE_SIZE_FIELD];
1559
+ var codeLine = axis[CODE_LINE_HEIGHT_FIELD];
1560
+ if (codeSize !== 0 || codeLine !== 0) {
1561
+ var codeScaled = scaleCodeTokens(
1562
+ baseTokens,
1563
+ round(scaleFor(codeSize), 6),
1564
+ codeLine
1565
+ );
1566
+ if (codeScaled.length > 0) declarations.push(blanket(codeScaled.join(";")));
1567
+ }
1568
+ var codeWeight = axis[CODE_WEIGHT_FIELD];
1569
+ if (codeWeight !== WEIGHT_UNSET) {
1570
+ declarations.push(codeRule("font-weight:" + codeWeight + " !important"));
1571
+ }
1572
+ var ligatures = axis[LIGATURES_FIELD];
1573
+ if (ligatures === LIGATURES_ON) {
1574
+ declarations.push(codeRule("font-variant-ligatures:contextual !important"));
1575
+ } else if (ligatures === LIGATURES_OFF) {
1576
+ declarations.push(codeRule("font-variant-ligatures:none !important"));
1577
+ }
1578
+ var features = axis[FEATURES_FIELD];
1579
+ if (features !== "") {
1580
+ declarations.push(codeRule("font-feature-settings:" + features + " !important"));
1581
+ }
1582
+
1583
+ // ---- synthesis (faux italic / faux bold) ----
1584
+ var noItalic = axis[NO_SYNTHETIC_ITALIC_FIELD] === true;
1585
+ var noBold = axis[NO_SYNTHETIC_BOLD_FIELD] === true;
1586
+ if (noItalic && noBold) {
1587
+ declarations.push(blanket("font-synthesis:none !important"));
1588
+ } else {
1589
+ if (noItalic) declarations.push(blanket("font-synthesis-style:none !important"));
1590
+ if (noBold) declarations.push(blanket("font-synthesis-weight:none !important"));
1591
+ }
1592
+
1593
+ return declarations.join("\n");
1594
+ }
1595
+
1596
+ /**
1597
+ * Re-declare the markdown code tokens at their own ratio and line offset.
1598
+ *
1599
+ * Two shapes exist and both must be covered, because the shipped stylesheets
1600
+ * consume the shorthand (`font: var(--dsw-font-markdown-code-block-small)`)
1601
+ * while the split tokens exist for anything that asks for a single part:
1602
+ * `11px/19px <family>` and a bare `11px`. The shorthand's height rides BOTH
1603
+ * the ratio and the additive offset; the size rides the ratio only.
1604
+ *
1605
+ * @param {Record<string, string>} baseTokens - token name to untouched value.
1606
+ * @param {number} scale - the size ratio to apply.
1607
+ * @param {number} [lineOffset=0] - additive px on the height.
1608
+ * @returns {string[]} declarations.
1609
+ */
1610
+ function scaleCodeTokens(baseTokens, scale, lineOffset) {
1611
+ var shift = typeof lineOffset === "number" && lineOffset !== 0 ? lineOffset : 0;
1612
+ // The code axis covers every `--dsw-font-markdown-code*` token exactly once:
1613
+ // a value that parses as a shorthand rides the shorthand path, everything
1614
+ // else the part path.
1615
+ var entries = [];
1616
+ var seen = {};
1617
+ var all = tokenNames(baseTokens).filter(isCodeToken);
1618
+ for (var aIndex = 0; aIndex < all.length; aIndex += 1) {
1619
+ seen[all[aIndex]] = true;
1620
+ entries.push({ name: all[aIndex], value: baseTokens[all[aIndex]] });
1621
+ }
1622
+ var shorthands = shorthandTokens(baseTokens);
1623
+ for (var sIndex = 0; sIndex < shorthands.length; sIndex += 1) {
1624
+ var entry = shorthands[sIndex];
1625
+ if (!isCodeToken(entry.name) || seen[entry.name] === true) continue;
1626
+ seen[entry.name] = true;
1627
+ entries.push({ name: entry.name, value: baseTokens[entry.name] });
1628
+ }
1629
+ var out = [];
1630
+ for (var index = 0; index < entries.length; index += 1) {
1631
+ var name = entries[index].name;
1632
+ var base = entries[index].value;
1633
+ if (typeof base !== "string" || base === "") continue;
1634
+ var parsed = parseShorthand(base);
1635
+ if (parsed !== null) {
1636
+ var size = scale === 1 ? parsed.size : "calc((" + parsed.size + ") * " + scale + ")";
1637
+ var height = "calc((" + parsed.height + ") * " + scale + ")";
1638
+ if (shift !== 0) height = "calc(" + height + " + " + shift + "px)";
1639
+ out.push(
1640
+ name +
1641
+ ":" +
1642
+ rebuildShorthand({ lead: parsed.lead, size: size, height: height, family: parsed.family }) +
1643
+ " !important"
1644
+ );
1645
+ continue;
1646
+ }
1647
+ // A part token that derives from another token follows that token's own
1648
+ // scaling; re-declaring it here would compound the ratio — except the
1649
+ // line-height parts, which must additionally carry the offset.
1650
+ if (base.indexOf("var(") !== -1 && !(name.indexOf("-line-height") >= 0 && shift !== 0)) continue;
1651
+ if (!isScalableValue(base)) continue;
1652
+ if (name.indexOf("-line-height") >= 0 && shift !== 0) {
1653
+ out.push(
1654
+ name +
1655
+ ":calc(((" + base + ") * " + scale + ") + " + shift + "px) !important"
1656
+ );
1657
+ } else {
1658
+ out.push(name + ":calc((" + base + ") * " + scale + ") !important");
1659
+ }
1660
+ }
1661
+ return out;
1662
+ }
1663
+
1664
+ /* ------------------------------------------------------------------ *
1665
+ * entry point
1666
+ * ------------------------------------------------------------------ */
1667
+
1668
+ /**
1669
+ * Build the stylesheet one configuration applies.
1670
+ *
1671
+ * The configuration resolves into a light set and — when `perTheme` is on — a
1672
+ * dark set; the light rules are emitted unprefixed and the dark rules carry
1673
+ * the dark-theme attribute, so the dark set wins by specificity exactly while
1674
+ * that theme is active. When `perTheme` is off, or the dark set equals the
1675
+ * light set, no prefixed rules are emitted at all.
1676
+ *
1677
+ * @param {unknown} config - normalized or raw configuration.
1678
+ * @param {Record<string, string>} [baseTokens] - token name to untouched value.
1679
+ * @returns {string} declarations for one `<style>` element ("" when dormant).
1680
+ */
1681
+ function buildFontCss(config, baseTokens) {
1682
+ var tokens = baseTokens === undefined || baseTokens === null ? FALLBACK_TOKENS : baseTokens;
1683
+ var sets = resolveAxes(config);
1684
+ if (isDormant(sets.light) && isDormant(sets.dark)) return "";
1685
+ var light = buildAxisCss(sets.light, tokens, false);
1686
+ if (sameValueSet(sets.light, sets.dark)) return light;
1687
+ var dark = buildAxisCss(sets.dark, tokens, true);
1688
+ if (dark === "") return light;
1689
+ return light + "\n" + dark;
1690
+ }
1691
+
1692
+ /**
1693
+ * Whether two axis sets produce the same declarations, compared over the
1694
+ * visual fields only (the storage fields — presets, the dark map itself —
1695
+ * never render).
1696
+ * @param {Record<string, unknown>} left - an expanded axis set.
1697
+ * @param {Record<string, unknown>} right - another expanded axis set.
1698
+ * @returns {boolean}
1699
+ */
1700
+ function sameValueSet(left, right) {
1701
+ for (var index = 0; index < VALUE_FIELDS.length; index += 1) {
1702
+ var field = VALUE_FIELDS[index];
1703
+ if (left[field] !== right[field]) return false;
1704
+ }
1705
+ return true;
1706
+ }
1707
+
1708
+ /* ------------------------------------------------------------------ *
1709
+ * embedded fallback map
1710
+ * ------------------------------------------------------------------ */
1711
+
725
1712
  /**
726
1713
  * Embedded fallback for the tokens the theme plugin defines at runtime.
727
1714
  *
728
1715
  * The live document is always preferred; this map only covers the window
729
1716
  * before those declarations exist (and any token a future release renames
730
- * away from the pattern). Values mirror the size/line-height pairs shipped in
731
- * DSH 0.1.5-rc.2.
1717
+ * away from the pattern). Values are read byte for byte from the theme's own
1718
+ * stylesheet in DSH 0.1.5-rc.2.
732
1719
  */
733
1720
  var FALLBACK_TOKENS = {
1721
+ // interface ladder shorthands
1722
+ "--dsw-font-xxxs-11": "11px/14px var(--dsw-font-family)",
1723
+ "--dsw-font-xxxs-strong-11": "500 11px/14px var(--dsw-font-family)",
1724
+ "--dsw-font-xxs-12": "12px/18px var(--dsw-font-family)",
1725
+ "--dsw-font-xxs-strong-12": "500 12px/18px var(--dsw-font-family)",
1726
+ "--dsw-font-xs-13": "13px/20px var(--dsw-font-family)",
1727
+ "--dsw-font-xs-strong-13": "500 13px/20px var(--dsw-font-family)",
1728
+ "--dsw-font-s-14": "14px/22px var(--dsw-font-family)",
1729
+ "--dsw-font-s-strong-14": "500 14px/22px var(--dsw-font-family)",
1730
+ "--dsw-font-base-16": "16px/24px var(--dsw-font-family)",
1731
+ "--dsw-font-base-strong-16": "500 16px/24px var(--dsw-font-family)",
1732
+ "--dsw-font-m-18": "500 16px/28px var(--dsw-font-family)",
1733
+ "--dsw-font-l-20": "500 20px/28px var(--dsw-font-family)",
1734
+ "--dsw-font-xl-24": "600 24px/32px var(--dsw-font-family)",
1735
+ // interface ladder parts
734
1736
  "--dsw-font-xxxs-11-font-size": "11px",
735
- "--dsw-font-xxxs-11-line-height": "18px",
1737
+ "--dsw-font-xxxs-11-line-height": "14px",
736
1738
  "--dsw-font-xxxs-strong-11-font-size": "11px",
737
- "--dsw-font-xxxs-strong-11-line-height": "18px",
1739
+ "--dsw-font-xxxs-strong-11-line-height": "14px",
738
1740
  "--dsw-font-xxs-12-font-size": "12px",
739
- "--dsw-font-xxs-12-line-height": "20px",
1741
+ "--dsw-font-xxs-12-line-height": "18px",
740
1742
  "--dsw-font-xxs-strong-12-font-size": "12px",
741
- "--dsw-font-xxs-strong-12-line-height": "20px",
1743
+ "--dsw-font-xxs-strong-12-line-height": "18px",
742
1744
  "--dsw-font-xs-13-font-size": "13px",
743
- "--dsw-font-xs-13-line-height": "22px",
1745
+ "--dsw-font-xs-13-line-height": "20px",
744
1746
  "--dsw-font-xs-strong-13-font-size": "13px",
745
- "--dsw-font-xs-strong-13-line-height": "22px",
1747
+ "--dsw-font-xs-strong-13-line-height": "20px",
746
1748
  "--dsw-font-s-14-font-size": "14px",
747
- "--dsw-font-s-14-line-height": "24px",
1749
+ "--dsw-font-s-14-line-height": "22px",
748
1750
  "--dsw-font-s-strong-14-font-size": "14px",
749
- "--dsw-font-s-strong-14-line-height": "24px",
1751
+ "--dsw-font-s-strong-14-line-height": "22px",
750
1752
  "--dsw-font-base-16-font-size": "16px",
751
- "--dsw-font-base-16-line-height": "26px",
1753
+ "--dsw-font-base-16-line-height": "24px",
752
1754
  "--dsw-font-base-strong-16-font-size": "16px",
753
- "--dsw-font-base-strong-16-line-height": "26px",
754
- "--dsw-font-m-18-font-size": "18px",
1755
+ "--dsw-font-base-strong-16-line-height": "24px",
1756
+ "--dsw-font-m-18-font-size": "16px",
755
1757
  "--dsw-font-m-18-line-height": "28px",
756
1758
  "--dsw-font-l-20-font-size": "20px",
757
- "--dsw-font-l-20-line-height": "30px",
1759
+ "--dsw-font-l-20-line-height": "28px",
758
1760
  "--dsw-font-xl-24-font-size": "24px",
759
- "--dsw-font-xl-24-line-height": "34px",
760
- "--dsw-font-markdown-base-font-size": "14px",
761
- "--dsw-font-markdown-base-line-height": "24px",
762
- "--dsw-font-markdown-small-font-size": "13px",
763
- "--dsw-font-markdown-small-line-height": "22px",
764
- "--dsw-font-markdown-h1-font-size": "21px",
765
- "--dsw-font-markdown-h1-line-height": "30px",
766
- "--dsw-font-markdown-h2-font-size": "19px",
767
- "--dsw-font-markdown-h2-line-height": "28px",
768
- "--dsw-font-markdown-h3-font-size": "17px",
769
- "--dsw-font-markdown-h3-line-height": "26px",
770
- "--dsw-font-markdown-h4-font-size": "15px",
771
- "--dsw-font-markdown-h4-line-height": "24px",
772
- // The code family is consumed through the `font` shorthand, so the shorthand
773
- // names matter more than their parts — both are listed, at rc.2's values.
1761
+ "--dsw-font-xl-24-line-height": "32px",
1762
+ // markdown (dialog) shorthands
1763
+ "--dsw-font-markdown-h1": "700 calc(21px + var(--dsh-content-font-delta)) / calc(30px + var(--dsh-content-font-delta)) var(--dsw-font-family)",
1764
+ "--dsw-font-markdown-h2": "700 calc(19px + var(--dsh-content-font-delta)) / calc(28px + var(--dsh-content-font-delta)) var(--dsw-font-family)",
1765
+ "--dsw-font-markdown-h3": "700 calc(18px + var(--dsh-content-font-delta)) / calc(26px + var(--dsh-content-font-delta)) var(--dsw-font-family)",
1766
+ "--dsw-font-markdown-h4": "600 var(--dsh-content-font-size,14px) / calc(24px + var(--dsh-content-font-delta)) var(--dsw-font-family)",
1767
+ "--dsw-font-markdown-base": "var(--dsh-content-font-size,14px) / calc(24px + var(--dsh-content-font-delta)) var(--dsw-font-family)",
1768
+ "--dsw-font-markdown-base-strong": "600 var(--dsh-content-font-size,14px) / calc(24px + var(--dsh-content-font-delta)) var(--dsw-font-family)",
1769
+ "--dsw-font-markdown-base-italic": "italic var(--dsh-content-font-size,14px) / calc(24px + var(--dsh-content-font-delta)) var(--dsw-font-family)",
1770
+ "--dsw-font-markdown-base-strong-italic": "italic 600 var(--dsh-content-font-size,14px) / calc(24px + var(--dsh-content-font-delta)) var(--dsw-font-family)",
1771
+ "--dsw-font-markdown-table": "var(--dsh-content-font-size-secondary,13px)/calc(22px + var(--dsh-content-font-delta-secondary,0px)) var(--dsw-font-family)",
1772
+ "--dsw-font-markdown-table-head": "500 var(--dsh-content-font-size-secondary,13px)/calc(22px + var(--dsh-content-font-delta-secondary,0px)) var(--dsw-font-family)",
1773
+ "--dsw-font-markdown-small": "12px/20px var(--dsw-font-family)",
1774
+ "--dsw-font-markdown-small-strong": "600 12px/20px var(--dsw-font-family)",
1775
+ "--dsw-font-markdown-small-italic": "italic 12px/20px var(--dsw-font-family)",
1776
+ "--dsw-font-markdown-small-strong-italic": "italic 600 12px/20px var(--dsw-font-family)",
1777
+ // markdown small-series literals (the additive dialog offset rewrites these)
1778
+ "--dsw-font-markdown-small-font-size": "12px",
1779
+ "--dsw-font-markdown-small-line-height": "20px",
1780
+ "--dsw-font-markdown-small-strong-font-size": "12px",
1781
+ "--dsw-font-markdown-small-strong-line-height": "20px",
1782
+ "--dsw-font-markdown-small-italic-font-size": "12px",
1783
+ "--dsw-font-markdown-small-italic-line-height": "20px",
1784
+ "--dsw-font-markdown-small-strong-italic-font-size": "12px",
1785
+ "--dsw-font-markdown-small-strong-italic-line-height": "20px",
1786
+ // markdown part tokens (the dialog line-height ratio scales their heights;
1787
+ // their var-derived sizes ride the delta tokens, never the ratio)
1788
+ "--dsw-font-markdown-base-font-size": "var(--dsh-content-font-size,14px)",
1789
+ "--dsw-font-markdown-base-line-height": "calc(24px + var(--dsh-content-font-delta))",
1790
+ "--dsw-font-markdown-base-strong-font-size": "var(--dsh-content-font-size,14px)",
1791
+ "--dsw-font-markdown-base-strong-line-height": "calc(24px + var(--dsh-content-font-delta))",
1792
+ "--dsw-font-markdown-base-italic-font-size": "var(--dsh-content-font-size,14px)",
1793
+ "--dsw-font-markdown-base-italic-line-height": "calc(24px + var(--dsh-content-font-delta))",
1794
+ "--dsw-font-markdown-base-strong-italic-font-size": "var(--dsh-content-font-size,14px)",
1795
+ "--dsw-font-markdown-base-strong-italic-line-height": "calc(24px + var(--dsh-content-font-delta))",
1796
+ "--dsw-font-markdown-h1-font-size": "calc(21px + var(--dsh-content-font-delta))",
1797
+ "--dsw-font-markdown-h1-line-height": "calc(30px + var(--dsh-content-font-delta))",
1798
+ "--dsw-font-markdown-h2-font-size": "calc(19px + var(--dsh-content-font-delta))",
1799
+ "--dsw-font-markdown-h2-line-height": "calc(28px + var(--dsh-content-font-delta))",
1800
+ "--dsw-font-markdown-h3-font-size": "calc(18px + var(--dsh-content-font-delta))",
1801
+ "--dsw-font-markdown-h3-line-height": "calc(26px + var(--dsh-content-font-delta))",
1802
+ "--dsw-font-markdown-h4-font-size": "var(--dsh-content-font-size,14px)",
1803
+ "--dsw-font-markdown-h4-line-height": "calc(24px + var(--dsh-content-font-delta))",
1804
+ "--dsw-font-markdown-table-font-size": "var(--dsh-content-font-size-secondary,13px)",
1805
+ "--dsw-font-markdown-table-line-height": "calc(22px + var(--dsh-content-font-delta-secondary,0px))",
1806
+ "--dsw-font-markdown-table-head-font-size": "var(--dsh-content-font-size-secondary,13px)",
1807
+ "--dsw-font-markdown-table-head-line-height": "calc(22px + var(--dsh-content-font-delta-secondary,0px))",
1808
+ // markdown delta tokens (the dialog offset shifts these additively)
1809
+ "--dsh-content-font-delta": "calc(var(--dsh-content-font-size,14px) - 14px)",
1810
+ "--dsh-content-font-delta-secondary": "calc(var(--dsh-content-font-size-secondary) - 13px)",
1811
+ // the code family: shorthand + parts, at rc.2's values
774
1812
  "--dsw-font-markdown-code": "12px/19px var(--ds-font-family-code)",
775
1813
  "--dsw-font-markdown-code-font-size": "12px",
776
1814
  "--dsw-font-markdown-code-line-height": "19px",
777
1815
  "--dsw-font-markdown-code-block": "11px/19px var(--ds-font-family-code)",
778
- "--dsw-font-markdown-code-block": "11px/19px var(--ds-font-family-code)",
779
1816
  "--dsw-font-markdown-code-block-font-size": "11px",
780
1817
  "--dsw-font-markdown-code-block-line-height": "19px",
781
1818
  "--dsw-font-markdown-code-block-small": "11px/16px var(--ds-font-family-code)",
782
1819
  "--dsw-font-markdown-code-block-small-font-size": "11px",
783
1820
  "--dsw-font-markdown-code-block-small-line-height": "16px",
784
- "--dsw-font-markdown-table-font-size": "13px",
785
- "--dsw-font-markdown-table-line-height": "22px",
1821
+ // content size (the interface chain's anchor, written inline by the theme)
786
1822
  "--dsh-content-font-size": "14px",
787
1823
  "--dsh-content-font-size-secondary": "13px",
788
1824
  };
789
1825
 
790
- /**
791
- * Round to a fixed number of decimals so generated CSS stays readable.
792
- * @param {number} value - the number.
793
- * @param {number} digits - decimals to keep.
794
- * @returns {number} the rounded number.
795
- */
796
- function round(value, digits) {
797
- var factor = Math.pow(10, digits);
798
- return Math.round(value * factor) / factor;
799
- }
1826
+ /* ------------------------------------------------------------------ *
1827
+ * curated family lists
1828
+ * ------------------------------------------------------------------ */
800
1829
 
801
1830
  /**
802
1831
  * Curated families offered when the browser cannot enumerate local fonts —
@@ -862,44 +1891,111 @@ var PRESETS = {
862
1891
  generic: ["system-ui", "sans-serif", "serif"],
863
1892
  };
864
1893
 
1894
+ /**
1895
+ * Legacy single-purpose helper kept for compatibility: scale the size and the
1896
+ * height inside one simple `size/height <family>` shorthand.
1897
+ * @param {string} value - a value shaped like `11px/19px var(--ds-font-family-code)`.
1898
+ * @param {number} scale - the ratio to apply.
1899
+ * @returns {string|null} the scaled shorthand, or null when the shape differs.
1900
+ */
1901
+ function scaleFontShorthand(value, scale) {
1902
+ var parts = parseShorthand(value);
1903
+ if (parts === null || parts.lead !== "") return null;
1904
+ return (
1905
+ "calc((" + parts.size + ") * " + scale + ")/calc((" + parts.height + ") * " + scale + ") " + parts.family
1906
+ );
1907
+ }
1908
+
865
1909
  var shared = {
866
1910
  NAMESPACE: NAMESPACE,
867
1911
  SANS_FIELD: SANS_FIELD,
1912
+ STACK_DIALOG_FIELD: STACK_DIALOG_FIELD,
868
1913
  MONO_FIELD: MONO_FIELD,
869
1914
  SIZE_FIELD: SIZE_FIELD,
1915
+ SIZE_DIALOG_FIELD: SIZE_DIALOG_FIELD,
870
1916
  CODE_SIZE_FIELD: CODE_SIZE_FIELD,
871
1917
  WEIGHT_FIELD: WEIGHT_FIELD,
1918
+ WEIGHT_DIALOG_FIELD: WEIGHT_DIALOG_FIELD,
872
1919
  CODE_WEIGHT_FIELD: CODE_WEIGHT_FIELD,
1920
+ LINE_HEIGHT_FIELD: LINE_HEIGHT_FIELD,
1921
+ LINE_HEIGHT_DIALOG_FIELD: LINE_HEIGHT_DIALOG_FIELD,
1922
+ CODE_LINE_HEIGHT_FIELD: CODE_LINE_HEIGHT_FIELD,
1923
+ LIGATURES_FIELD: LIGATURES_FIELD,
1924
+ FEATURES_FIELD: FEATURES_FIELD,
1925
+ NO_SYNTHETIC_ITALIC_FIELD: NO_SYNTHETIC_ITALIC_FIELD,
1926
+ NO_SYNTHETIC_BOLD_FIELD: NO_SYNTHETIC_BOLD_FIELD,
1927
+ PER_THEME_FIELD: PER_THEME_FIELD,
1928
+ DARK_VALUES_FIELD: DARK_VALUES_FIELD,
1929
+ PRESETS_FIELD: PRESETS_FIELD,
1930
+ ACTIVE_PRESET_FIELD: ACTIVE_PRESET_FIELD,
1931
+ UI_FOLLOWS_FIELD: UI_FOLLOWS_FIELD,
1932
+ VALUE_FIELDS: VALUE_FIELDS,
873
1933
  SIZE_MIN: SIZE_MIN,
874
1934
  SIZE_MAX: SIZE_MAX,
875
1935
  WEIGHT_MIN: WEIGHT_MIN,
876
1936
  WEIGHT_MAX: WEIGHT_MAX,
877
1937
  WEIGHT_UNSET: WEIGHT_UNSET,
1938
+ LINE_HEIGHT_MIN: LINE_HEIGHT_MIN,
1939
+ LINE_HEIGHT_MAX: LINE_HEIGHT_MAX,
1940
+ CODE_LINE_HEIGHT_MIN: CODE_LINE_HEIGHT_MIN,
1941
+ CODE_LINE_HEIGHT_MAX: CODE_LINE_HEIGHT_MAX,
1942
+ LIGATURES_DEFAULT: LIGATURES_DEFAULT,
1943
+ LIGATURES_ON: LIGATURES_ON,
1944
+ LIGATURES_OFF: LIGATURES_OFF,
878
1945
  MARKER: MARKER,
879
1946
  STYLE_TAG: STYLE_TAG,
880
1947
  CARD_STYLE_TAG: CARD_STYLE_TAG,
881
1948
  DEFAULTS: DEFAULTS,
882
1949
  MAX_STACK_LENGTH: MAX_STACK_LENGTH,
883
1950
  MAX_FAMILY_LENGTH: MAX_FAMILY_LENGTH,
1951
+ MAX_FEATURES_LENGTH: MAX_FEATURES_LENGTH,
1952
+ MAX_PRESETS: MAX_PRESETS,
1953
+ MAX_PRESET_NAME: MAX_PRESET_NAME,
884
1954
  FALLBACK_TOKENS: FALLBACK_TOKENS,
885
1955
  PRESETS: PRESETS,
1956
+ MARKDOWN_SELECTOR: MARKDOWN_SELECTOR,
1957
+ DARK_ATTR: DARK_ATTR,
886
1958
  sanitize: sanitize,
887
1959
  sanitizeFamily: sanitizeFamily,
1960
+ sanitizeFeatures: sanitizeFeatures,
888
1961
  quoteFamily: quoteFamily,
889
1962
  normalizeConfig: normalizeConfig,
1963
+ normalizeDarkValues: normalizeDarkValues,
1964
+ normalizePresets: normalizePresets,
1965
+ normalizeValueSet: normalizeValueSet,
890
1966
  clampOffset: clampOffset,
891
1967
  clampWeight: clampWeight,
1968
+ clampRatio: clampRatio,
1969
+ clampLineOffset: clampLineOffset,
1970
+ clampLigatures: clampLigatures,
892
1971
  CODE_SELECTOR: CODE_SELECTOR,
893
1972
  formatStack: formatStack,
894
1973
  parseStack: parseStack,
1974
+ parseShorthand: parseShorthand,
1975
+ rebuildShorthand: rebuildShorthand,
895
1976
  scaleFor: scaleFor,
896
1977
  isDormant: isDormant,
1978
+ buildAxisCss: buildAxisCss,
897
1979
  buildFontCss: buildFontCss,
1980
+ resolveAxes: resolveAxes,
1981
+ expandFollow: expandFollow,
898
1982
  isFontToken: isFontToken,
899
1983
  isCodeToken: isCodeToken,
1984
+ isMarkdownToken: isMarkdownToken,
1985
+ isDeltaToken: isDeltaToken,
1986
+ isShorthandName: isShorthandName,
1987
+ isShorthandToken: isShorthandToken,
900
1988
  isScaledToken: isScaledToken,
1989
+ tokenNames: tokenNames,
901
1990
  codeTokenNames: codeTokenNames,
1991
+ markdownTokenNames: markdownTokenNames,
902
1992
  bodyTokenNames: bodyTokenNames,
1993
+ uiLineTokenNames: uiLineTokenNames,
1994
+ deltaTokenNames: deltaTokenNames,
1995
+ shorthandTokens: shorthandTokens,
1996
+ markdownShorthandTokens: markdownShorthandTokens,
1997
+ uiShorthandTokens: uiShorthandTokens,
1998
+ codeShorthandTokens: codeShorthandTokens,
903
1999
  scaleFontShorthand: scaleFontShorthand,
904
2000
  isGenericFamilyName: isGenericFamilyName,
905
2001
  isCJKFamilyName: isCJKFamilyName,