Skip to content

text-1b: render <text> — the text rung - #71

Merged
softmarshmallow merged 2 commits into
mainfrom
text-1b-websem-text
Aug 4, 2026
Merged

text-1b: render <text> — the text rung#71
softmarshmallow merged 2 commits into
mainfrom
text-1b-websem-text

Conversation

@softmarshmallow

Copy link
Copy Markdown
Member

<text> leaves the refusal corpus. Six Chromium-baked cells, byte-exact, no tolerance — and the Web family now has a real shaped-text producer, which is the precondition D-M's shaped-text stage has been gated on (gridaco/nothing#69).

Stacked on #70 (the textlayout resolver), executing against the method ratified in #68.

The seam

The document says; the oracle resolves. websem's <text> arm owns source semantics only — character data with XML whitespace collapsing, x/y, text-anchor, and the cascaded family and size. It shapes nothing. textlayout resolves the run once and returns the immutable artifact; glyph outlines lower to rframe's existing path facts, so no font identity crosses the contract and the frame stays honestly glyphless. The D-M join is untouched.

Measured before written

Every claim in the cascade pair was probed against Chromium 149 first:

Probe Result Consequence
font-family attribute alone selects the face admitted as a presentation hint
author rule vs. attribute rule wins true hint precedence, pinned by law
font-family="" drops to default invalid-declaration semantics
text-anchor in CSS Chromium applies it; a rule beats the attribute but it is engine = "gecko" at the Stylo pin — the servo build has no such longhand, so a sheet declaring it was a silent drop. It joins the not-represented refusal list, same shape as the gradient rung's stop-color row.
whitespace: newline+indent, tab, 3-space run each collapses to one advance the collapsing rule, pinned

Note the correction that measurement forced: the plan was to admit both properties as hints. text-anchor cannot be represented at all, so the attribute is read directly and the CSS spelling refuses by name.

The numeric domain is enforced, not assumed

Integer position, font-size a multiple of 5, integer anchor-resolved start. Chromium snaps everything else by a rasterizer-internal rule; this refuses by name rather than codifying a rasterizer's rounding.

Hermetic by default

A run whose family the host never declared refuses by name — no tofu, no ambient face, no machine-local pixel. A generic family (including the initial value a document with no font-family computes to) names no declared font and says so.

The suite

fixtures/web-first/text/ on the animation/ precedent, under the corpus-growth law: its own manifest, baker, and test binary; the primitive root stays closed to text. The fixture is the document, the font is the environment — the committed .svg carries no font bytes (six copies of a pinned font is a corpus), and the baker declares the pinned identity to Chromium exactly as the host declares it to the engine, verified against its digest before any capture.

Graduation and the two legacy moves

svg-text graduates; what remains refused holds its own rows (svg-text-undeclared-font, svg-text-tspan). Two legacy expectations moved with the behavior, both in the right direction: basic-shapes.svg's stylesheet declares text-anchor and its labels ask for a generic family — silent drops before this rung, named holes after it.

Full workspace green, fmt and clippy clean.

…or in CSS

The text rung's cascade pair, both measured against Chromium 149 first.

font-family joins the admitted presentation-hint set: the attribute alone
selects the face, an author rule beats it, font-family="" drops like an
invalid declaration, and the property inherits — each pinned by the
precedence law.

text-anchor joins the not-represented refusal list. Chromium consumes it
from the cascade (a rule anchors an attribute-free <text>, and
text-anchor: end beats text-anchor="middle"), but the property is
engine = "gecko" at the Stylo pin, so the servo build has no such
longhand and a sheet declaring one was a silent drop. Same shape as the
gradient rung's stop-color row, one rung later. The attribute spelling
stays admitted and is read by the text compiler.
The Web family gains a real shaped-text producer, and <text> leaves the
refusal corpus for six Chromium-baked cells.

The document says, the oracle resolves:

- websem's <text> arm owns source semantics only — character data with
  XML whitespace collapsing (measured: a tab, a newline, and a run of
  spaces each collapse to one advance), x/y, text-anchor, and the
  cascaded family and size. It shapes nothing.
- textlayout resolves the run once against a declared font environment
  and returns the immutable artifact; glyph outlines lower to rframe's
  existing path facts, so no font identity crosses the contract and the
  frame stays honestly glyphless.
