Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 65 additions & 7 deletions crates/tsv_lang/src/doc/arena.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1440,6 +1440,25 @@ impl DocArena {
self.group(self.concat(&[self.line(), x]))
}

/// The inline-sibling wrap whose leading `line` carries
/// [`DocContext::hold_line_after_broken_flow`] — `group([WithContext(line, hold), X])`: the
/// line renders as a forced break when the immediately preceding flow probe answered yes,
/// and as the ordinary collapsible boundary otherwise (the renderer's `WithContext` arm reads
/// the flag off the `Line`). The flag rides INSIDE the wrap rather than on a wrapper around
/// it, so the wrap measures exactly as [`Self::inline_sibling_line_group`] does — a fits walk
/// descends through `WithContext` and sees an ordinary line — and so that
/// [`Self::strip_leading_line_group_ex`] still matches it and can report which form it found.
/// Built by `push_inline_child_doc` in `tsv_svelte` for `LeadBoundary::SpacedHeld`, the
/// layout-keyed sibling boundary.
#[inline]
pub fn inline_sibling_line_group_held(&self, x: DocId) -> DocId {
let held_line = self.with_context(
self.line(),
DocContext::default().with_hold_line_after_broken_flow(true),
);
self.group(self.concat(&[held_line, x]))
}

/// `id` as a `Fill`, wrapping it in a one-part fill when it is not already one.
///
/// A [`DocContext`] carrying a **render-side** flag ([`DocContext::break_before_wide_flow`],
Expand Down Expand Up @@ -1476,6 +1495,16 @@ impl DocArena {
/// continuation line, which the next pass reads as indentation and drops (non-idempotent).
#[inline]
pub fn strip_leading_line_group(&self, id: DocId) -> Option<DocId> {
self.strip_leading_line_group_ex(id).map(|(x, _)| x)
}

/// [`Self::strip_leading_line_group`] over both wrap forms: returns the inner `X` and whether
/// the wrap was the HELD one ([`Self::inline_sibling_line_group_held`], its leading line
/// hold-flagged), so a caller that strips, rebuilds around `X` and re-wraps can reproduce
/// the lead it found — dropping the hold on a rejoin would silently un-hold a boundary the
/// author wrote. Round-trip-tested against both producers.
#[inline]
pub fn strip_leading_line_group_ex(&self, id: DocId) -> Option<(DocId, bool)> {
let nodes = self.nodes.borrow();
let DocNode::Group {
contents,
Expand All @@ -1499,13 +1528,20 @@ impl DocArena {
let [first, x] = range.resolve(&children) else {
return None;
};
if !matches!(
nodes[first.index()],
DocNode::Line(LineKind::Normal | LineKind::Soft)
) {
return None;
}
Some(*x)
let held = match &nodes[first.index()] {
DocNode::Line(LineKind::Normal | LineKind::Soft) => false,
DocNode::WithContext { doc, context }
if context.hold_line_after_broken_flow()
&& matches!(
nodes[doc.index()],
DocNode::Line(LineKind::Normal | LineKind::Soft)
) =>
{
true
}
_ => return None,
};
Some((*x, held))
}

/// Tag `id` as the doc node that emits the comment at `span` in `source`.
Expand Down Expand Up @@ -3169,6 +3205,28 @@ mod inline_sibling_line_group_tests {
);
}

#[test]
fn strip_leading_line_group_ex_round_trips_both_forms() {
let a = DocArena::new();
let x = a.text("x");
assert_eq!(
a.strip_leading_line_group_ex(a.inline_sibling_line_group(x)),
Some((x, false)),
"the plain wrap strips to its inner doc and reports NOT held",
);
assert_eq!(
a.strip_leading_line_group_ex(a.inline_sibling_line_group_held(x)),
Some((x, true)),
"the held wrap strips to its inner doc and reports held — a rejoin must re-wrap held",
);
// The plain matcher strips the held wrap too (it is the same shape to every reader that
// does not re-wrap), so a caller that only needs `X` sees no difference.
assert_eq!(
a.strip_leading_line_group(a.inline_sibling_line_group_held(x)),
Some(x)
);
}

#[test]
fn strip_leading_line_group_rejects_other_shapes() {
let a = DocArena::new();
Expand Down
31 changes: 30 additions & 1 deletion crates/tsv_lang/src/doc/arena_render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1034,7 +1034,36 @@ fn render_doc_core<P: RenderPolicy>(
arena.flow_probe_begin(output.len());
}

if policy.tracking_suffix() {
// A hold-flagged LINE — the leading boundary of the held inline-sibling wrap
// (`DocArena::inline_sibling_line_group_held`) — consumes the immediately
// preceding flow probe and renders as a forced break when the probed
// predecessor broke; otherwise it descends as the ordinary collapsible line.
// The group-shaped twin of the fill hook in `render_fill_iterative`: same
// probe, same positional pairing (the wrap's command follows the sentinel
// directly), and measurement never sees the flag — `arena_fits` descends
// through `WithContext` — so the wrap's own fit decision is untouched.
if context.hold_line_after_broken_flow()
&& policy.tracking_suffix()
&& let DocNode::Line(kind @ (LineKind::Normal | LineKind::Soft)) =
&nodes[inner_doc.index()]
{
if arena.flow_probe_consume() {
render_line_node(
ctx,
*kind,
Mode::Break,
cmd.indent,
output,
pos,
policy.tracking_suffix(),
line_suffix,
should_remeasure,
);
} else {
cmd = cmd.with_doc(inner_doc);
continue;
}
} else if policy.tracking_suffix() {
if let DocNode::Fill(fill_range) = &nodes[inner_doc.index()] {
let context = context.clone();
let parts = fill_range.resolve(children_vec);
Expand Down
22 changes: 13 additions & 9 deletions crates/tsv_lang/src/doc/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,9 +339,10 @@ impl DocContext {
/// [`super::arena::DocNode::FlowProbeEnd`] sentinel behind the subtree, and the sentinel
/// stores "the subtree's output contained a newline" as the arena's most-recent flow-probe
/// answer. Paired with [`Self::hold_line_after_broken_flow`] on the *immediately following*
/// fill — the two are built together by the Svelte authored-newline boundary rule, and the
/// pairing is positional: the sentinel completes right before the fill renders, so the
/// answer cannot be stale. Invisible to measurement (`arena_fits` skips the sentinel), so
/// doc — a text tail's fill, or the held inline-sibling wrap's leading line — the two are
/// built together by the Svelte authored-newline boundary rule, and the pairing is
/// positional: the sentinel completes right before the paired doc renders, so the answer
/// cannot be stale. Invisible to measurement (`arena_fits` skips the sentinel), so
/// flagging a doc never changes any fit decision — the whole point, after a
/// `group([element, line])` join was measured through and re-broke the *preceding*
/// boundary (the razor-caught 2-cycle this replaced).
Expand All @@ -361,12 +362,15 @@ impl DocContext {
/// When set on a fill, its LEADING separator (a collapsible line in the first content
/// slot — the `leading_line` parity) renders as a forced break when the flow probe's
/// most-recent answer ([`Self::flow_break_probe`]) says the probed predecessor rendered
/// multiline; otherwise the fill renders exactly as an unflagged one. This is the Svelte
/// authored-newline boundary rule's render half: `</a>⏎text` keeps the text's own line
/// beside an element that actually rendered multiline, and reflows beside one that
/// rendered inline — layout-keyed at render, with no build-side prediction and no
/// measurement change (an outer fits walk sees an ordinary fill whose leading line is an
/// ordinary break opportunity).
/// multiline; otherwise the fill renders exactly as an unflagged one. Set on a bare `Line`
/// — the leading line of the held inline-sibling wrap,
/// [`crate::doc::arena::DocArena::inline_sibling_line_group_held`] — it is the same hook
/// read by the renderer's `WithContext` arm instead of the fill loop. This is the Svelte
/// authored-newline boundary rule's render half: `</a>⏎text` and `</a>⏎<b>x</b>` keep the
/// tail's own line beside an element that actually rendered multiline, and reflow beside
/// one that rendered inline — layout-keyed at render, with no build-side prediction and no
/// measurement change (an outer fits walk sees an ordinary fill or wrap whose leading line
/// is an ordinary break opportunity).
#[inline]
#[must_use]
pub const fn hold_line_after_broken_flow(&self) -> bool {
Expand Down
Loading