Skip to content

feat(style,layout,render): converge inline pipeline — counters + pseudo/generated content pre-layout (slice 3)#273

Merged
send merged 4 commits into
mainfrom
feat/inline-pipeline-slice3
Jun 2, 2026
Merged

feat(style,layout,render): converge inline pipeline — counters + pseudo/generated content pre-layout (slice 3)#273
send merged 4 commits into
mainfrom
feat/inline-pipeline-slice3

Conversation

@send
Copy link
Copy Markdown
Owner

@send send commented Jun 2, 2026

Summary

Slice 3 of the render↔layout inline-pipeline convergence (One-issue-one-way). Moves the CSS counter state machine + pseudo/marker generated-content resolution out of render's paint walk into a new pre-layout pass — the final phase of style resolution. The convergence target: generated-content text (counter values, pseudo content, list markers) is resolved once, in document order, before layout, and persisted as ECS state that both layout and render read.

Prior slices: #270 (horizontal LTR) / #271 (vertical WM).

Why it's a correctness fix, not just dedup

Pseudo entities were spawned at style time with counter() content left as placeholder text ([counter:list-item]). Layout measured that placeholder → wrong line-break/width. Render papered over it by re-resolving counters at paint time — so it painted the right glyphs at positions computed from the wrong text. Resolving once, pre-layout, fixes the measurement at the source and lets the single inline pipeline own pseudo runs too (InlineFlow now persists them).

Changes

  • elidex-ecs: + ListItemMarker(String) component — the single source of list-item marker text.
  • elidex-style: new generated_content pass — a document-order CounterState walk that writes resolved pseudo content → the pseudo's TextContent and a reconciled ListItemMarker (insert-or-remove every pass, mirroring slice-1's InlineFlow explicit-clear discipline). Wired as the final phase of resolve_styles_with_compat (one insertion point covers every style→layout caller). generate_pseudo_entity no longer pre-resolves content — the pass is the single resolver (the [counter:X] placeholder is gone).
  • elidex-layout-block: drop the has_pseudo persist_flow gate; the pseudo branch now applies the has_bidi / has_text_transform gates on the resolved text (so RTL / text-transformed pseudo content stays gated for later slices).
  • elidex-render: delete resolve_pseudo_text + resolve_content_items_with_counters; maybe_emit_list_marker reads ListItemMarker; the walk's counter machine is retained but gated paged-only — its sole remaining consumer is paged-media margin boxes (per-page running-header counters require post-pagination page assignment and cannot be precomputed pre-layout; analogous to bidi staying in render).

Folded-in spec fixes

  • CSS Lists 3 §4.5: a display:none element does not set/reset/increment a counter (the old paint walk processed counters before the display:none check). Applied in the pass and mirrored in the retained paged walk.
  • CSS Content 3 §2.1: attr() in pseudo content resolves against the originating element (was resolving against the pseudo entity → empty).
  • Citation drift: counter.rs / walk.rs cited "CSS Lists L3 §5.x" — counters live under §4 (§5 has no headings). Swept to §4.1/§4.2/§4.3/§4.6/§4.7; counter-value formatting → CSS Counter Styles 3 §6.

Edge matrix covered

Counter ops × scope (reset/increment/set, reversed, nested counters()); implicit list-item (ol/ul/li/start/value/reversed/nested); 12 list-style-type × marker formatting; pseudo content shapes (string/attr/counter/counters/mixed); §4.5 display:none-skip and visibility:hidden-still-counts; ::before-vs-::after counter timing; marker gate-OUT staleness (reconcile removal); re-resolution idempotency; marker × paged.

Tests

6 new generated_content pass tests (markers in document order, §4.5 display:none skip with value check, reconcile removal on gate-out, nested-ol reset, counter() pseudo content, attr-against-originating). Layout gate_excludes_pseudo_elementpersists_pseudo_element_flow (inverted). Render marker tests run the pass before building the display list. All 1053 tests across the touched crates + elidex-shell pass (no golden churn — output-preserving for existing cases).

Cap posture

Cap-neutral, no new #11-* slot — One-issue-one-way relocation + conformance/correctness, not a new web-platform feature (same posture as slices 1–2).

Deferred (own homes)

::marker as a real entity + layout marker-box generation (list-style-position); paged-media margin-box counter convergence; quotes/url/leader content values; visibility:collapse §4.5 edge.

🤖 Generated with Claude Code

send and others added 2 commits June 2, 2026 13:41
…do/generated content pre-layout (slice 3)

Move the CSS counter state machine + pseudo/marker generated-content
resolution out of render's paint walk into a new pre-layout pass (the
final phase of style resolution). Layout now measures resolved generated
text (was a `[counter:X]` placeholder) so InlineFlow persists pseudo runs;
render reads resolved pseudo TextContent + a ListItemMarker component
instead of running a counter machine for document content.

- elidex-ecs: + ListItemMarker(String) component (single source of marker text).
- elidex-style: new generated_content pass — a document-order CounterState
  walk writing pseudo `content` -> TextContent and a reconciled
  ListItemMarker; wired as the final phase of resolve_styles_with_compat.
  generate_pseudo_entity no longer pre-resolves content (the pass is the
  single resolver; the `[counter:X]` placeholder is gone).
- elidex-layout-block: drop the has_pseudo persist_flow gate; the pseudo
  branch now applies the bidi/text-transform gates on the resolved text.
- elidex-render: delete resolve_pseudo_text + resolve_content_items_with_counters;
  maybe_emit_list_marker reads ListItemMarker; the walk counter machine is
  retained (gated paged-only) solely for paged-media margin boxes.

Fold-in spec fixes: CSS Lists 3 §4.5 (display:none does not affect counters),
attr() resolves against the originating element, and the §5.x->§4.x counter
citation drift in counter.rs/walk.rs.