- the admitted numeric domain is enforced, not assumed: integer position,
  font-size a multiple of 5, integer anchor-resolved start. Chromium
  snaps everything else by a rasterizer-internal rule; this refuses by
  name rather than codifying it.

Hermetic by default: a run whose family the host never declared refuses
by name — no tofu, no ambient face, no machine-local pixel. A generic
family (including the initial value) names no declared font and says so.

fixtures/web-first/text/ is the suite, on the animation/ precedent and
under the corpus-growth law: the fixture is the document, the font is
the environment, and the baker declares the pinned identity to Chromium
exactly as the host declares it to the engine — verified against its
digest before any capture. All six cells are byte-exact with no
tolerance admissible.

svg-text graduates; what remains refused holds its own rows
(svg-text-undeclared-font, svg-text-tspan). Two legacy expectations move
with the behavior: basic-shapes.svg's stylesheet declares text-anchor
and its labels ask for a generic family — both were silent drops before
this rung and are named holes after it.
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nothing Ready Ready Preview Aug 4, 2026 12:46pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR adds SVG font-family presentation-attribute support in csscascade, and introduces text-layout font environment support in websem for SVG <text> element compilation. It adds a new svg_text module for glyph resolution, an unsupported_corpus/test update set, and fixture-based Chromium oracle testing infrastructure.

Changes

SVG font-family and text rendering

Layer / File(s) Summary
font-family presentation hint in cascade
crates/csscascade/src/dom.rs, crates/csscascade/tests/svg_presentation_hints.rs
Adds font-family to admitted SVG presentation properties, mapping it to LonghandId::FontFamily. Tests verify hint computation, author-rule override, invalid-value fallback, and inheritance.
Font environment dependency and threading
crates/websem/Cargo.toml, crates/websem/src/lib.rs, crates/websem/src/svg.rs
Adds the textlayout dependency and svg_text module. SvgFrameSource retains a declared Environment; new from_standalone_svg_with_fonts and from_standalone_svg_best_effort_with_fonts constructors thread it through compilation and sampling.
Text element compilation and attribute patrol
crates/websem/src/svg.rs
Adds compile_text for visibility/opacity handling, stroke rejection, text-node concatenation, whitespace collapsing, position and anchor resolution, plus rejection lists for unsupported text attributes and cascaded text-anchor.
svg_text glyph resolution module
crates/websem/src/svg_text.rs
Adds Anchor parsing/positioning, whitespace collapsing, TextError types, an OutlineSink for glyph-to-path lowering, numeric-domain validation, and resolve_text_path resolving text via textlayout into PathData.
Existing test and corpus updates
crates/n0_cli/src/main.rs, crates/websem/tests/best_effort.rs, crates/websem/tests/groups_contract.rs, crates/websem/tests/unsupported_corpus.rs
Updates test expectations from unsupported-<text> reasons to text-anchor and generic-font-family degradations; splits the svg-text unsupported fixture into undeclared-font and tspan fixtures.
SVG text integration tests
crates/websem/tests/svg_text.rs
Adds fixture loading, pinned Ahem font verification, admitted-text rendering checks, whitespace/anchor tests, refusal diagnostics, stylesheet cascade behavior, and byte-exact comparison against committed Chromium oracle images.
Chromium oracle baking script and fixture manifests
fixtures/web-first/text/bake_chromium.ts, fixtures/web-first/text/cases.json, fixtures/web-first/text/oracle-bake.json, fixtures/web-first/text/README.md
Adds a Playwright-based script that captures deterministic Chromium screenshots, verifies pinned font identity, and writes a hashed manifest. Adds fixture case and oracle manifests and README documentation.
Checklist and status documentation
docs/wg/consolidation/svg-checklist.md, fixtures/web-first/STATUS.md
Marks single-run text, text-anchor, and cascaded font-family/font-size as implemented. Updates refusal register count and entries for svg-text-tspan and svg-text-undeclared-font.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant SvgFrameSource
  participant compile_svg_element
  participant compile_text
  participant svg_text_module as svg_text
  participant Environment

  Caller->>SvgFrameSource: from_standalone_svg_with_fonts(source, viewport, fonts)
  SvgFrameSource->>compile_svg_element: compile(source, fonts)
  compile_svg_element->>compile_text: compile_text(node, fonts)
  compile_text->>svg_text_module: resolve_text_path(text, family, size, x, y, anchor, fonts)
  svg_text_module->>Environment: resolve styled text
  Environment-->>svg_text_module: glyph outlines
  svg_text_module-->>compile_text: PathData or TextError
  compile_text-->>compile_svg_element: path geometry or refusal
  compile_svg_element-->>SvgFrameSource: compiled cascade
