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
139 changes: 67 additions & 72 deletions crates/tsv_lang/src/doc/arena_render_fill.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,21 @@ pub(super) fn render_fill_iterative(
remaining
};

let content_fits = if is_final_segment && !rest_commands.is_empty() {
// A text-run fill glued to a following tag (`… ~{ratio}`) measures its last word ALONE:
// prettier keeps the tag outside the fill, so the fill never breaks before the word it is
// glued to (the word stays put, the tag rides past printWidth after it). Suppress the
// last-item look-ahead so the glued tag doesn't fold into the word's fit check.
let lookahead_rest: &[ArenaCommand] = if context.trailing_glued_tag && is_final_segment {
&[]
} else {
rest_commands
};
let content_fits = if is_final_segment && !lookahead_rest.is_empty() {
arena_fits_with_lookahead(
arena,
content,
Mode::Flat,
rest_commands,
lookahead_rest,
remaining as isize,
embed,
source,
Expand Down Expand Up @@ -86,13 +95,17 @@ pub(super) fn render_fill_iterative(
// `rest_commands` measurement already asks the right question.
let content_fits = if offset + 1 < parts.len() && arena.is_collapsible_line(content) {
let mut with_sep: SmallVec<[ArenaCommand; 8]> =
SmallVec::from_slice(if is_final_segment { rest_commands } else { &[] });
SmallVec::from_slice(if is_final_segment {
lookahead_rest
} else {
&[]
});
with_sep.push(ArenaCommand {
indent,
mode: Mode::Flat,
doc: parts[offset + 1],
});
let budget = if is_final_segment && !rest_commands.is_empty() {
let budget = if is_final_segment && !lookahead_rest.is_empty() {
remaining
} else {
available
Expand All @@ -110,55 +123,13 @@ pub(super) fn render_fill_iterative(
content_fits
};

// Dropped-first boundary (Svelte after-element fold of a sandwiched inline child): if the
// first fill item rendered at the start of its line, it was pushed there by a preceding
// break — it dropped to its own line — so break the separator after it and let the rest of
// the fill pack from there. A wide inline child that drops owns its line; trailing text
// wraps to the next line rather than hugging the child's `>`. Scoped by the context flag so
// greedy fills (text word-wrap, CSS value lists) are unaffected.
//
// Exception: a block-style element (its own content doesn't fit flat, so it wraps intact
// and dangles its `>` low) with *terminal* trailing text is the `hug_terminal_after_break`
// shape — the tail should hug the dangled `>` when it fits there, exactly as the first-child
// case (`inline_wide_content_trailing_long`) does; a preceding sibling doesn't change that,
// since nothing follows the tail (the hug stays convergent). Skip the unconditional break
// here and let Case 3's at-line-start arm run its `hug_terminal_after_break` decision, which
// hugs only when the tail actually fits and breaks otherwise. The plain dropped-whole case
// (`content_fits`) is unaffected and still owns its line.
if offset == 0
&& context.break_after_dropped_first
&& offset + 1 < parts.len()
&& (content_fits || !context.hug_terminal_after_break)
{
let line_start_pos = line_start_column(indent, render, embed);
if *pos == line_start_pos {
let content_mode = if content_fits {
Mode::Flat
} else {
Mode::Break
};
render_single_doc(
ctx,
content,
output,
pos,
indent,
content_mode,
should_remeasure,
);
render_single_doc(
ctx,
parts[offset + 1],
output,
pos,
indent,
Mode::Break,
should_remeasure,
);
offset += 2;
continue;
}
}
// A short inline element (its own content fits flat) that dropped to its own line — whether
// pushed there by a preceding break (already at line start) or dropped mid-fill below — no
// longer isolates its trailing text: it packs like every other fill word so the run flows
// after it (conformance_prettier.md §Svelte: Inline content block-style, "a text run flows as
// one fill"). The at-line-start case falls through to Case 3's `both_fit` flow; the mid-fill
// case flows via the `hug_terminal_after_break`-gated arm in Case 3. A *wide* element that
// wraps still hugs the dangled `>` (`hug_terminal_after_break`) / owns its line.

// Case 1: Last item
if offset + 1 >= parts.len() {
Expand Down Expand Up @@ -469,13 +440,21 @@ pub(super) fn render_fill_iterative(
Mode::Flat,
should_remeasure,
);
// The Svelte after-element fold's lead element dropped to its own line from
// mid-fill (a preceding word pushed it). It fits intact here, so let the trailing
// text flow greedily after it — the short inline element packs like any other fill
// word (conformance_prettier.md §Svelte: Inline content block-style, "a text run
// flows as one fill"), and the same-line-authored drop converges with the
// newline-authored one instead of one flowing and the other isolating (an F1
// break).
let sep_mode = hug_terminal_sep_mode(ctx, context, next_content, *pos);
render_single_doc(
ctx,
separator,
output,
pos,
indent,
Mode::Break,
sep_mode,
should_remeasure,
);
} else {
Expand Down Expand Up @@ -513,26 +492,11 @@ pub(super) fn render_fill_iterative(
Mode::Break,
should_remeasure,
);
// Exception (Svelte after-element fold, terminal trailing text): choose the
// separator by the *actual resulting column* after the wrapped element. If the next
// item fits after the dangled `>` (separator rendered flat = one space), hug it
// there — respecting the author's space boundary — instead of forcing its own line.
// Exception (Svelte after-element fold, terminal trailing text): hug the dangled `>`
// when the tail fits there, else own line — see `hug_terminal_sep_mode`.
// `next_content` (= `parts[offset + 2]`) is in bounds here: this is the at-line-start
// arm of Case 3, which Case 2 (`offset + 2 >= parts.len()`) has already excluded.
let sep_mode = if context.hug_terminal_after_break
&& arena_fits_with_lookahead(
arena,
next_content,
Mode::Flat,
&[],
render.print_width.saturating_sub(*pos + 1) as isize,
embed,
source,
) {
Mode::Flat
} else {
Mode::Break
};
let sep_mode = hug_terminal_sep_mode(ctx, context, next_content, *pos);
render_single_doc(
ctx,
separator,
Expand All @@ -548,3 +512,34 @@ pub(super) fn render_fill_iterative(
offset += 2;
}
}

/// Terminal-tail separator mode for the Svelte after-element fold, shared by Case 3's two drop
/// arms (the mid-fill drop and the at-line-start wrapped drop). After the fold's lead element has
/// rendered on its own line, the trailing text hugs the dangled `>` — separator rendered Flat, the
/// one space it stands for — when the next item actually fits at the resulting column (`+ 1` for
/// that space), and takes its own line (Break) otherwise. Gated on the fold via
/// [`DocContext::hug_terminal_after_break`]; every non-fold fill keeps the isolating Break, where a
/// wrapped item never lets the next hug its last line.
#[inline]
fn hug_terminal_sep_mode(
ctx: &RenderCtx<'_>,
context: &DocContext,
next_content: DocId,
pos: usize,
) -> Mode {
if context.hug_terminal_after_break
&& arena_fits_with_lookahead(
ctx.arena,
next_content,
Mode::Flat,
&[],
ctx.render.print_width.saturating_sub(pos + 1) as isize,
ctx.embed,
ctx.source,
)
{
Mode::Flat
} else {
Mode::Break
}
}
37 changes: 16 additions & 21 deletions crates/tsv_lang/src/doc/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,24 +63,6 @@ pub struct DocContext {
/// Example: CSS declarations add ";" after the value, so reserve 1 char.
pub trailing_reserve: usize,

/// When set, the fill's FIRST item, if it renders at the start of its line (i.e. it was
/// pushed to its own line by a preceding break — it "dropped"), forces the separator after it
/// to break, so the next item takes its own line.
///
/// Scoped to the Svelte after-element fold of a *sandwiched* inline element/component: a wide
/// inline child that drops to its own line owns that line — the trailing text after it wraps to
/// the next line rather than hugging the dropped child's `>`. Off for every other fill (text
/// word-wrap and CSS value lists pack greedily after a dropped item), so the flag never affects
/// them.
///
/// Exception: a *block-style* dropped element (its own content doesn't fit flat, so it wraps
/// intact and dangles its `>` low) with *terminal* trailing text yields to
/// [`Self::hug_terminal_after_break`] — the tail hugs the dangled `>` when it fits, the same as
/// the first-child case, since nothing follows the tail (the hug stays convergent). The render
/// loop skips this unconditional break in that case; the plain dropped-*whole* case (the element
/// stays flat on its own line) still owns its line.
pub break_after_dropped_first: bool,

/// When set, the fill's trailing separator (its terminal `line`, the only one reaching the
/// "content + separator" render case) measures the *immediately following* node — the next
/// item on the render stack — as a WHOLE flat unit, instead of letting that node's own internal
Expand All @@ -96,9 +78,8 @@ pub struct DocContext {
///
/// Scoped to the Svelte text→flow-element boundary fill (a text run whose next sibling is a
/// flowing inline element/component, ended with a trailing `line`). Off for every other fill, so
/// a small element after text still packs and CSS/value-list fills are unaffected. This is the
/// leading-boundary counterpart of [`Self::break_after_dropped_first`]: both re-couple the
/// width-driven drop decision to the boundary rule at render position so the space- and
/// a small element after text still packs and CSS/value-list fills are unaffected. It re-couples
/// the width-driven drop decision to the boundary rule at render position so the space- and
/// newline-authored forms converge to one fixed point.
pub break_before_wide_flow: bool,

Expand Down Expand Up @@ -126,6 +107,20 @@ pub struct DocContext {
/// newline, so it never reaches this fold). Off for every other fill — CSS value lists and
/// non-terminal text (`trailing_line`, the non-convergent cascade) keep their own-line break.
pub hug_terminal_after_break: bool,

/// When set, the fill's LAST item is measured **alone** (rest-of-render ignored) even though a
/// node follows it on the render stack. That following node is glued to the last word with no
/// whitespace — a Svelte `{expr}` / `{@html}` / `{@render}` tag welded to the end of a text run
/// (`… tsv is ~{ratio}`). prettier keeps the mustache *outside* the text `fill`, so the fill
/// never sees its width and never breaks before the word it is glued to: the word stays on the
/// line and the tag rides past printWidth after it. tsv's fill would otherwise fold the glued
/// tag into the last word's fit check (`~` + `{ratio}`) and break before `~`, stranding the tag
/// on its own line. This flag restores prettier's behavior for exactly that boundary.
///
/// Scoped to a text-run fill immediately followed by a glued tag (`next_is_tag && !trailing_ws`).
/// Off for every other fill, so the general last-item look-ahead (a hard-limit guard that keeps
/// the following node from overshooting printWidth) is unaffected.
pub trailing_glued_tag: bool,
}

/// Sentinel value for cached_width: text contains a newline.
Expand Down
41 changes: 23 additions & 18 deletions crates/tsv_svelte/src/printer/nodes/fragment_doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -624,12 +624,8 @@ impl<'a> Printer<'a> {
if is_last {
// Last child: fold the element and the trailing words into ONE fill so a
// wide element wraps its own content within printWidth and the words pack
// after it. `sandwiched` = there is content before the element (it can be
// pushed onto its own line by a preceding break); when it actually drops,
// the trailing text wraps to its own line rather than hugging the dropped
// element's `>` — see `build_after_element_fold`.
let sandwiched = !child_docs.is_empty();
child_docs.push(self.build_after_element_fold(last_doc, raw, sandwiched));
// after it — see `build_after_element_fold`.
child_docs.push(self.build_after_element_fold(last_doc, raw));
return;
}
// Non-last (text between two inline elements): keep the group-wrapped boundary.
Expand Down Expand Up @@ -742,6 +738,20 @@ impl<'a> Printer<'a> {
..Default::default()
},
)
} else if next_is_tag && !has_trailing_ws {
// The text's last word is welded to a following tag with no whitespace
// (`… tsv is ~{ratio}`). prettier keeps the tag outside the fill, so the fill never
// breaks before that word and the tag rides past printWidth after it. Measure the
// last word alone so tsv matches — otherwise the glued tag folds into the word's fit
// check and strands it on its own line. (`has_trailing_ws` false ⇒ `trailing_line`
// false, so this never coincides with the branch above.)
d.with_context(
fill_doc,
tsv_lang::doc::DocContext {
trailing_glued_tag: true,
..Default::default()
},
)
} else {
fill_doc
};
Expand Down Expand Up @@ -1125,14 +1135,12 @@ impl<'a> Printer<'a> {
/// non-convergent, pinned by
/// [`inline_wide_content_text_sibling_long`](../../../../../tests/fixtures/svelte/elements/inline_wide_content_text_sibling_long_prettier_divergence/).
///
/// `sandwiched` (the element has a preceding sibling, so a preceding break can push it onto its
/// own line) sets [`DocContext::break_after_dropped_first`]: when the element actually drops to
/// its own line (renders at line start) the trailing text wraps to the next line instead of
/// hugging the dropped element's `>` — a wide inline child owns its line, regardless of whether
/// the drop came from the element's own content wrapping or from the preceding text being too
/// long. A first-child element (`!sandwiched`) can't drop via a preceding sibling, so the
/// trailing text packs after it normally.
fn build_after_element_fold(&self, prev: DocId, raw: &str, sandwiched: bool) -> DocId {
/// A **short** element (its content fits flat) packs like every other fill word: when it drops
/// to its own line — whether pushed there by the preceding text or dropped mid-fill — the
/// trailing text flows greedily after it rather than being isolated (matching prettier's
/// pairwise fill; a preceding sibling doesn't change that). A **wide** element that wraps still
/// dangles its `>` and the terminal tail hugs it (`hug_terminal_after_break`).
fn build_after_element_fold(&self, prev: DocId, raw: &str) -> DocId {
let d = self.d();
let mut parts = d.pooled_docbuf();
parts.push(prev);
Expand All @@ -1142,14 +1150,11 @@ impl<'a> Printer<'a> {
// `hug_wide_first` is always set: the fold's first item is the inline element, and when it
// sits mid-line right after a parent element's `>` and is too wide for its own line, it must
// hug-and-break-internally rather than drop (which would strand a spurious `>⏎<child` break —
// the nested-`<span>` non-idempotency). `break_after_dropped_first` couples the *trailing*
// text to the drop, and only applies when the element is sandwiched (a preceding sibling can
// push it onto its own line); the two flags address opposite ends of the fold.
// the nested-`<span>` non-idempotency).
d.with_context(
fill,
tsv_lang::doc::DocContext {
hug_wide_first: true,
break_after_dropped_first: sandwiched,
// The fold only ever runs for terminal trailing text, which hugs the dangled `>`
// (respecting the author's space boundary).
hug_terminal_after_break: true,
Expand Down
Loading