diff --git a/package.json b/package.json index 6826f04..42e73b1 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "scripts": { "build": "tsc -p tsconfig.json", "verify": "npm run build && node scripts/check-charter.mjs && node scripts/verify-four-layer.mjs && node scripts/generate-sweep.mjs && node scripts/verify-sweep.mjs && node scripts/verify-cross-family.mjs && node scripts/verify-ua-styles.mjs && node scripts/verify-rect-contract.mjs && node scripts/verify-stress.mjs", - "verify:all": "npm run build && node scripts/check-charter.mjs && node scripts/verify-four-layer.mjs && node scripts/generate-sweep.mjs && node scripts/verify-sweep.mjs && node scripts/verify-cross-family.mjs && node scripts/verify-ua-styles.mjs && node scripts/verify-text-measure.mjs && node scripts/verify-calc.mjs && node scripts/verify-shadow.mjs && node scripts/verify-opacity.mjs && node scripts/verify-layers.mjs && node scripts/verify-paint-text.mjs && node scripts/verify-border-radius.mjs && node scripts/verify-layout-flexbox.mjs && node scripts/verify-layout-floats.mjs && node scripts/verify-layout-grid.mjs && node scripts/verify-layout-inline-block.mjs && node scripts/verify-layout-positioning.mjs && node scripts/verify-lists.mjs && node scripts/verify-media-queries.mjs && node scripts/verify-pseudo-elements.mjs && node scripts/verify-report.mjs && node scripts/verify-segmenter.mjs && node scripts/verify-text-align.mjs && node scripts/verify-white-space.mjs && node scripts/verify-firefox.mjs && node scripts/verify-font-registration.mjs && node scripts/verify-overflow.mjs && node scripts/verify-breaker.mjs && node scripts/verify-comments.mjs && node scripts/verify-rect-contract.mjs && node scripts/verify-stress.mjs", + "verify:all": "npm run build && tsc -p scripts/tsconfig.json && tsc -p scripts/tsconfig.all.json && node scripts/check-charter.mjs && node scripts/verify-four-layer.mjs && node scripts/generate-sweep.mjs && node scripts/verify-sweep.mjs && node scripts/verify-cross-family.mjs && node scripts/verify-ua-styles.mjs && node scripts/verify-text-measure.mjs && node scripts/verify-calc.mjs && node scripts/verify-shadow.mjs && node scripts/verify-opacity.mjs && node scripts/verify-layers.mjs && node scripts/verify-paint-text.mjs && node scripts/verify-border-radius.mjs && node scripts/verify-layout-flexbox.mjs && node scripts/verify-layout-floats.mjs && node scripts/verify-layout-grid.mjs && node scripts/verify-layout-inline-block.mjs && node scripts/verify-layout-positioning.mjs && node scripts/verify-lists.mjs && node scripts/verify-media-queries.mjs && node scripts/verify-pseudo-elements.mjs && node scripts/verify-report.mjs && node scripts/verify-segmenter.mjs && node scripts/verify-text-align.mjs && node scripts/verify-white-space.mjs && node scripts/verify-firefox.mjs && node scripts/verify-font-registration.mjs && node scripts/verify-overflow.mjs && node scripts/verify-breaker.mjs && node scripts/verify-comments.mjs && node scripts/verify-rect-contract.mjs && node scripts/verify-stress.mjs && node scripts/verify-float-content.mjs && node scripts/verify-atomic-perf.mjs", "verify:four-layer": "npm run build && node scripts/verify-four-layer.mjs", "verify:border-radius": "npm run build && node scripts/verify-border-radius.mjs", "verify:overflow": "npm run build && node scripts/verify-overflow.mjs", @@ -51,6 +51,9 @@ "verify:property-coverage": "npm run build && node scripts/verify-property-coverage.mjs", "verify:breaker": "npm run build && node scripts/verify-breaker.mjs", "verify:comments": "npm run build && node scripts/verify-comments.mjs", + "typecheck:scripts": "npm run build && tsc -p scripts/tsconfig.json && tsc -p scripts/tsconfig.all.json", + "verify:float-content": "npm run build && node scripts/verify-float-content.mjs", + "verify:atomic-perf": "npm run build && node scripts/verify-atomic-perf.mjs", "probe:browser-gap": "npm run build && node probes/probe-browser-gap.mjs", "bench:engine-vs-oracle": "npm run build && node scripts/bench-engine-vs-oracle.mjs", "test:probe": "npm run build && node --test probes/probe-gap-lib.test.mjs", diff --git a/scripts/bench-engine-vs-oracle.mjs b/scripts/bench-engine-vs-oracle.mjs index 208d3b0..d1d14c4 100644 --- a/scripts/bench-engine-vs-oracle.mjs +++ b/scripts/bench-engine-vs-oracle.mjs @@ -98,19 +98,19 @@ async function measureChromeRender(browser, raw) { const h = raw.harvest; const page = await browser.newPage({ viewport: { width: h.viewport.width, height: h.viewport.height } }); await page.addInitScript(() => { - window.__paints = []; + (/** @type {any} */ (window)).__paints = []; try { new PerformanceObserver((list) => { - for (const e of list.getEntries()) window.__paints.push({ name: e.name, startTime: e.startTime }); + for (const e of list.getEntries()) (/** @type {any} */ (window)).__paints.push({ name: e.name, startTime: e.startTime }); }).observe({ type: 'paint' }); } catch (e) { - window.__paints.push({ name: 'observer-error', startTime: 0 }); + (/** @type {any} */ (window)).__paints.push({ name: 'observer-error', startTime: 0 }); } }); await page.goto('data:text/html;charset=utf-8,' + encodeURIComponent(h.html)); await page.evaluate(() => document.fonts.ready); await page.waitForTimeout(50); - const paints = await page.evaluate(() => window.__paints); + const paints = await page.evaluate(() => (/** @type {any} */ (window)).__paints); const fcp = paints.find((e) => e.name === 'first-contentful-paint') ?? paints.find((e) => e.name === 'first-paint'); await page.close(); if (!fcp) throw new Error(`fixture ${raw.name}: no paint timing entry observed`); diff --git a/scripts/check-charter.mjs b/scripts/check-charter.mjs index 19a12e9..4f5461d 100644 --- a/scripts/check-charter.mjs +++ b/scripts/check-charter.mjs @@ -61,7 +61,7 @@ const requires = [ ]; for (const [label, re] of requires) { - if (!re.test(charter)) fail(`charter.md missing: ${label}`); + if (!(/** @type {RegExp} */ (re)).test(charter)) fail(`charter.md missing: ${label}`); } // --- corpus gap-fixture schema (improvement-plan §4) --- diff --git a/scripts/generate-sweep.mjs b/scripts/generate-sweep.mjs index cf2772e..755247b 100644 --- a/scripts/generate-sweep.mjs +++ b/scripts/generate-sweep.mjs @@ -67,7 +67,7 @@ try { const viewport = combo.viewport; const page = await browser.newPage({ viewport: { width: viewport.width, height: viewport.height } }); await page.setContent(combo.html); - const referenceRects = {}; + /** @type {Record} */ const referenceRects = {}; for (const id of combo.rects) { referenceRects[id] = await page.$eval(`#${id}`, (el) => { const r = el.getBoundingClientRect(); @@ -100,9 +100,9 @@ try { width: refImg.width, height: refImg.height, }; - const harness = evaluateFixture(fixture); - const rectLayer = harness.layers.rect; - const shotLayer = harness.layers.screenshot; + const harness = evaluateFixture(/** @type {import('../dist/harness/fixtures.js').Fixture} */ (fixture)); + const rectLayer = /** @type {import('../dist/harness/evaluate.js').RectLayerResult} */ (harness.layers.rect); + const shotLayer = /** @type {import('../dist/harness/evaluate.js').ScreenshotLayerResult} */ (harness.layers.screenshot); const expected = { measureText: 'pass', computedStyle: 'pass' }; expected.rect = rectLayer.pass ? 'pass' : gapFor(combo.label); diff --git a/scripts/lib/render.mjs b/scripts/lib/render.mjs new file mode 100644 index 0000000..dab7b14 --- /dev/null +++ b/scripts/lib/render.mjs @@ -0,0 +1,46 @@ +/** + * Shared render harness for the engine-only verify gates (no browser oracle): + * the FONT_FILE / FONT_FAMILY fallbacks, the document wrapper, and a typed + * `render(body)` over `renderHtml`. Centralizing this keeps each gate to just + * its assertions and gives the gates a single typed surface — `renderHtml` + * options are checked against the engine's own `RenderOptions` type (see + * scripts/tsconfig.json). + * + * The repo default font is the bundled Hack face, so these gates need no system + * fonts; FONT_FILE / FONT_FAMILY still override it (matching the oracle gates). + */ +import { resolve } from 'node:path'; +import { renderHtml } from '../../dist/index.js'; + +export const FONT_FILE = process.env.FONT_FILE ?? resolve('fonts/HackNerdFont-Regular.ttf'); +export const FONT_FAMILY = process.env.FONT_FAMILY ?? 'Hack Nerd Font'; + +/** + * Wrap body markup in a minimal document with a zero-margin body at the shared + * font. `style` is extra CSS appended to the body rule's stylesheet. + * @param {string} body + * @param {string} [style] + * @returns {string} + */ +export function doc(body, style = '') { + return `${body}`; +} + +/** + * Render body markup through the engine at the given viewport. + * @param {string} body + * @param {{ width?: number, height?: number, style?: string }} [opts] + */ +export function render(body, opts = {}) { + const { width = 800, height = 600, style = '' } = opts; + return renderHtml(doc(body, style), { width, height, fontFamily: FONT_FAMILY, fontFile: FONT_FILE }); +} + +/** + * Normalize a caught value (typed `unknown` under strict) to a message string. + * @param {unknown} e + * @returns {string} + */ +export function errorMessage(e) { + return e instanceof Error ? e.message : String(e); +} diff --git a/scripts/tsconfig.all.json b/scripts/tsconfig.all.json new file mode 100644 index 0000000..01ffc09 --- /dev/null +++ b/scripts/tsconfig.all.json @@ -0,0 +1,21 @@ +{ + "//": "Broad type-check for the remaining scripts (npm run typecheck:scripts runs this after the strict gate config). checkJs against dist/*.d.ts catches the errors that matter for test code — a wrong renderHtml/BrowserConfig option, a wrong harness field or property — across the oracle/bench/generator scripts. noImplicitAny and strictNullChecks are relaxed here (the strict tier in tsconfig.json covers the engine-only gates): the oracle scripts run untyped browser-context code inside page.evaluate and build result objects dynamically, where per-parameter annotations and DOM null-guards are noise rather than signal. As scripts are hardened they can graduate to the strict tier.", + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "..", + "noEmit": true, + "allowJs": true, + "checkJs": true, + "declaration": false, + "sourceMap": false, + "noImplicitAny": false, + "strictNullChecks": false + }, + "include": ["**/*.mjs"], + "exclude": [ + "lib/render.mjs", + "verify-comments.mjs", + "verify-float-content.mjs", + "verify-atomic-perf.mjs" + ] +} diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json new file mode 100644 index 0000000..575b2ad --- /dev/null +++ b/scripts/tsconfig.json @@ -0,0 +1,19 @@ +{ + "//": "Type-checks the engine-only verify gates and their shared harness (npm run typecheck:scripts). checkJs brings the src/ strict safety net to the .mjs gates that pin the engine's behavior, without converting all of scripts/ at once. renderHtml options and RenderOutput fields are checked against dist/*.d.ts, so a mistyped option or a wrong rects field is caught before runtime. Add new engine-only gates and lib helpers to `include` as they land.", + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "..", + "noEmit": true, + "allowJs": true, + "checkJs": true, + "declaration": false, + "sourceMap": false + }, + "include": [ + "lib/render.mjs", + "verify-comments.mjs", + "verify-float-content.mjs", + "verify-atomic-perf.mjs" + ], + "exclude": [] +} diff --git a/scripts/verify-atomic-perf.mjs b/scripts/verify-atomic-perf.mjs new file mode 100644 index 0000000..0756cac --- /dev/null +++ b/scripts/verify-atomic-perf.mjs @@ -0,0 +1,54 @@ +#!/usr/bin/env node +/** + * `npm run verify:atomic-perf` + * + * Intrinsic sizing / measurement of inline-block ("atomic") boxes used to be + * unmemoized: sizing an auto-width inline-block measures its whole subtree, and + * every atomic was measured once for line-breaking and again for placement — so + * nested inline-blocks re-measured every level below them, twice, giving + * O(2^depth) work. Deeply nested inline-blocks (a real WPT flex reference, or + * the progressive nesting a stray self-closing `
` produces in the HTML + * parser) hung the engine for minutes / OOM-killed the process. + * + * atomicBoxSize/measureAtomic are now memoized per (element, width), which is + * linear. This gate renders pathological nesting and asserts it finishes fast; + * the pre-fix engine could not complete depth 14 in two minutes. + */ +import assert from 'node:assert/strict'; +import { render, errorMessage } from './lib/render.mjs'; + +const BUDGET_MS = Number(process.env.ATOMIC_BUDGET_MS ?? 5000); // generous; pre-fix hung > 120s at depth 14 + +let pass = 0; +/** @type {string[]} */ +const fails = []; +/** @param {string} name @param {string} body */ +const timed = (name, body) => { + const t = Date.now(); + try { render(body); } catch (e) { fails.push(`${name}: threw ${errorMessage(e)}`); return; } + const ms = Date.now() - t; + if (ms > BUDGET_MS) fails.push(`${name}: took ${ms}ms > ${BUDGET_MS}ms budget (exponential regression?)`); + else { pass++; console.log(` ${name}: ${ms}ms`); } +}; + +// Deep nested inline-blocks — the exponential shape. +let ib = 'x'; +for (let i = 0; i < 200; i++) ib = `${ib}`; +timed('nested inline-block depth 200', ib); + +// Progressive nesting from self-closing
in HTML parsing (the WPT ref). +let sc = ''; +for (let i = 0; i < 80; i++) sc += `
`; +timed('80 self-closing-div blocks', sc); + +// Correctness: memoization must not change layout — deep nesting still resolves. +try { + const r = render(``).rects; + assert.ok(r.o && r.i && r.i.width === 30, 'nested inline-block rects wrong'); + pass++; +} catch (e) { fails.push(`nested rect correctness: ${errorMessage(e)}`); } + +for (const f of fails) console.log(`FAIL ${f}`); +console.log(`${pass}/${pass + fails.length} atomic-perf checks passed`); +if (fails.length) { console.error('verify:atomic-perf: FAIL'); process.exit(1); } +console.log('verify:atomic-perf: PASS'); diff --git a/scripts/verify-border-radius.mjs b/scripts/verify-border-radius.mjs index b5e2b5e..189b14b 100644 --- a/scripts/verify-border-radius.mjs +++ b/scripts/verify-border-radius.mjs @@ -86,7 +86,7 @@ try { await page.setContent(h.html); await page.evaluate(() => document.fonts.ready); - const referenceRects = {}; + /** @type {Record} */ const referenceRects = {}; for (const id of h.rects ?? []) { referenceRects[id] = await page.$eval(`#${id}`, (el) => { const r = el.getBoundingClientRect(); @@ -94,7 +94,7 @@ try { }); } - const referenceMeasure = {}; + /** @type {Record} */ const referenceMeasure = {}; if (h.measureText) { for (const { text, font } of h.measureText) { referenceMeasure[`${font} | ${text}`] = await page.evaluate( @@ -108,7 +108,7 @@ try { } } - const referenceComputed = {}; + /** @type {Record} */ const referenceComputed = {}; if (h.computedStyle) { for (const { id, props } of h.computedStyle) { referenceComputed[id] = await page.evaluate( @@ -156,7 +156,7 @@ try { const candImg = decodePng(out.rgba); const candidateRects = out.rects; - const candidateMeasure = {}; + /** @type {Record} */ const candidateMeasure = {}; if (h.measureText) { const { measureTextWidth } = await import('../dist/layout/measure.js'); for (const { text, font } of h.measureText) { diff --git a/scripts/verify-breaker.mjs b/scripts/verify-breaker.mjs index e9235df..f364fc8 100644 --- a/scripts/verify-breaker.mjs +++ b/scripts/verify-breaker.mjs @@ -98,6 +98,7 @@ if (fontsByFamily.size === 0) { console.error(`verify:breaker: no fixtures found under ${corpus}`); process.exit(1); } +/** @type {import('../dist/index.js').BrowserConfig} */ const config = { browser: 'chrome', fonts: [...fontsByFamily.values()], diff --git a/scripts/verify-calc.mjs b/scripts/verify-calc.mjs index 7336d7d..942f57e 100644 --- a/scripts/verify-calc.mjs +++ b/scripts/verify-calc.mjs @@ -51,7 +51,7 @@ try { await page.setContent(h.html); await page.evaluate(() => document.fonts.ready); - const referenceRects = {}; + /** @type {Record} */ const referenceRects = {}; for (const id of h.rects ?? []) { referenceRects[id] = await page.$eval(`#${id}`, (el) => { const r = el.getBoundingClientRect(); @@ -97,7 +97,7 @@ try { height, }; - results.push(evaluateFixture(fixture)); + results.push(evaluateFixture(/** @type {import('../dist/harness/fixtures.js').Fixture} */ (fixture))); console.log(`verified ${name}: ${width}x${height}`); } } finally { diff --git a/scripts/verify-comments.mjs b/scripts/verify-comments.mjs index 05307f7..5d37338 100644 --- a/scripts/verify-comments.mjs +++ b/scripts/verify-comments.mjs @@ -19,17 +19,10 @@ */ import assert from 'node:assert/strict'; -import { resolve } from 'node:path'; -import { renderHtml } from '../dist/index.js'; +import { render as renderAt, errorMessage } from './lib/render.mjs'; -const FONT_FILE = process.env.FONT_FILE ?? resolve('fonts/HackNerdFont-Regular.ttf'); -const FONT_FAMILY = process.env.FONT_FAMILY ?? 'Hack Nerd Font'; - -const doc = (body, style = '') => - `${body}`; - -const render = (body, style) => - renderHtml(doc(body, style), { width: 400, height: 300, fontFamily: FONT_FAMILY, fontFile: FONT_FILE }); +/** @param {string} body @param {string} [style] */ +const render = (body, style = '') => renderAt(body, { width: 400, height: 300, style }); // Each case: [name, withoutComments, withComments, style]. The two markups must // lay out identically — the only difference is interleaved comment nodes. @@ -61,6 +54,7 @@ const cases = [ ]; let pass = 0; +/** @type {string[]} */ const fails = []; // Property 1 + 2 over the matrix. @@ -70,7 +64,7 @@ for (const [name, plain, commented, style] of cases) { base = render(plain, style); withc = render(commented, style); } catch (e) { - fails.push(`${name}: threw ${e.message}`); + fails.push(`${name}: threw ${errorMessage(e)}`); continue; } try { @@ -86,7 +80,7 @@ try { render(`

hi

`); pass++; } catch (e) { - fails.push(`original repro still throws: ${e.message}`); + fails.push(`original repro still throws: ${errorMessage(e)}`); } const total = cases.length + 1; diff --git a/scripts/verify-cross-family.mjs b/scripts/verify-cross-family.mjs index b5484f9..b947732 100644 --- a/scripts/verify-cross-family.mjs +++ b/scripts/verify-cross-family.mjs @@ -101,7 +101,7 @@ try { await page.setContent(h.html); await page.evaluate(() => document.fonts.ready); - const referenceRects = {}; + /** @type {Record} */ const referenceRects = {}; for (const id of h.rects ?? []) { referenceRects[id] = await page.$eval(`#${id}`, (el) => { const r = el.getBoundingClientRect(); @@ -109,7 +109,7 @@ try { }); } - const referenceMeasure = {}; + /** @type {Record} */ const referenceMeasure = {}; if (h.measureText) { for (const { text, font } of h.measureText) { referenceMeasure[`${font} | ${text}`] = await page.evaluate( @@ -123,7 +123,7 @@ try { } } - const referenceComputed = {}; + /** @type {Record} */ const referenceComputed = {}; if (h.computedStyle) { for (const { id, props } of h.computedStyle) { referenceComputed[id] = await page.evaluate( @@ -173,7 +173,7 @@ try { const candImg = decodePng(out.rgba); const candidateRects = out.rects; - const candidateMeasure = {}; + /** @type {Record} */ const candidateMeasure = {}; if (h.measureText) { const { measureTextWidth } = await import('../dist/layout/measure.js'); for (const { text, font } of h.measureText) { @@ -238,7 +238,7 @@ try { height, }; - results.push(evaluateFixture(fixture)); + results.push(evaluateFixture(/** @type {import('../dist/harness/fixtures.js').Fixture} */ (fixture))); console.log(`verified ${name} (${target}): ${width}x${height}, ${fragments.length} text fragments, ${mask.some((b) => b === 1) ? 'masked' : 'no mask'}`); } } finally { diff --git a/scripts/verify-firefox.mjs b/scripts/verify-firefox.mjs index 5a319fc..a44df79 100644 --- a/scripts/verify-firefox.mjs +++ b/scripts/verify-firefox.mjs @@ -130,7 +130,7 @@ try { await page.setContent(h.html); await page.evaluate(() => document.fonts.ready); - const referenceRects = {}; + /** @type {Record} */ const referenceRects = {}; for (const id of h.rects ?? []) { referenceRects[id] = await page.$eval(`#${id}`, (el) => { const r = el.getBoundingClientRect(); @@ -138,7 +138,7 @@ try { }); } - const referenceMeasure = {}; + /** @type {Record} */ const referenceMeasure = {}; if (h.measureText) { for (const { text, font } of h.measureText) { referenceMeasure[`${font} | ${text}`] = await page.evaluate( @@ -152,7 +152,7 @@ try { } } - const referenceComputed = {}; + /** @type {Record} */ const referenceComputed = {}; if (h.computedStyle) { for (const { id, props } of h.computedStyle) { referenceComputed[id] = await page.evaluate( @@ -208,7 +208,7 @@ try { const candImg = decodePng(out.rgba); const candidateRects = out.rects; - const candidateMeasure = {}; + /** @type {Record} */ const candidateMeasure = {}; if (h.measureText) { const { measureTextWidth } = await import('../dist/layout/measure.js'); for (const { text, font } of h.measureText) { diff --git a/scripts/verify-float-content.mjs b/scripts/verify-float-content.mjs new file mode 100644 index 0000000..45bb98e --- /dev/null +++ b/scripts/verify-float-content.mjs @@ -0,0 +1,59 @@ +#!/usr/bin/env node +/** + * `npm run verify:float-content` + * + * A float used to lay out only its inline content: `layoutFloat` walked text + * lines and set `children: []`, so block-level or floated descendants of a + * float were dropped. Because the engine asserts rect-completeness (every + * id-bearing element must get a rect), any float containing a block/floated + * child with an id crashed the whole render ("no rect collected for id(s)"). + * This is common real markup (a floated card/sidebar with block content, the + * Acid1 shape of floated list items inside a floated container). + * + * This gate asserts such documents render, and that the inner block/float + * children get sensible rects inside the float. Uses the repo-bundled font, no + * browser oracle. + */ +import assert from 'node:assert/strict'; +import { render, errorMessage } from './lib/render.mjs'; + +/** @param {string} body */ +const at = (body) => render(body, { width: 400, height: 300 }); + +let pass = 0; +/** @type {string[]} */ +const fails = []; +/** @param {string} name @param {() => void} fn */ +const check = (name, fn) => { try { fn(); pass++; } catch (e) { fails.push(`${name}: ${errorMessage(e)}`); } }; + +// 1. Block child of a float gets a rect inside the float. +check('block child of float', () => { + const r = at(`
x
`).rects; + assert.ok(r.inner, 'inner block has no rect'); + assert.equal(r.inner.x, 10, 'inner x should sit at float padding'); + assert.equal(r.inner.width, 100, 'inner block fills float content width'); +}); + +// 2. Float nested inside a float gets a rect (the original crash / Acid1 shape). +check('float nested in float', () => { + const r = at(`
b
`).rects; + assert.ok(r.nf, 'nested float has no rect'); + assert.equal(r.nf.width, 20); +}); + +// 3. Floated list items inside a floated container (Acid1-like) all render. +check('floated list items in floated container', () => { + const r = at(`
  • a
  • b
`).rects; + assert.ok(r.a && r.b, 'floated list items missing rects'); +}); + +// 4. Inline-only floats are unchanged (no regression to the existing path). +check('inline float still works', () => { + const r = at(`
hello
`).rects; + assert.ok(r.f && r.f.width === 80 && r.f.height === 30, 'inline float rect changed'); +}); + +for (const f of fails) console.log(`FAIL ${f}`); +console.log(`${pass}/${pass + fails.length} float-content checks passed`); +if (fails.length) { console.error('verify:float-content: FAIL'); process.exit(1); } +console.log('verify:float-content: PASS'); diff --git a/scripts/verify-four-layer.mjs b/scripts/verify-four-layer.mjs index 579424b..82f5512 100644 --- a/scripts/verify-four-layer.mjs +++ b/scripts/verify-four-layer.mjs @@ -120,7 +120,7 @@ try { await page.setContent(h.html); await page.evaluate(() => document.fonts.ready); - const referenceRects = {}; + /** @type {Record} */ const referenceRects = {}; for (const id of h.rects ?? []) { referenceRects[id] = await page.$eval(`#${id}`, (el) => { const r = el.getBoundingClientRect(); @@ -128,7 +128,7 @@ try { }); } - const referenceMeasure = {}; + /** @type {Record} */ const referenceMeasure = {}; if (h.measureText) { for (const { text, font } of h.measureText) { referenceMeasure[`${font} | ${text}`] = await page.evaluate( @@ -142,7 +142,7 @@ try { } } - const referenceComputed = {}; + /** @type {Record} */ const referenceComputed = {}; if (h.computedStyle) { for (const { id, props } of h.computedStyle) { referenceComputed[id] = await page.evaluate( @@ -196,7 +196,7 @@ try { const candImg = decodePng(out.rgba); const candidateRects = out.rects; - const candidateMeasure = {}; + /** @type {Record} */ const candidateMeasure = {}; if (h.measureText) { const { measureTextWidth } = await import('../dist/layout/measure.js'); for (const { text, font } of h.measureText) { diff --git a/scripts/verify-layout-flexbox.mjs b/scripts/verify-layout-flexbox.mjs index 280b3ca..2abad13 100644 --- a/scripts/verify-layout-flexbox.mjs +++ b/scripts/verify-layout-flexbox.mjs @@ -86,7 +86,7 @@ try { await page.setContent(h.html); await page.evaluate(() => document.fonts.ready); - const referenceRects = {}; + /** @type {Record} */ const referenceRects = {}; for (const id of h.rects ?? []) { referenceRects[id] = await page.$eval(`#${id}`, (el) => { const r = el.getBoundingClientRect(); @@ -94,7 +94,7 @@ try { }); } - const referenceMeasure = {}; + /** @type {Record} */ const referenceMeasure = {}; if (h.measureText) { for (const { text, font } of h.measureText) { referenceMeasure[`${font} | ${text}`] = await page.evaluate( @@ -140,7 +140,7 @@ try { const candImg = decodePng(out.rgba); const candidateRects = out.rects; - const candidateMeasure = {}; + /** @type {Record} */ const candidateMeasure = {}; if (h.measureText) { const { measureTextWidth } = await import('../dist/layout/measure.js'); for (const { text, font } of h.measureText) { diff --git a/scripts/verify-layout-floats.mjs b/scripts/verify-layout-floats.mjs index cd68797..05de9c2 100644 --- a/scripts/verify-layout-floats.mjs +++ b/scripts/verify-layout-floats.mjs @@ -88,7 +88,7 @@ try { await page.setContent(h.html); await page.evaluate(() => document.fonts.ready); - const referenceRects = {}; + /** @type {Record} */ const referenceRects = {}; for (const id of h.rects ?? []) { referenceRects[id] = await page.$eval(`#${id}`, (el) => { const r = el.getBoundingClientRect(); @@ -96,7 +96,7 @@ try { }); } - const referenceMeasure = {}; + /** @type {Record} */ const referenceMeasure = {}; if (h.measureText) { for (const { text, font } of h.measureText) { referenceMeasure[`${font} | ${text}`] = await page.evaluate( @@ -142,7 +142,7 @@ try { const candImg = decodePng(out.rgba); const candidateRects = out.rects; - const candidateMeasure = {}; + /** @type {Record} */ const candidateMeasure = {}; if (h.measureText) { const { measureTextWidth } = await import('../dist/layout/measure.js'); for (const { text, font } of h.measureText) { diff --git a/scripts/verify-layout-grid.mjs b/scripts/verify-layout-grid.mjs index 3d4b062..157b6a8 100644 --- a/scripts/verify-layout-grid.mjs +++ b/scripts/verify-layout-grid.mjs @@ -86,7 +86,7 @@ try { await page.setContent(h.html); await page.evaluate(() => document.fonts.ready); - const referenceRects = {}; + /** @type {Record} */ const referenceRects = {}; for (const id of h.rects ?? []) { referenceRects[id] = await page.$eval(`#${id}`, (el) => { const r = el.getBoundingClientRect(); @@ -94,7 +94,7 @@ try { }); } - const referenceMeasure = {}; + /** @type {Record} */ const referenceMeasure = {}; if (h.measureText) { for (const { text, font } of h.measureText) { referenceMeasure[`${font} | ${text}`] = await page.evaluate( @@ -140,7 +140,7 @@ try { const candImg = decodePng(out.rgba); const candidateRects = out.rects; - const candidateMeasure = {}; + /** @type {Record} */ const candidateMeasure = {}; if (h.measureText) { const { measureTextWidth } = await import('../dist/layout/measure.js'); for (const { text, font } of h.measureText) { diff --git a/scripts/verify-layout-inline-block.mjs b/scripts/verify-layout-inline-block.mjs index f027644..e8da496 100644 --- a/scripts/verify-layout-inline-block.mjs +++ b/scripts/verify-layout-inline-block.mjs @@ -88,7 +88,7 @@ try { await page.setContent(h.html); await page.evaluate(() => document.fonts.ready); - const referenceRects = {}; + /** @type {Record} */ const referenceRects = {}; for (const id of h.rects ?? []) { referenceRects[id] = await page.$eval(`#${id}`, (el) => { const r = el.getBoundingClientRect(); @@ -96,7 +96,7 @@ try { }); } - const referenceMeasure = {}; + /** @type {Record} */ const referenceMeasure = {}; if (h.measureText) { for (const { text, font } of h.measureText) { referenceMeasure[`${font} | ${text}`] = await page.evaluate( @@ -110,7 +110,7 @@ try { } } - const referenceComputed = {}; + /** @type {Record} */ const referenceComputed = {}; if (h.computedStyle) { for (const { id, props } of h.computedStyle) { referenceComputed[id] = await page.evaluate( @@ -158,7 +158,7 @@ try { const candImg = decodePng(out.rgba); const candidateRects = out.rects; - const candidateMeasure = {}; + /** @type {Record} */ const candidateMeasure = {}; if (h.measureText) { const { measureTextWidth } = await import('../dist/layout/measure.js'); for (const { text, font } of h.measureText) { diff --git a/scripts/verify-layout-positioning.mjs b/scripts/verify-layout-positioning.mjs index b97a543..e9e5e6e 100644 --- a/scripts/verify-layout-positioning.mjs +++ b/scripts/verify-layout-positioning.mjs @@ -87,7 +87,7 @@ try { await page.setContent(h.html); await page.evaluate(() => document.fonts.ready); - const referenceRects = {}; + /** @type {Record} */ const referenceRects = {}; for (const id of h.rects ?? []) { referenceRects[id] = await page.$eval(`#${id}`, (el) => { const r = el.getBoundingClientRect(); @@ -95,7 +95,7 @@ try { }); } - const referenceMeasure = {}; + /** @type {Record} */ const referenceMeasure = {}; if (h.measureText) { for (const { text, font } of h.measureText) { referenceMeasure[`${font} | ${text}`] = await page.evaluate( @@ -141,7 +141,7 @@ try { const candImg = decodePng(out.rgba); const candidateRects = out.rects; - const candidateMeasure = {}; + /** @type {Record} */ const candidateMeasure = {}; if (h.measureText) { const { measureTextWidth } = await import('../dist/layout/measure.js'); for (const { text, font } of h.measureText) { diff --git a/scripts/verify-lists.mjs b/scripts/verify-lists.mjs index 6c9789a..a5c84dd 100644 --- a/scripts/verify-lists.mjs +++ b/scripts/verify-lists.mjs @@ -149,7 +149,7 @@ try { await page.evaluate(() => document.fonts.ready); // --- Chrome oracle quantities --- - const referenceRects = {}; + /** @type {Record} */ const referenceRects = {}; for (const id of h.rects ?? []) { referenceRects[id] = await page.$eval(`#${id}`, (el) => { const r = el.getBoundingClientRect(); @@ -157,7 +157,7 @@ try { }); } - const referenceMeasure = {}; + /** @type {Record} */ const referenceMeasure = {}; if (h.measureText) { for (const { text, font } of h.measureText) { referenceMeasure[`${font} | ${text}`] = await page.evaluate( @@ -171,7 +171,7 @@ try { } } - const referenceComputed = {}; + /** @type {Record} */ const referenceComputed = {}; if (h.computedStyle) { for (const { id, props } of h.computedStyle) { referenceComputed[id] = await page.evaluate( @@ -224,7 +224,7 @@ try { const candidateRects = out.rects; const candidateComputed = out.computedStyles; - const candidateMeasure = {}; + /** @type {Record} */ const candidateMeasure = {}; if (h.measureText) { const { measureTextWidth } = await import('../dist/layout/measure.js'); for (const { text, font } of h.measureText) { diff --git a/scripts/verify-opacity.mjs b/scripts/verify-opacity.mjs index 3029370..0639908 100644 --- a/scripts/verify-opacity.mjs +++ b/scripts/verify-opacity.mjs @@ -78,7 +78,7 @@ try { await page.setContent(h.html); await page.evaluate(() => document.fonts.ready); - const referenceRects = {}; + /** @type {Record} */ const referenceRects = {}; for (const id of h.rects ?? []) { referenceRects[id] = await page.$eval(`#${id}`, (el) => { const r = el.getBoundingClientRect(); @@ -86,7 +86,7 @@ try { }); } - const referenceComputed = {}; + /** @type {Record} */ const referenceComputed = {}; if (h.computedStyle) { for (const { id, props } of h.computedStyle) { referenceComputed[id] = await page.evaluate( diff --git a/scripts/verify-overflow.mjs b/scripts/verify-overflow.mjs index be2dd33..f269194 100644 --- a/scripts/verify-overflow.mjs +++ b/scripts/verify-overflow.mjs @@ -85,7 +85,7 @@ try { await page.setContent(h.html); await page.evaluate(() => document.fonts.ready); - const referenceRects = {}; + /** @type {Record} */ const referenceRects = {}; for (const id of h.rects ?? []) { referenceRects[id] = await page.$eval(`#${id}`, (el) => { const r = el.getBoundingClientRect(); @@ -93,7 +93,7 @@ try { }); } - const referenceMeasure = {}; + /** @type {Record} */ const referenceMeasure = {}; if (h.measureText) { for (const { text, font } of h.measureText) { referenceMeasure[`${font} | ${text}`] = await page.evaluate( @@ -107,7 +107,7 @@ try { } } - const referenceComputed = {}; + /** @type {Record} */ const referenceComputed = {}; if (h.computedStyle) { for (const { id, props } of h.computedStyle) { referenceComputed[id] = await page.evaluate( @@ -155,7 +155,7 @@ try { const candImg = decodePng(out.rgba); const candidateRects = out.rects; - const candidateMeasure = {}; + /** @type {Record} */ const candidateMeasure = {}; if (h.measureText) { const { measureTextWidth } = await import('../dist/layout/measure.js'); for (const { text, font } of h.measureText) { diff --git a/scripts/verify-paint-text.mjs b/scripts/verify-paint-text.mjs index 6a70594..0b06d00 100644 --- a/scripts/verify-paint-text.mjs +++ b/scripts/verify-paint-text.mjs @@ -127,7 +127,7 @@ try { await page.setContent(h.html); await page.evaluate(() => document.fonts.ready); - const referenceRects = {}; + /** @type {Record} */ const referenceRects = {}; for (const id of h.rects ?? []) { referenceRects[id] = await page.$eval(`#${id}`, (el) => { const r = el.getBoundingClientRect(); @@ -135,7 +135,7 @@ try { }); } - const referenceMeasure = {}; + /** @type {Record} */ const referenceMeasure = {}; if (h.measureText) { for (const { text, font } of h.measureText) { referenceMeasure[`${font} | ${text}`] = await page.evaluate( @@ -189,7 +189,7 @@ try { const candImg = decodePng(out.rgba); const candidateRects = out.rects; - const candidateMeasure = {}; + /** @type {Record} */ const candidateMeasure = {}; const paintRuns = {}; if (h.measureText) { const { measureTextWidth } = await import('../dist/layout/measure.js'); diff --git a/scripts/verify-property-coverage.mjs b/scripts/verify-property-coverage.mjs index ba500e2..6a60334 100644 --- a/scripts/verify-property-coverage.mjs +++ b/scripts/verify-property-coverage.mjs @@ -113,7 +113,7 @@ for (const f of fixturesToRender) { rectsOf(f.html, { width: 800, height: 600, fontFamily: FONT_FAMILY, fontFile: FONT_FILE }); rendered++; } catch (err) { - console.error(`verify-property-coverage: fixture ${f.name} failed to render: ${err.message}`); + console.error(`verify-property-coverage: fixture ${f.name} failed to render: ${(/** @type {Error} */ (err)).message}`); process.exit(1); } } diff --git a/scripts/verify-pseudo-elements.mjs b/scripts/verify-pseudo-elements.mjs index 2aac9ba..be45339 100644 --- a/scripts/verify-pseudo-elements.mjs +++ b/scripts/verify-pseudo-elements.mjs @@ -101,7 +101,7 @@ try { await page.setContent(h.html); await page.evaluate(() => document.fonts.ready); - const referenceRects = {}; + /** @type {Record} */ const referenceRects = {}; for (const id of h.rects ?? []) { referenceRects[id] = await page.$eval(`#${id}`, (el) => { const r = el.getBoundingClientRect(); @@ -109,7 +109,7 @@ try { }); } - const referenceMeasure = {}; + /** @type {Record} */ const referenceMeasure = {}; if (h.measureText) { for (const { text, font } of h.measureText) { referenceMeasure[`${font} | ${text}`] = await page.evaluate( @@ -123,7 +123,7 @@ try { } } - const referenceComputed = {}; + /** @type {Record} */ const referenceComputed = {}; if (h.computedStyle) { for (const { id, pseudo, props } of h.computedStyle) { const key = pseudo ? `${id}::${pseudo}` : id; @@ -172,7 +172,7 @@ try { const candImg = decodePng(out.rgba); const candidateRects = out.rects; - const candidateMeasure = {}; + /** @type {Record} */ const candidateMeasure = {}; if (h.measureText) { const { measureTextWidth } = await import('../dist/layout/measure.js'); for (const { text, font } of h.measureText) { diff --git a/scripts/verify-rect-contract.mjs b/scripts/verify-rect-contract.mjs index 3875fb0..dc076a2 100644 --- a/scripts/verify-rect-contract.mjs +++ b/scripts/verify-rect-contract.mjs @@ -43,7 +43,7 @@ let out; try { out = renderHtml(HTML, opts); } catch (err) { - console.error(`FAIL: renderHtml threw — 'no rect collected' must not fire for DOM-present ids: ${err.message}`); + console.error(`FAIL: renderHtml threw — 'no rect collected' must not fire for DOM-present ids: ${(/** @type {Error} */ (err)).message}`); process.exit(1); } diff --git a/scripts/verify-rtl.mjs b/scripts/verify-rtl.mjs index 2d6005f..fd9d3f0 100644 --- a/scripts/verify-rtl.mjs +++ b/scripts/verify-rtl.mjs @@ -70,6 +70,7 @@ try { fixtureFonts.find((f) => f.family === defaultFamily)?.filePath ?? cfg.fonts.find((f) => f.family === defaultFamily)?.filePath ?? FONT_FILE; + /** @type {import('../dist/index.js').BrowserConfig} */ const browserConfig = { browser: 'chrome', fonts: [...cfg.fonts, ...fixtureFonts], @@ -82,7 +83,7 @@ try { await page.setContent(h.html); await page.evaluate(() => document.fonts.ready); - const referenceRects = {}; + /** @type {Record} */ const referenceRects = {}; for (const id of h.rects ?? []) { referenceRects[id] = await page.$eval(`#${id}`, (el) => { const r = el.getBoundingClientRect(); @@ -90,7 +91,7 @@ try { }); } - const referenceComputed = {}; + /** @type {Record} */ const referenceComputed = {}; if (h.computedStyle) { for (const { id, props } of h.computedStyle) { referenceComputed[id] = await page.evaluate( @@ -105,7 +106,7 @@ try { } } - const referenceFragments = {}; + /** @type {Record} */ const referenceFragments = {}; if (h.textElements && h.textElements.length > 0) { for (const id of h.textElements) { referenceFragments[id] = await page.evaluate((id) => { diff --git a/scripts/verify-segmenter.mjs b/scripts/verify-segmenter.mjs index a7980f7..92e45f0 100644 --- a/scripts/verify-segmenter.mjs +++ b/scripts/verify-segmenter.mjs @@ -93,7 +93,7 @@ for (const { raw } of fixtureList) { try { skiaCanvasFactory.registerFont(f.file, f.family); } catch (err) { - failFast(`font registration failed for '${f.family}': ${err.message}`); + failFast(`font registration failed for '${f.family}': ${(/** @type {Error} */ (err)).message}`); } registeredFamilies.add(f.family); } @@ -162,8 +162,8 @@ try { res.end('not found'); } }); - await new Promise((r) => server.listen(0, '127.0.0.1', r)); - const baseUrl = `http://127.0.0.1:${server.address().port}`; + await new Promise((r) => server.listen(0, '127.0.0.1', () => r())); + const baseUrl = `http://127.0.0.1:${(/** @type {import('net').AddressInfo} */ (server.address())).port}`; await page.goto(`${baseUrl}/`); const layoutRequests = fixtureList.flatMap(({ raw }) => diff --git a/scripts/verify-shadow.mjs b/scripts/verify-shadow.mjs index b3612a4..11c90e0 100644 --- a/scripts/verify-shadow.mjs +++ b/scripts/verify-shadow.mjs @@ -86,7 +86,7 @@ try { await page.setContent(h.html); await page.evaluate(() => document.fonts.ready); - const referenceRects = {}; + /** @type {Record} */ const referenceRects = {}; for (const id of h.rects ?? []) { referenceRects[id] = await page.$eval(`#${id}`, (el) => { const r = el.getBoundingClientRect(); @@ -94,7 +94,7 @@ try { }); } - const referenceComputed = {}; + /** @type {Record} */ const referenceComputed = {}; if (h.computedStyle) { for (const { id, props } of h.computedStyle) { referenceComputed[id] = await page.evaluate( diff --git a/scripts/verify-stress.mjs b/scripts/verify-stress.mjs index 8870bcb..5da7eef 100644 --- a/scripts/verify-stress.mjs +++ b/scripts/verify-stress.mjs @@ -104,7 +104,7 @@ try { await page.setContent(h.html); await page.evaluate(() => document.fonts.ready); - const referenceRects = {}; + /** @type {Record} */ const referenceRects = {}; for (const id of h.rects ?? []) { referenceRects[id] = await page.$eval(`#${id}`, (el) => { const r = el.getBoundingClientRect(); @@ -112,7 +112,7 @@ try { }); } - const referenceMeasure = {}; + /** @type {Record} */ const referenceMeasure = {}; if (h.measureText) { for (const { text, font } of h.measureText) { referenceMeasure[`${font} | ${text}`] = await page.evaluate( @@ -126,7 +126,7 @@ try { } } - const referenceComputed = {}; + /** @type {Record} */ const referenceComputed = {}; if (specs.length > 0) { for (const { id, pseudo, props } of specs) { const key = pseudo ? `${id}::${pseudo}` : id; @@ -175,7 +175,7 @@ try { const candImg = decodePng(out.rgba); const candidateRects = out.rects; - const candidateMeasure = {}; + /** @type {Record} */ const candidateMeasure = {}; if (h.measureText) { const { measureTextWidth } = await import('../dist/layout/measure.js'); for (const { text, font } of h.measureText) { diff --git a/scripts/verify-sweep.mjs b/scripts/verify-sweep.mjs index a7ccaa2..71120b7 100644 --- a/scripts/verify-sweep.mjs +++ b/scripts/verify-sweep.mjs @@ -61,7 +61,7 @@ try { await page.setContent(h.html); await page.evaluate(() => document.fonts.ready); - const referenceRects = {}; + /** @type {Record} */ const referenceRects = {}; for (const id of h.rects ?? []) { referenceRects[id] = await page.$eval(`#${id}`, (el) => { const r = el.getBoundingClientRect(); @@ -112,10 +112,10 @@ try { height, }; - const result = evaluateFixture(fixture); + const result = evaluateFixture(/** @type {import('../dist/harness/fixtures.js').Fixture} */ (fixture)); results.push(result); - const rectLayer = result.layers.rect; - const shotLayer = result.layers.screenshot; + const rectLayer = /** @type {import('../dist/harness/evaluate.js').RectLayerResult} */ (result.layers.rect); + const shotLayer = /** @type {import('../dist/harness/evaluate.js').ScreenshotLayerResult} */ (result.layers.screenshot); const gapLayers = ['rect', 'screenshot'].filter((l) => isGapExpectation(raw.expected?.[l])); rows.push({ fixture: name, diff --git a/scripts/verify-text-align.mjs b/scripts/verify-text-align.mjs index 9aa2488..fe2f099 100644 --- a/scripts/verify-text-align.mjs +++ b/scripts/verify-text-align.mjs @@ -105,7 +105,7 @@ try { await page.setContent(h.html); await page.evaluate(() => document.fonts.ready); - const referenceRects = {}; + /** @type {Record} */ const referenceRects = {}; for (const id of h.rects ?? []) { referenceRects[id] = await page.$eval(`#${id}`, (el) => { const r = el.getBoundingClientRect(); @@ -113,7 +113,7 @@ try { }); } - const referenceMeasure = {}; + /** @type {Record} */ const referenceMeasure = {}; if (h.measureText) { for (const { text, font } of h.measureText) { referenceMeasure[`${font} | ${text}`] = await page.evaluate( @@ -127,7 +127,7 @@ try { } } - const referenceComputed = {}; + /** @type {Record} */ const referenceComputed = {}; if (h.computedStyle) { for (const { id, props } of h.computedStyle) { referenceComputed[id] = await page.evaluate( @@ -175,7 +175,7 @@ try { const candidateRects = out.rects; const candidateComputed = out.computedStyles; - const candidateMeasure = {}; + /** @type {Record} */ const candidateMeasure = {}; if (h.measureText) { const { measureTextWidth } = await import('../dist/layout/measure.js'); for (const { text, font } of h.measureText) { diff --git a/scripts/verify-text-measure.mjs b/scripts/verify-text-measure.mjs index 36b3298..ae277b2 100644 --- a/scripts/verify-text-measure.mjs +++ b/scripts/verify-text-measure.mjs @@ -62,7 +62,7 @@ for (const { name, raw } of fixtures()) { try { skiaCanvasFactory.registerFont(f.file, f.family); } catch (err) { - console.error(`verify:text-measure: failed to register font '${f.family}' from ${f.file}: ${err.message}`); + console.error(`verify:text-measure: failed to register font '${f.family}' from ${f.file}: ${(/** @type {Error} */ (err)).message}`); process.exit(1); } registeredFamilies.add(f.family); @@ -148,7 +148,7 @@ try { // Harness layer-1 runner decides tolerance on the pass corpus of this fixture. const fixture = { name, tolerances, candidate, reference }; - const layer = evaluateMeasureText(fixture); + const layer = evaluateMeasureText(/** @type {import('../dist/harness/fixtures.js').Fixture} */ (fixture)); let checkPass; let detail; diff --git a/scripts/verify-ua-styles.mjs b/scripts/verify-ua-styles.mjs index 686b4f7..156e0a2 100644 --- a/scripts/verify-ua-styles.mjs +++ b/scripts/verify-ua-styles.mjs @@ -76,7 +76,7 @@ try { await page.setContent(h.html); await page.evaluate(() => document.fonts.ready); - const referenceRects = {}; + /** @type {Record} */ const referenceRects = {}; for (const id of h.rects ?? []) { referenceRects[id] = await page.$eval(`#${id}`, (el) => { const r = el.getBoundingClientRect(); @@ -84,7 +84,7 @@ try { }); } - const referenceMeasure = {}; + /** @type {Record} */ const referenceMeasure = {}; if (h.measureText) { for (const { text, font } of h.measureText) { referenceMeasure[`${font} | ${text}`] = await page.evaluate( @@ -98,7 +98,7 @@ try { } } - const referenceComputed = {}; + /** @type {Record} */ const referenceComputed = {}; if (h.computedStyle) { for (const { id, props } of h.computedStyle) { referenceComputed[id] = await page.evaluate( @@ -149,7 +149,7 @@ try { const candImg = decodePng(out.rgba); const candidateRects = out.rects; - const candidateMeasure = {}; + /** @type {Record} */ const candidateMeasure = {}; if (h.measureText) { const { measureTextWidth } = await import('../dist/layout/measure.js'); for (const { text, font } of h.measureText) { diff --git a/scripts/verify-white-space.mjs b/scripts/verify-white-space.mjs index 78bf0c0..d0c8f2a 100644 --- a/scripts/verify-white-space.mjs +++ b/scripts/verify-white-space.mjs @@ -132,7 +132,7 @@ try { await page.setContent(h.html); await page.evaluate(() => document.fonts.ready); - const referenceRects = {}; + /** @type {Record} */ const referenceRects = {}; for (const id of h.rects ?? []) { referenceRects[id] = await page.$eval(`#${id}`, (el) => { const r = el.getBoundingClientRect(); @@ -140,7 +140,7 @@ try { }); } - const referenceMeasure = {}; + /** @type {Record} */ const referenceMeasure = {}; if (h.measureText) { for (const { text, font } of h.measureText) { referenceMeasure[`${font} | ${text}`] = await page.evaluate( @@ -154,7 +154,7 @@ try { } } - const referenceComputed = {}; + /** @type {Record} */ const referenceComputed = {}; if (h.computedStyle) { for (const { id, props } of h.computedStyle) { referenceComputed[id] = await page.evaluate( @@ -202,7 +202,7 @@ try { const candidateRects = out.rects; const candidateComputed = out.computedStyles; - const candidateMeasure = {}; + /** @type {Record} */ const candidateMeasure = {}; if (h.measureText) { const { measureTextWidth } = await import('../dist/layout/measure.js'); for (const { text, font } of h.measureText) { diff --git a/src/layout/block-inline.ts b/src/layout/block-inline.ts index 168cfbe..0ba55f6 100644 --- a/src/layout/block-inline.ts +++ b/src/layout/block-inline.ts @@ -1149,11 +1149,17 @@ function layoutFloat( const bT = style.borderWidth.top; const bB = style.borderWidth.bottom; + const floatHasBlocks = hasBlockLevelChild(el, styles); const specW = resolveLength(style.width, contentWidth, viewport); const padBorderH = padL + padR + bL + bR; let borderBoxWidth: number; if (specW !== null) { borderBoxWidth = style.boxSizing === 'border-box' ? specW : specW + padBorderH; + } else if (floatHasBlocks) { + // Auto-width float with block-level content: approximate shrink-to-fit with + // the available inline width. A full max-content pass over block descendants + // is future work; explicit widths — the common case — are exact. + borderBoxWidth = Math.max(0, contentWidth - marginL - marginR); } else { const text = collectInlineText(el, styles).trim(); const ls = style.letterSpacing; @@ -1167,10 +1173,24 @@ function layoutFloat( } const floatContentWidth = Math.max(0, borderBoxWidth - bL - bR - padL - padR); - // Float content lays out in its own BFC (a fresh float list). + // Float content lays out in its own BFC (a fresh float list). Block-level + // children (including nested floats) go through the normal block-children + // path so they are sized, positioned, painted, and get rects; inline content + // uses the line layouter. A measurement pass sizes the float box here; the + // real positioned pass runs after placement (below) so the children paint + // above the float's own background. let contentHeight = 0; let lines: LineBox[] = []; - if (hasInlineContent(el, styles)) { + if (floatHasBlocks) { + const measFm = new FloatManager(0, floatContentWidth); + const measState: LayoutBlockInput = { fm: measFm, contentX: 0, contentWidth: floatContentWidth, y: 0, prevBottomMargin: 0, cbDirection: style.direction }; + const snapshot = paints.length; + const meas = layoutBlockChildren(el, measState, styles, paints, nextOrder, viewport); + paints.length = snapshot; + contentHeight = meas.height; + const lowest = measFm.lowestFloatBottom('both'); + if (Number.isFinite(lowest)) contentHeight = Math.max(contentHeight, lowest); + } else if (hasInlineContent(el, styles)) { const lineRes = layoutTextLines({ text: collectInlineText(el, styles), x: 0, @@ -1271,6 +1291,15 @@ function layoutFloat( }); } pushBorders(paints, nextOrder, floatKey, style, placed.borderX, placed.borderY, borderBoxWidth, borderHeight); + if (floatHasBlocks) { + // Real, positioned block-children layout: at the placed content origin and + // after the float's background/border ops, so children paint on top. Their + // rects flow into the tree via node.children (collectRects walks it). + const childFm = new FloatManager(node.contentX, floatContentWidth); + const childState: LayoutBlockInput = { fm: childFm, contentX: node.contentX, contentWidth: floatContentWidth, y: node.contentY, prevBottomMargin: 0, cbDirection: style.direction }; + const real = layoutBlockChildren(el, childState, styles, paints, nextOrder, viewport); + node.children.push(...real.nodes); + } if (lines.length > 0) { pushPaintOp(paints, { key: inFlowPaintKey(STEP_INLINE), @@ -1817,6 +1846,33 @@ function isInlineBoxStyle(s: ComputedStyle): boolean { return s.display === 'inline-block' && s.float === 'none' && s.position === 'static'; } +/** + * Render-scoped memoization for atomic (inline-block) intrinsic sizing and + * measurement. Sizing an inline-block whose `width` is auto measures its whole + * subtree, and an atomic is measured once for line-breaking and again for + * placement — so with nested inline-blocks each level re-measures every level + * below it, twice, giving O(2^depth) work. Deeply nested inline-blocks (a real + * WPT flexbox reference, or the progressive nesting a stray self-closing + * `
` produces in the HTML parser) then hang the engine outright. + * + * Both `atomicBoxSize` and `measureAtomic` are pure functions of + * `(element, width)` for a fixed render (styles/viewport are constant), so + * caching by width collapses the exponential to linear. Element objects are + * unique per `renderHtml` (each call re-parses), so a WeakMap keyed by element + * is inherently render-scoped and never yields a stale cross-render result. + */ +const atomicSizeMemo = new WeakMap>(); +const measureAtomicMemo = new WeakMap>(); + +function memoMapFor(wm: WeakMap>, el: P5Element): Map { + let m = wm.get(el); + if (!m) { + m = new Map(); + wm.set(el, m); + } + return m; +} + function atomicBoxSize( el: P5Element, style: ComputedStyle, @@ -1824,6 +1880,9 @@ function atomicBoxSize( refWidth: number, viewport: Viewport | undefined, ): { borderWidth: number; contentWidth: number } { + const memo = memoMapFor(atomicSizeMemo, el); + const hit = memo.get(refWidth); + if (hit) return hit; const padL = resolveLength(style.padding.left, refWidth, viewport) ?? 0; const padR = resolveLength(style.padding.right, refWidth, viewport) ?? 0; const bL = style.borderWidth.left; @@ -1846,7 +1905,9 @@ function atomicBoxSize( const maxW = resolveLength(style.maxWidth, refWidth, viewport); if (minW !== null) borderWidth = Math.max(borderWidth, minW); if (maxW !== null) borderWidth = Math.min(borderWidth, maxW); - return { borderWidth, contentWidth: Math.max(0, borderWidth - padBorderH) }; + const result = { borderWidth, contentWidth: Math.max(0, borderWidth - padBorderH) }; + memo.set(refWidth, result); + return result; } function piecesContentSizes(pieces: InlinePiece[], style: ComputedStyle, ws: WhiteSpaceValue): { min: number; max: number } { @@ -2056,6 +2117,14 @@ function measureAtomic( viewport: Viewport | undefined, ): MeasuredAtomic { const s = piece.style; + // Measurement is a throwaway layout (its paints are discarded below) that + // depends only on (element, contentWidth); memoize to avoid re-measuring the + // subtree once per line-break trial and once per placement (see + // atomicSizeMemo). Only the height/baseline are cached; the returned `piece` + // is the caller's current piece. + const memo = memoMapFor(measureAtomicMemo, piece.el); + const hit = memo.get(piece.contentWidth); + if (hit) return { piece, borderHeight: hit.borderHeight, baselineOffset: hit.baselineOffset }; const padL = resolveLength(s.padding.left, piece.contentWidth, viewport) ?? 0; const padT = resolveLength(s.padding.top, piece.contentWidth, viewport) ?? 0; const bL = s.borderWidth.left; @@ -2078,7 +2147,9 @@ function measureAtomic( viewport, ); paints.length = snapshot; - return { piece, borderHeight: node.borderHeight, baselineOffset: atomicBaselineOffset(node, s) }; + const measured = { borderHeight: node.borderHeight, baselineOffset: atomicBaselineOffset(node, s) }; + memo.set(piece.contentWidth, measured); + return { piece, borderHeight: measured.borderHeight, baselineOffset: measured.baselineOffset }; } /**