Loading

Possibly related PRs

  • gridaco/nothing#68: This PR implements the text-oracle method proposed there, using hermetic Ahem font resolution and deterministic SVG text path/oracle tests.
  • gridaco/nothing#70: This PR integrates the textlayout environment and resolver APIs from PR #70 into SVG text compilation and path generation.
  • gridaco/nothing#26: Both PRs address the SVG text-layout domain; PR #26 adds specifications while this PR implements runtime rendering.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: rendering SVG <text> elements.
Description check ✅ Passed The description directly explains SVG text rendering, shaping, font handling, fixtures, and refusal cases added by the pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch text-1b-websem-text

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/websem/src/svg_text.rs`:
- Around line 147-170: Extend the numeric-domain enforcement beyond
admit_numeric_domain to validate every resolved glyph outline before
ResolvedTextLayout::outline streams it through FlipSink. Reject glyph contours
containing quad_to or curve_to, or establish and require a validated-face
contract that guarantees line-only geometry; preserve existing rejection
behavior for unsupported faces. Add a non-Ahem regression covering a face with
non-line glyph outlines.

In `@crates/websem/src/svg.rs`:
- Around line 2419-2420: Update the text-to-path flow around collapse_whitespace
and resolve_text_path so collapsed-empty content returns Ok(None) after
supported coordinate and anchor validation, before reading or validating
font-family. Preserve the existing font-selection and path-resolution behavior
for non-empty content.
- Around line 529-585: Add public strict and best-effort
from_html_inline_svg_with_fonts constructors that accept textlayout::Environment
and delegate to from_source_with_fonts with SourceEntry::InlineHtml, the
appropriate CompileMode, and the existing viewport handling. Keep the existing
no-font constructors unchanged, and add a regression covering inline Ahem text
resolving through the supplied font environment.

In `@fixtures/web-first/text/bake_chromium.ts`:
- Around line 68-83: Update capture in fixtures/web-first/text/bake_chromium.ts
to wait for the declared font family with document.fonts.load and
document.fonts.ready before svg.screenshot(), then require document.fonts.check
to pass before capturing. Preserve the existing SVG validation; in
fixtures/web-first/text/README.md at line 41, retain the “awaited ready” wording
because the baker now enforces that guarantee.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 930b4676-30e9-414b-b77d-bd265e9174db

📥 Commits

Reviewing files that changed from the base of the PR and between 6ce9851 and 30f2092.

⛔ Files ignored due to path filters (15)
  • Cargo.lock is excluded by !**/*.lock
  • fixtures/web-first/text/chromium/svg-text-anchor-end.png is excluded by !**/*.png
  • fixtures/web-first/text/chromium/svg-text-anchor-middle.png is excluded by !**/*.png
  • fixtures/web-first/text/chromium/svg-text-em-box.png is excluded by !**/*.png
  • fixtures/web-first/text/chromium/svg-text-fill-color.png is excluded by !**/*.png
  • fixtures/web-first/text/chromium/svg-text-space-advance.png is excluded by !**/*.png
  • fixtures/web-first/text/chromium/svg-text-whitespace-collapse.png is excluded by !**/*.png
  • fixtures/web-first/text/svg-text-anchor-end.svg is excluded by !**/*.svg
  • fixtures/web-first/text/svg-text-anchor-middle.svg is excluded by !**/*.svg
  • fixtures/web-first/text/svg-text-em-box.svg is excluded by !**/*.svg
  • fixtures/web-first/text/svg-text-fill-color.svg is excluded by !**/*.svg
  • fixtures/web-first/text/svg-text-space-advance.svg is excluded by !**/*.svg
  • fixtures/web-first/text/svg-text-whitespace-collapse.svg is excluded by !**/*.svg
  • fixtures/web-first/unsupported/svg-text-tspan.svg is excluded by !**/*.svg
  • fixtures/web-first/unsupported/svg-text-undeclared-font.svg is excluded by !**/*.svg
📒 Files selected for processing (17)
  • crates/csscascade/src/dom.rs
  • crates/csscascade/tests/svg_presentation_hints.rs
  • crates/n0_cli/src/main.rs
  • crates/websem/Cargo.toml
  • crates/websem/src/lib.rs
  • crates/websem/src/svg.rs
  • crates/websem/src/svg_text.rs
  • crates/websem/tests/best_effort.rs
  • crates/websem/tests/groups_contract.rs
  • crates/websem/tests/svg_text.rs
  • crates/websem/tests/unsupported_corpus.rs
  • docs/wg/consolidation/svg-checklist.md
  • fixtures/web-first/STATUS.md
  • fixtures/web-first/text/README.md
  • fixtures/web-first/text/bake_chromium.ts
  • fixtures/web-first/text/cases.json
  • fixtures/web-first/text/oracle-bake.json

Comment on lines +147 to +170
/// The admitted numeric domain (the ratified text-oracle method): a glyph box
/// edge must land on an integer coordinate, so every rasterizer's per-pixel
/// coverage is 0 or 1 and bilevel and antialiased raster agree.
///
/// `font_size` divisible by 5 keeps a 0.8/0.2 em split integral; integer
/// authored and anchor-resolved positions keep the run's origin integral.
fn admit_numeric_domain(x: f32, y: f32, font_size: f32, start_x: f32) -> Result<(), TextError> {
let integral = |value: f32| value.fract() == 0.0;
if !integral(x) || !integral(y) {
return Err(TextError::OutsideNumericDomain(format!(
"position ({x}, {y}) is not integral"
)));
}
if !integral(font_size) || font_size % 5.0 != 0.0 {
return Err(TextError::OutsideNumericDomain(format!(
"font-size {font_size} is not an integer multiple of 5"
)));
}
if !integral(start_x) {
return Err(TextError::OutsideNumericDomain(format!(
"the anchor-resolved start x {start_x} is not integral"
)));
}
Ok(())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline crates/textlayout/src/environment.rs --items all --view expanded
ast-grep outline crates/textlayout/src/artifact.rs --items all --view expanded
rg -n -C 5 'FontResource|pub struct Environment|impl Environment|trait OutlineSink|fn outline|pub fn resolve' crates/textlayout/src

Repository: gridaco/nothing

Length of output: 9287


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,290p' crates/textlayout/src/artifact.rs
printf '\n--- environment.rs ---\n'
sed -n '1,120p' crates/textlayout/src/environment.rs
printf '\n--- resolve.rs top and resolve outline checks ---\n'
sed -n '1,220p' crates/textlayout/src/resolve.rs
printf '\n--- websem svg_text relevant ranges ---\n'
sed -n '120,285p' crates/websem/src/svg_text.rs

Repository: gridaco/nothing

Length of output: 27249


Enforce the numeric domain on every resolved glyph outline.

admit_numeric_domain(...) only checks page position, font size, and total advance. Resolution only rejects color/bitmap tables and unsupported positioning, while ResolvedTextLayout::outline(...) streams every parsed contour through FlipSink. quad_to and curve_to do not preserve binary raster coverage, so this gate can pass a declared face while producing outside-domain geometry. Add face-level geometry validation, reject non-line-to glyphs, or require a separate validated-face contract, and add a non-Ahem regression.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/websem/src/svg_text.rs` around lines 147 - 170, Extend the
numeric-domain enforcement beyond admit_numeric_domain to validate every
resolved glyph outline before ResolvedTextLayout::outline streams it through
FlipSink. Reject glyph contours containing quad_to or curve_to, or establish and
require a validated-face contract that guarantees line-only geometry; preserve
existing rejection behavior for unsupported faces. Add a non-Ahem regression
covering a face with non-line glyph outlines.