Slice 3 of the render<->layout inline-pipeline convergence (One-issue-one-way,
cap-neutral). Plan: render-layout-inline-convergence-slice3-plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r helper + clone-guard the pass

/code-review + /simplify follow-ups (no behavior change):
- counter.rs: extract `apply_implicit_list_counters_from_dom` (DOM tag/attrs/li-count
  marshalling) shared by the cascade walk, the generated-content pass, and render's
  paged counter walk — was triplicated.
- generated_content.rs: probe the Copy marker fields + a has-counter-props flag from a
  scoped borrow and clone `ComputedStyle` only when the element carries counter
  influence (list tag or explicit counter-*), avoiding a full-style clone per common
  element on the restyle hot path; `resolve_pseudo_content` reads `content` internally.
- doc: note that the pass's implicit-counter application is load-bearing (the parallel
  cascade path does not bake implicit counters); tidy a test binding.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 2, 2026 08:30
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Moves CSS counters + generated content resolution (pseudo content, list markers) out of render’s paint walk into a new pre-layout style pass, so layout measures resolved text and render consumes persisted ECS state.

Changes:

  • Add generated_content pass in elidex-style, invoked as the final phase of resolve_styles_with_compat.
  • Introduce ListItemMarker(String) ECS component as the single source of list marker text; render reads it instead of evaluating counters.
  • Remove the inline-flow “has_pseudo” gate so pseudo text can persist in InlineFlow once resolved pre-layout.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
crates/layout/elidex-layout-block/src/inline/tests/inline_flow.rs Updates gating test to assert pseudo flows now persist with resolved text.
crates/layout/elidex-layout-block/src/inline/mod.rs Removes has_pseudo gate; adds bidi/text-transform gating for pseudo text runs.
crates/css/elidex-style/src/walk.rs Centralizes implicit list counter application via apply_implicit_list_counters_from_dom.
crates/css/elidex-style/src/resolve/box_model/mod.rs Spec citation update for reversed counters.
crates/css/elidex-style/src/pseudo.rs Stops pre-resolving pseudo text; creates pseudo entities with empty TextContent.
crates/css/elidex-style/src/lib.rs Wires resolve_generated_content as final style-resolution phase; exports it.
crates/css/elidex-style/src/generated_content.rs New pass: document-order counter machine + pseudo/list marker resolution into ECS.
crates/css/elidex-style/src/counter.rs Spec citation sweep + new apply_implicit_list_counters_from_dom helper.
crates/core/elidex-render/src/builder/walk.rs Reads ListItemMarker; gates counter mutation to paged builds only.
crates/core/elidex-render/src/builder/tests/display_types.rs Runs generated-content pass before display list build in marker tests.
crates/core/elidex-render/src/builder/tests/counter.rs Runs generated-content pass before display list build; updates display:none expectations.
crates/core/elidex-render/src/builder/mod.rs Adds paged flag to PaintContext setup.
crates/core/elidex-render/src/builder/inline.rs Removes render-time pseudo counter evaluation; reads resolved TextContent only.
crates/core/elidex-ecs/src/lib.rs Re-exports new ListItemMarker component.
crates/core/elidex-ecs/src/components.rs Defines ListItemMarker(String) component and its semantics.
Comments suppressed due to low confidence (1)

crates/core/elidex-render/src/builder/walk.rs:575

  • maybe_emit_list_marker clones ListItemMarker’s String for every list item even though the marker is immediately passed by reference. This adds avoidable allocation/copy overhead on large lists; you can borrow the stored marker text directly from the ECS ref.
            let marker_text = match ctx.dom.world().get::<&ListItemMarker>(child) {
                Ok(m) => m.0.clone(),
                Err(_) => return,
            };
            if let Ok(child_lb) = ctx.dom.world().get::<&LayoutBox>(child) {
                emit_list_marker_with_counter(
                    &child_lb,
                    &child_style,
                    &marker_text,
                    ctx.font_db,
                    ctx.font_cache,
                    ctx.dl,
                );
            }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/css/elidex-style/src/counter.rs Outdated
Comment thread crates/css/elidex-style/src/generated_content.rs
- generated_content.rs: align the recursion cap with the other document-order
  walks (`> MAX_ANCESTOR_DEPTH`, not `>=`) so generated-content resolution covers
  exactly the depths layout/render process, not one level shallower.
- counter.rs: `apply_implicit_list_counters_from_dom` bails for non-list tags
  before cloning Attributes (runs on every element on every restyle via the
  cascade walk) and only clones for <ol>/<li> (the tags that read attributes).

Deferred (pre-existing, not introduced by this PR): the implicit list-item
increment keys on the <li> tag rather than `display: list-item` (CSS Lists 3
§4.6) — a separate spec-conformance follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.

Comment thread crates/css/elidex-style/src/generated_content.rs
Comment thread crates/css/elidex-style/src/generated_content.rs
Comment thread crates/css/elidex-style/src/generated_content.rs
generated_content pre-layout pass:
- Hoist the list-item marker reconcile-remove BEFORE the display:none /
  display:contents early-returns, so the "insert-or-remove every pass"
  invariant holds on all exit paths (a list-item → display:none/contents
  transition no longer leaves a stale ListItemMarker). The insert half stays
  after counter processing for visible-type list-items.
- Use a fresh CounterState per root (find_roots may return multiple
  disconnected trees = independent documents; counter scope must not leak
  across them).

Regression tests: marker_removed_when_element_becomes_display_none (none +
contents), counters_independent_across_roots.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@send send merged commit 2fcc668 into main Jun 2, 2026
6 checks passed
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.

2 participants