dsh-fonttune 0.2.1 → 0.2.3

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
@@ -152,9 +152,6 @@ var LIGATURES_DEFAULT = 0;
152
152
  var LIGATURES_ON = 1;
153
153
  var LIGATURES_OFF = 2;
154
154
 
155
- /** Marker on both injected style tags, used for scoping every rule we write. */
156
- var MARKER = "dfp";
157
-
158
155
  /** `data-plugin-css` value of the tag applying the configuration. */
159
156
  var STYLE_TAG = "dsh-fonttune";
160
157
 
@@ -187,20 +184,18 @@ var DEFAULTS = {
187
184
  };
188
185
 
189
186
  /**
190
- * The axis fields a preset snapshots: everything the card edits, excluding
191
- * the storage fields themselves.
187
+ * The axis fields a preset snapshots and the light/dark sets are compared
188
+ * over: everything the card edits, excluding the storage fields themselves.
192
189
  */
193
190
  var VALUE_FIELDS = [
194
191
  SANS_FIELD,
195
192
  STACK_DIALOG_FIELD,
196
193
  MONO_FIELD,
197
- SIZE_FIELD,
198
194
  SIZE_DIALOG_FIELD,
199
195
  CODE_SIZE_FIELD,
200
196
  WEIGHT_FIELD,
201
197
  WEIGHT_DIALOG_FIELD,
202
198
  CODE_WEIGHT_FIELD,
203
- LINE_HEIGHT_FIELD,
204
199
  LINE_HEIGHT_DIALOG_FIELD,
205
200
  CODE_LINE_HEIGHT_FIELD,
206
201
  LIGATURES_FIELD,
@@ -210,6 +205,25 @@ var VALUE_FIELDS = [
210
205
  UI_FOLLOWS_FIELD,
211
206
  ];
212
207
 
208
+ /**
209
+ * Axes that are still stored but no longer render: the interface's own size
210
+ * and line-height.
211
+ *
212
+ * DSH 0.1.5-rc.2 publishes no interface size or line-height hook at all (the
213
+ * measurement lives in `buildAxisCss`), so these two fields inject nothing.
214
+ * A document written by 0.1.x still carries them, so they keep their schema
215
+ * entries and stay part of normalization — but they are not value axes: a
216
+ * preset no longer snapshots a dead field, and a light/dark pair that differs
217
+ * only here no longer counts as two sets. The card's "reset every axis" clears
218
+ * them too, so a value nothing shows cannot hide in a document forever.
219
+ */
220
+ var RETIRED_FIELDS = [SIZE_FIELD, LINE_HEIGHT_FIELD];
221
+
222
+ /**
223
+ * Every durable axis field, live and retired: what normalization covers.
224
+ */
225
+ var DURABLE_FIELDS = VALUE_FIELDS.concat(RETIRED_FIELDS);
226
+
213
227
  /**
214
228
  * Longest accepted font stack, in characters (mirrored by the host schema).
215
229
  */
@@ -372,8 +386,8 @@ function normalizeField(field, value) {
372
386
  function normalizeConfig(value) {
373
387
  var source = value !== null && typeof value === "object" ? value : {};
374
388
  var config = {};
375
- for (var index = 0; index < VALUE_FIELDS.length; index += 1) {
376
- var field = VALUE_FIELDS[index];
389
+ for (var index = 0; index < DURABLE_FIELDS.length; index += 1) {
390
+ var field = DURABLE_FIELDS[index];
377
391
  config[field] = normalizeField(field, source[field]);
378
392
  }
379
393
  config[PER_THEME_FIELD] = toBool(source[PER_THEME_FIELD]);
@@ -608,7 +622,7 @@ function isGenericFamilyName(name) {
608
622
  function isCJKFamilyName(name) {
609
623
  var lower = name.toLowerCase();
610
624
  if (
611
- /(?: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(
625
+ /(?:yahei|jhenghei|pingfang|hiragino|simsun|simhei|nsimsun|kaiti|fangsong|meiryo|yu ?goth|yu ?minch|ms ?p?(?:gothic|mincho)|noto (?:sans|serif) (?:sc|tc|cjk|jp|kr|hk)|source han|sourcehansc|sourcehanserifc|sarasa|misans|harmonyos|wenquanyi|lxgw|unifont|dengxian)/.test(
612
626
  lower
613
627
  )
614
628
  ) {
@@ -802,6 +816,11 @@ function scaleLineTokens(names, baseTokens, factor) {
802
816
  */
803
817
  function shiftTokens(names, baseTokens, offset) {
804
818
  var out = [];
819
+ // Zero is the neutral value of every additive axis: emitting `calc((14px) + 0px)`
820
+ // would be a real declaration that disagrees with `isDormant` and with an
821
+ // otherwise identical configuration, so the guard lives here too, not only at
822
+ // the call sites.
823
+ if (offset === 0) return out;
805
824
  for (var index = 0; index < names.length; index += 1) {
806
825
  var name = names[index];
807
826
  var base = baseTokens[name];
@@ -1018,17 +1037,6 @@ function isShorthandName(name) {
1018
1037
  return true;
1019
1038
  }
1020
1039
 
1021
- /**
1022
- * Whether one token (name + value) is a `font` shorthand this plugin may
1023
- * rewrite: a bare `--dsw-font-…` name whose value parses as a shorthand.
1024
- * @param {string} name - custom property name.
1025
- * @param {string} value - the token's untouched value.
1026
- * @returns {boolean}
1027
- */
1028
- function isShorthandToken(name, value) {
1029
- return isShorthandName(name) && parseShorthand(value) !== null;
1030
- }
1031
-
1032
1040
  /**
1033
1041
  * Whether a token is one this plugin reads and re-declares at all.
1034
1042
  * @param {string} name - custom property name including the leading dashes.
@@ -1057,35 +1065,6 @@ function tokenNames(baseTokens) {
1057
1065
  return names;
1058
1066
  }
1059
1067
 
1060
- /**
1061
- * The interface PART size tokens: everything scalable except the code and
1062
- * markdown families and the delta tokens (which the dialog offset shifts
1063
- * additively, never multiplicatively).
1064
- * @param {Record<string, string>} [baseTokens] - token name to value.
1065
- * @returns {string[]} the names the interface offset re-declares.
1066
- */
1067
- function bodyTokenNames(baseTokens) {
1068
- return tokenNames(baseTokens).filter(function (name) {
1069
- return (
1070
- !isCodeToken(name) &&
1071
- !isMarkdownToken(name) &&
1072
- !isDeltaToken(name) &&
1073
- !isContentToken(name)
1074
- );
1075
- });
1076
- }
1077
-
1078
- /**
1079
- * The interface line-height PART tokens (non-code, non-markdown).
1080
- * @param {Record<string, string>} [baseTokens] - token name to value.
1081
- * @returns {string[]}
1082
- */
1083
- function uiLineTokenNames(baseTokens) {
1084
- return bodyTokenNames(baseTokens).filter(function (name) {
1085
- return name.indexOf("-line-height") >= 0;
1086
- });
1087
- }
1088
-
1089
1068
  /**
1090
1069
  * The markdown (dialog) PART tokens: sizes and line heights of the markdown
1091
1070
  * family, excluding code.
@@ -1096,30 +1075,6 @@ function markdownTokenNames(baseTokens) {
1096
1075
  return tokenNames(baseTokens).filter(isMarkdownToken);
1097
1076
  }
1098
1077
 
1099
- /**
1100
- * The markdown content-size DELTA tokens present in a base map.
1101
- * @param {Record<string, string>} [baseTokens] - token name to value.
1102
- * @returns {string[]}
1103
- */
1104
- function deltaTokenNames(baseTokens) {
1105
- if (baseTokens === null || typeof baseTokens !== "object") return [];
1106
- return Object.keys(baseTokens).filter(isDeltaToken);
1107
- }
1108
-
1109
- /**
1110
- * The code PART tokens: the `font` shorthands' size/height parts, when the
1111
- * live stylesheets declare them. The shorthands themselves are handled by
1112
- * `codeShorthandTokens`, so they are excluded here to avoid double
1113
- * declarations.
1114
- * @param {Record<string, string>} [baseTokens] - token name to value.
1115
- * @returns {string[]}
1116
- */
1117
- function codeTokenNames(baseTokens) {
1118
- return tokenNames(baseTokens).filter(function (name) {
1119
- return isCodeToken(name) && !isShorthandName(name);
1120
- });
1121
- }
1122
-
1123
1078
  /**
1124
1079
  * Every `font` shorthand token in a base map that parses, sorted by name
1125
1080
  * length (families before variants, mirroring the part-token order).
@@ -1152,17 +1107,6 @@ function markdownShorthandTokens(baseTokens) {
1152
1107
  });
1153
1108
  }
1154
1109
 
1155
- /**
1156
- * The interface (non-markdown, non-code) `font` shorthand tokens.
1157
- * @param {Record<string, string>} [baseTokens] - token name to value.
1158
- * @returns {{name: string, parts: object}[]}
1159
- */
1160
- function uiShorthandTokens(baseTokens) {
1161
- return shorthandTokens(baseTokens).filter(function (entry) {
1162
- return !isMarkdownToken(entry.name) && !isCodeToken(entry.name);
1163
- });
1164
- }
1165
-
1166
1110
  /**
1167
1111
  * The code `font` shorthand tokens.
1168
1112
  * @param {Record<string, string>} [baseTokens] - token name to value.
@@ -1178,6 +1122,66 @@ function codeShorthandTokens(baseTokens) {
1178
1122
  * selectors
1179
1123
  * ------------------------------------------------------------------ */
1180
1124
 
1125
+ /**
1126
+ * Build one exclusion that keeps a surface out of a rule.
1127
+ *
1128
+ * The whole list goes into a single `:not(…)`. The long form —
1129
+ * `:not(pre):not(pre *)…` — repeats `:not(` and `)` per argument, and that
1130
+ * chain is copied into both dialog alternatives and the interface rule, so the
1131
+ * compact form saves roughly 120 bytes per copy.
1132
+ *
1133
+ * The trade is specificity: `:not(a):not(b)` SUMS its arguments while
1134
+ * `:not(a, b)` takes the most specific one. Nothing here depends on that sum.
1135
+ * Every rule below carries `!important`, which already beats the theme's
1136
+ * non-important class rules whatever the specificity; the shipped DSH 0.1.5
1137
+ * assets hold exactly one `!important` typography declaration
1138
+ * (`font-size` on inline markdown code, and no size declaration is ever
1139
+ * emitted from here); and the rules that could collide with each other are
1140
+ * held apart by these very exclusions rather than by their weight. Verified
1141
+ * against the shipped bundle and every installed plugin: no other
1142
+ * `!important` `font-weight`/`font-family` rule exists to lose to.
1143
+ *
1144
+ * @param {string[]} selectors - the selectors to exclude.
1145
+ * @returns {string} one `:not(…)` carrying the whole list.
1146
+ */
1147
+ function excludeAll(selectors) {
1148
+ return ":not(" + selectors.join(",") + ")";
1149
+ }
1150
+
1151
+ /**
1152
+ * A code surface, and its descendants: a highlight `<span>` inside a `<pre>`
1153
+ * is not a `pre`, so an exclusion has to reach through ancestry.
1154
+ */
1155
+ var CODE_SURFACE_SELECTORS = [
1156
+ "pre",
1157
+ "pre *",
1158
+ "code",
1159
+ "code *",
1160
+ "kbd",
1161
+ "kbd *",
1162
+ "samp",
1163
+ "samp *",
1164
+ "var",
1165
+ "var *",
1166
+ "tt",
1167
+ "tt *",
1168
+ "textarea",
1169
+ "textarea *",
1170
+ '[class*="code" i]',
1171
+ '[class*="code" i] *',
1172
+ '[class*="terminal" i]',
1173
+ '[class*="terminal" i] *',
1174
+ ];
1175
+
1176
+ /**
1177
+ * Everything a code surface must be excluded from a DIALOG-scoped rule: the
1178
+ * code elements themselves AND their descendants (a highlight `<span>` inside
1179
+ * a `<pre>` is not a `pre`, so the exclusion has to reach through ancestry
1180
+ * with a complex `:not()` argument — supported by the Chromium line DSH
1181
+ * targets).
1182
+ */
1183
+ var CODE_EXCLUDES = excludeAll(CODE_SURFACE_SELECTORS);
1184
+
1181
1185
  /**
1182
1186
  * What counts as code text for the code axes.
1183
1187
  *
@@ -1216,38 +1220,6 @@ var CODE_SELECTOR = [
1216
1220
  })
1217
1221
  .join(",");
1218
1222
 
1219
- /**
1220
- * Everything a code surface must be excluded from a DIALOG-scoped rule:
1221
- * the code elements themselves AND their descendants (a highlight `<span>`
1222
- * inside a `<pre>` is not a `pre`, so the exclusion has to reach through
1223
- * ancestry with complex `:not()` arguments — supported by the Chromium line
1224
- * DSH targets).
1225
- */
1226
- var CODE_EXCLUDES = [
1227
- "pre",
1228
- "pre *",
1229
- "code",
1230
- "code *",
1231
- "kbd",
1232
- "kbd *",
1233
- "samp",
1234
- "samp *",
1235
- "var",
1236
- "var *",
1237
- "tt",
1238
- "tt *",
1239
- "textarea",
1240
- "textarea *",
1241
- '[class*="code" i]',
1242
- '[class*="code" i] *',
1243
- '[class*="terminal" i]',
1244
- '[class*="terminal" i] *',
1245
- ]
1246
- .map(function (selector) {
1247
- return ":not(" + selector + ")";
1248
- })
1249
- .join("");
1250
-
1251
1223
  /**
1252
1224
  * The dialog surface: DSH renders the conversation markdown in a CSS-module
1253
1225
  * container whose class hashes per release (`_markdown_<hash>_…`). A
@@ -1279,11 +1251,57 @@ var DARK_ATTR = "body[data-ds-dark-theme]";
1279
1251
  * interface's own preview stays inside the rule, and `dfp-previewCode` is a
1280
1252
  * code surface like any other.
1281
1253
  */
1282
- var INTERFACE_EXCLUDES =
1283
- CODE_EXCLUDES +
1284
- ":not(.cm-editor):not(.cm-editor *):not(.dfp-previewCode):not(.dfp-previewCode *)" +
1285
- ':not([class*="_markdown_" i]):not([class*="_markdown_" i] *)' +
1286
- ":not(.dfp-previewDialog):not(.dfp-previewDialog *)";
1254
+ var INTERFACE_EXCLUDES = excludeAll(
1255
+ CODE_SURFACE_SELECTORS.concat([
1256
+ ".cm-editor",
1257
+ ".cm-editor *",
1258
+ ".dfp-previewCode",
1259
+ ".dfp-previewCode *",
1260
+ '[class*="_markdown_" i]',
1261
+ '[class*="_markdown_" i] *',
1262
+ ".dfp-previewDialog",
1263
+ ".dfp-previewDialog *",
1264
+ ])
1265
+ );
1266
+
1267
+ /**
1268
+ * Split a selector list on its top-level commas only.
1269
+ *
1270
+ * The exclusions hold a selector LIST inside `:not(…)`, so a plain
1271
+ * `split(",")` would cut a selector in half — and the per-theme rules prefix
1272
+ * every selector in the list.
1273
+ * @param {string} selectorList - a comma-separated selector list.
1274
+ * @returns {string[]} the top-level parts, verbatim.
1275
+ */
1276
+ function splitSelectorList(selectorList) {
1277
+ var parts = [];
1278
+ var current = "";
1279
+ var depth = 0;
1280
+ var quote = "";
1281
+ for (var index = 0; index < selectorList.length; index += 1) {
1282
+ var character = selectorList[index];
1283
+ if (quote !== "") {
1284
+ current += character;
1285
+ if (character === quote) quote = "";
1286
+ continue;
1287
+ }
1288
+ if (character === '"' || character === "'") {
1289
+ quote = character;
1290
+ current += character;
1291
+ continue;
1292
+ }
1293
+ if (character === "(" || character === "[") depth += 1;
1294
+ if (character === ")" || character === "]") depth -= 1;
1295
+ if (character === "," && depth === 0) {
1296
+ parts.push(current);
1297
+ current = "";
1298
+ continue;
1299
+ }
1300
+ current += character;
1301
+ }
1302
+ parts.push(current);
1303
+ return parts;
1304
+ }
1287
1305
 
1288
1306
  /**
1289
1307
  * Prefix every selector in a comma list, for the per-theme rules.
@@ -1292,7 +1310,7 @@ var INTERFACE_EXCLUDES =
1292
1310
  * @returns {string} the prefixed list.
1293
1311
  */
1294
1312
  function prefixSelector(selectorList, prefix) {
1295
- var parts = selectorList.split(",");
1313
+ var parts = splitSelectorList(selectorList);
1296
1314
  var out = [];
1297
1315
  for (var index = 0; index < parts.length; index += 1) {
1298
1316
  out.push(prefix + " " + parts[index]);
@@ -1454,9 +1472,13 @@ function buildAxisCss(axis, baseTokens, isDark) {
1454
1472
  */
1455
1473
  function pushScopedCodeFamily() {
1456
1474
  var scopedCode = [];
1457
- var surfaces = CODE_SELECTOR.split(",");
1475
+ var surfaces = splitSelectorList(CODE_SELECTOR);
1458
1476
  for (var cIndex = 0; cIndex < surfaces.length; cIndex += 1) {
1459
- scopedCode.push('[class*="_markdown_" i] ' + surfaces[cIndex]);
1477
+ var scoped = '[class*="_markdown_" i] ' + surfaces[cIndex];
1478
+ // The dark variant has to carry the theme attribute like every other
1479
+ // rule here, or a per-theme set would re-assert the LIGHT code family
1480
+ // inside the dark dialog.
1481
+ scopedCode.push(isDark ? DARK_ATTR + " " + scoped : scoped);
1460
1482
  }
1461
1483
  declarations.push(scopedCode.join(",") + "{font-family:" + mono + " !important}");
1462
1484
  }
@@ -1553,8 +1575,8 @@ function buildAxisCss(axis, baseTokens, isDark) {
1553
1575
  // (secondary size, both deltas, delta-based heading sizes, heights) at their
1554
1576
  // body-computed values. The offset is therefore added once, at the source —
1555
1577
  // `--dsh-content-font-size` becomes "official base + offset" — and the whole
1556
- // ladder follows by derivation, with nothing counted twice. The interface
1557
- // axis never touches this chain (see `bodyTokenNames`), so the conversation
1578
+ // ladder follows by derivation, with nothing counted twice. The retired
1579
+ // interface size axis never touches this chain at all, so the conversation
1558
1580
  // moves only when this axis says so, and a dialog that FOLLOWS the interface
1559
1581
  // follows it through this value.
1560
1582
  var dialogOffsetValue = axis[SIZE_DIALOG_FIELD];
@@ -1966,18 +1988,137 @@ var PRESETS = {
1966
1988
  };
1967
1989
 
1968
1990
  /**
1969
- * Legacy single-purpose helper kept for compatibility: scale the size and the
1970
- * height inside one simple `size/height <family>` shorthand.
1971
- * @param {string} value - a value shaped like `11px/19px var(--ds-font-family-code)`.
1972
- * @param {number} scale - the ratio to apply.
1973
- * @returns {string|null} the scaled shorthand, or null when the shape differs.
1991
+ * The curated groups, in the order the picker lists them.
1974
1992
  */
1975
- function scaleFontShorthand(value, scale) {
1976
- var parts = parseShorthand(value);
1977
- if (parts === null || parts.lead !== "") return null;
1978
- return (
1979
- "calc((" + parts.size + ") * " + scale + ")/calc((" + parts.height + ") * " + scale + ") " + parts.family
1980
- );
1993
+ var PRESET_GROUPS = [
1994
+ { label: "stack.groupMono", families: PRESETS.mono },
1995
+ { label: "stack.groupCjk", families: PRESETS.cjk },
1996
+ { label: "stack.groupLatin", families: PRESETS.latin },
1997
+ { label: "stack.groupGeneric", families: PRESETS.generic },
1998
+ ];
1999
+
2000
+ /**
2001
+ * How many enumerated families the "installed on this machine" group shows.
2002
+ * The curated groups are the short list; this one is the complete one, so the
2003
+ * cap is only a guard against a machine with an absurd number of families.
2004
+ */
2005
+ var MAX_LOCAL_FONTS = 240;
2006
+
2007
+ /**
2008
+ * Build the family picker's groups.
2009
+ *
2010
+ * The curated groups are ALWAYS listed: whether or not the browser enumerated
2011
+ * local fonts, and whether or not a search is running. The old shape skipped
2012
+ * them whenever a working catalog met a non-empty query, so a curated family
2013
+ * this machine has not installed could not be found by name at all. Every
2014
+ * family is listed in exactly one group — the local enumeration gives way to
2015
+ * the curated headings rather than repeating what they already offer.
2016
+ *
2017
+ * @param {object} input - the picker's state.
2018
+ * @param {string[]} [input.stack] - the families the stored value names.
2019
+ * @param {boolean} [input.single] - single-pick mode (a West/CJK slot).
2020
+ * @param {string} [input.query] - the search text.
2021
+ * @param {{status: string, families: string[]}} [input.catalog] - enumeration.
2022
+ * @param {number} [input.maxLocal] - cap on the local group's rows.
2023
+ * @returns {Array<{label: string, families: string[]}>} the groups, in the
2024
+ * order the picker displays them.
2025
+ */
2026
+ function pickerGroups(input) {
2027
+ var stack = Array.isArray(input.stack) ? input.stack : [];
2028
+ var single = input.single === true;
2029
+ var query = typeof input.query === "string" ? input.query : "";
2030
+ var catalog =
2031
+ input.catalog && typeof input.catalog === "object"
2032
+ ? input.catalog
2033
+ : { status: "loading", families: [] };
2034
+ var maxLocal = typeof input.maxLocal === "number" ? input.maxLocal : MAX_LOCAL_FONTS;
2035
+ var needle = query.trim().toLowerCase();
2036
+ var index;
2037
+ var matches = function (name) {
2038
+ return needle === "" || String(name).toLowerCase().indexOf(needle) >= 0;
2039
+ };
2040
+
2041
+ // Which families the stack already names. In single mode the stack IS the
2042
+ // one pick, and it stays visible in the curated groups (with its check mark)
2043
+ // so the reader can see where it sits among its own kind.
2044
+ var inStack = {};
2045
+ if (!single) {
2046
+ for (index = 0; index < stack.length; index += 1) {
2047
+ inStack[String(stack[index]).toLowerCase()] = true;
2048
+ }
2049
+ }
2050
+
2051
+ // The curated groups are built FIRST, and the local list gives way to them:
2052
+ // they are the short list the reader scans, and on a real Windows machine
2053
+ // most of the Latin group is installed, so letting the local list win would
2054
+ // empty the curated headings. Everything lands in exactly one group.
2055
+ var curatedLower = {};
2056
+ var curatedGroups = [];
2057
+ for (index = 0; index < PRESET_GROUPS.length; index += 1) {
2058
+ var curated = PRESET_GROUPS[index];
2059
+ var rows = [];
2060
+ for (var nameIndex = 0; nameIndex < curated.families.length; nameIndex += 1) {
2061
+ var name = sanitizeFamily(curated.families[nameIndex]);
2062
+ if (name === "" || !matches(name)) continue;
2063
+ var key = name.toLowerCase();
2064
+ if (inStack[key] === true || curatedLower[key] === true) continue;
2065
+ curatedLower[key] = true;
2066
+ rows.push(name);
2067
+ }
2068
+ if (rows.length > 0) curatedGroups.push({ label: curated.label, families: rows });
2069
+ }
2070
+
2071
+ // Then the enumeration: the complete list, minus everything already offered
2072
+ // by a heading above it.
2073
+ var localRows = [];
2074
+ if (catalog.status === "ready") {
2075
+ var locals = Array.isArray(catalog.families) ? catalog.families : [];
2076
+ for (index = 0; index < locals.length; index += 1) {
2077
+ var localName = sanitizeFamily(locals[index]);
2078
+ if (localName === "" || !matches(localName)) continue;
2079
+ var localKey = localName.toLowerCase();
2080
+ if (inStack[localKey] === true || curatedLower[localKey] === true) continue;
2081
+ curatedLower[localKey] = true;
2082
+ localRows.push(localName);
2083
+ if (localRows.length >= maxLocal) break;
2084
+ }
2085
+ }
2086
+
2087
+ var groups = [];
2088
+ var selectedRows = [];
2089
+ for (index = 0; index < stack.length; index += 1) {
2090
+ if (matches(stack[index])) selectedRows.push(stack[index]);
2091
+ }
2092
+ if (selectedRows.length > 0) {
2093
+ groups.push({ label: "stack.groupSelected", families: selectedRows });
2094
+ }
2095
+ for (index = 0; index < curatedGroups.length; index += 1) {
2096
+ groups.push(curatedGroups[index]);
2097
+ }
2098
+ if (localRows.length > 0) {
2099
+ groups.push({ label: "stack.groupLocal", families: localRows });
2100
+ }
2101
+ return groups;
2102
+ }
2103
+
2104
+ /**
2105
+ * Whether a cached font-enumeration verdict may be asked for again.
2106
+ *
2107
+ * A working enumeration is final for the session, and a browser that has no
2108
+ * such API at all will never grow one — but a refusal is not: the user can
2109
+ * grant the font permission from the browser's prompt or its site settings.
2110
+ * Retrying is cheap, because a still-blocked permission is rejected instantly
2111
+ * instead of prompting again.
2112
+ *
2113
+ * @param {{status?: string, at?: number, permanent?: boolean}|null} cache - the cached verdict.
2114
+ * @param {number} now - the current time, in milliseconds.
2115
+ * @param {number} retryMs - how long a refusal is trusted.
2116
+ * @returns {boolean} true when the enumeration should run again.
2117
+ */
2118
+ function catalogRefreshDue(cache, now, retryMs) {
2119
+ if (cache === null || cache === undefined) return true;
2120
+ if (cache.permanent === true || cache.status === "ready") return false;
2121
+ return now - (cache.at || 0) >= retryMs;
1981
2122
  }
1982
2123
 
1983
2124
  var shared = {
@@ -2004,6 +2145,8 @@ var shared = {
2004
2145
  ACTIVE_PRESET_FIELD: ACTIVE_PRESET_FIELD,
2005
2146
  UI_FOLLOWS_FIELD: UI_FOLLOWS_FIELD,
2006
2147
  VALUE_FIELDS: VALUE_FIELDS,
2148
+ RETIRED_FIELDS: RETIRED_FIELDS,
2149
+ DURABLE_FIELDS: DURABLE_FIELDS,
2007
2150
  SIZE_MIN: SIZE_MIN,
2008
2151
  SIZE_MAX: SIZE_MAX,
2009
2152
  WEIGHT_MIN: WEIGHT_MIN,
@@ -2016,7 +2159,6 @@ var shared = {
2016
2159
  LIGATURES_DEFAULT: LIGATURES_DEFAULT,
2017
2160
  LIGATURES_ON: LIGATURES_ON,
2018
2161
  LIGATURES_OFF: LIGATURES_OFF,
2019
- MARKER: MARKER,
2020
2162
  STYLE_TAG: STYLE_TAG,
2021
2163
  CARD_STYLE_TAG: CARD_STYLE_TAG,
2022
2164
  DEFAULTS: DEFAULTS,
@@ -2027,7 +2169,17 @@ var shared = {
2027
2169
  MAX_PRESET_NAME: MAX_PRESET_NAME,
2028
2170
  FALLBACK_TOKENS: FALLBACK_TOKENS,
2029
2171
  PRESETS: PRESETS,
2172
+ PRESET_GROUPS: PRESET_GROUPS,
2173
+ MAX_LOCAL_FONTS: MAX_LOCAL_FONTS,
2174
+ pickerGroups: pickerGroups,
2175
+ catalogRefreshDue: catalogRefreshDue,
2030
2176
  MARKDOWN_SELECTOR: MARKDOWN_SELECTOR,
2177
+ CODE_SURFACE_SELECTORS: CODE_SURFACE_SELECTORS,
2178
+ CODE_EXCLUDES: CODE_EXCLUDES,
2179
+ INTERFACE_EXCLUDES: INTERFACE_EXCLUDES,
2180
+ excludeAll: excludeAll,
2181
+ splitSelectorList: splitSelectorList,
2182
+ prefixSelector: prefixSelector,
2031
2183
  DARK_ATTR: DARK_ATTR,
2032
2184
  sanitize: sanitize,
2033
2185
  sanitizeFamily: sanitizeFamily,
@@ -2058,19 +2210,12 @@ var shared = {
2058
2210
  isMarkdownToken: isMarkdownToken,
2059
2211
  isDeltaToken: isDeltaToken,
2060
2212
  isShorthandName: isShorthandName,
2061
- isShorthandToken: isShorthandToken,
2062
2213
  isScaledToken: isScaledToken,
2063
2214
  tokenNames: tokenNames,
2064
- codeTokenNames: codeTokenNames,
2065
2215
  markdownTokenNames: markdownTokenNames,
2066
- bodyTokenNames: bodyTokenNames,
2067
- uiLineTokenNames: uiLineTokenNames,
2068
- deltaTokenNames: deltaTokenNames,
2069
2216
  shorthandTokens: shorthandTokens,
2070
2217
  markdownShorthandTokens: markdownShorthandTokens,
2071
- uiShorthandTokens: uiShorthandTokens,
2072
2218
  codeShorthandTokens: codeShorthandTokens,
2073
- scaleFontShorthand: scaleFontShorthand,
2074
2219
  isGenericFamilyName: isGenericFamilyName,
2075
2220
  isCJKFamilyName: isCJKFamilyName,
2076
2221
  setWestEntry: setWestEntry,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-fonttune",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Font plugin for the DeepSeek Harness Web GUI: the conversation gets its own font, size, line height and weight, the interface font and weight follow it, code keeps its own axis with ligature control, and whole setups save as presets.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -43,11 +43,16 @@
43
43
  "optional": true
44
44
  }
45
45
  },
46
+ "devDependencies": {
47
+ "@deepseek-ai/schemastery": "^3.18.0"
48
+ },
46
49
  "scripts": {
47
50
  "build": "node build.mjs",
48
51
  "watch": "node build.mjs --watch",
49
52
  "test": "node test/run.mjs && node test/render-card.mjs",
50
- "prepublishOnly": "node build.mjs && node test/run.mjs && node test/render-card.mjs"
53
+ "verify": "node test/run.mjs && node test/render-card.mjs && node test/artifacts.mjs",
54
+ "verify:browser": "node test/browser-verify.mjs",
55
+ "prepublishOnly": "node build.mjs && node test/run.mjs && node test/render-card.mjs && node test/artifacts.mjs"
51
56
  },
52
57
  "license": "MIT",
53
58
  "keywords": [