Comment thread crates/websem/src/svg.rs
Comment on lines +529 to +585
/// Retain a standalone SVG document together with the fonts a `<text>`
/// run may resolve against. The environment is a manifest of exact bytes
/// the host has already verified against their declared digests — this
/// crate reads no font file and consults no ambient font database.
pub fn from_standalone_svg_with_fonts(
source: impl Into<Arc<str>>,
initial_viewport: InitialViewport,
fonts: textlayout::Environment,
) -> Result<Self, CompileError> {
Self::from_source_with_fonts(
source.into(),
SourceEntry::StandaloneSvg,
CompileMode::Strict,
Some(initial_viewport),
fonts,
)
}

/// The best-effort variant of [`Self::from_standalone_svg_with_fonts`].
pub fn from_standalone_svg_best_effort_with_fonts(
source: impl Into<Arc<str>>,
initial_viewport: InitialViewport,
fonts: textlayout::Environment,
) -> Result<Self, CompileError> {
Self::from_source_with_fonts(
source.into(),
SourceEntry::StandaloneSvg,
CompileMode::BestEffort,
Some(initial_viewport),
fonts,
)
}

fn from_source(
source: Arc<str>,
entry: SourceEntry,
mode: CompileMode,
initial_viewport: Option<InitialViewport>,
) -> Result<Self, CompileError> {
// No declared fonts: a `<text>` run refuses by name rather than
// reaching for an ambient face. That is the hermetic default the
// text-oracle method ratified, not an omission.
Self::from_source_with_fonts(
source,
entry,
mode,
initial_viewport,
textlayout::Environment::default(),
)
}

