render-tag 0.1.29 → 0.1.30
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/README.md +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/layout.d.ts +13 -0
- package/lib/layout.d.ts.map +1 -1
- package/lib/layout.js +219 -107
- package/lib/layout.js.map +1 -1
- package/lib/render-tag.umd.js +7 -7
- package/lib/render-tag.umd.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -198,6 +198,7 @@ li::marker { content: none; font-size: 0; line-height: 0; }
|
|
|
198
198
|
|
|
199
199
|
- **Chrome-first.** When a rendering choice must favor one browser over another, Chrome wins.
|
|
200
200
|
- **Cross-browser consistency over per-browser DOM fidelity.** Same canvas output in every browser, not pixel-matching each browser's quirks. Use `accuracy: 'balanced'` if you'd rather match each browser's own DOM rendering.
|
|
201
|
+
- **The line-box baseline follows the engine's own rounding.** Blink floors half-leading + ascent onto a whole CSS pixel (`FontHeight::AddLeading`); Gecko and WebKit keep the exact value. render-tag does what the engine it runs in does, so canvas text lands on the baseline that browser's own DOM would use — which is what keeps a canvas render and a contenteditable overlay of the same text on one line. Safari is the closest fit rather than a match: its canvas metrics disagree with its own layout metrics, so no canvas-side rule reaches its DOM exactly. This is the one place per-browser fidelity wins over identical output.
|
|
201
202
|
|
|
202
203
|
## License
|
|
203
204
|
|
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { RenderConfig, RenderResult, LayoutConfig, LayoutResult, DrawConfig, LayoutLine, AnyCanvas } from './types.js';
|
|
2
2
|
export type { RenderConfig, RenderResult, LayoutConfig, LayoutResult, DrawConfig, LayoutLine };
|
|
3
3
|
export { setDOMParser, type DOMParserLike } from './dom.js';
|
|
4
|
+
export { FLOORS_LINE_BASELINE } from './layout.js';
|
|
4
5
|
/**
|
|
5
6
|
* Compute layout for an HTML string without rendering.
|
|
6
7
|
* Returns a reusable LayoutResult that can be drawn onto multiple targets via drawLayout().
|
package/lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EAAE,YAAY,EAC1B,YAAY,EAAE,YAAY,EAAE,UAAU,EACtC,UAAU,EAAE,SAAS,EACtB,MAAM,YAAY,CAAC;AAMpB,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC;AAC/F,OAAO,EAAE,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EAAE,YAAY,EAC1B,YAAY,EAAE,YAAY,EAAE,UAAU,EACtC,UAAU,EAAE,SAAS,EACtB,MAAM,YAAY,CAAC;AAMpB,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC;AAC/F,OAAO,EAAE,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,UAAU,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAWnD;;;GAGG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CAgCzD;AAID;;;GAGG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,CAsCpE;AAID;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CA8BzD"}
|
package/lib/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { resolveStylesFromCSS } from './css-resolver.js';
|
|
|
3
3
|
import { buildLayoutTree } from './layout.js';
|
|
4
4
|
import { renderNode } from './render.js';
|
|
5
5
|
export { setDOMParser } from './dom.js';
|
|
6
|
+
export { FLOORS_LINE_BASELINE } from './layout.js';
|
|
6
7
|
import { createFallbackMeasureCtx } from './dom.js';
|
|
7
8
|
// Default measurement context, created lazily and reused across layout()
|
|
8
9
|
// calls — safe because font/letterSpacing state is set before every
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/lib/layout.d.ts
CHANGED
|
@@ -4,6 +4,19 @@ import type { StyledNode, LayoutBox, ResolvedStyle, LayoutLine, DecorationEntry
|
|
|
4
4
|
*/
|
|
5
5
|
export declare function applyFont(ctx: CanvasRenderingContext2D, style: ResolvedStyle): void;
|
|
6
6
|
export declare function buildCanvasFont(style: ResolvedStyle): string;
|
|
7
|
+
/**
|
|
8
|
+
* True where the engine floors a line's baseline onto a whole CSS pixel.
|
|
9
|
+
*
|
|
10
|
+
* Blink alone does (`FontHeight::AddLeading`). Gecko and WebKit both lay the
|
|
11
|
+
* exact half-leading out — measured over the whole 530-case corpus, giving
|
|
12
|
+
* Safari the Blink branch cost 214 wins against 223 losses (avg 7.50% ->
|
|
13
|
+
* 9.21%) where the exact value wins 48 against 3 (7.50% -> 6.52%).
|
|
14
|
+
*
|
|
15
|
+
* Public API: the parity suites expect per engine, and every renderer that has
|
|
16
|
+
* to place a baseline beside a render-tag canvas (@polotno/svg-export) must
|
|
17
|
+
* round the same way this does.
|
|
18
|
+
*/
|
|
19
|
+
export declare const FLOORS_LINE_BASELINE: boolean;
|
|
7
20
|
export declare function isTransparent(color: string): boolean;
|
|
8
21
|
export declare function getFontMetrics(ctx: CanvasRenderingContext2D, style: ResolvedStyle): {
|
|
9
22
|
ascent: number;
|
package/lib/layout.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../src/layout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAc,SAAS,EAAc,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AA4C5H;;GAEG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,wBAAwB,EAAE,KAAK,EAAE,aAAa,GAAG,IAAI,CAGnF;AAgBD,wBAAgB,eAAe,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAc5D;
|
|
1
|
+
{"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../src/layout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAc,SAAS,EAAc,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AA4C5H;;GAEG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,wBAAwB,EAAE,KAAK,EAAE,aAAa,GAAG,IAAI,CAGnF;AAgBD,wBAAgB,eAAe,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAc5D;AA4GD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,oBAAoB,SAAW,CAAC;AAmF7C,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEpD;AAMD,wBAAgB,cAAc,CAAC,GAAG,EAAE,wBAAwB,EAAE,KAAK,EAAE,aAAa,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAWvH;AA0DD,uEAAuE;AACvE,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,eAAe,EAAE,CAAC,EAAE,eAAe,GAAG,OAAO,CAiBlF;AA4CD;;kFAEkF;AAClF,wBAAgB,WAAW,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAIzD;AA4tED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,wBAAwB,EAC7B,UAAU,EAAE,UAAU,EACtB,cAAc,EAAE,MAAM,EACtB,kBAAkB,UAAO,EACzB,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,YAAY,EAAE,UAAU,KAAK,IAAI,GACvD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,UAAU,EAAE,CAAA;CAAE,CAmD1D"}
|
package/lib/layout.js
CHANGED
|
@@ -157,12 +157,80 @@ function getLineHeight(ctx, style, useBulletProbe = false) {
|
|
|
157
157
|
return ascent + descent;
|
|
158
158
|
}
|
|
159
159
|
/**
|
|
160
|
-
*
|
|
161
|
-
*
|
|
160
|
+
* Which engine's line rules to follow. Only the UA string can say, because
|
|
161
|
+
* `accuracy: 'performance'` promises not to touch the DOM: Gecko is the one
|
|
162
|
+
* engine that still sends a real `Gecko/<date>` product token (Blink and
|
|
163
|
+
* WebKit carry only the "like Gecko" comment, which has no slash), and Blink
|
|
164
|
+
* is the one that says `Chrome/` — matched with NO word boundary, because
|
|
165
|
+
* headless Chrome says `HeadlessChrome/`. Safari sends neither token, which is
|
|
166
|
+
* how it is told apart. With no navigator (Node, workers) we take the Blink
|
|
167
|
+
* branch, for the same target.
|
|
162
168
|
*/
|
|
163
|
-
|
|
169
|
+
const UA = typeof navigator === 'undefined' ? '' : navigator.userAgent;
|
|
170
|
+
const IS_BLINK = UA === '' || /Chrome\/\d/.test(UA);
|
|
171
|
+
const IS_GECKO = /\bGecko\/\d/.test(UA);
|
|
172
|
+
/**
|
|
173
|
+
* True where the engine floors a line's baseline onto a whole CSS pixel.
|
|
174
|
+
*
|
|
175
|
+
* Blink alone does (`FontHeight::AddLeading`). Gecko and WebKit both lay the
|
|
176
|
+
* exact half-leading out — measured over the whole 530-case corpus, giving
|
|
177
|
+
* Safari the Blink branch cost 214 wins against 223 losses (avg 7.50% ->
|
|
178
|
+
* 9.21%) where the exact value wins 48 against 3 (7.50% -> 6.52%).
|
|
179
|
+
*
|
|
180
|
+
* Public API: the parity suites expect per engine, and every renderer that has
|
|
181
|
+
* to place a baseline beside a render-tag canvas (@polotno/svg-export) must
|
|
182
|
+
* round the same way this does.
|
|
183
|
+
*/
|
|
184
|
+
export const FLOORS_LINE_BASELINE = IS_BLINK;
|
|
185
|
+
/**
|
|
186
|
+
* `super` and `sub` are engine constants, not CSS. Blink and WebKit share
|
|
187
|
+
* theirs (`fontSize/3 + 1`, `fontSize/5 + 1`); Gecko raises by 0.34em and
|
|
188
|
+
* lowers by 0.20em. This is a separate question from the baseline rounding
|
|
189
|
+
* above — Safari rounds like nobody and shifts like Blink.
|
|
190
|
+
*/
|
|
191
|
+
const BLINK_SUPER_SUB = !IS_GECKO;
|
|
192
|
+
/**
|
|
193
|
+
* Baseline offset from the top of a line box, the way the engine places it.
|
|
194
|
+
*
|
|
195
|
+
* The CSS half-leading is `(lineHeight - (ascent + descent)) / 2`, and the
|
|
196
|
+
* baseline sits that far below the line top, plus the ascent. Blink FLOORS that
|
|
197
|
+
* sum to a whole CSS pixel (`FontHeight::AddLeading`), so its DOM text stands up
|
|
198
|
+
* to 1px HIGHER than the exact value. Gecko lays the exact value out in app
|
|
199
|
+
* units. WebKit floors on 80 of 90 measured size × line-height combinations and
|
|
200
|
+
* has no exact rule we can state, so it takes the Blink branch — the one that
|
|
201
|
+
* fits it best, not one it matches everywhere.
|
|
202
|
+
*
|
|
203
|
+
* So the rounding is the engine's, not a style choice: each browser's canvas
|
|
204
|
+
* lands on the baseline that browser's own DOM would use, which is what keeps a
|
|
205
|
+
* canvas render and a contenteditable overlay of the same text on one line.
|
|
206
|
+
*/
|
|
207
|
+
function lineBaselineOffset(lineHeight, ascent, descent) {
|
|
208
|
+
const exact = (lineHeight - (ascent + descent)) / 2 + ascent;
|
|
209
|
+
return FLOORS_LINE_BASELINE ? Math.floor(exact) : exact;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* The vertical space an inline-block's margin box adds around its content, over
|
|
213
|
+
* and above the font's own leading. Written once because the wrap pass grows
|
|
214
|
+
* the line by the same six values.
|
|
215
|
+
*/
|
|
216
|
+
function inlineBlockExtra(bs) {
|
|
217
|
+
return {
|
|
218
|
+
top: bs.marginTop + bs.borderTopWidth + bs.paddingTop,
|
|
219
|
+
bottom: bs.paddingBottom + bs.borderBottomWidth + bs.marginBottom,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* One box's half of a line: how far it reaches above its own baseline and how
|
|
224
|
+
* far below, over its OWN line-height. This is the inline box CSS 2.1 §10.8
|
|
225
|
+
* talks about — the font's content area plus its half-leading — not the bare
|
|
226
|
+
* font metrics. `vertical-align: text-top` and `text-bottom` align THIS box's
|
|
227
|
+
* edges, and the line box is the union of these over everything on the line.
|
|
228
|
+
*/
|
|
229
|
+
function leadedBox(ctx, style, useBulletProbe = false) {
|
|
164
230
|
const { ascent, descent } = getFontMetrics(ctx, style);
|
|
165
|
-
|
|
231
|
+
const lineHeight = getLineHeight(ctx, style, useBulletProbe);
|
|
232
|
+
const boxAscent = lineBaselineOffset(lineHeight, ascent, descent);
|
|
233
|
+
return { ascent: boxAscent, descent: lineHeight - boxAscent };
|
|
166
234
|
}
|
|
167
235
|
function applyTextTransform(text, transform) {
|
|
168
236
|
switch (transform) {
|
|
@@ -210,25 +278,48 @@ export function getFontMetrics(ctx, style) {
|
|
|
210
278
|
* pass (the box position depends on the final line box it helps size), so they
|
|
211
279
|
* fall back to baseline rather than being approximated wrongly.
|
|
212
280
|
*
|
|
213
|
-
* - super/sub
|
|
214
|
-
*
|
|
281
|
+
* - super/sub the engine's own rule, measured off the DOM across
|
|
282
|
+
* 8-56px × sans-serif/serif/monospace and fitting every
|
|
283
|
+
* point to within 0.06px (LayoutUnit's 1/64). Neither
|
|
284
|
+
* engine reads the font's metrics — the family does not
|
|
285
|
+
* move the number.
|
|
286
|
+
* - text-top/-bottom the box's LEADED edge against the parent's CONTENT-area
|
|
287
|
+
* edge (bare ascent/descent, no leading). Taking the box's
|
|
288
|
+
* bare metrics instead costs 25px on a line holding both.
|
|
215
289
|
* - middle box midpoint at parent baseline + half the x-height
|
|
216
290
|
* - <length>/<%> raise (positive value) by the length / % of line-height
|
|
217
291
|
*/
|
|
218
|
-
function verticalAlignShift(va,
|
|
292
|
+
function verticalAlignShift(va, ctx, style, parentStyle, useBulletProbe) {
|
|
219
293
|
switch (va) {
|
|
220
|
-
case 'super':
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
case '
|
|
224
|
-
|
|
294
|
+
case 'super':
|
|
295
|
+
return BLINK_SUPER_SUB
|
|
296
|
+
? -(parentStyle.fontSize / 3 + 1) : -parentStyle.fontSize * 0.34;
|
|
297
|
+
case 'sub':
|
|
298
|
+
return BLINK_SUPER_SUB
|
|
299
|
+
? parentStyle.fontSize / 5 + 1 : parentStyle.fontSize * 0.2;
|
|
300
|
+
// Against the PARENT's content area (CSS 2.1 §10.8.1) — its bare
|
|
301
|
+
// ascent/descent, no leading. Measured against Chrome, taking the line's
|
|
302
|
+
// tallest box instead of the real parent put this 14px out.
|
|
303
|
+
case 'text-top':
|
|
304
|
+
return leadedBox(ctx, style, useBulletProbe).ascent - getFontMetrics(ctx, parentStyle).ascent;
|
|
305
|
+
case 'text-bottom':
|
|
306
|
+
return getFontMetrics(ctx, parentStyle).descent - leadedBox(ctx, style, useBulletProbe).descent;
|
|
307
|
+
case 'middle': {
|
|
308
|
+
const { ascent, descent } = getFontMetrics(ctx, style);
|
|
309
|
+
return -(parentStyle.fontSize * 0.25) - (descent - ascent) / 2;
|
|
310
|
+
}
|
|
225
311
|
default: {
|
|
226
|
-
// baseline / top / bottom / '' all parseFloat to NaN → 0
|
|
227
|
-
//
|
|
312
|
+
// baseline / top / bottom / '' all parseFloat to NaN → 0, which is what
|
|
313
|
+
// an unshifted run wants — the line-box pass calls this for every word.
|
|
228
314
|
const n = parseFloat(va);
|
|
229
315
|
if (!Number.isFinite(n))
|
|
230
316
|
return 0;
|
|
231
|
-
|
|
317
|
+
// A percentage resolves against the ELEMENT's own line-height (CSS 2.1
|
|
318
|
+
// §10.8.1), not the line's. Measured against Chrome: the line's put the
|
|
319
|
+
// box 10px out on a line whose tallest run was not this one.
|
|
320
|
+
return va.endsWith('%')
|
|
321
|
+
? -(n / 100) * getLineHeight(ctx, style, useBulletProbe)
|
|
322
|
+
: -n;
|
|
232
323
|
}
|
|
233
324
|
}
|
|
234
325
|
}
|
|
@@ -351,6 +442,9 @@ function applyEllipsisToLine(ctx, line, maxWidth) {
|
|
|
351
442
|
return;
|
|
352
443
|
const lastStyle = line.words[styleIdx].style;
|
|
353
444
|
const boxStyle = line.words[styleIdx].boxStyle;
|
|
445
|
+
// The ellipsis takes the trimmed run's style, so it has to take the parent
|
|
446
|
+
// that style's vertical-align measures against too.
|
|
447
|
+
const parentStyle = line.words[styleIdx].parentStyle;
|
|
354
448
|
applyFont(ctx, lastStyle);
|
|
355
449
|
// ALWAYS assign (don't gate on truthy) — otherwise a previous segment's
|
|
356
450
|
// non-zero letter-spacing leaks into the ellipsis measurement.
|
|
@@ -386,6 +480,7 @@ function applyEllipsisToLine(ctx, line, maxWidth) {
|
|
|
386
480
|
text: '…',
|
|
387
481
|
width: ellipsisWidth,
|
|
388
482
|
style: lastStyle,
|
|
483
|
+
parentStyle,
|
|
389
484
|
isSpace: false,
|
|
390
485
|
boxStyle,
|
|
391
486
|
};
|
|
@@ -400,9 +495,15 @@ function applyEllipsisToLine(ctx, line, maxWidth) {
|
|
|
400
495
|
*/
|
|
401
496
|
function collectTextRuns(node) {
|
|
402
497
|
const runs = [];
|
|
403
|
-
function walk(n, boxStyle, clipStyle, strokeImageStyle) {
|
|
498
|
+
function walk(n, boxStyle, clipStyle, strokeImageStyle, parentStyle) {
|
|
404
499
|
if (n.tagName === '#text' && n.textContent) {
|
|
405
|
-
|
|
500
|
+
// A #text node carries its parent ELEMENT's style, so the element that
|
|
501
|
+
// owns any vertical-align here is that parent — and what the shift
|
|
502
|
+
// measures against is ITS parent, which is the `parentStyle` handed to
|
|
503
|
+
// this element's walk.
|
|
504
|
+
runs.push({
|
|
505
|
+
text: n.textContent, style: n.style, parentStyle, boxStyle, clipStyle, strokeImageStyle,
|
|
506
|
+
});
|
|
406
507
|
return;
|
|
407
508
|
}
|
|
408
509
|
const isInlineBlock = n.style.display === 'inline-block';
|
|
@@ -425,6 +526,7 @@ function collectTextRuns(node) {
|
|
|
425
526
|
runs.push({
|
|
426
527
|
text: allText,
|
|
427
528
|
style: n.style,
|
|
529
|
+
parentStyle,
|
|
428
530
|
boxStyle: newBoxStyle,
|
|
429
531
|
clipStyle: newClipStyle,
|
|
430
532
|
strokeImageStyle: newStrokeImageStyle,
|
|
@@ -445,7 +547,11 @@ function collectTextRuns(node) {
|
|
|
445
547
|
runs.push({ text: '', style: n.style, boxStyle: newBoxStyle, boxOpen: n.style });
|
|
446
548
|
}
|
|
447
549
|
for (const child of n.children) {
|
|
448
|
-
walk(child, isBox ? newBoxStyle : boxStyle, newClipStyle, newStrokeImageStyle
|
|
550
|
+
walk(child, isBox ? newBoxStyle : boxStyle, newClipStyle, newStrokeImageStyle,
|
|
551
|
+
// An element child measures against this element; a text child's
|
|
552
|
+
// vertical-align belongs to this element, so it measures against what
|
|
553
|
+
// this element measures against.
|
|
554
|
+
child.tagName === '#text' ? parentStyle : n.style);
|
|
449
555
|
}
|
|
450
556
|
if (hasHorizSpacing) {
|
|
451
557
|
runs.push({ text: '', style: n.style, boxStyle: newBoxStyle, boxClose: n.style });
|
|
@@ -465,8 +571,9 @@ function collectTextRuns(node) {
|
|
|
465
571
|
runs.push(...seg);
|
|
466
572
|
}
|
|
467
573
|
}
|
|
574
|
+
// The block itself is the parent every top-level run measures against.
|
|
468
575
|
for (const child of node.children) {
|
|
469
|
-
walk(child);
|
|
576
|
+
walk(child, undefined, undefined, undefined, node.style);
|
|
470
577
|
}
|
|
471
578
|
return runs;
|
|
472
579
|
}
|
|
@@ -550,6 +657,7 @@ function tokenizeString(ctx, text, run, allWords, cumState) {
|
|
|
550
657
|
text: '\t',
|
|
551
658
|
width: tabStopInterval, // placeholder — recalculated in flowWordsIntoLines
|
|
552
659
|
style: run.style,
|
|
660
|
+
parentStyle: run.parentStyle,
|
|
553
661
|
isSpace: true,
|
|
554
662
|
isTab: true,
|
|
555
663
|
boxStyle: run.boxStyle,
|
|
@@ -563,6 +671,7 @@ function tokenizeString(ctx, text, run, allWords, cumState) {
|
|
|
563
671
|
text: w,
|
|
564
672
|
width: cachedMeasureWidth(ctx, w),
|
|
565
673
|
style: run.style,
|
|
674
|
+
parentStyle: run.parentStyle,
|
|
566
675
|
isSpace,
|
|
567
676
|
boxStyle: run.boxStyle,
|
|
568
677
|
clipStyle: run.clipStyle,
|
|
@@ -600,6 +709,7 @@ function tokenizeString(ctx, text, run, allWords, cumState) {
|
|
|
600
709
|
text: ' ',
|
|
601
710
|
width: spaceWidth,
|
|
602
711
|
style: run.style,
|
|
712
|
+
parentStyle: run.parentStyle,
|
|
603
713
|
isSpace: true,
|
|
604
714
|
boxStyle: run.boxStyle,
|
|
605
715
|
clipStyle: run.clipStyle,
|
|
@@ -620,6 +730,7 @@ function tokenizeString(ctx, text, run, allWords, cumState) {
|
|
|
620
730
|
text: s,
|
|
621
731
|
width: cumWidth - prevCum,
|
|
622
732
|
style: run.style,
|
|
733
|
+
parentStyle: run.parentStyle,
|
|
623
734
|
isSpace: false,
|
|
624
735
|
boxStyle: run.boxStyle,
|
|
625
736
|
clipStyle: run.clipStyle,
|
|
@@ -645,6 +756,7 @@ function tokenizeString(ctx, text, run, allWords, cumState) {
|
|
|
645
756
|
text: w,
|
|
646
757
|
width,
|
|
647
758
|
style: run.style,
|
|
759
|
+
parentStyle: run.parentStyle,
|
|
648
760
|
isSpace: false,
|
|
649
761
|
boxStyle: run.boxStyle,
|
|
650
762
|
clipStyle: run.clipStyle,
|
|
@@ -688,6 +800,7 @@ function tokenizeRuns(ctx, runs) {
|
|
|
688
800
|
text,
|
|
689
801
|
width: totalWidth,
|
|
690
802
|
style: run.style,
|
|
803
|
+
parentStyle: run.parentStyle,
|
|
691
804
|
isSpace: false,
|
|
692
805
|
boxStyle: run.boxStyle,
|
|
693
806
|
boxOpen: run.boxOpen,
|
|
@@ -962,6 +1075,7 @@ function flowWordsIntoLines(ctx, words, contentWidth, whiteSpace, useBulletProbe
|
|
|
962
1075
|
text: '-',
|
|
963
1076
|
width: hyphenWidth,
|
|
964
1077
|
style: lastWord.style,
|
|
1078
|
+
parentStyle: lastWord.parentStyle,
|
|
965
1079
|
isSpace: false,
|
|
966
1080
|
// The visible hyphen continues the broken word, so it inherits the
|
|
967
1081
|
// word's clip/stroke-image declarer (else it paints transparent).
|
|
@@ -991,9 +1105,11 @@ function flowWordsIntoLines(ctx, words, contentWidth, whiteSpace, useBulletProbe
|
|
|
991
1105
|
let wordLineHeight = getLineHeight(ctx, word.style, useBulletProbe);
|
|
992
1106
|
// Inline-block elements expand line height with their vertical padding+margin
|
|
993
1107
|
if (word.boxStyle && word.boxStyle.display === 'inline-block') {
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1108
|
+
// Clamped at 0: negative margins shrink the margin box, but the original
|
|
1109
|
+
// `Math.max(h, h + extra)` never let them shrink the LINE, and nothing
|
|
1110
|
+
// here is measuring a case that says they should.
|
|
1111
|
+
const extra = inlineBlockExtra(word.boxStyle);
|
|
1112
|
+
wordLineHeight += Math.max(0, extra.top + extra.bottom);
|
|
997
1113
|
}
|
|
998
1114
|
if (word.text === '\n') {
|
|
999
1115
|
if (currentLine.words.length === 0) {
|
|
@@ -1042,6 +1158,7 @@ function flowWordsIntoLines(ctx, words, contentWidth, whiteSpace, useBulletProbe
|
|
|
1042
1158
|
cells.push({
|
|
1043
1159
|
ch,
|
|
1044
1160
|
style: words[j].style,
|
|
1161
|
+
parentStyle: words[j].parentStyle,
|
|
1045
1162
|
clipStyle: words[j].clipStyle,
|
|
1046
1163
|
strokeImageStyle: words[j].strokeImageStyle,
|
|
1047
1164
|
});
|
|
@@ -1076,6 +1193,7 @@ function flowWordsIntoLines(ctx, words, contentWidth, whiteSpace, useBulletProbe
|
|
|
1076
1193
|
// word), so capturing it at the run start covers every push below.
|
|
1077
1194
|
const clipStyle = cs[i].clipStyle;
|
|
1078
1195
|
const strokeImageStyle = cs[i].strokeImageStyle;
|
|
1196
|
+
const parentStyle = cs[i].parentStyle;
|
|
1079
1197
|
applyFont(ctx, st);
|
|
1080
1198
|
ctx.letterSpacing = formatLetterSpacing(st.letterSpacing);
|
|
1081
1199
|
const lh = getLineHeight(ctx, st, useBulletProbe);
|
|
@@ -1088,7 +1206,7 @@ function flowWordsIntoLines(ctx, words, contentWidth, whiteSpace, useBulletProbe
|
|
|
1088
1206
|
if (chars && currentLine.totalWidth + candW > effWidth() &&
|
|
1089
1207
|
(currentLine.words.length > 0 || cur)) {
|
|
1090
1208
|
if (cur) {
|
|
1091
|
-
currentLine.words.push({ text: cur, width: curW, style: st, isSpace: false, clipStyle, strokeImageStyle });
|
|
1209
|
+
currentLine.words.push({ text: cur, width: curW, style: st, isSpace: false, parentStyle, clipStyle, strokeImageStyle });
|
|
1092
1210
|
currentLine.totalWidth += curW;
|
|
1093
1211
|
currentLine.lineHeight = Math.max(currentLine.lineHeight, lh);
|
|
1094
1212
|
}
|
|
@@ -1104,7 +1222,7 @@ function flowWordsIntoLines(ctx, words, contentWidth, whiteSpace, useBulletProbe
|
|
|
1104
1222
|
i++;
|
|
1105
1223
|
}
|
|
1106
1224
|
if (cur) {
|
|
1107
|
-
currentLine.words.push({ text: cur, width: curW, style: st, isSpace: false, clipStyle, strokeImageStyle });
|
|
1225
|
+
currentLine.words.push({ text: cur, width: curW, style: st, isSpace: false, parentStyle, clipStyle, strokeImageStyle });
|
|
1108
1226
|
currentLine.totalWidth += curW;
|
|
1109
1227
|
currentLine.lineHeight = Math.max(currentLine.lineHeight, lh);
|
|
1110
1228
|
afterHardBreak = false;
|
|
@@ -1458,7 +1576,9 @@ function layoutInlineContent(ctx, node, x, y, contentWidth, useBulletProbe = fal
|
|
|
1458
1576
|
// only content is a SMALLER inline font sits on the strut baseline (lower in
|
|
1459
1577
|
// the box), not centered in it. Seed each line's ascent/descent with the
|
|
1460
1578
|
// block font's metrics so the baseline lands where the DOM puts it.
|
|
1461
|
-
|
|
1579
|
+
// The block is the parent of any run with no inline ancestor, and the emit
|
|
1580
|
+
// loop shadows `node` with the LayoutText it builds.
|
|
1581
|
+
const blockStyle = node.style;
|
|
1462
1582
|
let curY = y;
|
|
1463
1583
|
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
1464
1584
|
const line = lines[lineIdx];
|
|
@@ -1466,7 +1586,6 @@ function layoutInlineContent(ctx, node, x, y, contentWidth, useBulletProbe = fal
|
|
|
1466
1586
|
curY += line.lineHeight;
|
|
1467
1587
|
continue;
|
|
1468
1588
|
}
|
|
1469
|
-
const lineHeight = line.lineHeight;
|
|
1470
1589
|
const isLastLine = lineIdx === lines.length - 1;
|
|
1471
1590
|
const isFirstLine = lineIdx === 0;
|
|
1472
1591
|
// Per-line alignment: lines ending at a forced break or the last line
|
|
@@ -1517,85 +1636,78 @@ function layoutInlineContent(ctx, node, x, y, contentWidth, useBulletProbe = fal
|
|
|
1517
1636
|
const lineLeftX = curX;
|
|
1518
1637
|
// Inline background boxes and text are emitted after baseline computation
|
|
1519
1638
|
// (below) so that emitInlineBox can use line-level metrics for alignment.
|
|
1520
|
-
//
|
|
1521
|
-
//
|
|
1522
|
-
//
|
|
1523
|
-
//
|
|
1524
|
-
//
|
|
1525
|
-
|
|
1526
|
-
let
|
|
1639
|
+
// The line box is the union of every box on it — strut, run, shifted run,
|
|
1640
|
+
// inline-block — each carrying its own leading over its own line-height:
|
|
1641
|
+
// lineAscent = max(ascent - shift), lineDescent = max(descent + shift).
|
|
1642
|
+
// One font, one line-height and no shift collapse that back to the plain
|
|
1643
|
+
// half-leading every single-style line already had.
|
|
1644
|
+
const strutBox = leadedBox(ctx, node.style, useBulletProbe);
|
|
1645
|
+
let lineAscent = strutBox.ascent;
|
|
1646
|
+
let lineDescent = strutBox.descent;
|
|
1527
1647
|
for (const word of line.words) {
|
|
1528
1648
|
if (word.text === '')
|
|
1529
1649
|
continue;
|
|
1530
|
-
//
|
|
1531
|
-
//
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
maxDescent = descent;
|
|
1548
|
-
break;
|
|
1650
|
+
// A wrapper element with no text of its own — `<span lh:3><span>x</span>`
|
|
1651
|
+
// — never becomes a Word, but it is still a box on the line and still
|
|
1652
|
+
// brings its own line-height. Its run children carry it as `parentStyle`,
|
|
1653
|
+
// so take it from there, AT ITS OWN SHIFT: added unshifted, a wrapper
|
|
1654
|
+
// that carries a vertical-align and direct text enters the union twice
|
|
1655
|
+
// at two different places, and the line spans both (measured 60px where
|
|
1656
|
+
// the DOM has 40). With the shift it is idempotent — a wrapper with
|
|
1657
|
+
// direct text contributes the identical box through its own run.
|
|
1658
|
+
if (word.parentStyle) {
|
|
1659
|
+
const parentBox = leadedBox(ctx, word.parentStyle, useBulletProbe);
|
|
1660
|
+
const parentShift = verticalAlignShift(word.parentStyle.verticalAlign, ctx, word.parentStyle, blockStyle, useBulletProbe);
|
|
1661
|
+
if (parentBox.ascent - parentShift > lineAscent) {
|
|
1662
|
+
lineAscent = parentBox.ascent - parentShift;
|
|
1663
|
+
}
|
|
1664
|
+
if (parentBox.descent + parentShift > lineDescent) {
|
|
1665
|
+
lineDescent = parentBox.descent + parentShift;
|
|
1666
|
+
}
|
|
1549
1667
|
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
verticalAlignShift(va, wAscent, wDescent, parentFontSize, maxAscent, maxDescent, lineHeight);
|
|
1575
|
-
const wordTop = shiftedBaseline - wAscent;
|
|
1576
|
-
const wordBottom = shiftedBaseline + wDescent;
|
|
1577
|
-
if (wordTop < lineTop)
|
|
1578
|
-
lineTop = wordTop;
|
|
1579
|
-
if (wordBottom > lineBottom)
|
|
1580
|
-
lineBottom = wordBottom;
|
|
1581
|
-
}
|
|
1582
|
-
const effectiveLineHeight = lineBottom - lineTop;
|
|
1668
|
+
const box = leadedBox(ctx, word.style, useBulletProbe);
|
|
1669
|
+
// An inline-block joins the line as an ATOMIC box: its own content
|
|
1670
|
+
// baseline with its margin box stacked around it. It takes the extra
|
|
1671
|
+
// space, but no shift — the emit pass puts its content on the line
|
|
1672
|
+
// baseline and does not honour vertical-align on it, so shifting the box
|
|
1673
|
+
// here would grow the line one way while the paint went the other.
|
|
1674
|
+
const atomic = word.boxStyle?.display === 'inline-block' ? word.boxStyle : null;
|
|
1675
|
+
if (atomic) {
|
|
1676
|
+
const extra = inlineBlockExtra(atomic);
|
|
1677
|
+
box.ascent += extra.top;
|
|
1678
|
+
box.descent += extra.bottom;
|
|
1679
|
+
}
|
|
1680
|
+
// A shift moves the box, not the line's baseline: positive is downward,
|
|
1681
|
+
// so it lifts the box's demand on the ascent side and adds to the descent
|
|
1682
|
+
// side. The parent-size fallback is the one the emit pass uses, so a line
|
|
1683
|
+
// whose only content is shifted still sizes around it.
|
|
1684
|
+
const shift = atomic ? 0 : verticalAlignShift(word.style.verticalAlign, ctx, word.style, word.parentStyle ?? blockStyle, useBulletProbe);
|
|
1685
|
+
if (box.ascent - shift > lineAscent)
|
|
1686
|
+
lineAscent = box.ascent - shift;
|
|
1687
|
+
if (box.descent + shift > lineDescent)
|
|
1688
|
+
lineDescent = box.descent + shift;
|
|
1689
|
+
}
|
|
1690
|
+
const lineBoxHeight = lineAscent + lineDescent;
|
|
1691
|
+
const lineBaselineY = curY + lineAscent;
|
|
1583
1692
|
// Emit inline background box using line-level baseline for vertical alignment.
|
|
1584
1693
|
// Uses the line's ascent/descent (not the box's own font) so box aligns with text.
|
|
1585
1694
|
const emitInlineBox = (style, bx, bw) => {
|
|
1586
|
-
//
|
|
1587
|
-
//
|
|
1588
|
-
|
|
1695
|
+
// The box's OWN font decides its height, not the line's largest. An
|
|
1696
|
+
// inline-block's content box is its LINE-HEIGHT, though, not the bare
|
|
1697
|
+
// font metrics — measured against Chrome, bare metrics put it at
|
|
1698
|
+
// y=6 h=29 where the DOM has y=4 h=33.2.
|
|
1699
|
+
const { ascent: boxAscent, descent: boxDescent } = style.display === 'inline-block'
|
|
1700
|
+
? leadedBox(ctx, style, useBulletProbe)
|
|
1701
|
+
: getFontMetrics(ctx, style);
|
|
1589
1702
|
const padTop = style.paddingTop + style.borderTopWidth;
|
|
1590
1703
|
const padBottom = style.paddingBottom + style.borderBottomWidth;
|
|
1591
1704
|
const boxHeight = boxAscent + boxDescent + padTop + padBottom;
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
}
|
|
1705
|
+
// Every inline box hangs off the line's baseline, an inline-block too:
|
|
1706
|
+
// its content is emitted on that baseline, so a box pinned to the line
|
|
1707
|
+
// TOP instead detached from its own glyphs as soon as something taller
|
|
1708
|
+
// shared the line — measured, a background at y 4..33 around text whose
|
|
1709
|
+
// baseline was 46.
|
|
1710
|
+
const boxY = lineBaselineY - boxAscent - padTop;
|
|
1599
1711
|
results.push({
|
|
1600
1712
|
type: 'box', style, x: bx, y: boxY, width: bw, height: boxHeight,
|
|
1601
1713
|
tagName: 'span', children: [],
|
|
@@ -1786,9 +1898,7 @@ function layoutInlineContent(ctx, node, x, y, contentWidth, useBulletProbe = fal
|
|
|
1786
1898
|
let baselineY = lineBaselineY;
|
|
1787
1899
|
const va = word.style.verticalAlign;
|
|
1788
1900
|
if (isShiftedVAlign(va)) {
|
|
1789
|
-
|
|
1790
|
-
const { ascent: wA, descent: wD } = getFontMetrics(ctx, word.style);
|
|
1791
|
-
baselineY += verticalAlignShift(va, wA, wD, pfs, maxAscent, maxDescent, lineHeight);
|
|
1901
|
+
baselineY += verticalAlignShift(va, ctx, word.style, word.parentStyle ?? blockStyle, useBulletProbe);
|
|
1792
1902
|
}
|
|
1793
1903
|
const effectiveWidth = word.width + (word.isSpace ? justifyExtraPerSpace : 0);
|
|
1794
1904
|
const node = {
|
|
@@ -1822,14 +1932,15 @@ function layoutInlineContent(ctx, node, x, y, contentWidth, useBulletProbe = fal
|
|
|
1822
1932
|
text: line.words.map(w => w.text).join(''),
|
|
1823
1933
|
bounds: {
|
|
1824
1934
|
x: lineLeftX,
|
|
1825
|
-
//
|
|
1826
|
-
//
|
|
1827
|
-
|
|
1935
|
+
// The line box starts at curY: shifted content grew the box through
|
|
1936
|
+
// `lineAscent`/`lineDescent` above, so nothing on the line reaches
|
|
1937
|
+
// outside it.
|
|
1938
|
+
y: curY,
|
|
1828
1939
|
width: lineWidth,
|
|
1829
|
-
height:
|
|
1940
|
+
height: lineBoxHeight,
|
|
1830
1941
|
},
|
|
1831
1942
|
});
|
|
1832
|
-
curY +=
|
|
1943
|
+
curY += lineBoxHeight;
|
|
1833
1944
|
}
|
|
1834
1945
|
assignInlineFragmentBoxes(ctx, results, clipRuns, (node, s, box) => {
|
|
1835
1946
|
node.clip = {
|
|
@@ -2188,9 +2299,10 @@ function addListMarker(ctx, box, node) {
|
|
|
2188
2299
|
const ms = node.markerStyle;
|
|
2189
2300
|
const markerStyleObj = ms ? { ...style, ...ms } : style;
|
|
2190
2301
|
ctx.font = buildCanvasFont(markerStyleObj);
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2302
|
+
// ascent + descent is the li's line-height by construction, so one call
|
|
2303
|
+
// gives both the marker's baseline and the box it reports.
|
|
2304
|
+
const strut = leadedBox(ctx, style);
|
|
2305
|
+
const baselineY = box.y + style.borderTopWidth + style.paddingTop + strut.ascent;
|
|
2194
2306
|
const markerWidth = cachedMeasureWidth(ctx, node.listMarker);
|
|
2195
2307
|
const isRTL = style.direction === 'rtl';
|
|
2196
2308
|
const isBullet = BULLET_MARKERS.has(style.listStyleType);
|
|
@@ -2289,7 +2401,7 @@ function addListMarker(ctx, box, node) {
|
|
|
2289
2401
|
x: markerLeftX,
|
|
2290
2402
|
y: box.y + style.borderTopWidth + style.paddingTop,
|
|
2291
2403
|
width: markerDrawWidth,
|
|
2292
|
-
height:
|
|
2404
|
+
height: strut.ascent + strut.descent,
|
|
2293
2405
|
},
|
|
2294
2406
|
});
|
|
2295
2407
|
}
|