render-tag 0.1.29 → 0.1.31
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 +225 -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;AAmHD;;;;;;;;;;;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,86 @@ 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.
|
|
162
|
+
*
|
|
163
|
+
* Blink is the DEFAULT, and the other two are what we detect: a server-side
|
|
164
|
+
* render (no navigator, or jsdom) targets headless Chrome, so anything we
|
|
165
|
+
* cannot positively identify has to round the way Chrome does.
|
|
166
|
+
*
|
|
167
|
+
* - Gecko is the one engine that still sends a real `Gecko/<date>` product
|
|
168
|
+
* token; Blink and WebKit carry only the "like Gecko" comment, no slash.
|
|
169
|
+
* - Safari is WebKit that says neither `Chrome/` nor `jsdom/`. jsdom borrows
|
|
170
|
+
* WebKit's UA and would otherwise be mistaken for it.
|
|
171
|
+
* - `Chrome/` is matched with NO word boundary, because headless Chrome sends
|
|
172
|
+
* `HeadlessChrome/`.
|
|
173
|
+
*/
|
|
174
|
+
const UA = typeof navigator === 'undefined' ? '' : navigator.userAgent;
|
|
175
|
+
const IS_GECKO = /\bGecko\/\d/.test(UA);
|
|
176
|
+
const IS_SAFARI = /AppleWebKit/.test(UA) && !/Chrome\/\d/.test(UA) && !/\bjsdom\//.test(UA);
|
|
177
|
+
const IS_BLINK = !IS_GECKO && !IS_SAFARI;
|
|
178
|
+
/**
|
|
179
|
+
* True where the engine floors a line's baseline onto a whole CSS pixel.
|
|
180
|
+
*
|
|
181
|
+
* Blink alone does (`FontHeight::AddLeading`). Gecko and WebKit both lay the
|
|
182
|
+
* exact half-leading out — measured over the whole 530-case corpus, giving
|
|
183
|
+
* Safari the Blink branch cost 214 wins against 223 losses (avg 7.50% ->
|
|
184
|
+
* 9.21%) where the exact value wins 48 against 3 (7.50% -> 6.52%).
|
|
185
|
+
*
|
|
186
|
+
* Public API: the parity suites expect per engine, and every renderer that has
|
|
187
|
+
* to place a baseline beside a render-tag canvas (@polotno/svg-export) must
|
|
188
|
+
* round the same way this does.
|
|
189
|
+
*/
|
|
190
|
+
export const FLOORS_LINE_BASELINE = IS_BLINK;
|
|
191
|
+
/**
|
|
192
|
+
* `super` and `sub` are engine constants, not CSS. Blink and WebKit share
|
|
193
|
+
* theirs (`fontSize/3 + 1`, `fontSize/5 + 1`); Gecko raises by 0.34em and
|
|
194
|
+
* lowers by 0.20em. This is a separate question from the baseline rounding
|
|
195
|
+
* above — Safari rounds like nobody and shifts like Blink.
|
|
196
|
+
*/
|
|
197
|
+
const BLINK_SUPER_SUB = !IS_GECKO;
|
|
198
|
+
/**
|
|
199
|
+
* Baseline offset from the top of a line box, the way the engine places it.
|
|
200
|
+
*
|
|
201
|
+
* The CSS half-leading is `(lineHeight - (ascent + descent)) / 2`, and the
|
|
202
|
+
* baseline sits that far below the line top, plus the ascent. Blink FLOORS that
|
|
203
|
+
* sum to a whole CSS pixel (`FontHeight::AddLeading`), so its DOM text stands up
|
|
204
|
+
* to 1px HIGHER than the exact value. Gecko lays the exact value out in app
|
|
205
|
+
* units. WebKit floors on 80 of 90 measured size × line-height combinations and
|
|
206
|
+
* has no exact rule we can state, so it takes the Blink branch — the one that
|
|
207
|
+
* fits it best, not one it matches everywhere.
|
|
208
|
+
*
|
|
209
|
+
* So the rounding is the engine's, not a style choice: each browser's canvas
|
|
210
|
+
* lands on the baseline that browser's own DOM would use, which is what keeps a
|
|
211
|
+
* canvas render and a contenteditable overlay of the same text on one line.
|
|
212
|
+
*/
|
|
213
|
+
function lineBaselineOffset(lineHeight, ascent, descent) {
|
|
214
|
+
const exact = (lineHeight - (ascent + descent)) / 2 + ascent;
|
|
215
|
+
return FLOORS_LINE_BASELINE ? Math.floor(exact) : exact;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* The vertical space an inline-block's margin box adds around its content, over
|
|
219
|
+
* and above the font's own leading. Written once because the wrap pass grows
|
|
220
|
+
* the line by the same six values.
|
|
221
|
+
*/
|
|
222
|
+
function inlineBlockExtra(bs) {
|
|
223
|
+
return {
|
|
224
|
+
top: bs.marginTop + bs.borderTopWidth + bs.paddingTop,
|
|
225
|
+
bottom: bs.paddingBottom + bs.borderBottomWidth + bs.marginBottom,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* One box's half of a line: how far it reaches above its own baseline and how
|
|
230
|
+
* far below, over its OWN line-height. This is the inline box CSS 2.1 §10.8
|
|
231
|
+
* talks about — the font's content area plus its half-leading — not the bare
|
|
232
|
+
* font metrics. `vertical-align: text-top` and `text-bottom` align THIS box's
|
|
233
|
+
* edges, and the line box is the union of these over everything on the line.
|
|
162
234
|
*/
|
|
163
|
-
function
|
|
235
|
+
function leadedBox(ctx, style, useBulletProbe = false) {
|
|
164
236
|
const { ascent, descent } = getFontMetrics(ctx, style);
|
|
165
|
-
|
|
237
|
+
const lineHeight = getLineHeight(ctx, style, useBulletProbe);
|
|
238
|
+
const boxAscent = lineBaselineOffset(lineHeight, ascent, descent);
|
|
239
|
+
return { ascent: boxAscent, descent: lineHeight - boxAscent };
|
|
166
240
|
}
|
|
167
241
|
function applyTextTransform(text, transform) {
|
|
168
242
|
switch (transform) {
|
|
@@ -210,25 +284,48 @@ export function getFontMetrics(ctx, style) {
|
|
|
210
284
|
* pass (the box position depends on the final line box it helps size), so they
|
|
211
285
|
* fall back to baseline rather than being approximated wrongly.
|
|
212
286
|
*
|
|
213
|
-
* - super/sub
|
|
214
|
-
*
|
|
287
|
+
* - super/sub the engine's own rule, measured off the DOM across
|
|
288
|
+
* 8-56px × sans-serif/serif/monospace and fitting every
|
|
289
|
+
* point to within 0.06px (LayoutUnit's 1/64). Neither
|
|
290
|
+
* engine reads the font's metrics — the family does not
|
|
291
|
+
* move the number.
|
|
292
|
+
* - text-top/-bottom the box's LEADED edge against the parent's CONTENT-area
|
|
293
|
+
* edge (bare ascent/descent, no leading). Taking the box's
|
|
294
|
+
* bare metrics instead costs 25px on a line holding both.
|
|
215
295
|
* - middle box midpoint at parent baseline + half the x-height
|
|
216
296
|
* - <length>/<%> raise (positive value) by the length / % of line-height
|
|
217
297
|
*/
|
|
218
|
-
function verticalAlignShift(va,
|
|
298
|
+
function verticalAlignShift(va, ctx, style, parentStyle, useBulletProbe) {
|
|
219
299
|
switch (va) {
|
|
220
|
-
case 'super':
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
case '
|
|
224
|
-
|
|
300
|
+
case 'super':
|
|
301
|
+
return BLINK_SUPER_SUB
|
|
302
|
+
? -(parentStyle.fontSize / 3 + 1) : -parentStyle.fontSize * 0.34;
|
|
303
|
+
case 'sub':
|
|
304
|
+
return BLINK_SUPER_SUB
|
|
305
|
+
? parentStyle.fontSize / 5 + 1 : parentStyle.fontSize * 0.2;
|
|
306
|
+
// Against the PARENT's content area (CSS 2.1 §10.8.1) — its bare
|
|
307
|
+
// ascent/descent, no leading. Measured against Chrome, taking the line's
|
|
308
|
+
// tallest box instead of the real parent put this 14px out.
|
|
309
|
+
case 'text-top':
|
|
310
|
+
return leadedBox(ctx, style, useBulletProbe).ascent - getFontMetrics(ctx, parentStyle).ascent;
|
|
311
|
+
case 'text-bottom':
|
|
312
|
+
return getFontMetrics(ctx, parentStyle).descent - leadedBox(ctx, style, useBulletProbe).descent;
|
|
313
|
+
case 'middle': {
|
|
314
|
+
const { ascent, descent } = getFontMetrics(ctx, style);
|
|
315
|
+
return -(parentStyle.fontSize * 0.25) - (descent - ascent) / 2;
|
|
316
|
+
}
|
|
225
317
|
default: {
|
|
226
|
-
// baseline / top / bottom / '' all parseFloat to NaN → 0
|
|
227
|
-
//
|
|
318
|
+
// baseline / top / bottom / '' all parseFloat to NaN → 0, which is what
|
|
319
|
+
// an unshifted run wants — the line-box pass calls this for every word.
|
|
228
320
|
const n = parseFloat(va);
|
|
229
321
|
if (!Number.isFinite(n))
|
|
230
322
|
return 0;
|
|
231
|
-
|
|
323
|
+
// A percentage resolves against the ELEMENT's own line-height (CSS 2.1
|
|
324
|
+
// §10.8.1), not the line's. Measured against Chrome: the line's put the
|
|
325
|
+
// box 10px out on a line whose tallest run was not this one.
|
|
326
|
+
return va.endsWith('%')
|
|
327
|
+
? -(n / 100) * getLineHeight(ctx, style, useBulletProbe)
|
|
328
|
+
: -n;
|
|
232
329
|
}
|
|
233
330
|
}
|
|
234
331
|
}
|
|
@@ -351,6 +448,9 @@ function applyEllipsisToLine(ctx, line, maxWidth) {
|
|
|
351
448
|
return;
|
|
352
449
|
const lastStyle = line.words[styleIdx].style;
|
|
353
450
|
const boxStyle = line.words[styleIdx].boxStyle;
|
|
451
|
+
// The ellipsis takes the trimmed run's style, so it has to take the parent
|
|
452
|
+
// that style's vertical-align measures against too.
|
|
453
|
+
const parentStyle = line.words[styleIdx].parentStyle;
|
|
354
454
|
applyFont(ctx, lastStyle);
|
|
355
455
|
// ALWAYS assign (don't gate on truthy) — otherwise a previous segment's
|
|
356
456
|
// non-zero letter-spacing leaks into the ellipsis measurement.
|
|
@@ -386,6 +486,7 @@ function applyEllipsisToLine(ctx, line, maxWidth) {
|
|
|
386
486
|
text: '…',
|
|
387
487
|
width: ellipsisWidth,
|
|
388
488
|
style: lastStyle,
|
|
489
|
+
parentStyle,
|
|
389
490
|
isSpace: false,
|
|
390
491
|
boxStyle,
|
|
391
492
|
};
|
|
@@ -400,9 +501,15 @@ function applyEllipsisToLine(ctx, line, maxWidth) {
|
|
|
400
501
|
*/
|
|
401
502
|
function collectTextRuns(node) {
|
|
402
503
|
const runs = [];
|
|
403
|
-
function walk(n, boxStyle, clipStyle, strokeImageStyle) {
|
|
504
|
+
function walk(n, boxStyle, clipStyle, strokeImageStyle, parentStyle) {
|
|
404
505
|
if (n.tagName === '#text' && n.textContent) {
|
|
405
|
-
|
|
506
|
+
// A #text node carries its parent ELEMENT's style, so the element that
|
|
507
|
+
// owns any vertical-align here is that parent — and what the shift
|
|
508
|
+
// measures against is ITS parent, which is the `parentStyle` handed to
|
|
509
|
+
// this element's walk.
|
|
510
|
+
runs.push({
|
|
511
|
+
text: n.textContent, style: n.style, parentStyle, boxStyle, clipStyle, strokeImageStyle,
|
|
512
|
+
});
|
|
406
513
|
return;
|
|
407
514
|
}
|
|
408
515
|
const isInlineBlock = n.style.display === 'inline-block';
|
|
@@ -425,6 +532,7 @@ function collectTextRuns(node) {
|
|
|
425
532
|
runs.push({
|
|
426
533
|
text: allText,
|
|
427
534
|
style: n.style,
|
|
535
|
+
parentStyle,
|
|
428
536
|
boxStyle: newBoxStyle,
|
|
429
537
|
clipStyle: newClipStyle,
|
|
430
538
|
strokeImageStyle: newStrokeImageStyle,
|
|
@@ -445,7 +553,11 @@ function collectTextRuns(node) {
|
|
|
445
553
|
runs.push({ text: '', style: n.style, boxStyle: newBoxStyle, boxOpen: n.style });
|
|
446
554
|
}
|
|
447
555
|
for (const child of n.children) {
|
|
448
|
-
walk(child, isBox ? newBoxStyle : boxStyle, newClipStyle, newStrokeImageStyle
|
|
556
|
+
walk(child, isBox ? newBoxStyle : boxStyle, newClipStyle, newStrokeImageStyle,
|
|
557
|
+
// An element child measures against this element; a text child's
|
|
558
|
+
// vertical-align belongs to this element, so it measures against what
|
|
559
|
+
// this element measures against.
|
|
560
|
+
child.tagName === '#text' ? parentStyle : n.style);
|
|
449
561
|
}
|
|
450
562
|
if (hasHorizSpacing) {
|
|
451
563
|
runs.push({ text: '', style: n.style, boxStyle: newBoxStyle, boxClose: n.style });
|
|
@@ -465,8 +577,9 @@ function collectTextRuns(node) {
|
|
|
465
577
|
runs.push(...seg);
|
|
466
578
|
}
|
|
467
579
|
}
|
|
580
|
+
// The block itself is the parent every top-level run measures against.
|
|
468
581
|
for (const child of node.children) {
|
|
469
|
-
walk(child);
|
|
582
|
+
walk(child, undefined, undefined, undefined, node.style);
|
|
470
583
|
}
|
|
471
584
|
return runs;
|
|
472
585
|
}
|
|
@@ -550,6 +663,7 @@ function tokenizeString(ctx, text, run, allWords, cumState) {
|
|
|
550
663
|
text: '\t',
|
|
551
664
|
width: tabStopInterval, // placeholder — recalculated in flowWordsIntoLines
|
|
552
665
|
style: run.style,
|
|
666
|
+
parentStyle: run.parentStyle,
|
|
553
667
|
isSpace: true,
|
|
554
668
|
isTab: true,
|
|
555
669
|
boxStyle: run.boxStyle,
|
|
@@ -563,6 +677,7 @@ function tokenizeString(ctx, text, run, allWords, cumState) {
|
|
|
563
677
|
text: w,
|
|
564
678
|
width: cachedMeasureWidth(ctx, w),
|
|
565
679
|
style: run.style,
|
|
680
|
+
parentStyle: run.parentStyle,
|
|
566
681
|
isSpace,
|
|
567
682
|
boxStyle: run.boxStyle,
|
|
568
683
|
clipStyle: run.clipStyle,
|
|
@@ -600,6 +715,7 @@ function tokenizeString(ctx, text, run, allWords, cumState) {
|
|
|
600
715
|
text: ' ',
|
|
601
716
|
width: spaceWidth,
|
|
602
717
|
style: run.style,
|
|
718
|
+
parentStyle: run.parentStyle,
|
|
603
719
|
isSpace: true,
|
|
604
720
|
boxStyle: run.boxStyle,
|
|
605
721
|
clipStyle: run.clipStyle,
|
|
@@ -620,6 +736,7 @@ function tokenizeString(ctx, text, run, allWords, cumState) {
|
|
|
620
736
|
text: s,
|
|
621
737
|
width: cumWidth - prevCum,
|
|
622
738
|
style: run.style,
|
|
739
|
+
parentStyle: run.parentStyle,
|
|
623
740
|
isSpace: false,
|
|
624
741
|
boxStyle: run.boxStyle,
|
|
625
742
|
clipStyle: run.clipStyle,
|
|
@@ -645,6 +762,7 @@ function tokenizeString(ctx, text, run, allWords, cumState) {
|
|
|
645
762
|
text: w,
|
|
646
763
|
width,
|
|
647
764
|
style: run.style,
|
|
765
|
+
parentStyle: run.parentStyle,
|
|
648
766
|
isSpace: false,
|
|
649
767
|
boxStyle: run.boxStyle,
|
|
650
768
|
clipStyle: run.clipStyle,
|
|
@@ -688,6 +806,7 @@ function tokenizeRuns(ctx, runs) {
|
|
|
688
806
|
text,
|
|
689
807
|
width: totalWidth,
|
|
690
808
|
style: run.style,
|
|
809
|
+
parentStyle: run.parentStyle,
|
|
691
810
|
isSpace: false,
|
|
692
811
|
boxStyle: run.boxStyle,
|
|
693
812
|
boxOpen: run.boxOpen,
|
|
@@ -962,6 +1081,7 @@ function flowWordsIntoLines(ctx, words, contentWidth, whiteSpace, useBulletProbe
|
|
|
962
1081
|
text: '-',
|
|
963
1082
|
width: hyphenWidth,
|
|
964
1083
|
style: lastWord.style,
|
|
1084
|
+
parentStyle: lastWord.parentStyle,
|
|
965
1085
|
isSpace: false,
|
|
966
1086
|
// The visible hyphen continues the broken word, so it inherits the
|
|
967
1087
|
// word's clip/stroke-image declarer (else it paints transparent).
|
|
@@ -991,9 +1111,11 @@ function flowWordsIntoLines(ctx, words, contentWidth, whiteSpace, useBulletProbe
|
|
|
991
1111
|
let wordLineHeight = getLineHeight(ctx, word.style, useBulletProbe);
|
|
992
1112
|
// Inline-block elements expand line height with their vertical padding+margin
|
|
993
1113
|
if (word.boxStyle && word.boxStyle.display === 'inline-block') {
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1114
|
+
// Clamped at 0: negative margins shrink the margin box, but the original
|
|
1115
|
+
// `Math.max(h, h + extra)` never let them shrink the LINE, and nothing
|
|
1116
|
+
// here is measuring a case that says they should.
|
|
1117
|
+
const extra = inlineBlockExtra(word.boxStyle);
|
|
1118
|
+
wordLineHeight += Math.max(0, extra.top + extra.bottom);
|
|
997
1119
|
}
|
|
998
1120
|
if (word.text === '\n') {
|
|
999
1121
|
if (currentLine.words.length === 0) {
|
|
@@ -1042,6 +1164,7 @@ function flowWordsIntoLines(ctx, words, contentWidth, whiteSpace, useBulletProbe
|
|
|
1042
1164
|
cells.push({
|
|
1043
1165
|
ch,
|
|
1044
1166
|
style: words[j].style,
|
|
1167
|
+
parentStyle: words[j].parentStyle,
|
|
1045
1168
|
clipStyle: words[j].clipStyle,
|
|
1046
1169
|
strokeImageStyle: words[j].strokeImageStyle,
|
|
1047
1170
|
});
|
|
@@ -1076,6 +1199,7 @@ function flowWordsIntoLines(ctx, words, contentWidth, whiteSpace, useBulletProbe
|
|
|
1076
1199
|
// word), so capturing it at the run start covers every push below.
|
|
1077
1200
|
const clipStyle = cs[i].clipStyle;
|
|
1078
1201
|
const strokeImageStyle = cs[i].strokeImageStyle;
|
|
1202
|
+
const parentStyle = cs[i].parentStyle;
|
|
1079
1203
|
applyFont(ctx, st);
|
|
1080
1204
|
ctx.letterSpacing = formatLetterSpacing(st.letterSpacing);
|
|
1081
1205
|
const lh = getLineHeight(ctx, st, useBulletProbe);
|
|
@@ -1088,7 +1212,7 @@ function flowWordsIntoLines(ctx, words, contentWidth, whiteSpace, useBulletProbe
|
|
|
1088
1212
|
if (chars && currentLine.totalWidth + candW > effWidth() &&
|
|
1089
1213
|
(currentLine.words.length > 0 || cur)) {
|
|
1090
1214
|
if (cur) {
|
|
1091
|
-
currentLine.words.push({ text: cur, width: curW, style: st, isSpace: false, clipStyle, strokeImageStyle });
|
|
1215
|
+
currentLine.words.push({ text: cur, width: curW, style: st, isSpace: false, parentStyle, clipStyle, strokeImageStyle });
|
|
1092
1216
|
currentLine.totalWidth += curW;
|
|
1093
1217
|
currentLine.lineHeight = Math.max(currentLine.lineHeight, lh);
|
|
1094
1218
|
}
|
|
@@ -1104,7 +1228,7 @@ function flowWordsIntoLines(ctx, words, contentWidth, whiteSpace, useBulletProbe
|
|
|
1104
1228
|
i++;
|
|
1105
1229
|
}
|
|
1106
1230
|
if (cur) {
|
|
1107
|
-
currentLine.words.push({ text: cur, width: curW, style: st, isSpace: false, clipStyle, strokeImageStyle });
|
|
1231
|
+
currentLine.words.push({ text: cur, width: curW, style: st, isSpace: false, parentStyle, clipStyle, strokeImageStyle });
|
|
1108
1232
|
currentLine.totalWidth += curW;
|
|
1109
1233
|
currentLine.lineHeight = Math.max(currentLine.lineHeight, lh);
|
|
1110
1234
|
afterHardBreak = false;
|
|
@@ -1458,7 +1582,9 @@ function layoutInlineContent(ctx, node, x, y, contentWidth, useBulletProbe = fal
|
|
|
1458
1582
|
// only content is a SMALLER inline font sits on the strut baseline (lower in
|
|
1459
1583
|
// the box), not centered in it. Seed each line's ascent/descent with the
|
|
1460
1584
|
// block font's metrics so the baseline lands where the DOM puts it.
|
|
1461
|
-
|
|
1585
|
+
// The block is the parent of any run with no inline ancestor, and the emit
|
|
1586
|
+
// loop shadows `node` with the LayoutText it builds.
|
|
1587
|
+
const blockStyle = node.style;
|
|
1462
1588
|
let curY = y;
|
|
1463
1589
|
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
1464
1590
|
const line = lines[lineIdx];
|
|
@@ -1466,7 +1592,6 @@ function layoutInlineContent(ctx, node, x, y, contentWidth, useBulletProbe = fal
|
|
|
1466
1592
|
curY += line.lineHeight;
|
|
1467
1593
|
continue;
|
|
1468
1594
|
}
|
|
1469
|
-
const lineHeight = line.lineHeight;
|
|
1470
1595
|
const isLastLine = lineIdx === lines.length - 1;
|
|
1471
1596
|
const isFirstLine = lineIdx === 0;
|
|
1472
1597
|
// Per-line alignment: lines ending at a forced break or the last line
|
|
@@ -1517,85 +1642,78 @@ function layoutInlineContent(ctx, node, x, y, contentWidth, useBulletProbe = fal
|
|
|
1517
1642
|
const lineLeftX = curX;
|
|
1518
1643
|
// Inline background boxes and text are emitted after baseline computation
|
|
1519
1644
|
// (below) so that emitInlineBox can use line-level metrics for alignment.
|
|
1520
|
-
//
|
|
1521
|
-
//
|
|
1522
|
-
//
|
|
1523
|
-
//
|
|
1524
|
-
//
|
|
1525
|
-
|
|
1526
|
-
let
|
|
1645
|
+
// The line box is the union of every box on it — strut, run, shifted run,
|
|
1646
|
+
// inline-block — each carrying its own leading over its own line-height:
|
|
1647
|
+
// lineAscent = max(ascent - shift), lineDescent = max(descent + shift).
|
|
1648
|
+
// One font, one line-height and no shift collapse that back to the plain
|
|
1649
|
+
// half-leading every single-style line already had.
|
|
1650
|
+
const strutBox = leadedBox(ctx, node.style, useBulletProbe);
|
|
1651
|
+
let lineAscent = strutBox.ascent;
|
|
1652
|
+
let lineDescent = strutBox.descent;
|
|
1527
1653
|
for (const word of line.words) {
|
|
1528
1654
|
if (word.text === '')
|
|
1529
1655
|
continue;
|
|
1530
|
-
//
|
|
1531
|
-
//
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
maxDescent = descent;
|
|
1548
|
-
break;
|
|
1656
|
+
// A wrapper element with no text of its own — `<span lh:3><span>x</span>`
|
|
1657
|
+
// — never becomes a Word, but it is still a box on the line and still
|
|
1658
|
+
// brings its own line-height. Its run children carry it as `parentStyle`,
|
|
1659
|
+
// so take it from there, AT ITS OWN SHIFT: added unshifted, a wrapper
|
|
1660
|
+
// that carries a vertical-align and direct text enters the union twice
|
|
1661
|
+
// at two different places, and the line spans both (measured 60px where
|
|
1662
|
+
// the DOM has 40). With the shift it is idempotent — a wrapper with
|
|
1663
|
+
// direct text contributes the identical box through its own run.
|
|
1664
|
+
if (word.parentStyle) {
|
|
1665
|
+
const parentBox = leadedBox(ctx, word.parentStyle, useBulletProbe);
|
|
1666
|
+
const parentShift = verticalAlignShift(word.parentStyle.verticalAlign, ctx, word.parentStyle, blockStyle, useBulletProbe);
|
|
1667
|
+
if (parentBox.ascent - parentShift > lineAscent) {
|
|
1668
|
+
lineAscent = parentBox.ascent - parentShift;
|
|
1669
|
+
}
|
|
1670
|
+
if (parentBox.descent + parentShift > lineDescent) {
|
|
1671
|
+
lineDescent = parentBox.descent + parentShift;
|
|
1672
|
+
}
|
|
1549
1673
|
}
|
|
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;
|
|
1674
|
+
const box = leadedBox(ctx, word.style, useBulletProbe);
|
|
1675
|
+
// An inline-block joins the line as an ATOMIC box: its own content
|
|
1676
|
+
// baseline with its margin box stacked around it. It takes the extra
|
|
1677
|
+
// space, but no shift — the emit pass puts its content on the line
|
|
1678
|
+
// baseline and does not honour vertical-align on it, so shifting the box
|
|
1679
|
+
// here would grow the line one way while the paint went the other.
|
|
1680
|
+
const atomic = word.boxStyle?.display === 'inline-block' ? word.boxStyle : null;
|
|
1681
|
+
if (atomic) {
|
|
1682
|
+
const extra = inlineBlockExtra(atomic);
|
|
1683
|
+
box.ascent += extra.top;
|
|
1684
|
+
box.descent += extra.bottom;
|
|
1685
|
+
}
|
|
1686
|
+
// A shift moves the box, not the line's baseline: positive is downward,
|
|
1687
|
+
// so it lifts the box's demand on the ascent side and adds to the descent
|
|
1688
|
+
// side. The parent-size fallback is the one the emit pass uses, so a line
|
|
1689
|
+
// whose only content is shifted still sizes around it.
|
|
1690
|
+
const shift = atomic ? 0 : verticalAlignShift(word.style.verticalAlign, ctx, word.style, word.parentStyle ?? blockStyle, useBulletProbe);
|
|
1691
|
+
if (box.ascent - shift > lineAscent)
|
|
1692
|
+
lineAscent = box.ascent - shift;
|
|
1693
|
+
if (box.descent + shift > lineDescent)
|
|
1694
|
+
lineDescent = box.descent + shift;
|
|
1695
|
+
}
|
|
1696
|
+
const lineBoxHeight = lineAscent + lineDescent;
|
|
1697
|
+
const lineBaselineY = curY + lineAscent;
|
|
1583
1698
|
// Emit inline background box using line-level baseline for vertical alignment.
|
|
1584
1699
|
// Uses the line's ascent/descent (not the box's own font) so box aligns with text.
|
|
1585
1700
|
const emitInlineBox = (style, bx, bw) => {
|
|
1586
|
-
//
|
|
1587
|
-
//
|
|
1588
|
-
|
|
1701
|
+
// The box's OWN font decides its height, not the line's largest. An
|
|
1702
|
+
// inline-block's content box is its LINE-HEIGHT, though, not the bare
|
|
1703
|
+
// font metrics — measured against Chrome, bare metrics put it at
|
|
1704
|
+
// y=6 h=29 where the DOM has y=4 h=33.2.
|
|
1705
|
+
const { ascent: boxAscent, descent: boxDescent } = style.display === 'inline-block'
|
|
1706
|
+
? leadedBox(ctx, style, useBulletProbe)
|
|
1707
|
+
: getFontMetrics(ctx, style);
|
|
1589
1708
|
const padTop = style.paddingTop + style.borderTopWidth;
|
|
1590
1709
|
const padBottom = style.paddingBottom + style.borderBottomWidth;
|
|
1591
1710
|
const boxHeight = boxAscent + boxDescent + padTop + padBottom;
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
}
|
|
1711
|
+
// Every inline box hangs off the line's baseline, an inline-block too:
|
|
1712
|
+
// its content is emitted on that baseline, so a box pinned to the line
|
|
1713
|
+
// TOP instead detached from its own glyphs as soon as something taller
|
|
1714
|
+
// shared the line — measured, a background at y 4..33 around text whose
|
|
1715
|
+
// baseline was 46.
|
|
1716
|
+
const boxY = lineBaselineY - boxAscent - padTop;
|
|
1599
1717
|
results.push({
|
|
1600
1718
|
type: 'box', style, x: bx, y: boxY, width: bw, height: boxHeight,
|
|
1601
1719
|
tagName: 'span', children: [],
|
|
@@ -1786,9 +1904,7 @@ function layoutInlineContent(ctx, node, x, y, contentWidth, useBulletProbe = fal
|
|
|
1786
1904
|
let baselineY = lineBaselineY;
|
|
1787
1905
|
const va = word.style.verticalAlign;
|
|
1788
1906
|
if (isShiftedVAlign(va)) {
|
|
1789
|
-
|
|
1790
|
-
const { ascent: wA, descent: wD } = getFontMetrics(ctx, word.style);
|
|
1791
|
-
baselineY += verticalAlignShift(va, wA, wD, pfs, maxAscent, maxDescent, lineHeight);
|
|
1907
|
+
baselineY += verticalAlignShift(va, ctx, word.style, word.parentStyle ?? blockStyle, useBulletProbe);
|
|
1792
1908
|
}
|
|
1793
1909
|
const effectiveWidth = word.width + (word.isSpace ? justifyExtraPerSpace : 0);
|
|
1794
1910
|
const node = {
|
|
@@ -1822,14 +1938,15 @@ function layoutInlineContent(ctx, node, x, y, contentWidth, useBulletProbe = fal
|
|
|
1822
1938
|
text: line.words.map(w => w.text).join(''),
|
|
1823
1939
|
bounds: {
|
|
1824
1940
|
x: lineLeftX,
|
|
1825
|
-
//
|
|
1826
|
-
//
|
|
1827
|
-
|
|
1941
|
+
// The line box starts at curY: shifted content grew the box through
|
|
1942
|
+
// `lineAscent`/`lineDescent` above, so nothing on the line reaches
|
|
1943
|
+
// outside it.
|
|
1944
|
+
y: curY,
|
|
1828
1945
|
width: lineWidth,
|
|
1829
|
-
height:
|
|
1946
|
+
height: lineBoxHeight,
|
|
1830
1947
|
},
|
|
1831
1948
|
});
|
|
1832
|
-
curY +=
|
|
1949
|
+
curY += lineBoxHeight;
|
|
1833
1950
|
}
|
|
1834
1951
|
assignInlineFragmentBoxes(ctx, results, clipRuns, (node, s, box) => {
|
|
1835
1952
|
node.clip = {
|
|
@@ -2188,9 +2305,10 @@ function addListMarker(ctx, box, node) {
|
|
|
2188
2305
|
const ms = node.markerStyle;
|
|
2189
2306
|
const markerStyleObj = ms ? { ...style, ...ms } : style;
|
|
2190
2307
|
ctx.font = buildCanvasFont(markerStyleObj);
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2308
|
+
// ascent + descent is the li's line-height by construction, so one call
|
|
2309
|
+
// gives both the marker's baseline and the box it reports.
|
|
2310
|
+
const strut = leadedBox(ctx, style);
|
|
2311
|
+
const baselineY = box.y + style.borderTopWidth + style.paddingTop + strut.ascent;
|
|
2194
2312
|
const markerWidth = cachedMeasureWidth(ctx, node.listMarker);
|
|
2195
2313
|
const isRTL = style.direction === 'rtl';
|
|
2196
2314
|
const isBullet = BULLET_MARKERS.has(style.listStyleType);
|
|
@@ -2289,7 +2407,7 @@ function addListMarker(ctx, box, node) {
|
|
|
2289
2407
|
x: markerLeftX,
|
|
2290
2408
|
y: box.y + style.borderTopWidth + style.paddingTop,
|
|
2291
2409
|
width: markerDrawWidth,
|
|
2292
|
-
height:
|
|
2410
|
+
height: strut.ascent + strut.descent,
|
|
2293
2411
|
},
|
|
2294
2412
|
});
|
|
2295
2413
|
}
|