fn from_source_with_fonts(
source: Arc<str>,
entry: SourceEntry,
mode: CompileMode,
initial_viewport: Option<InitialViewport>,
fonts: textlayout::Environment,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)svg\.rs$|textlayout|Cargo\.toml$|grida|n0' | head -200

echo "== outline svg =="
ast-grep outline crates/websem/src/svg.rs --view compact || true

echo "== relevant svg sections =="
sed -n '1,120p' crates/websem/src/svg.rs
sed -n '480,640p' crates/websem/src/svg.rs
sed -n '1150,1550p' crates/websem/src/svg.rs

echo "== symbol search =="
rg -n "from_html_inline|InlineHtml|StandaloneSvg|from_source_with_fonts|compile_text|textlayout::Environment|public_api|regression|Ahem" crates/websem crates -g '*.rs' | head -300

Repository: gridaco/nothing

Length of output: 9473


🏁 Script executed:

#!/bin/bash
set -u

echo "== locate svg.rs =="
fd -a 'svg\.rs$' . | sed 's#^\./##' | head -200

echo "== locate websem =="
git ls-files | rg '(^|/)websem|crates/websem|crates/.*svg\.rs$' | head -300

echo "== textlayout env mentions in tracked rust files =="
rg -n "textlayout::Environment|SourceEntry|from_html_inline|from_standalone|from_source_with_fonts|compile_text|InlineHtml|StandaloneSvg" --glob '*.rs' CRATES crates . 2>/dev/null | head -400 || true

Repository: gridaco/nothing

Length of output: 46391


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Relevant websem/svg.rs sections =="
sed -n '130,160p' crates/websem/src/svg.rs
sed -n '430,585p' crates/websem/src/svg.rs
sed -n '2340,2410p' crates/websem/src/svg.rs

echo "== websem tests around Ahem/font environment =="
sed -n '1,220p' crates/websem/tests/svg_text.rs

echo "== Check public API constructors available via docs/source grep =="
rg -n "pub fn from_html|pub fn from_standalone|with_fonts|textlayout::Environment" crates/websem/src/svg.rs crates/websem/tests/svg_text.rs

echo "== behavioral route probe from source text =="
python3 - <<'PY'
from pathlib import Path
p = Path("crates/websem/src/svg.rs").read_text()
def body(start_marker, end_marker):
    i = p.index(start_marker)
    j = p.index(end_marker, i+1)
    return p[i:j]
print("from_html_inline_svg calls from_source:", "Self::from_source(source;" in body("    pub fn from_html_inline_svg", "    /// The best-effort variant"))
print("from_source defaults fonts:", "textlayout::Environment::default()" in body("    fn from_source(", "    fn from_source_with_fonts("))
print("compile_text font arg:", "fonts: &textlayout::Environment" in body("fn compile_text", "} // compile_text"))
PY

Repository: gridaco/nothing

Length of output: 20260


Add a font-aware inline SVG constructor.

