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/CHANGELOG.en.md +38 -0
- package/CHANGELOG.md +38 -0
- package/README.en.md +10 -6
- package/README.md +10 -6
- package/lib/client.js +506 -303
- package/lib/shared.cjs +291 -146
- package/package.json +7 -2
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
|
|
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 <
|
|
376
|
-
var field =
|
|
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|
|
|
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
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
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
|
|
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
|
|
1475
|
+
var surfaces = splitSelectorList(CODE_SELECTOR);
|
|
1458
1476
|
for (var cIndex = 0; cIndex < surfaces.length; cIndex += 1) {
|
|
1459
|
-
|
|
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
|
|
1557
|
-
// axis never touches this chain
|
|
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
|
-
*
|
|
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
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
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.
|
|
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
|
-
"
|
|
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": [
|