dsh-fonttune 0.7.2 → 0.7.4
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 +17 -0
- package/CHANGELOG.md +17 -0
- package/README.en.md +104 -104
- package/README.md +104 -104
- package/lib/client.js +483 -29
- package/lib/shared.cjs +324 -9
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -2968,19 +2968,36 @@ function codeSelector(hints) {
|
|
|
2968
2968
|
CODE_SELECTOR = codeSelector(null);
|
|
2969
2969
|
|
|
2970
2970
|
/**
|
|
2971
|
-
* The dialog surface
|
|
2972
|
-
*
|
|
2973
|
-
*
|
|
2974
|
-
*
|
|
2975
|
-
*
|
|
2976
|
-
*
|
|
2977
|
-
*
|
|
2978
|
-
*
|
|
2971
|
+
* The dialog surface, across two host generations.
|
|
2972
|
+
*
|
|
2973
|
+
* Up to DSH 0.2.1 the conversation markdown sat in a CSS-module container whose
|
|
2974
|
+
* class carried a `_markdown_` hash segment (`_markdown_<hash>_…`), wrapped in
|
|
2975
|
+
* a stable `[data-dss-prose]` container. DSH 0.2.2 dropped both: the class is
|
|
2976
|
+
* now `<hash>_root` and the prose wrapper is gone, which left every dialog rule
|
|
2977
|
+
* matching zero elements and the whole conversation axis silently inert.
|
|
2978
|
+
*
|
|
2979
|
+
* What 0.2.2 does still expose is the content container
|
|
2980
|
+
* `[data-conversation-content]`, so the markdown block elements inside it
|
|
2981
|
+
* stand in for the scope. A class-substring match degrades safely — no match
|
|
2982
|
+
* means the dialog follows the interface axis, the same behavior as an unknown
|
|
2983
|
+
* token name — but it costs an attribute scan per element per rule, so the
|
|
2984
|
+
* browser half prefers the stable wrapper when it can see one and falls back to
|
|
2985
|
+
* the element list otherwise.
|
|
2979
2986
|
*/
|
|
2987
|
+
var MARKDOWN_BLOCK_ELEMENTS = "p,li,h1,h2,h3,h4,h5,h6,blockquote,td,th,dt,dd";
|
|
2988
|
+
|
|
2980
2989
|
var MARKDOWN_SELECTOR =
|
|
2981
|
-
'[
|
|
2990
|
+
'[data-dss-prose]' +
|
|
2991
|
+
CODE_EXCLUDES +
|
|
2992
|
+
',[data-dss-prose] *' +
|
|
2993
|
+
CODE_EXCLUDES +
|
|
2994
|
+
',[class*="_markdown_" i]' +
|
|
2982
2995
|
CODE_EXCLUDES +
|
|
2983
2996
|
',[class*="_markdown_" i] *' +
|
|
2997
|
+
CODE_EXCLUDES +
|
|
2998
|
+
',[data-conversation-content] :where(' +
|
|
2999
|
+
MARKDOWN_BLOCK_ELEMENTS +
|
|
3000
|
+
')' +
|
|
2984
3001
|
CODE_EXCLUDES;
|
|
2985
3002
|
|
|
2986
3003
|
/**
|
|
@@ -3246,6 +3263,294 @@ function ceilTo(value, step) {
|
|
|
3246
3263
|
return Math.ceil(value / step) * step;
|
|
3247
3264
|
}
|
|
3248
3265
|
|
|
3266
|
+
/**
|
|
3267
|
+
* How far apart two families may land on one notch before the stack refuses it.
|
|
3268
|
+
*
|
|
3269
|
+
* Measured on the reporter's pair (issue #4): one 100-weight notch adds +21.5%
|
|
3270
|
+
* ink to the Latin family and +23.9% to the Chinese one — 2.4 points apart, which
|
|
3271
|
+
* no reader notices. One 200-weight notch adds +44% and +51% — 7.4 points apart,
|
|
3272
|
+
* and that is the reported "one side suits, the other is too heavy". Five points
|
|
3273
|
+
* is the budget between those two observations.
|
|
3274
|
+
*/
|
|
3275
|
+
var INK_BALANCE_SPREAD = 0.05;
|
|
3276
|
+
|
|
3277
|
+
/**
|
|
3278
|
+
* How far apart a pair may land when the strict budget would leave no control.
|
|
3279
|
+
*
|
|
3280
|
+
* Measured on the reporter's pair (issue #4): going one notch LIGHTER, the Latin
|
|
3281
|
+
* family loses 29.7% of its ink and the Chinese one 17.9% — 11.9 points apart, and
|
|
3282
|
+
* no finer notch makes it better, because the two families simply space their faces
|
|
3283
|
+
* differently. Under the strict budget the reader would get a weight control that
|
|
3284
|
+
* cannot go lighter at all (two positions). The wider budget is what keeps that
|
|
3285
|
+
* physical pair usable, and it is only consulted when the strict one is: a stack
|
|
3286
|
+
* that has four even positions never sees it.
|
|
3287
|
+
*/
|
|
3288
|
+
var INK_BALANCE_SPREAD_WIDE = 0.15;
|
|
3289
|
+
|
|
3290
|
+
/** How many notches the evenness walk looks at, one way. */
|
|
3291
|
+
var WEIGHT_MAX_POSITIONS = 24;
|
|
3292
|
+
|
|
3293
|
+
/**
|
|
3294
|
+
* The least ink one notch has to add before a position is worth having.
|
|
3295
|
+
*
|
|
3296
|
+
* This is the bar `weightProfileFrom` already measured on this host: a 200-weight
|
|
3297
|
+
* step is +30% ink and a 100-weight step +16%, and 100 was not enough — a reader
|
|
3298
|
+
* could not see it. Five per cent would put a variable family's stack at twenty-odd
|
|
3299
|
+
* notches that each move a little (measured live: 29 positions of +6%), which is the
|
|
3300
|
+
* dead-control the coarse step exists to avoid. Fifteen keeps every pair the faces
|
|
3301
|
+
* do allow (the reported one tops out at +19%/+22%) and stops there.
|
|
3302
|
+
*/
|
|
3303
|
+
var INK_BALANCE_STEP = 0.15;
|
|
3304
|
+
|
|
3305
|
+
/**
|
|
3306
|
+
* One family's ink at one weight.
|
|
3307
|
+
*
|
|
3308
|
+
* A family answers through `inkOf`, the sampler its own measurement already uses;
|
|
3309
|
+
* a weight it cannot measure — or a family that cannot be measured at all — answers
|
|
3310
|
+
* null and is left out of a comparison instead of counting as a zero.
|
|
3311
|
+
* @param {Object} entry - one measured family.
|
|
3312
|
+
* @param {number} weight - CSS weight.
|
|
3313
|
+
* @returns {number|null} the ink, or null when there is no measurement.
|
|
3314
|
+
*/
|
|
3315
|
+
function entryInk(entry, weight) {
|
|
3316
|
+
if (entry === null || typeof entry !== "object") return null;
|
|
3317
|
+
if (typeof entry.inkOf !== "function") return null;
|
|
3318
|
+
var ink = null;
|
|
3319
|
+
try {
|
|
3320
|
+
ink = entry.inkOf(weight);
|
|
3321
|
+
} catch (error) {
|
|
3322
|
+
return null;
|
|
3323
|
+
}
|
|
3324
|
+
ink = Number(ink);
|
|
3325
|
+
if (!isFinite(ink) || ink <= 0) return null;
|
|
3326
|
+
return ink;
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
/**
|
|
3330
|
+
* How much moving from one weight to another changes each family's ink.
|
|
3331
|
+
*
|
|
3332
|
+
* The share is relative to the family's OWN ink at `from`, so two families that
|
|
3333
|
+
* start out different sizes are still comparable: +20% means the same thing for
|
|
3334
|
+
* both. Families that cannot be measured on both sides are left out.
|
|
3335
|
+
* @param {Array<Object>} entries - the measured families.
|
|
3336
|
+
* @param {number} from - the weight the reader is at.
|
|
3337
|
+
* @param {number} to - the weight being compared.
|
|
3338
|
+
* @returns {number[]|null} the relative changes, or null when fewer than two
|
|
3339
|
+
* families can be measured.
|
|
3340
|
+
*/
|
|
3341
|
+
function inkShares(entries, from, to) {
|
|
3342
|
+
var shares = [];
|
|
3343
|
+
for (var index = 0; index < entries.length; index += 1) {
|
|
3344
|
+
var before = entryInk(entries[index], from);
|
|
3345
|
+
var after = entryInk(entries[index], to);
|
|
3346
|
+
if (before === null || after === null) continue;
|
|
3347
|
+
shares.push((after - before) / before);
|
|
3348
|
+
}
|
|
3349
|
+
return shares.length < 2 ? null : shares;
|
|
3350
|
+
}
|
|
3351
|
+
|
|
3352
|
+
/**
|
|
3353
|
+
* How far apart the families land, and how little the quietest one moved.
|
|
3354
|
+
* @param {number[]} shares - one relative change per family.
|
|
3355
|
+
* @returns {{spread: number, smallest: number}} `spread` is the signed range, so
|
|
3356
|
+
* families moving in opposite directions count as far apart.
|
|
3357
|
+
*/
|
|
3358
|
+
function shareSpread(shares) {
|
|
3359
|
+
var least = shares[0];
|
|
3360
|
+
var most = shares[0];
|
|
3361
|
+
var smallest = Math.abs(shares[0]);
|
|
3362
|
+
for (var index = 1; index < shares.length; index += 1) {
|
|
3363
|
+
if (shares[index] < least) least = shares[index];
|
|
3364
|
+
if (shares[index] > most) most = shares[index];
|
|
3365
|
+
var size = Math.abs(shares[index]);
|
|
3366
|
+
if (size < smallest) smallest = size;
|
|
3367
|
+
}
|
|
3368
|
+
return { spread: most - least, smallest: smallest };
|
|
3369
|
+
}
|
|
3370
|
+
|
|
3371
|
+
/**
|
|
3372
|
+
* The evenness verdict for one move of the stack.
|
|
3373
|
+
* @param {Array<Object>} entries - the measured families.
|
|
3374
|
+
* @param {number} from - the weight the move starts at.
|
|
3375
|
+
* @param {number} to - the weight it ends at.
|
|
3376
|
+
* @returns {{spread: number, smallest: number}|null} the verdict, or null when it
|
|
3377
|
+
* cannot be measured.
|
|
3378
|
+
*/
|
|
3379
|
+
function evennessOf(entries, from, to) {
|
|
3380
|
+
var shares = inkShares(entries, from, to);
|
|
3381
|
+
if (shares === null) return null;
|
|
3382
|
+
return shareSpread(shares);
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
/** Is one move of the stack both visible and even? */
|
|
3386
|
+
function moveIsEven(verdict, spread) {
|
|
3387
|
+
return verdict !== null && verdict.smallest >= INK_BALANCE_STEP && verdict.spread <= spread;
|
|
3388
|
+
}
|
|
3389
|
+
|
|
3390
|
+
/**
|
|
3391
|
+
* The shape a stack of families can follow together, chosen for EVENNESS.
|
|
3392
|
+
*
|
|
3393
|
+
* Issue #4 is not about the range: it is about two families being moved by one
|
|
3394
|
+
* offset and landing on faces that are not the same distance apart. The test is
|
|
3395
|
+
* therefore walked outwards from the neutral position, one notch at a time, and a
|
|
3396
|
+
* notch only survives when BOTH hold:
|
|
3397
|
+
* - every family's ink changes by at least `INK_BALANCE_STEP` (the notch is
|
|
3398
|
+
* visible), and
|
|
3399
|
+
* - the families land within `INK_BALANCE_SPREAD` of each other, measured both for
|
|
3400
|
+
* that one notch and for the whole way back to the neutral position — a stack
|
|
3401
|
+
* can be even notch by notch and still drift apart after two of them.
|
|
3402
|
+
*
|
|
3403
|
+
* The walk is what removes the synthetic-bold step without ever having to ask the
|
|
3404
|
+
* browser whether a weight is drawn or faked: above its last real face a family is
|
|
3405
|
+
* stroked thicker by the browser, which adds a much larger share of ink than a drawn
|
|
3406
|
+
* face does — so the notch where one family jumps and the other does not simply
|
|
3407
|
+
* fails the budget and the range stops in front of it.
|
|
3408
|
+
*
|
|
3409
|
+
* Among the shapes that survive, the one with the MOST positions wins: the visible
|
|
3410
|
+
* floor above is what makes a finer notch worth having, and more positions is more
|
|
3411
|
+
* control. (A single family is not put through this at all — it keeps the coarse
|
|
3412
|
+
* shape `weightProfileFrom` measured for it.)
|
|
3413
|
+
* @param {Array<{min: number, max: number, step: number, inkOf?: Function}>} entries
|
|
3414
|
+
* one record per measured family.
|
|
3415
|
+
* @param {number} base - the weight the offset is added to (body text).
|
|
3416
|
+
* @param {number} low - the lowest offset every family can express.
|
|
3417
|
+
* @param {number} high - the highest offset every family can express.
|
|
3418
|
+
* @returns {{min: number, max: number, step: number}|null} the shape, or null when
|
|
3419
|
+
* no step leaves the stack two positions to move between.
|
|
3420
|
+
*/
|
|
3421
|
+
function balancedStackShape(entries, base, low, high, spread) {
|
|
3422
|
+
var budget = typeof spread === "number" && spread > 0 ? spread : INK_BALANCE_SPREAD;
|
|
3423
|
+
var best = null;
|
|
3424
|
+
var bestCount = 1;
|
|
3425
|
+
var bestScore = Infinity;
|
|
3426
|
+
for (var index = 0; index < WEIGHT_STEP_CANDIDATES.length; index += 1) {
|
|
3427
|
+
var step = WEIGHT_STEP_CANDIDATES[index];
|
|
3428
|
+
if (!(step > 0)) continue;
|
|
3429
|
+
var up = 0;
|
|
3430
|
+
var down = 0;
|
|
3431
|
+
// A walk is bounded: no useful shape has more than a couple of dozen
|
|
3432
|
+
// positions, and an unbounded one on a one-unit grid would cost hundreds of
|
|
3433
|
+
// canvas renders on the very first measurement.
|
|
3434
|
+
var limit = step * WEIGHT_MAX_POSITIONS;
|
|
3435
|
+
for (var position = step; position <= high && position <= limit; position += step) {
|
|
3436
|
+
if (!moveIsEven(evennessOf(entries, base + position - step, base + position), budget)) break;
|
|
3437
|
+
if (!moveIsEven(evennessOf(entries, base, base + position), budget)) break;
|
|
3438
|
+
up = position;
|
|
3439
|
+
}
|
|
3440
|
+
for (var back = step; back <= -low && back <= limit; back += step) {
|
|
3441
|
+
if (!moveIsEven(evennessOf(entries, base - back + step, base - back), budget)) break;
|
|
3442
|
+
if (!moveIsEven(evennessOf(entries, base, base - back), budget)) break;
|
|
3443
|
+
down = back;
|
|
3444
|
+
}
|
|
3445
|
+
var count = (up + down) / step + 1;
|
|
3446
|
+
if (count < 2) continue;
|
|
3447
|
+
// How close the two families are on one notch from the neutral position: a tie
|
|
3448
|
+
// between two shapes that move the stack over the same number of positions.
|
|
3449
|
+
var headroom = evennessOf(entries, base, base + step);
|
|
3450
|
+
if (headroom === null) headroom = evennessOf(entries, base, base - step);
|
|
3451
|
+
var score = headroom === null ? Infinity : headroom.spread;
|
|
3452
|
+
if (count > bestCount || (count === bestCount && score < bestScore)) {
|
|
3453
|
+
// -0 would read as a different number to a strict comparison.
|
|
3454
|
+
best = { min: down === 0 ? 0 : -down, max: up, step: step };
|
|
3455
|
+
bestCount = count;
|
|
3456
|
+
bestScore = score;
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
return best;
|
|
3460
|
+
}
|
|
3461
|
+
|
|
3462
|
+
/**
|
|
3463
|
+
* One slider shape for a stack whose text can land on more than one family.
|
|
3464
|
+
*
|
|
3465
|
+
* A reader sets ONE offset; the browser then renders English with the first
|
|
3466
|
+
* family and Chinese with the fallback, each snapping that same requested value
|
|
3467
|
+
* to its own faces. When the fallback ships three faces and the primary is a
|
|
3468
|
+
* variable font, a step measured on the primary alone leaves the fallback
|
|
3469
|
+
* unchanged on most notches — the reported "one side suits, the other is too
|
|
3470
|
+
* heavy or too light" (issue #4: Inclusive Sans beside a three-face CJK family).
|
|
3471
|
+
*
|
|
3472
|
+
* The combined shape keeps every family moving on every notch, but the notch is
|
|
3473
|
+
* chosen for EVENNESS (see `balancedStackShape`): each family has to change by a
|
|
3474
|
+
* comparable share of its own ink, and the stack has to stay together over the
|
|
3475
|
+
* whole length of the range. Families that can report their ink (`inkOf`) get that
|
|
3476
|
+
* shape; a stack measured without a canvas keeps the plain shared-multiple shape,
|
|
3477
|
+
* which is also what a single family gets.
|
|
3478
|
+
* @param {Array<{min: number, max: number, step: number, inkOf?: Function}|null>} profiles
|
|
3479
|
+
* one shape per family.
|
|
3480
|
+
* @param {{base?: number}} [options] `base` is the weight the offset is added to
|
|
3481
|
+
* (body text); it anchors the ink comparison.
|
|
3482
|
+
* @returns {{min: number, max: number, step: number}|null} the combined shape, or
|
|
3483
|
+
* null when nothing usable was measured.
|
|
3484
|
+
*/
|
|
3485
|
+
function combineWeightProfiles(profiles, options) {
|
|
3486
|
+
var list = [];
|
|
3487
|
+
if (Array.isArray(profiles)) {
|
|
3488
|
+
for (var index = 0; index < profiles.length; index += 1) {
|
|
3489
|
+
var entry = profiles[index];
|
|
3490
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
3491
|
+
if (!isFinite(entry.min) || !isFinite(entry.max) || !isFinite(entry.step)) continue;
|
|
3492
|
+
if (!(entry.step > 0)) continue;
|
|
3493
|
+
list.push(entry);
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
if (list.length === 0) return null;
|
|
3497
|
+
if (list.length === 1) return { min: list[0].min, max: list[0].max, step: list[0].step };
|
|
3498
|
+
var step = list[0].step;
|
|
3499
|
+
for (var other = 1; other < list.length; other += 1) {
|
|
3500
|
+
step = commonStep(step, list[other].step);
|
|
3501
|
+
}
|
|
3502
|
+
var low = list[0].min;
|
|
3503
|
+
var high = list[0].max;
|
|
3504
|
+
for (var more = 1; more < list.length; more += 1) {
|
|
3505
|
+
low = Math.max(low, list[more].min);
|
|
3506
|
+
high = Math.min(high, list[more].max);
|
|
3507
|
+
}
|
|
3508
|
+
var settings = options === null || typeof options !== "object" ? {} : options;
|
|
3509
|
+
var base = isFinite(settings.base) ? Number(settings.base) : WEIGHT_BASE;
|
|
3510
|
+
// Every family that can report ink is put through the evenness walk; a stack whose
|
|
3511
|
+
// canvas could not answer keeps the plain shared-multiple shape.
|
|
3512
|
+
var measured = false;
|
|
3513
|
+
for (var scan = 0; scan < list.length; scan += 1) {
|
|
3514
|
+
if (typeof list[scan].inkOf === "function") measured = true;
|
|
3515
|
+
}
|
|
3516
|
+
if (measured) {
|
|
3517
|
+
var even = balancedStackShape(list, base, low, high);
|
|
3518
|
+
var positions = even === null ? 0 : (even.max - even.min) / even.step + 1;
|
|
3519
|
+
if (positions >= 3) return even;
|
|
3520
|
+
// The pair's own faces are spaced so unevenly that the strict budget leaves
|
|
3521
|
+
// no control; the wider one is what keeps a lighter position reachable.
|
|
3522
|
+
var relaxed = balancedStackShape(list, base, low, high, INK_BALANCE_SPREAD_WIDE);
|
|
3523
|
+
var relaxedPositions = relaxed === null ? 0 : (relaxed.max - relaxed.min) / relaxed.step + 1;
|
|
3524
|
+
if (relaxedPositions > positions) return relaxed;
|
|
3525
|
+
if (even !== null) return even;
|
|
3526
|
+
}
|
|
3527
|
+
var onGrid = { min: ceilTo(low, step), max: floorTo(high, step) };
|
|
3528
|
+
if (onGrid.max >= onGrid.min) return { min: onGrid.min, max: onGrid.max, step: step };
|
|
3529
|
+
var widest = list[0];
|
|
3530
|
+
for (var alt = 1; alt < list.length; alt += 1) {
|
|
3531
|
+
if (list[alt].max - list[alt].min > widest.max - widest.min) widest = list[alt];
|
|
3532
|
+
}
|
|
3533
|
+
return { min: widest.min, max: widest.max, step: widest.step };
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
/**
|
|
3537
|
+
* The smallest candidate step that is a whole multiple of both inputs.
|
|
3538
|
+
*
|
|
3539
|
+
* Profiles come from one candidate list, so a shared multiple usually exists
|
|
3540
|
+
* there; two steps that share none fall back to the coarser one, which still
|
|
3541
|
+
* moves the coarser family on every notch.
|
|
3542
|
+
* @param {number} a - one step.
|
|
3543
|
+
* @param {number} b - another step.
|
|
3544
|
+
* @returns {number} the combined step.
|
|
3545
|
+
*/
|
|
3546
|
+
function commonStep(a, b) {
|
|
3547
|
+
for (var index = 0; index < WEIGHT_STEP_CANDIDATES.length; index += 1) {
|
|
3548
|
+
var candidate = WEIGHT_STEP_CANDIDATES[index];
|
|
3549
|
+
if (candidate % a === 0 && candidate % b === 0) return candidate;
|
|
3550
|
+
}
|
|
3551
|
+
return Math.max(a, b);
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3249
3554
|
/**
|
|
3250
3555
|
* Does every position of `[min..max]` in `step` increments render differently?
|
|
3251
3556
|
*
|
|
@@ -4829,6 +5134,16 @@ var shared = {
|
|
|
4829
5134
|
WEIGHT_CSS_MIN: WEIGHT_CSS_MIN,
|
|
4830
5135
|
WEIGHT_CSS_MAX: WEIGHT_CSS_MAX,
|
|
4831
5136
|
weightProfileFrom: weightProfileFrom,
|
|
5137
|
+
combineWeightProfiles: combineWeightProfiles,
|
|
5138
|
+
balancedStackShape: balancedStackShape,
|
|
5139
|
+
evennessOf: evennessOf,
|
|
5140
|
+
INK_BALANCE_SPREAD: INK_BALANCE_SPREAD,
|
|
5141
|
+
INK_BALANCE_STEP: INK_BALANCE_STEP,
|
|
5142
|
+
INK_BALANCE_SPREAD_WIDE: INK_BALANCE_SPREAD_WIDE,
|
|
5143
|
+
WEIGHT_PROBE_LOW: WEIGHT_PROBE_LOW,
|
|
5144
|
+
WEIGHT_PROBE_HIGH: WEIGHT_PROBE_HIGH,
|
|
5145
|
+
WEIGHT_PROBE_STEP: WEIGHT_PROBE_STEP,
|
|
5146
|
+
GENERIC_FAMILIES: GENERIC_FAMILIES,
|
|
4832
5147
|
weightStepRange: weightStepRange,
|
|
4833
5148
|
isShorthandName: isShorthandName,
|
|
4834
5149
|
isScaledToken: isScaledToken,
|
|
@@ -6150,6 +6465,114 @@ var weightProbeCanvas = null;
|
|
|
6150
6465
|
/** Cache of measured slider shapes, keyed by the resolved family stack. */
|
|
6151
6466
|
var weightProfileCache = {};
|
|
6152
6467
|
|
|
6468
|
+
/**
|
|
6469
|
+
* The families a stack can actually render text with.
|
|
6470
|
+
*
|
|
6471
|
+
* The first family draws the Latin text and a later one draws whatever the first
|
|
6472
|
+
* cannot (that is where the Chinese comes from), so the notch step has to be one
|
|
6473
|
+
* every one of them can follow. Generic keywords are dropped: `serif` is not a
|
|
6474
|
+
* family anyone ships faces for.
|
|
6475
|
+
* @param {string} stack - resolved `font-family` value.
|
|
6476
|
+
* @returns {string[]} the family names, primary first.
|
|
6477
|
+
*/
|
|
6478
|
+
function stackFamilies(stack) {
|
|
6479
|
+
var parsed = shared.parseStack(String(stack));
|
|
6480
|
+
var out = [];
|
|
6481
|
+
for (var index = 0; index < parsed.length; index += 1) {
|
|
6482
|
+
var name = String(parsed[index]).trim();
|
|
6483
|
+
if (name === "") continue;
|
|
6484
|
+
if (shared.GENERIC_FAMILIES.indexOf(name.toLowerCase()) >= 0) continue;
|
|
6485
|
+
if (out.indexOf(name) >= 0) continue;
|
|
6486
|
+
out.push(name);
|
|
6487
|
+
}
|
|
6488
|
+
return out;
|
|
6489
|
+
}
|
|
6490
|
+
|
|
6491
|
+
/**
|
|
6492
|
+
* How many families of one stack are measured.
|
|
6493
|
+
*
|
|
6494
|
+
* Two covers the reported case — a Latin family with the CJK fallback behind it
|
|
6495
|
+
* — and keeps the first measurement to about twice what it was: the probe renders
|
|
6496
|
+
* the family on a canvas for every weight it asks about.
|
|
6497
|
+
*/
|
|
6498
|
+
var WEIGHT_STACK_FAMILIES = 2;
|
|
6499
|
+
|
|
6500
|
+
/** Measured shapes per family, so a combined stack measures each family once. */
|
|
6501
|
+
var familyProfileCache = {};
|
|
6502
|
+
|
|
6503
|
+
/**
|
|
6504
|
+
* The text each family of a stack is measured on when the two have to move together.
|
|
6505
|
+
*
|
|
6506
|
+
* A stack is one Latin family followed by whatever draws the Chinese, and each of
|
|
6507
|
+
* them draws only part of a mixed sample: measured on mixed text the Latin family
|
|
6508
|
+
* borrows the Chinese from the browser's own fallback — a third family the reader
|
|
6509
|
+
* never sees beside it — and its ink curve would then partly describe that one. The
|
|
6510
|
+
* evenness comparison therefore measures each family on the script it renders.
|
|
6511
|
+
*/
|
|
6512
|
+
var WEIGHT_SAMPLE_LATIN = "Hamburgefonstiv 0189";
|
|
6513
|
+
var WEIGHT_SAMPLE_FALLBACK = "对话字体粗细大小变化";
|
|
6514
|
+
|
|
6515
|
+
/**
|
|
6516
|
+
* One family's slider shape, measured through its alias when it is promoted.
|
|
6517
|
+
*
|
|
6518
|
+
* `null` means the canvas could not answer; the caller then falls back to the
|
|
6519
|
+
* nominal window instead of offering a control built on no data.
|
|
6520
|
+
*
|
|
6521
|
+
* When the stack has a second family, the record also carries `inkOf`, a sampler
|
|
6522
|
+
* for that family's ink on the script it draws. `combineWeightProfiles` walks it to
|
|
6523
|
+
* find the notch and range that move both families evenly (issue #4).
|
|
6524
|
+
* @param {string} family - the family name, unquoted.
|
|
6525
|
+
* @param {number} base - the heaviest weight the offset lands on.
|
|
6526
|
+
* @param {string} alias - the variable alias to measure through, "" when none.
|
|
6527
|
+
* @param {string|null} [inkText] - the text the ink is measured on, or null when the
|
|
6528
|
+
* stack has nothing to be even with.
|
|
6529
|
+
* @returns {{min: number, max: number, step: number, inkOf: Function|null}|null}
|
|
6530
|
+
* the shape plus the ink sampler.
|
|
6531
|
+
*/
|
|
6532
|
+
function familyWeightProfile(family, base, alias, inkText) {
|
|
6533
|
+
var text = typeof inkText === "string" && inkText !== "" ? inkText : null;
|
|
6534
|
+
var key =
|
|
6535
|
+
family +
|
|
6536
|
+
"\u0000" +
|
|
6537
|
+
base +
|
|
6538
|
+
"\u0000" +
|
|
6539
|
+
(alias === "" ? "stepped" : "continuous") +
|
|
6540
|
+
"\u0000" +
|
|
6541
|
+
(text === null ? "plain" : "ink");
|
|
6542
|
+
if (Object.prototype.hasOwnProperty.call(familyProfileCache, key)) {
|
|
6543
|
+
return familyProfileCache[key];
|
|
6544
|
+
}
|
|
6545
|
+
var record = null;
|
|
6546
|
+
try {
|
|
6547
|
+
var target = '"' + (alias === "" ? family : alias) + '"';
|
|
6548
|
+
var seen = {};
|
|
6549
|
+
var sample = function (weight) {
|
|
6550
|
+
if (!Object.prototype.hasOwnProperty.call(seen, weight)) {
|
|
6551
|
+
seen[weight] = sampleWeightSignature(target, weight);
|
|
6552
|
+
}
|
|
6553
|
+
return seen[weight];
|
|
6554
|
+
};
|
|
6555
|
+
var shape =
|
|
6556
|
+
alias === ""
|
|
6557
|
+
? weightProfileFrom(sample, { top: base })
|
|
6558
|
+
: weightProfileFrom(sample, { top: base, candidates: shared.CONTINUOUS_CANDIDATES });
|
|
6559
|
+
record = { min: shape.min, max: shape.max, step: shape.step, inkOf: null };
|
|
6560
|
+
if (text !== null) {
|
|
6561
|
+
var inks = {};
|
|
6562
|
+
record.inkOf = function (weight) {
|
|
6563
|
+
if (!Object.prototype.hasOwnProperty.call(inks, weight)) {
|
|
6564
|
+
inks[weight] = inkOfSignature(sampleWeightSignature(target, weight, text));
|
|
6565
|
+
}
|
|
6566
|
+
return inks[weight];
|
|
6567
|
+
};
|
|
6568
|
+
}
|
|
6569
|
+
} catch (error) {
|
|
6570
|
+
record = null;
|
|
6571
|
+
}
|
|
6572
|
+
familyProfileCache[key] = record;
|
|
6573
|
+
return record;
|
|
6574
|
+
}
|
|
6575
|
+
|
|
6153
6576
|
/**
|
|
6154
6577
|
* Values the card has moved but the settings document has not confirmed yet.
|
|
6155
6578
|
*
|
|
@@ -6480,13 +6903,17 @@ function probeLog(kind, detail) {
|
|
|
6480
6903
|
* — and report a coarse family where the weights are in fact continuous.
|
|
6481
6904
|
* @param {string} stack - resolved `font-family` value.
|
|
6482
6905
|
* @param {number} weight - CSS weight to draw with.
|
|
6906
|
+
* @param {string} [text] - the text to draw; the card's shared sample by default.
|
|
6483
6907
|
* @returns {string} the signature; equal signatures mean identical rendering.
|
|
6484
6908
|
*/
|
|
6485
|
-
function sampleWeightSignature(stack, weight) {
|
|
6909
|
+
function sampleWeightSignature(stack, weight, text) {
|
|
6910
|
+
var sampleText = typeof text === "string" && text !== "" ? text : WEIGHT_SAMPLE_TEXT;
|
|
6911
|
+
// Wide and tall enough for the longest sample (a run of CJK glyphs at 32px) and
|
|
6912
|
+
// for the line box no family clips: a clipped glyph would undercount its ink.
|
|
6486
6913
|
if (weightProbeCanvas === null) {
|
|
6487
6914
|
weightProbeCanvas = document.createElement("canvas");
|
|
6488
|
-
weightProbeCanvas.width =
|
|
6489
|
-
weightProbeCanvas.height =
|
|
6915
|
+
weightProbeCanvas.width = 420;
|
|
6916
|
+
weightProbeCanvas.height = 64;
|
|
6490
6917
|
}
|
|
6491
6918
|
// `willReadFrequently` matters here: the profile probe reads the canvas back once
|
|
6492
6919
|
// per sampled weight (~80 times on the first measurement), and without it the
|
|
@@ -6497,8 +6924,8 @@ var context = weightProbeCanvas.getContext("2d", { willReadFrequently: true });
|
|
|
6497
6924
|
context.font = weight + " 32px " + stack;
|
|
6498
6925
|
context.textBaseline = "top";
|
|
6499
6926
|
context.fillStyle = "#000";
|
|
6500
|
-
context.fillText(
|
|
6501
|
-
var advance = Math.round(context.measureText(
|
|
6927
|
+
context.fillText(sampleText, 2, 6);
|
|
6928
|
+
var advance = Math.round(context.measureText(sampleText).width * 1000);
|
|
6502
6929
|
var pixels = context.getImageData(0, 0, weightProbeCanvas.width, weightProbeCanvas.height).data;
|
|
6503
6930
|
var ink = 0;
|
|
6504
6931
|
for (var index = 3; index < pixels.length; index += 4) ink += pixels[index];
|
|
@@ -6552,21 +6979,38 @@ function weightProfileFor(stack, top) {
|
|
|
6552
6979
|
var profile = promoted
|
|
6553
6980
|
? shared.continuousWeightProfile()
|
|
6554
6981
|
: { min: WEIGHT_DELTA_MIN, max: WEIGHT_DELTA_MAX, step: 1 };
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
6982
|
+
// EVERY family the text can land on has to follow the same notch: the reader
|
|
6983
|
+
// sets one offset, and a Chinese family with three faces behind a variable
|
|
6984
|
+
// Latin one only moves where the Latin one has moved five times (issue #4).
|
|
6985
|
+
// The shapes are combined into the coarsest shared step and the range they
|
|
6986
|
+
// can all still express; the primary family alone is the answer only when the
|
|
6987
|
+
// stack names one family.
|
|
6988
|
+
// The shapes are combined into one notch that moves BOTH families evenly: the
|
|
6989
|
+
// first family draws the Latin text and the one behind it draws the Chinese, so
|
|
6990
|
+
// each is measured on its own script (issue #4).
|
|
6991
|
+
var families = stackFamilies(resolved);
|
|
6992
|
+
var shapes = [];
|
|
6993
|
+
// Comparing the families' ink costs a second set of canvas renders, so it runs
|
|
6994
|
+
// only when there IS a second family to be even with.
|
|
6995
|
+
var keepEven = families.length > 1;
|
|
6996
|
+
for (var index = 0; index < families.length && index < WEIGHT_STACK_FAMILIES; index += 1) {
|
|
6997
|
+
// A continuous family is measured THROUGH ITS ALIAS, on the fine candidate
|
|
6998
|
+
// grid: the axis answers at every notch, so the finest one that still moves
|
|
6999
|
+
// body text wins, and the range stops where the family's own axis does.
|
|
7000
|
+
var shape = familyWeightProfile(
|
|
7001
|
+
families[index],
|
|
7002
|
+
base,
|
|
7003
|
+
index === 0 && promoted ? alias : "",
|
|
7004
|
+
keepEven ? (index === 0 ? WEIGHT_SAMPLE_LATIN : WEIGHT_SAMPLE_FALLBACK) : null
|
|
7005
|
+
);
|
|
7006
|
+
if (shape !== null) shapes.push(shape);
|
|
7007
|
+
}
|
|
7008
|
+
// `base` is body text: the weight the two families' ink is compared at.
|
|
7009
|
+
var combined = shared.combineWeightProfiles(shapes, { base: shared.WEIGHT_BASE });
|
|
7010
|
+
if (combined !== null) {
|
|
7011
|
+
profile = combined;
|
|
7012
|
+
} else if (families.length > 0) {
|
|
7013
|
+
// Nothing measured (no canvas, no 2D context): the nominal window it is.
|
|
6570
7014
|
profile = promoted
|
|
6571
7015
|
? shared.continuousWeightProfile()
|
|
6572
7016
|
: { min: WEIGHT_DELTA_MIN, max: WEIGHT_DELTA_MAX, step: 1 };
|
|
@@ -10859,9 +11303,18 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
10859
11303
|
* The hint is what `harvestScope` names the dialog rules by where the host has
|
|
10860
11304
|
* it; the substring form is the same fallback the stylesheet builder uses, so
|
|
10861
11305
|
* the panel measures the very element the type axes already style.
|
|
11306
|
+
*
|
|
11307
|
+
* DSH 0.2.1-rc.4 and 0.2.2 dropped BOTH of them — no `_markdown_` class-hash
|
|
11308
|
+
* segment and no `[data-dss-prose]` wrapper — which left the panel with no
|
|
11309
|
+
* candidate region at all: it fell back to the whole window and the dot parked
|
|
11310
|
+
* on the window's corner instead of the conversation's. Those lines still mark
|
|
11311
|
+
* the content container, so its block elements are the third anchor, the same
|
|
11312
|
+
* shape the stylesheet builder grew (`MARKDOWN_BLOCK_ELEMENTS`).
|
|
10862
11313
|
*/
|
|
10863
11314
|
var PROSE_HINT = "[data-dss-prose]";
|
|
10864
11315
|
var PROSE_FALLBACK = '[class*="_markdown_" i]';
|
|
11316
|
+
var PROSE_CONTENT =
|
|
11317
|
+
"[data-conversation-content] :where(p,li,h1,h2,h3,h4,h5,h6,blockquote,td,th)";
|
|
10865
11318
|
|
|
10866
11319
|
/**
|
|
10867
11320
|
* How many candidate regions are read before the list is handed over.
|
|
@@ -10941,6 +11394,7 @@ function mountFloatPanel(scope, t, hostCtx) {
|
|
|
10941
11394
|
try {
|
|
10942
11395
|
add(document.querySelectorAll(PROSE_HINT));
|
|
10943
11396
|
add(document.querySelectorAll(PROSE_FALLBACK));
|
|
11397
|
+
add(document.querySelectorAll(PROSE_CONTENT));
|
|
10944
11398
|
} catch (error) {
|
|
10945
11399
|
// A page without a queryable document keeps the empty list (the window).
|
|
10946
11400
|
}
|