dsh-fonttune 0.4.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.md +24 -0
- package/CHANGELOG.md +24 -0
- package/README.en.md +14 -3
- package/README.md +14 -3
- package/docs/card-shaping.jpg +0 -0
- package/docs/panel.jpg +0 -0
- package/lib/client.js +1299 -43
- package/lib/index.js +38 -0
- package/lib/shared.cjs +445 -10
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +4 -2
package/lib/index.js
CHANGED
|
@@ -247,6 +247,44 @@ export const Config = volatileWhenSupported(z.object({
|
|
|
247
247
|
.description(
|
|
248
248
|
"Code line-height offset in px (0 keeps DSH's own line heights)"
|
|
249
249
|
),
|
|
250
|
+
[shared.LETTER_SPACING_DIALOG_FIELD]: z
|
|
251
|
+
.number()
|
|
252
|
+
.min(shared.LETTER_SPACING_MIN)
|
|
253
|
+
.max(shared.LETTER_SPACING_MAX)
|
|
254
|
+
.default(0)
|
|
255
|
+
.description(
|
|
256
|
+
"Conversation letter-spacing offset in px (0 keeps DSH's own tracking)"
|
|
257
|
+
),
|
|
258
|
+
[shared.WORD_SPACING_DIALOG_FIELD]: z
|
|
259
|
+
.number()
|
|
260
|
+
.min(shared.WORD_SPACING_MIN)
|
|
261
|
+
.max(shared.WORD_SPACING_MAX)
|
|
262
|
+
.default(0)
|
|
263
|
+
.description(
|
|
264
|
+
"Conversation word-spacing offset in px (0 keeps DSH's own word gaps)"
|
|
265
|
+
),
|
|
266
|
+
[shared.PARAGRAPH_SPACING_DIALOG_FIELD]: z
|
|
267
|
+
.number()
|
|
268
|
+
.min(shared.PARAGRAPH_SPACING_MIN)
|
|
269
|
+
.max(shared.PARAGRAPH_SPACING_MAX)
|
|
270
|
+
.default(0)
|
|
271
|
+
.description(
|
|
272
|
+
"Conversation paragraph-gap offset in px, added to DSH's own paragraph spacing (0 keeps it)"
|
|
273
|
+
),
|
|
274
|
+
[shared.INDENT_DIALOG_FIELD]: z
|
|
275
|
+
.number()
|
|
276
|
+
.min(shared.INDENT_MIN)
|
|
277
|
+
.max(shared.INDENT_MAX)
|
|
278
|
+
.default(0)
|
|
279
|
+
.description(
|
|
280
|
+
"Conversation first-line indent in em (0 keeps DSH's own flush-left paragraphs)"
|
|
281
|
+
),
|
|
282
|
+
[shared.LINE_BREAK_STRICT_FIELD]: z
|
|
283
|
+
.boolean()
|
|
284
|
+
.default(false)
|
|
285
|
+
.description(
|
|
286
|
+
"Strict CJK line breaking in the conversation: keeps punctuation and small kana off the start of a line"
|
|
287
|
+
),
|
|
250
288
|
[LIGATURES_FIELD]: z
|
|
251
289
|
.number()
|
|
252
290
|
.min(0)
|
package/lib/shared.cjs
CHANGED
|
@@ -309,6 +309,67 @@ var LINE_HEIGHT_MAX = 160;
|
|
|
309
309
|
var CODE_LINE_HEIGHT_MIN = -4;
|
|
310
310
|
var CODE_LINE_HEIGHT_MAX = 8;
|
|
311
311
|
|
|
312
|
+
/* ------------------------------------------------------------------ *
|
|
313
|
+
* conversation text shaping (measured against DSH's own markdown)
|
|
314
|
+
* ------------------------------------------------------------------ */
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Field carrying the letter-spacing offset in px (0 = DSH's own `normal`).
|
|
318
|
+
* DSH leaves tracking at `normal` on the markdown scope (measured 2026-10-13), so
|
|
319
|
+
* the offset can be written as an absolute value: 0 emits no rule at all.
|
|
320
|
+
*/
|
|
321
|
+
var LETTER_SPACING_DIALOG_FIELD = "letterSpacingDialog";
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Field carrying the word-spacing offset in px (0 = DSH's own `0px`).
|
|
325
|
+
* Word spacing is measured 0px on the same scope, so it is an absolute value too.
|
|
326
|
+
*/
|
|
327
|
+
var WORD_SPACING_DIALOG_FIELD = "wordSpacingDialog";
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Field carrying the paragraph-gap offset in px, added on top of DSH's own 16px.
|
|
331
|
+
* The paragraphs' margins are NOT zero on the shipped markdown (first 0/16, the
|
|
332
|
+
* rest 16/16, headings 32/16 — measured 2026-10-13), so this axis is an offset
|
|
333
|
+
* built on that baseline rather than an absolute value.
|
|
334
|
+
*/
|
|
335
|
+
var PARAGRAPH_SPACING_DIALOG_FIELD = "paragraphSpacingDialog";
|
|
336
|
+
|
|
337
|
+
/** Field carrying the first-line indent, in em (0 = DSH's own none). */
|
|
338
|
+
var INDENT_DIALOG_FIELD = "indentDialog";
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Field asking for strict CJK line breaking (`line-break:strict`) in the
|
|
342
|
+
* conversation: no small kana, no dash starting a line. DSH leaves `auto`.
|
|
343
|
+
*/
|
|
344
|
+
var LINE_BREAK_STRICT_FIELD = "lineBreakStrict";
|
|
345
|
+
|
|
346
|
+
/** Letter-spacing offset range, in px, and its notch (measured: 0.1px is visible). */
|
|
347
|
+
var LETTER_SPACING_MIN = -0.4;
|
|
348
|
+
var LETTER_SPACING_MAX = 2.0;
|
|
349
|
+
var LETTER_SPACING_STEP = 0.1;
|
|
350
|
+
|
|
351
|
+
/** Word-spacing offset range, in px, and its notch. */
|
|
352
|
+
var WORD_SPACING_MIN = -1.0;
|
|
353
|
+
var WORD_SPACING_MAX = 4.0;
|
|
354
|
+
var WORD_SPACING_STEP = 0.2;
|
|
355
|
+
|
|
356
|
+
/** Paragraph-gap offset range, in px, over DSH's own 16px. */
|
|
357
|
+
var PARAGRAPH_SPACING_MIN = -8;
|
|
358
|
+
var PARAGRAPH_SPACING_MAX = 24;
|
|
359
|
+
var PARAGRAPH_SPACING_STEP = 1;
|
|
360
|
+
|
|
361
|
+
/** First-line indent range, in em, and its notch. */
|
|
362
|
+
var INDENT_MIN = 0;
|
|
363
|
+
var INDENT_MAX = 4;
|
|
364
|
+
var INDENT_STEP = 0.25;
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The paragraph gap DSH's own markdown ships, in px. Measured on the conversation
|
|
368
|
+
* scope (2026-10-13): paragraphs carry 16px top/bottom margins, so that is the
|
|
369
|
+
* baseline every offset is added to.
|
|
370
|
+
*/
|
|
371
|
+
var PARAGRAPH_BASELINE_PX = 16;
|
|
372
|
+
|
|
312
373
|
/** Ligature modes. */
|
|
313
374
|
var LIGATURES_DEFAULT = 0;
|
|
314
375
|
var LIGATURES_ON = 1;
|
|
@@ -334,6 +395,11 @@ var DEFAULTS = {
|
|
|
334
395
|
lineHeight: LINE_HEIGHT_MIN,
|
|
335
396
|
lineHeightDialog: LINE_HEIGHT_MIN,
|
|
336
397
|
lineHeightCode: 0,
|
|
398
|
+
letterSpacingDialog: 0,
|
|
399
|
+
wordSpacingDialog: 0,
|
|
400
|
+
paragraphSpacingDialog: 0,
|
|
401
|
+
indentDialog: 0,
|
|
402
|
+
lineBreakStrict: false,
|
|
337
403
|
codeLigatures: LIGATURES_DEFAULT,
|
|
338
404
|
codeFeatures: "",
|
|
339
405
|
noSyntheticItalic: false,
|
|
@@ -365,6 +431,11 @@ var VALUE_FIELDS = [
|
|
|
365
431
|
CODE_WEIGHT_FIELD,
|
|
366
432
|
LINE_HEIGHT_DIALOG_FIELD,
|
|
367
433
|
CODE_LINE_HEIGHT_FIELD,
|
|
434
|
+
LETTER_SPACING_DIALOG_FIELD,
|
|
435
|
+
WORD_SPACING_DIALOG_FIELD,
|
|
436
|
+
PARAGRAPH_SPACING_DIALOG_FIELD,
|
|
437
|
+
INDENT_DIALOG_FIELD,
|
|
438
|
+
LINE_BREAK_STRICT_FIELD,
|
|
368
439
|
LIGATURES_FIELD,
|
|
369
440
|
FEATURES_FIELD,
|
|
370
441
|
NO_SYNTHETIC_ITALIC_FIELD,
|
|
@@ -1507,6 +1578,16 @@ function normalizeField(field, value, relative) {
|
|
|
1507
1578
|
return clampRatio(value);
|
|
1508
1579
|
case CODE_LINE_HEIGHT_FIELD:
|
|
1509
1580
|
return clampLineOffset(value);
|
|
1581
|
+
case LETTER_SPACING_DIALOG_FIELD:
|
|
1582
|
+
return clampSpacing(value, LETTER_SPACING_MIN, LETTER_SPACING_MAX);
|
|
1583
|
+
case WORD_SPACING_DIALOG_FIELD:
|
|
1584
|
+
return clampSpacing(value, WORD_SPACING_MIN, WORD_SPACING_MAX);
|
|
1585
|
+
case PARAGRAPH_SPACING_DIALOG_FIELD:
|
|
1586
|
+
return clampSpacing(value, PARAGRAPH_SPACING_MIN, PARAGRAPH_SPACING_MAX);
|
|
1587
|
+
case INDENT_DIALOG_FIELD:
|
|
1588
|
+
return clampIndent(value);
|
|
1589
|
+
case LINE_BREAK_STRICT_FIELD:
|
|
1590
|
+
return toBool(value);
|
|
1510
1591
|
case LIGATURES_FIELD:
|
|
1511
1592
|
return clampLigatures(value);
|
|
1512
1593
|
case WEIGHT_OFFSETS_FIELD:
|
|
@@ -1590,6 +1671,52 @@ function clampWeight(value) {
|
|
|
1590
1671
|
return Math.min(WEIGHT_MAX, Math.max(WEIGHT_MIN, weight));
|
|
1591
1672
|
}
|
|
1592
1673
|
|
|
1674
|
+
/**
|
|
1675
|
+
* Clamp one spacing offset (letter, word) to its allowed range, in px.
|
|
1676
|
+
*
|
|
1677
|
+
* These axes sit on top of a DSH default that is already zero (`normal` / `0px`,
|
|
1678
|
+
* measured on the conversation scope), so no migration and no baseline is involved:
|
|
1679
|
+
* the offset is written as it stands, and 0 means "emit nothing".
|
|
1680
|
+
* @param {unknown} value - candidate offset.
|
|
1681
|
+
* @param {number} min - the lowest allowed offset.
|
|
1682
|
+
* @param {number} max - the highest allowed offset.
|
|
1683
|
+
* @returns {number} the offset the stylesheet may use.
|
|
1684
|
+
*/
|
|
1685
|
+
function clampSpacing(value, min, max) {
|
|
1686
|
+
var offset = Number(value);
|
|
1687
|
+
if (!isFinite(offset)) return 0;
|
|
1688
|
+
// Rounded to two decimals: the notches are tenths, and the sum of a few of them
|
|
1689
|
+
// must not turn into 0.30000000000000004 in the emitted CSS.
|
|
1690
|
+
offset = Math.round(offset * 100) / 100;
|
|
1691
|
+
if (offset > max) return max;
|
|
1692
|
+
if (offset < min) return min;
|
|
1693
|
+
return offset;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
/**
|
|
1697
|
+
* Clamp the first-line indent to its allowed range, in em.
|
|
1698
|
+
* @param {unknown} value - candidate indent.
|
|
1699
|
+
* @returns {number} the indent the stylesheet may use.
|
|
1700
|
+
*/
|
|
1701
|
+
function clampIndent(value) {
|
|
1702
|
+
var indent = Number(value);
|
|
1703
|
+
if (!isFinite(indent)) return 0;
|
|
1704
|
+
indent = Math.round(indent * 100) / 100;
|
|
1705
|
+
if (indent > INDENT_MAX) return INDENT_MAX;
|
|
1706
|
+
if (indent < INDENT_MIN) return INDENT_MIN;
|
|
1707
|
+
return indent;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
/**
|
|
1711
|
+
* Format a spacing value for CSS, without scientific notation or float noise.
|
|
1712
|
+
* @param {number} value - a clamped px value.
|
|
1713
|
+
* @returns {string} the value as a px length.
|
|
1714
|
+
*/
|
|
1715
|
+
function pxText(value) {
|
|
1716
|
+
var rounded = Math.round(value * 100) / 100;
|
|
1717
|
+
return String(rounded) + "px";
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1593
1720
|
/**
|
|
1594
1721
|
* Clamp one RELATIVE weight to its allowed range, migrating an absolute value.
|
|
1595
1722
|
*
|
|
@@ -2329,6 +2456,27 @@ function rebuildShorthand(parts) {
|
|
|
2329
2456
|
* token classification
|
|
2330
2457
|
* ------------------------------------------------------------------ */
|
|
2331
2458
|
|
|
2459
|
+
/**
|
|
2460
|
+
* Whether a CSS selector declares something for the WHOLE page.
|
|
2461
|
+
*
|
|
2462
|
+
* The typography tokens live on `:root`, `html` or `body`. A selector scoped to a class
|
|
2463
|
+
* or an attribute declares a LOCAL value: another plugin's module stylesheet may reuse a
|
|
2464
|
+
* token name for its own compact panel (measured: `dsh-better-sidebar` sets
|
|
2465
|
+
* `--dsw-font-markdown-h1` to `600 13px/18px` on `.…_taskMarkdown`), and reading that as
|
|
2466
|
+
* the base hands the conversation a size that panel picked for itself.
|
|
2467
|
+
* @param {string} selector - a rule's selector text.
|
|
2468
|
+
* @returns {boolean} true when every selector in the list is a root scope.
|
|
2469
|
+
*/
|
|
2470
|
+
function isRootScopeSelector(selector) {
|
|
2471
|
+
if (typeof selector !== "string" || selector.trim() === "") return false;
|
|
2472
|
+
var parts = selector.split(",");
|
|
2473
|
+
for (var index = 0; index < parts.length; index += 1) {
|
|
2474
|
+
var part = parts[index].trim().toLowerCase();
|
|
2475
|
+
if (part !== ":root" && part !== "html" && part !== "body" && part !== "*") return false;
|
|
2476
|
+
}
|
|
2477
|
+
return true;
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2332
2480
|
/**
|
|
2333
2481
|
* Match DSH's typography PART tokens: the design system's sizes plus the
|
|
2334
2482
|
* content size the theme plugin writes on `body` (whose secondary variant
|
|
@@ -3169,6 +3317,180 @@ function weightStepRange(profile, value) {
|
|
|
3169
3317
|
return { min: min, max: max, unit: unit, value: count };
|
|
3170
3318
|
}
|
|
3171
3319
|
|
|
3320
|
+
/* ------------------------------------------------------------------ *
|
|
3321
|
+
* continuous weight (FT-060)
|
|
3322
|
+
* ------------------------------------------------------------------ */
|
|
3323
|
+
|
|
3324
|
+
/** Prefix of the alias family a continuous family is drawn through. */
|
|
3325
|
+
var VARIABLE_ALIAS_PREFIX = "dfp-vf-";
|
|
3326
|
+
|
|
3327
|
+
/**
|
|
3328
|
+
* The weights the continuity probe lays out. Dense on purpose: a variable font
|
|
3329
|
+
* that moves in small steps shows a width change at every one of them, while a
|
|
3330
|
+
* family that snaps to named instances repeats its width across each stretch.
|
|
3331
|
+
*/
|
|
3332
|
+
var CONTINUITY_WEIGHTS = [400, 420, 440, 460, 480, 500, 550, 600, 650, 700, 750, 800, 900];
|
|
3333
|
+
|
|
3334
|
+
/**
|
|
3335
|
+
* The share of adjacent probe pairs that must differ before a family's laid-out
|
|
3336
|
+
* WIDTH counts as continuous. Measured: a variable family drawn through its alias
|
|
3337
|
+
* changes width at every sampled weight (share 1.0), while a family snapping to
|
|
3338
|
+
* named instances repeats its width across each stretch (share 0.25–0.42).
|
|
3339
|
+
*/
|
|
3340
|
+
var CONTINUOUS_MIN_SHARE = 0.9;
|
|
3341
|
+
|
|
3342
|
+
/**
|
|
3343
|
+
* The notches offered for a continuous family, finest first.
|
|
3344
|
+
*
|
|
3345
|
+
* The shape is still measured (the alias is drawn on the same weight grid as any
|
|
3346
|
+
* other family), but the candidates are fine: a continuous axis can answer at every
|
|
3347
|
+
* one of them, which is the whole point of the mode. `weightProfileFrom` walks this
|
|
3348
|
+
* list from the END, so the finest candidate that still yields enough visible
|
|
3349
|
+
* positions wins.
|
|
3350
|
+
*/
|
|
3351
|
+
var CONTINUOUS_CANDIDATES = [10, 20, 25];
|
|
3352
|
+
|
|
3353
|
+
/**
|
|
3354
|
+
* The nominal continuous slider, used when the measured shape cannot be taken
|
|
3355
|
+
* (no canvas, a refused probe). One notch is 20 weight units; the range is the
|
|
3356
|
+
* widest offset the ceiling allows either way.
|
|
3357
|
+
*/
|
|
3358
|
+
var CONTINUOUS_STEP = 20;
|
|
3359
|
+
var CONTINUOUS_RANGE = 200;
|
|
3360
|
+
|
|
3361
|
+
/**
|
|
3362
|
+
* How many different values a list of measurements holds.
|
|
3363
|
+
* @param {number[]} values - the measurements.
|
|
3364
|
+
* @returns {number} the count of distinct values.
|
|
3365
|
+
*/
|
|
3366
|
+
function distinctCount(values) {
|
|
3367
|
+
if (!Array.isArray(values)) return 0;
|
|
3368
|
+
var seen = [];
|
|
3369
|
+
for (var index = 0; index < values.length; index += 1) {
|
|
3370
|
+
if (seen.indexOf(values[index]) < 0) seen.push(values[index]);
|
|
3371
|
+
}
|
|
3372
|
+
return seen.length;
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
/**
|
|
3376
|
+
* Does the same family answer more finely through its alias than through itself?
|
|
3377
|
+
*
|
|
3378
|
+
* This is the discriminator the continuity probe trusts, and it needs no threshold:
|
|
3379
|
+
* a family that snaps to named instances renders IDENTICALLY either way, so both
|
|
3380
|
+
* responses hold the same number of distinct values; a variable family drawn through
|
|
3381
|
+
* the alias moves at weights where the system path already stopped. Comparing the
|
|
3382
|
+
* family against ITSELF also keeps the answer honest for a narrow axis, where a
|
|
3383
|
+
* share-of-neighbours rule would read saturation as "static".
|
|
3384
|
+
* @param {number[]} throughAlias - measurements taken through the alias face.
|
|
3385
|
+
* @param {number[]} plain - the same measurements taken through the family itself.
|
|
3386
|
+
* @returns {boolean} true when the alias resolves finer detail.
|
|
3387
|
+
*/
|
|
3388
|
+
function respondsFinerThan(throughAlias, plain) {
|
|
3389
|
+
if (!Array.isArray(throughAlias) || !Array.isArray(plain)) return false;
|
|
3390
|
+
var fine = distinctCount(throughAlias);
|
|
3391
|
+
return fine >= 3 && fine > distinctCount(plain);
|
|
3392
|
+
}
|
|
3393
|
+
|
|
3394
|
+
|
|
3395
|
+
/**
|
|
3396
|
+
* The alias family name for one family. A hash, not the family itself, so the
|
|
3397
|
+
* name is a plain identifier whatever the family is called.
|
|
3398
|
+
* @param {string} family - a family name.
|
|
3399
|
+
* @returns {string} the alias, stable across sessions.
|
|
3400
|
+
*/
|
|
3401
|
+
function variableAliasFor(family) {
|
|
3402
|
+
var text = sanitizeFamily(family);
|
|
3403
|
+
var hash = 0;
|
|
3404
|
+
for (var index = 0; index < text.length; index += 1) {
|
|
3405
|
+
hash = (hash * 31 + text.charCodeAt(index)) >>> 0;
|
|
3406
|
+
}
|
|
3407
|
+
return VARIABLE_ALIAS_PREFIX + hash.toString(36);
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
/**
|
|
3411
|
+
* The `@font-face` rule that draws one family through its own variable axis.
|
|
3412
|
+
*
|
|
3413
|
+
* `local()` points the face at the installed family, and the weight RANGE is the
|
|
3414
|
+
* part that matters: it tells the engine the family is continuous, so `font-weight`
|
|
3415
|
+
* picks a point on the axis instead of the nearest named instance. (Without the
|
|
3416
|
+
* range the same family snaps; `font-variation-settings` does not help a system
|
|
3417
|
+
* family at all — measured, see FT-060.)
|
|
3418
|
+
* @param {string} family - the installed family name.
|
|
3419
|
+
* @param {string} alias - the alias from `variableAliasFor`.
|
|
3420
|
+
* @returns {string} one CSS rule.
|
|
3421
|
+
*/
|
|
3422
|
+
function variableFaceRule(family, alias) {
|
|
3423
|
+
var text = sanitizeFamily(family).replace(/"/g, "");
|
|
3424
|
+
return '@font-face{font-family:"' + alias + '";src:local("' + text + '");font-weight:100 900;}';
|
|
3425
|
+
}
|
|
3426
|
+
|
|
3427
|
+
/**
|
|
3428
|
+
* Every `@font-face` rule for a map of family → alias.
|
|
3429
|
+
* @param {Record<string, string>} variable - the families that measured continuous.
|
|
3430
|
+
* @returns {string} the rules, one per line; "" when there are none.
|
|
3431
|
+
*/
|
|
3432
|
+
function buildVariableFaces(variable) {
|
|
3433
|
+
var rules = [];
|
|
3434
|
+
if (variable === null || typeof variable !== "object") return "";
|
|
3435
|
+
for (var family in variable) {
|
|
3436
|
+
if (Object.prototype.hasOwnProperty.call(variable, family)) {
|
|
3437
|
+
rules.push(variableFaceRule(family, variable[family]));
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
return rules.join("\n");
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
/**
|
|
3444
|
+
* Put the alias IN FRONT of every continuous family a stack names.
|
|
3445
|
+
*
|
|
3446
|
+
* The family stays in the list after its alias, so a glyph the alias lacks still
|
|
3447
|
+
* falls through to the same face the reader chose, and a stack that names no
|
|
3448
|
+
* continuous family is returned unchanged.
|
|
3449
|
+
* @param {string} stack - a formatted font-family value.
|
|
3450
|
+
* @param {Record<string, string>} variable - family → alias map.
|
|
3451
|
+
* @returns {string} the stack, with aliases prepended where they apply.
|
|
3452
|
+
*/
|
|
3453
|
+
function withVariableFaces(stack, variable) {
|
|
3454
|
+
if (typeof stack !== "string" || stack === "" || variable === null || typeof variable !== "object") {
|
|
3455
|
+
return stack;
|
|
3456
|
+
}
|
|
3457
|
+
// The stack may already carry its alias — a family read back from the page's own
|
|
3458
|
+
// computed style does, once the mode is on. Prepending it again would stack up a
|
|
3459
|
+
// duplicate on every rebuild.
|
|
3460
|
+
var names = parseStack(stack);
|
|
3461
|
+
var lead = names.length > 0 ? names[0] : "";
|
|
3462
|
+
var prefix = [];
|
|
3463
|
+
for (var family in variable) {
|
|
3464
|
+
if (!Object.prototype.hasOwnProperty.call(variable, family)) continue;
|
|
3465
|
+
if (stack.indexOf(quoteFamily(family)) < 0) continue;
|
|
3466
|
+
if (lead === variable[family]) continue;
|
|
3467
|
+
prefix.push('"' + variable[family] + '"');
|
|
3468
|
+
}
|
|
3469
|
+
return prefix.length === 0 ? stack : prefix.join(", ") + ", " + stack;
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
/**
|
|
3473
|
+
* Did a family respond continuously to weight in the probe?
|
|
3474
|
+
* @param {number[]} widths - laid-out widths, in `CONTINUITY_WEIGHTS` order.
|
|
3475
|
+
* @returns {boolean} true when enough neighbours differ.
|
|
3476
|
+
*/
|
|
3477
|
+
function isContinuousResponse(widths) {
|
|
3478
|
+
if (!Array.isArray(widths) || widths.length < 3) return false;
|
|
3479
|
+
var changes = 0;
|
|
3480
|
+
for (var index = 1; index < widths.length; index += 1) {
|
|
3481
|
+
if (widths[index] !== widths[index - 1]) changes += 1;
|
|
3482
|
+
}
|
|
3483
|
+
return changes / (widths.length - 1) >= CONTINUOUS_MIN_SHARE;
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
/**
|
|
3487
|
+
* The slider shape for a continuous family: the finest notch, centred on 0.
|
|
3488
|
+
* @returns {{min: number, max: number, step: number}} the profile, in weight units.
|
|
3489
|
+
*/
|
|
3490
|
+
function continuousWeightProfile() {
|
|
3491
|
+
return { min: -CONTINUOUS_RANGE, max: CONTINUOUS_RANGE, step: CONTINUOUS_STEP };
|
|
3492
|
+
}
|
|
3493
|
+
|
|
3172
3494
|
/**
|
|
3173
3495
|
* What a slider's local (unconfirmed) value should do when a value arrives from
|
|
3174
3496
|
* the settings document.
|
|
@@ -3232,6 +3554,11 @@ function isDormant(config) {
|
|
|
3232
3554
|
clampWeightDelta(axis[CODE_WEIGHT_FIELD], true) === WEIGHT_UNSET &&
|
|
3233
3555
|
clampRatio(axis[LINE_HEIGHT_DIALOG_FIELD]) === LINE_HEIGHT_MIN &&
|
|
3234
3556
|
clampLineOffset(axis[CODE_LINE_HEIGHT_FIELD]) === 0 &&
|
|
3557
|
+
clampSpacing(axis[LETTER_SPACING_DIALOG_FIELD], LETTER_SPACING_MIN, LETTER_SPACING_MAX) === 0 &&
|
|
3558
|
+
clampSpacing(axis[WORD_SPACING_DIALOG_FIELD], WORD_SPACING_MIN, WORD_SPACING_MAX) === 0 &&
|
|
3559
|
+
clampSpacing(axis[PARAGRAPH_SPACING_DIALOG_FIELD], PARAGRAPH_SPACING_MIN, PARAGRAPH_SPACING_MAX) === 0 &&
|
|
3560
|
+
clampIndent(axis[INDENT_DIALOG_FIELD]) === 0 &&
|
|
3561
|
+
toBool(axis[LINE_BREAK_STRICT_FIELD]) === false &&
|
|
3235
3562
|
clampLigatures(axis[LIGATURES_FIELD]) === LIGATURES_DEFAULT &&
|
|
3236
3563
|
sanitizeFeatures(axis[FEATURES_FIELD]) === "" &&
|
|
3237
3564
|
toBool(axis[NO_SYNTHETIC_ITALIC_FIELD]) === false &&
|
|
@@ -3256,10 +3583,12 @@ function isDormant(config) {
|
|
|
3256
3583
|
* @param {boolean} isDark - prefix every rule with the dark-theme attribute.
|
|
3257
3584
|
* @returns {string} declarations for one `<style>` element.
|
|
3258
3585
|
*/
|
|
3259
|
-
function buildAxisCss(axis, baseTokens, isDark, hints) {
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
var
|
|
3586
|
+
function buildAxisCss(axis, baseTokens, isDark, hints, variable) {
|
|
3587
|
+
// A continuous family leads its stack with its alias (see `withVariableFaces`);
|
|
3588
|
+
// with no map, every stack stays exactly as written.
|
|
3589
|
+
var sans = withVariableFaces(formatStack(parseStack(axis[SANS_FIELD])), variable);
|
|
3590
|
+
var mono = withVariableFaces(formatStack(parseStack(axis[MONO_FIELD])), variable);
|
|
3591
|
+
var dialog = withVariableFaces(formatStack(parseStack(axis[STACK_DIALOG_FIELD])), variable);
|
|
3263
3592
|
// Normalised once: an expanded axis always carries a number, but a caller
|
|
3264
3593
|
// handing in a raw object may leave the field out, and "absent" has to mean
|
|
3265
3594
|
// "unset" rather than "write the string undefined". The conversation's value
|
|
@@ -3613,6 +3942,60 @@ function buildAxisCss(axis, baseTokens, isDark, hints) {
|
|
|
3613
3942
|
}
|
|
3614
3943
|
}
|
|
3615
3944
|
|
|
3945
|
+
// ---- conversation text shaping (tracking, word gap, paragraph gap, indent) ----
|
|
3946
|
+
//
|
|
3947
|
+
// Every value here is written on the CONVERSATION scope, and the measurements that
|
|
3948
|
+
// fixed the shape were taken there (2026-10-13): the markdown's tracking is `normal`
|
|
3949
|
+
// and its word gap `0px`, so those two axes are absolute and 0 emits nothing; the
|
|
3950
|
+
// paragraphs carry 16px margins, so the paragraph axis is an offset over that
|
|
3951
|
+
// baseline. Code surfaces are excluded by the same scope rule the weight ladder uses,
|
|
3952
|
+
// so a code block's own tracking is never rewritten by a conversation axis.
|
|
3953
|
+
var letterSpacing = clampSpacing(axis[LETTER_SPACING_DIALOG_FIELD], LETTER_SPACING_MIN, LETTER_SPACING_MAX);
|
|
3954
|
+
var wordSpacing = clampSpacing(axis[WORD_SPACING_DIALOG_FIELD], WORD_SPACING_MIN, WORD_SPACING_MAX);
|
|
3955
|
+
var paragraphSpacing = clampSpacing(
|
|
3956
|
+
axis[PARAGRAPH_SPACING_DIALOG_FIELD],
|
|
3957
|
+
PARAGRAPH_SPACING_MIN,
|
|
3958
|
+
PARAGRAPH_SPACING_MAX
|
|
3959
|
+
);
|
|
3960
|
+
var indent = clampIndent(axis[INDENT_DIALOG_FIELD]);
|
|
3961
|
+
if (letterSpacing !== 0) {
|
|
3962
|
+
declarations.push(weightScopeRule("letter-spacing:" + pxText(letterSpacing) + " !important"));
|
|
3963
|
+
}
|
|
3964
|
+
if (wordSpacing !== 0) {
|
|
3965
|
+
declarations.push(weightScopeRule("word-spacing:" + pxText(wordSpacing) + " !important"));
|
|
3966
|
+
}
|
|
3967
|
+
if (indent !== 0) {
|
|
3968
|
+
// A first-line indent is a PROSE convention, so it belongs to paragraphs. Emitted for
|
|
3969
|
+
// the whole conversation scope it inherited into every block under it; each of these
|
|
3970
|
+
// was measured on the page with the axis at 2em (32px on a 16px base):
|
|
3971
|
+
// - a heading sat 32px in, where no Chinese reader expects one;
|
|
3972
|
+
// - an ordered-list item's TEXT moved 32px while the number stayed at the content
|
|
3973
|
+
// edge (the marker sits outside the content box), so the number ended up in front
|
|
3974
|
+
// of the indent the text had taken;
|
|
3975
|
+
// - inline code is `display: inline-flex` in DSH's markdown — a block container of
|
|
3976
|
+
// its own — so the indent landed INSIDE the chip as blank space before its own
|
|
3977
|
+
// text, wherever the chip sat in the line;
|
|
3978
|
+
// - a code block's first line shifted the same way.
|
|
3979
|
+
// Paragraphs are the only place the convention exists, and their descendants are
|
|
3980
|
+
// zeroed so a chip that happens to be its own block still draws flush.
|
|
3981
|
+
var indentDeclarations = "text-indent:" + String(indent) + "em !important";
|
|
3982
|
+
declarations.push(weightScopeRule("p{" + indentDeclarations + "}"));
|
|
3983
|
+
declarations.push(weightScopeRule("p *{text-indent:0 !important}"));
|
|
3984
|
+
}
|
|
3985
|
+
if (paragraphSpacing !== 0) {
|
|
3986
|
+
// Both margins move together: adjacent siblings collapse to the LARGER of the two,
|
|
3987
|
+
// so shifting only `margin-top` would silently ignore every negative offset.
|
|
3988
|
+
var gap =
|
|
3989
|
+
paragraphSpacing > 0
|
|
3990
|
+
? "calc(" + PARAGRAPH_BASELINE_PX + "px + " + pxText(paragraphSpacing) + ")"
|
|
3991
|
+
: "calc(" + PARAGRAPH_BASELINE_PX + "px - " + pxText(-paragraphSpacing) + ")";
|
|
3992
|
+
declarations.push(weightScopeRule("p{margin-bottom:" + gap + " !important}"));
|
|
3993
|
+
declarations.push(weightScopeRule("p:not(:first-child){margin-top:" + gap + " !important}"));
|
|
3994
|
+
}
|
|
3995
|
+
if (toBool(axis[LINE_BREAK_STRICT_FIELD])) {
|
|
3996
|
+
declarations.push(weightScopeRule("line-break:strict !important"));
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3616
3999
|
// ---- code axes ----
|
|
3617
4000
|
var codeSize = axis[CODE_SIZE_FIELD];
|
|
3618
4001
|
var codeLine = axis[CODE_LINE_HEIGHT_FIELD];
|
|
@@ -3659,7 +4042,28 @@ function buildAxisCss(axis, baseTokens, isDark, hints) {
|
|
|
3659
4042
|
if (noBold) declarations.push(bodyRule("font-synthesis-weight:none !important"));
|
|
3660
4043
|
}
|
|
3661
4044
|
|
|
3662
|
-
return declarations.join("\n");
|
|
4045
|
+
return hardenDeltaRefs(declarations.join("\n"));
|
|
4046
|
+
}
|
|
4047
|
+
|
|
4048
|
+
/**
|
|
4049
|
+
* Give every reference to the content-size delta a fallback value.
|
|
4050
|
+
*
|
|
4051
|
+
* A heading's `font` shorthand is the markdown token, and DSH's own text for it reads
|
|
4052
|
+
* `calc(21px + var(--dsh-content-font-delta))` — a reference with NO fallback. Nothing
|
|
4053
|
+
* outside this plugin defines that property, so any rebuild that emits the reference
|
|
4054
|
+
* without the definition leaves it resolving to nothing, which makes the WHOLE shorthand
|
|
4055
|
+
* invalid at computed-value time: every font longhand falls back to `inherit`, and the
|
|
4056
|
+
* heading is painted at body size. Measured on a live page: h1 23px → 16px the moment the
|
|
4057
|
+
* property resolved to nothing, back to 23px when it returned.
|
|
4058
|
+
*
|
|
4059
|
+
* The fallback cannot change a page that defines the property (the definition wins), and
|
|
4060
|
+
* where the definition is missing it keeps the design's own size instead of collapsing.
|
|
4061
|
+
* @param {string} css - generated declarations.
|
|
4062
|
+
* @returns {string} the same declarations, with the reference made safe.
|
|
4063
|
+
*/
|
|
4064
|
+
function hardenDeltaRefs(css) {
|
|
4065
|
+
if (typeof css !== "string" || css === "") return css;
|
|
4066
|
+
return css.split("var(--dsh-content-font-delta)").join("var(--dsh-content-font-delta, 0px)");
|
|
3663
4067
|
}
|
|
3664
4068
|
|
|
3665
4069
|
/**
|
|
@@ -3751,7 +4155,7 @@ function scaleCodeTokens(baseTokens, scale, lineOffset) {
|
|
|
3751
4155
|
* correct everywhere and costs an attribute scan per element per rule.
|
|
3752
4156
|
* @returns {string} declarations for one `<style>` element ("" when dormant).
|
|
3753
4157
|
*/
|
|
3754
|
-
function buildFontCss(config, baseTokens, hints) {
|
|
4158
|
+
function buildFontCss(config, baseTokens, hints, variable) {
|
|
3755
4159
|
var tokens = baseTokens === undefined || baseTokens === null ? FALLBACK_TOKENS : baseTokens;
|
|
3756
4160
|
var sets = resolveAxes(config);
|
|
3757
4161
|
// The rendering preference is NOT an axis: it must paint even when every
|
|
@@ -3759,16 +4163,16 @@ function buildFontCss(config, baseTokens, hints) {
|
|
|
3759
4163
|
// use case with everything else untouched.
|
|
3760
4164
|
var smoothing = smoothingRule(config !== null && typeof config === "object" ? config[SMOOTHING_FIELD] : "");
|
|
3761
4165
|
if (isDormant(sets.light) && isDormant(sets.dark)) return smoothing;
|
|
3762
|
-
var light = buildAxisCss(sets.light, tokens, false, hints);
|
|
4166
|
+
var light = buildAxisCss(sets.light, tokens, false, hints, variable);
|
|
3763
4167
|
if (sameValueSet(sets.light, sets.dark)) {
|
|
3764
|
-
return smoothing === "" ? light : light === "" ? smoothing : light + "\n" + smoothing;
|
|
4168
|
+
return hardenDeltaRefs(smoothing === "" ? light : light === "" ? smoothing : light + "\n" + smoothing);
|
|
3765
4169
|
}
|
|
3766
|
-
var dark = buildAxisCss(sets.dark, tokens, true, hints);
|
|
4170
|
+
var dark = buildAxisCss(sets.dark, tokens, true, hints, variable);
|
|
3767
4171
|
var parts = [];
|
|
3768
4172
|
if (light !== "") parts.push(light);
|
|
3769
4173
|
if (dark !== "") parts.push(dark);
|
|
3770
4174
|
if (smoothing !== "") parts.push(smoothing);
|
|
3771
|
-
return parts.join("\n");
|
|
4175
|
+
return hardenDeltaRefs(parts.join("\n"));
|
|
3772
4176
|
}
|
|
3773
4177
|
|
|
3774
4178
|
/**
|
|
@@ -4213,6 +4617,24 @@ var shared = {
|
|
|
4213
4617
|
LINE_HEIGHT_MAX: LINE_HEIGHT_MAX,
|
|
4214
4618
|
CODE_LINE_HEIGHT_MIN: CODE_LINE_HEIGHT_MIN,
|
|
4215
4619
|
CODE_LINE_HEIGHT_MAX: CODE_LINE_HEIGHT_MAX,
|
|
4620
|
+
LETTER_SPACING_DIALOG_FIELD: LETTER_SPACING_DIALOG_FIELD,
|
|
4621
|
+
WORD_SPACING_DIALOG_FIELD: WORD_SPACING_DIALOG_FIELD,
|
|
4622
|
+
PARAGRAPH_SPACING_DIALOG_FIELD: PARAGRAPH_SPACING_DIALOG_FIELD,
|
|
4623
|
+
INDENT_DIALOG_FIELD: INDENT_DIALOG_FIELD,
|
|
4624
|
+
LINE_BREAK_STRICT_FIELD: LINE_BREAK_STRICT_FIELD,
|
|
4625
|
+
LETTER_SPACING_MIN: LETTER_SPACING_MIN,
|
|
4626
|
+
LETTER_SPACING_MAX: LETTER_SPACING_MAX,
|
|
4627
|
+
LETTER_SPACING_STEP: LETTER_SPACING_STEP,
|
|
4628
|
+
WORD_SPACING_MIN: WORD_SPACING_MIN,
|
|
4629
|
+
WORD_SPACING_MAX: WORD_SPACING_MAX,
|
|
4630
|
+
WORD_SPACING_STEP: WORD_SPACING_STEP,
|
|
4631
|
+
PARAGRAPH_SPACING_MIN: PARAGRAPH_SPACING_MIN,
|
|
4632
|
+
PARAGRAPH_SPACING_MAX: PARAGRAPH_SPACING_MAX,
|
|
4633
|
+
PARAGRAPH_SPACING_STEP: PARAGRAPH_SPACING_STEP,
|
|
4634
|
+
INDENT_MIN: INDENT_MIN,
|
|
4635
|
+
INDENT_MAX: INDENT_MAX,
|
|
4636
|
+
INDENT_STEP: INDENT_STEP,
|
|
4637
|
+
PARAGRAPH_BASELINE_PX: PARAGRAPH_BASELINE_PX,
|
|
4216
4638
|
LIGATURES_DEFAULT: LIGATURES_DEFAULT,
|
|
4217
4639
|
SMOOTHING_FIELD: SMOOTHING_FIELD,
|
|
4218
4640
|
SMOOTHING_AUTO: SMOOTHING_AUTO,
|
|
@@ -4290,6 +4712,19 @@ var shared = {
|
|
|
4290
4712
|
WEIGHT_DELTA_FLOOR: WEIGHT_DELTA_FLOOR,
|
|
4291
4713
|
WEIGHT_DELTA_CEIL: WEIGHT_DELTA_CEIL,
|
|
4292
4714
|
WEIGHT_MIN_POSITIONS: WEIGHT_MIN_POSITIONS,
|
|
4715
|
+
VARIABLE_ALIAS_PREFIX: VARIABLE_ALIAS_PREFIX,
|
|
4716
|
+
CONTINUITY_WEIGHTS: CONTINUITY_WEIGHTS,
|
|
4717
|
+
isRootScopeSelector: isRootScopeSelector,
|
|
4718
|
+
hardenDeltaRefs: hardenDeltaRefs,
|
|
4719
|
+
CONTINUOUS_CANDIDATES: CONTINUOUS_CANDIDATES,
|
|
4720
|
+
distinctCount: distinctCount,
|
|
4721
|
+
respondsFinerThan: respondsFinerThan,
|
|
4722
|
+
variableAliasFor: variableAliasFor,
|
|
4723
|
+
variableFaceRule: variableFaceRule,
|
|
4724
|
+
buildVariableFaces: buildVariableFaces,
|
|
4725
|
+
withVariableFaces: withVariableFaces,
|
|
4726
|
+
isContinuousResponse: isContinuousResponse,
|
|
4727
|
+
continuousWeightProfile: continuousWeightProfile,
|
|
4293
4728
|
WEIGHT_LADDER_TOP: WEIGHT_LADDER_TOP,
|
|
4294
4729
|
WEIGHT_LADDER_STRONG: WEIGHT_LADDER_STRONG,
|
|
4295
4730
|
WEIGHT_DELTA_UP_LIMIT: WEIGHT_DELTA_UP_LIMIT,
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Font tune (dsh-fonttune)",
|
|
4
|
-
"description": "Interface and conversation fonts,
|
|
4
|
+
"description": "Interface and conversation fonts, size, weight and line height, CJK type shaping (tracking, paragraph gaps, first-line indent), code extras and presets"
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/zh.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-fonttune",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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",
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
"README.en.md",
|
|
19
19
|
"CHANGELOG.md",
|
|
20
20
|
"CHANGELOG.en.md",
|
|
21
|
-
"LICENSE"
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"docs/panel.jpg",
|
|
23
|
+
"docs/card-shaping.jpg"
|
|
22
24
|
],
|
|
23
25
|
"engines": {
|
|
24
26
|
"dsh": ">=0.1.7-alpha.1 <0.3.0-0"
|