from_html_inline_svg* calls from_source, and from_source always passes textlayout::Environment::default(). No public inline SVG API can supply the declared font, so inline <text font-family="Ahem"> can only refuse by name. Add strict and best-effort from_html_inline_svg*_with_fonts constructors using SourceEntry::InlineHtml; add a covered regression for inline Ahem text.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/websem/src/svg.rs` around lines 529 - 585, Add public strict and
best-effort from_html_inline_svg_with_fonts constructors that accept
textlayout::Environment and delegate to from_source_with_fonts with
SourceEntry::InlineHtml, the appropriate CompileMode, and the existing viewport
handling. Keep the existing no-font constructors unchanged, and add a regression
covering inline Ahem text resolving through the supplied font environment.

Comment thread crates/websem/src/svg.rs
Comment on lines +2419 to +2420
let content = crate::svg_text::collapse_whitespace(&raw);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Return before font selection for collapsed-empty content.

Line 2419 can produce an empty string. Lines 2444-2455 reject the generic initial family before resolve_text_path can return Ok(None). Therefore <text/> and whitespace-only text fail unless an unnecessary declared font exists.

Return Ok(None) after supported coordinate and anchor validation, but before reading font-family.

Proposed fix
     let anchor = match get_attr(el, "text-anchor") {
         // ...
     };
 
+    if content.is_empty() {
+        return Ok(None);
+    }
+
     let data = el.borrow_data().expect("styled element");

Also applies to: 2432-2464

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/websem/src/svg.rs` around lines 2419 - 2420, Update the text-to-path
flow around collapse_whitespace and resolve_text_path so collapsed-empty content
returns Ok(None) after supported coordinate and anchor validation, before
reading or validating font-family. Preserve the existing font-selection and
path-resolution behavior for non-empty content.

Comment on lines +68 to +83
async function capture(page: Page, fixture: Case, source: string): Promise<Buffer> {
await page.setViewportSize({ width: fixture.width, height: fixture.height });
const dataUrl = `data:image/svg+xml;base64,${Buffer.from(source).toString("base64")}`;
await page.goto(dataUrl, { waitUntil: "load" });

const svg = page.locator("svg").first();
if ((await svg.count()) !== 1) {
throw new Error(`${fixture.id}: expected a first <svg> element`);
}
const box = await svg.boundingBox();
if (!box || box.width !== fixture.width || box.height !== fixture.height) {
throw new Error(
`${fixture.id}: unexpected SVG box ${JSON.stringify(box)}; expected ${fixture.width}x${fixture.height}`,
);
}
return svg.screenshot({ omitBackground: true, type: "png" });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## files"
git ls-files | rg '(^fixtures/web-first/text/(bake_chromium\.ts|README\.md|.*\.ts)|^fixtures/web-first/text)' || true

echo "## bake_chromium outline"
ast-grep outline fixtures/web-first/text/bake_chromium.ts --view expanded || true

echo "## relevant bake_chromium lines"
cat -n fixtures/web-first/text/bake_chromium.ts | sed -n '1,180p'

echo "## relevant README lines"
cat -n fixtures/web-first/text/README.md | sed -n '1,120p'

echo "## search font declarations/await/ready in fixtures/text"
rg -n "font|`@font-face`|document\\.fonts|ready|readyState|load|expect\\(--ready\\|readiness|capture\\(" fixtures/web-first/text -S

Repository: gridaco/nothing

Length of output: 1916


Wait for the injected font before each screenshot.

capture waits for the page load event and locator actionability only. It does not wait for the injected @font-face used by the text case. Add a document.fonts.load(...)/document.fonts.ready wait for the declared family before svg.screenshot(), then require document.fonts.check(...), and keep the README “awaited ready” language only while the baker enforces it.

📍 Affects 2 files
  • fixtures/web-first/text/bake_chromium.ts#L68-L83 (this comment)
  • fixtures/web-first/text/README.md#L41-L41
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@fixtures/web-first/text/bake_chromium.ts` around lines 68 - 83, Update
capture in fixtures/web-first/text/bake_chromium.ts to wait for the declared
font family with document.fonts.load and document.fonts.ready before
svg.screenshot(), then require document.fonts.check to pass before capturing.
Preserve the existing SVG validation; in fixtures/web-first/text/README.md at
line 41, retain the “awaited ready” wording because the baker now enforces that
guarantee.

@softmarshmallow
softmarshmallow merged commit 3a06dd9 into main Aug 4, 2026
20 of 22 checks passed
@softmarshmallow
softmarshmallow deleted the text-1b-websem-text branch August 4, 2026 18:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant