diff --git a/crates/tsv_svelte/src/printer/nodes/fragment_doc.rs b/crates/tsv_svelte/src/printer/nodes/fragment_doc.rs index 5060819f3..989d4875b 100644 --- a/crates/tsv_svelte/src/printer/nodes/fragment_doc.rs +++ b/crates/tsv_svelte/src/printer/nodes/fragment_doc.rs @@ -30,7 +30,11 @@ use tsv_lang::is_format_ignore_directive; pub(super) enum LeadBoundary { /// The previous text trimmed a space-only boundary and deferred the separator to this sibling /// (prettier's `handleWhitespaceOfPrevTextNode`): lead with a collapsible `line` inside a - /// group — a space when the fill fits, a break when it wraps. + /// group — a space when the fill fits, a break when it wraps. Every follower kind but an + /// own-line declaration takes it: an inline element, a component, a tag, a comment, a + /// `{@debug}`, a control-flow block — a space is the follower's own per-width boundary after + /// any sibling, as it is after text, and a unit that renders multiline breaks the group and + /// drops to a fresh line whole (`inline_sibling_space_before_bounding`). Spaced, /// [`Self::Spaced`] whose leading `line` carries the layout-keyed hold /// (`tsv_lang::doc::DocContext::hold_line_after_broken_flow`): it renders as a forced break @@ -41,18 +45,20 @@ pub(super) enum LeadBoundary { /// the `line` inside it (`DocArena::inline_sibling_line_group_held`, whose inverse /// `strip_leading_line_group_ex` reports it so a rejoin re-wraps held-as-held). SpacedHeld, - /// The same deferred boundary in front of a sibling that ENDS the inline run — a comment, a - /// `{@debug}`: a **bare** collapsible `line`, a space while the parent group is flat and a - /// newline once it breaks. The block arm spells its own answer the same way - /// ([`Printer::handle_block_child`]'s `sep()`), and so does the separator site itself for a - /// tag follower ([`Printer::handle_separator_text_child`]'s `next_is_tag` case). + /// The same deferred boundary in front of a DECLARATION that owns its line (`{@const}` / + /// `{const}` / `{let}` / `{#snippet}`), reached outside the multiline path where the + /// declaration arm has not given it that line yet: a **bare** collapsible `line`, a space + /// while the parent group is flat and a newline once it breaks — so the moment the container + /// breaks by width the declaration already sits where the multiline arm will hold it, and + /// the two passes agree. The block arm spells its own answer the same way + /// ([`Printer::handle_block_child`]'s `sep()`). /// - /// ⚠️ **Not [`Self::Spaced`]** — the group form decides the separator on its OWN width, - /// independently of whether the parent broke, and these followers own no fill to be measured - /// with. A run whose content line still fits then keeps the sibling on it while prettier (and - /// tsv's own multiline arm, reading the emitted break back) drops it to the next line: pass 2 - /// re-splits what pass 1 packed, an F1 break. The wrap's own site states the same rule from the - /// other side — see the `next_is_tag` comment in `fragment_text_doc.rs`. + /// ⚠️ This used to be every run-ending follower's lead — a comment, a `{@debug}` — on the + /// argument that such a follower owns no fill to be measured with, so the wrap would keep it + /// on a content line the multiline arm then broke: pass 2 re-splitting what pass 1 packed. + /// That cycle was the multiline arm's bare `line` disagreeing with this arm's wrap, not a + /// property of the follower; both arms now defer to [`Self::Spaced`], and the hazard is gone + /// with the disagreement. SpacedBare, /// Byte-glued to the sibling before it: there is no boundary space to honor, and the doc is /// instead **marked** as the continuation of a welded run (`glued_lead` + `glued_atom`). The @@ -76,8 +82,9 @@ pub(super) enum LeadBoundary { #[derive(Clone, Copy, Default)] pub(super) struct DeferredBoundary { /// The previous text trimmed a boundary space and left the separator to this sibling to - /// emit — the wrap for an inline element or component, a bare `line` for a run-ending - /// comment / `{@debug}`, a block's own `break_before`. + /// emit — the per-width wrap for an inline element, component, tag, comment, `{@debug}` or + /// control-flow block, a bare `line` for a declaration that owns its line, a block's own + /// `break_before`. pub(super) trimmed: bool, /// That deferred separator is the layout-keyed hold ([`LeadBoundary::SpacedHeld`]): an /// authored newline whose run flows, after a predecessor now carrying the flow probe. @@ -302,7 +309,6 @@ impl<'a> Printer<'a> { TextChildContext { cause, run_has_prose: Self::run_is_prose(run_words), - run_has_word: Self::run_has_word(run_words), content_bounds, glued_prefix: pending_glued_prefix.take(), prev_sibling_head, @@ -345,6 +351,9 @@ impl<'a> Printer<'a> { if let Some((element_doc, block_doc)) = self.try_block_sibling_gt_dangle(trimmed_nodes, i) { + // Glued to the element: no whitespace node stands between them, so no + // separator can have deferred to this block. + debug_assert!(!prev_text_ws); if let Some(last) = child_docs.last_mut() { *last = element_doc; } else { @@ -367,8 +376,16 @@ impl<'a> Printer<'a> { } else { self.build_fragment_node_doc_in_multiline(node) }; + // The deferred boundary space, honored exactly as the inline arm does: the + // block leads with the per-width wrap, and one that renders multiline breaks + // it and drops to a fresh line whole ([`LeadBoundary::Spaced`]). if let Some(node_doc) = node_doc { - child_docs.push(node_doc); + let lead = if prev_text_ws { + LeadBoundary::Spaced + } else { + LeadBoundary::Plain + }; + self.push_inline_child_doc(&mut child_docs, node_doc, lead); } } } else if is_inline_content(node) { @@ -451,13 +468,12 @@ impl<'a> Printer<'a> { // raw path, and `!prev_text_ws` so a trimmed boundary space from the previous text // is never dropped — a fused prefix has nowhere to carry that space, so the comments // take the ordinary per-node path below, where the final arm emits it - // ([`LeadBoundary::SpacedBare`]) and `glued_lead` guards the text's own boundary as + // ([`LeadBoundary::Spaced`]) and `glued_lead` guards the text's own boundary as // before. // - // ⚠️ The space is a **bare** `line` there, NOT a `group([line, …])` wrap: the - // comment run ends the inline run, so its separator is the parent group's to - // resolve. Reading it as the wrap is what makes a fix reach for `Spaced` and - // break F1 — see the variant's own note. + // The comments then take the ordinary per-width wrap in front of the fused text + // ([`LeadBoundary::Spaced`]): a space before a comment is the comment's own + // boundary after any sibling. pending_glued_prefix = Some((prefix, i)); glued_run_consumed_until = text_idx; } else { @@ -474,15 +490,19 @@ impl<'a> Printer<'a> { // is render-visible: both node kinds render nothing, so the separator beside them is // the only thing holding the two runs apart (`inline_adjacent_comment_space`). // - // `SpacedBare`, not `Spaced`: these followers end the inline run, so the boundary is - // the parent group's to resolve rather than its own — see the variant's own note. + // `Spaced` for a comment, a `{@debug}` and (outside the multiline path) a + // control-flow block: a space before a run-ending follower is that follower's own + // per-width wrap after any sibling, as it is after text. `SpacedBare` only for a + // declaration that owns its line — see the variants' contracts. if let Some(node_doc) = self .build_fragment_node_doc_with_preceding_context(node, has_preceding_breakable) { - let lead = if prev_text_ws { + let lead = if !prev_text_ws { + LeadBoundary::Plain + } else if self.is_own_line_declaration(trimmed_nodes, i) { LeadBoundary::SpacedBare } else { - LeadBoundary::Plain + LeadBoundary::Spaced }; self.push_inline_child_doc(&mut child_docs, node_doc, lead); } @@ -763,27 +783,9 @@ impl<'a> Printer<'a> { /// reason: the printer only ever asks `== 0` / `>= 1` / `>= 2` of it. const PROSE_WORDS_CAP: usize = 2; - /// Whether a run whose [`Self::prose_words`] maximum is `words` holds a **word** — one thing - /// for a fill to pack, prose or not. The gate the SPACE-spelled separator before a tag keeps - /// (`TextChildContext::run_has_word`): the prose gate is a hold on an authored NEWLINE and - /// must never turn a space into one, so a one-word run's `text1 {a} {b}` defers to the - /// per-width group exactly as a prose run's does - /// (`elements/inline_sibling_newline_label_hold_tag_pair_space_prettier_divergence`), and - /// only a WORDLESS run's tag pair (`{a} {b}`) breaks with its container. - /// - /// A word, not "a content text": the two part on a content text that carries none, which is - /// a text whose *decoded* form is all whitespace — an ` ` node, or an entity-spelled - /// space (` `, the `inline_separator_entity_newline` case). Such a node is a separator - /// wearing content's clothing ([`Self::is_separator_like_text`]) and gives the separator - /// nothing to sit in, so the run answers `false` here even though a `Text` is present. - #[inline] - pub(super) fn run_has_word(words: usize) -> bool { - words >= 1 - } - /// The most words any one node of `nodes` carries ([`Self::prose_words`]) — the run count - /// [`Self::run_is_prose`] and [`Self::run_has_word`] grade. The one counter for both readers - /// of a run: [`Self::scan_inline_run`] over the run it has just bounded, and + /// [`Self::run_is_prose`] grades. The one counter for both readers of a run: + /// [`Self::scan_inline_run`] over the run it has just bounded, and /// `Printer::content_is_reflowable_fill` over an element's already-bounded content. pub(super) fn run_prose_words(&self, nodes: &[FragmentNode<'_>]) -> usize { let mut words = 0; @@ -798,8 +800,9 @@ impl<'a> Printer<'a> { /// Scan the inline run beginning at `start`: its exclusive end, and its /// [`Self::run_prose_words`] — graded by [`Self::run_is_prose`] for the newline hold (a - /// `fill` to reflow into) and by [`Self::run_has_word`] for the space-spelled tag separator - /// (a fill to sit in at all). + /// `fill` to reflow into). The run's count gates a NEWLINE alone: a space-spelled separator + /// before a tag defers to the tag's per-width group whatever the run holds and whatever + /// precedes it (`handle_separator_text_child`'s `tag_space_wraps`). /// /// A run ends at a node [`Self::breaks_inline_run`] names, and at an authored blank line a /// content text carries on its edge ([`Self::text_edge_has_blank`]) — the boundary set @@ -869,7 +872,14 @@ impl<'a> Printer<'a> { } } - /// Whether a **single-newline** separator beside `node` may collapse to a plain space. + /// Whether a **single-newline** separator beside `node` may collapse to a plain space — the + /// neighbour question of the sibling-newline flow rule, asked of the NEWLINE spelling alone. + /// A **space** never asks it: a space before a tag, an inline element or a component is that + /// follower's own per-width wrap whatever stands before it (` x` and + /// ` {x}` hug alike — `inline_adjacent_component_space`, + /// `inline_tag_pair_space_bounded`, `inline_sibling_space_before_bounding`), and only a block + /// element on either side breaks one — a unit that renders multiline drops to a fresh line + /// whole because its own hardlines break the wrap (`Printer::handle_separator_text_child`). /// /// Svelte 5 collapses an inter-sibling whitespace run to one whitespace, so a space and a /// newline between two siblings render identically — the newline's *spelling* carries no diff --git a/crates/tsv_svelte/src/printer/nodes/fragment_text_doc.rs b/crates/tsv_svelte/src/printer/nodes/fragment_text_doc.rs index ce110c377..1bb1c20d3 100644 --- a/crates/tsv_svelte/src/printer/nodes/fragment_text_doc.rs +++ b/crates/tsv_svelte/src/printer/nodes/fragment_text_doc.rs @@ -84,16 +84,6 @@ pub(super) struct TextChildContext { /// `next_is_tag` case, and the two emitting a different doc for one logical separator is /// exactly the period-2 cycle `cause` exists to close. pub(super) run_has_prose: bool, - /// Whether this node's inline run holds a word at all ([`Printer::run_has_word`]) — the gate - /// the SPACE-spelled whitespace-only separator before a tag keeps, in both arms of the - /// separator site. Split from `run_has_prose` on purpose: the prose gate decides whether - /// an authored NEWLINE may reflow (a one-word run holds it), and a hold must never become a - /// forced break — so a one-word run's space-spelled tag pair defers to the per-width group - /// as a prose run's does (`inline_sibling_newline_label_hold_tag_pair_space`), while a - /// WORDLESS run's keeps the bare `line` that breaks with the container. Both arms read - /// this one value, which is what keeps the width-broken and newline-authored twins of one - /// document on one layout. - pub(super) run_has_word: bool, /// The first and last index in `trimmed_nodes` that the whitespace rules see — the fragment's /// content bounds once every HOISTED node is skipped /// ([`FragmentNode::content_bounds`]). `handle_content_text_child`'s `is_first` / `is_last` are @@ -208,7 +198,6 @@ impl<'a> Printer<'a> { let TextChildContext { cause, run_has_prose, - run_has_word, content_bounds, prev_sibling_head, .. @@ -268,6 +257,13 @@ impl<'a> Printer<'a> { let next_owns_line = i + 1 < trimmed_nodes.len() && self.is_own_line_declaration(trimmed_nodes, i + 1); let next_is_tag = next_node.is_some_and(Self::is_tag_node); + // A follower that ends the inline run and still takes a space as its own wrap: a + // comment, a `{@debug}`, a control-flow block. (A declaration owns its line outright — + // `next_owns_line` above; a `{#snippet}` reaches here only glued, never past a space.) + let next_bounds_run = next_node.is_some_and(|n| { + matches!(n, FragmentNode::Comment(_) | FragmentNode::DebugTag(_)) + || is_control_flow_block(n) + }); // Whether the next sibling is an HTML *inline* element vs a *block* element — // the two kinds prettier-plugin-svelte trims boundary whitespace *into* (the // trimmed text emits nothing; the element's own group([line, …]) / @@ -312,7 +308,7 @@ impl<'a> Printer<'a> { // parallel form. // // ⚠️ **The run and neighbour facts are asked once, ahead of the multiline split, because - // both arms below need the same answer** (`tag_space_defers`; the newline hold + // both arms below need the same answer** (`neighbours_flow`; the newline hold // `separator_flows` is read by the multiline arm alone, the only place a newline is a // hardline question). Nothing in either depends on WHY the container went multiline. A // conjunct on the cause would be dead in the arm the `!multiline` test already selected, @@ -327,49 +323,47 @@ impl<'a> Printer<'a> { // Flowing converges a tag pair onto one line where prettier splits it — a deliberate // divergence in the same family as the rest of this rule, pinned by // `elements/inline_adjacent_sibling_newline_flow_prettier_divergence`. + // The neighbour question, asked once for the NEWLINE spelling: a newline-spelled + // separator flows only when both neighbours flow AND the run holds prose + // (`separator_flows`, read by the multiline arm alone — the only place a newline is a + // hardline question). A SPACE-spelled separator never asks it: see `tag_space_wraps`. let neighbours_flow = self.neighbour_newline_flows(prev_node) && self.neighbour_newline_flows(next_node); let separator_flows = run_has_prose && neighbours_flow; - // The SPACE-spelled separator before a tag asks the weaker run gate — any word in the - // run (`run_has_word`), not prose. The prose gate is a HOLD on an authored newline and - // must never turn a space into one: a one-word run's `text1 {a} {b}` packs per width - // exactly as a prose run's does (`inline_sibling_newline_label_hold_tag_pair_space`), and - // only a prose-FREE run's tag pair keeps the bare `line` that breaks with the container. - // Read by both arms below, so the width-broken and newline-authored twins agree. A - // flowing NEWLINE reaches the tag case too, re-spelled as the space by `ws_flows`; prose - // implies text, so it defers as before. - let tag_space_defers = run_has_word && neighbours_flow; + // A SPACE before a tag is the tag's own per-width wrap — `group([line, tag])`, the same + // wrap an inline element or component takes at that boundary — whatever the run holds + // and whatever precedes it. The prose gate is a HOLD on an authored newline and must + // never turn a space into one, and neither may the predecessor's kind: a wordless + // `{a} {b}` packs exactly as `text1 {a} {b}` does (`inline_tag_pair_space`, + // `inline_tag_pair_space_container`), and ` {a}`, `
{a}`, `{/if} {a}` + // keep their space exactly as ` ` and `{/if} ` already do + // (`inline_tag_pair_space_bounded`). Gating this on the neighbours' kinds was the one + // place a space still became a newline the author did not write — prettier's `line` + // between two tags surviving for one neighbour class. The one exclusion is a BLOCK + // ELEMENT predecessor, whose `handle_block_child` already supplies the break — the + // same `prev_is_block_el` the inline-element follower reads in `trim_to_collapsible`. + // A comment, a `{@debug}` or a control-flow FOLLOWER takes its space the same way + // (`next_bounds_run`, below); a declaration owns its line (`next_owns_line`). Read by + // both arms, so the width-broken and newline-authored twins agree; a flowing NEWLINE + // reaches the same deferral, re-spelled as the space by `ws_flows`. + let tag_space_wraps = next_is_tag && !prev_is_block_el; if !multiline { - // Before a tag the separator is a bare collapsible break — a space while - // the fragment fits, a newline once it breaks — exactly as the multiline - // arm below emits it. `group([line, tag])` (the inline-element form) would - // instead decide the separator on its own width, independently of whether - // the parent broke: a compact `{a} {b}` that overflows would - // pack `{a} {b}` onto the block-style content line, while the same document - // authored across lines splits them. That makes the layout follow the - // content-boundary whitespace — which is render-free under Svelte 5, and - // which tsv *injects* when it converts an authoring to block-style, so the - // emitted form would reflow on the next pass. + // ⚠️ The separator before a tag must build the SAME doc in this arm as in the + // multiline arm below — it defers to the tag's per-width group, a block-element + // predecessor excepted — decided ahead of the split (`tag_space_wraps`) so one + // run's interior does not depend on WHY its element went multiline. A hugged + // `{a} {b}` that overflows converts to block-style and INJECTS + // boundary air, so the next pass reads the same separator through the multiline + // arm; two arms with two policies for it is a two-pass cycle, not a difference of + // taste (`inline_content_spaced_tags_long`, whose compact authoring is a one-pass + // fixed point only because the arms agree; `inline_content_spaced_tags_tail_long` + // for the prose run). The bare `line` this arm still emits after a block element + // resolves all-or-nothing with the parent group — the same answer the multiline + // arm's fallthrough gives that boundary. // - // An inline ELEMENT or component keeps `group([line, el])` deliberately: it - // carries its own tags, so the group is what lets a wide element drop to its - // own line whole instead of breaking its tag in place, and both formatters - // settle on a stable (if authoring-dependent) form there — the sanctioned - // Tier-2 element-expansion class, not this bug. A tag has no such structure - // to protect, so the bare break is strictly better. - // - // ⚠️ A run holding a content text is the exception, and it is the same question - // the multiline arm's `next_is_tag && tag_space_defers` case asks — asked here so - // one run's interior does not depend on WHY its element went multiline. A - // width-broken element and a newline-authored one lay the same run out identically: - // both defer to the next sibling's per-width `group([line, tag])`, so the run - // packs. Without this the two modes hold contradictory interior policies — the - // bare `line` resolves all-or-nothing with the parent group, which is already - // broken whenever the element overflowed, so every separator in the run breaks - // while the newline-authored twin packs. That difference is invisible while the - // reflowable-fill suppression keeps the authored form out of the multiline arm, - // and becomes a two-pass cycle the moment it doesn't - // (`inline_content_spaced_tags_tail_long`). + // An inline ELEMENT or component takes `group([line, el])` here for a second + // reason as well: it carries its own tags, so the group is what lets a wide + // element drop to its own line whole instead of breaking its tag in place. // // ⚠️ A **component** follower takes the inline-sibling wrap here exactly as an inline // element does, and the multiline arm below wraps it too (`next_is_inline_flow`). @@ -385,10 +379,11 @@ impl<'a> Printer<'a> { // The layout-keyed hold (`arm_hold` above). The candidate does not read // `run_has_prose`; it asks the two neighbours directly — a tag follower always // flows, an inline element or component flows by kind. A TAG follower that takes - // the hold takes the wrap with it (the `!*hold_next_lead` below): its bare `line` - // renders flat past a multiline predecessor (` {expr}` through a glued head), - // which is the one answer the multiline arm never gives that boundary — the arm - // disagreement this whole handler is built to avoid. + // the hold takes the wrap with it (the deferral below, read back as + // `deferred.held`): a bare `line` there would render flat past a multiline + // predecessor (` {expr}` through a glued head), the one answer the multiline + // arm never gives that boundary — the arm disagreement this whole handler is built + // to avoid. if text.newline_count == 1 && !prev_is_tag && (next_is_inline_el || next_is_component || next_is_tag) @@ -396,15 +391,20 @@ impl<'a> Printer<'a> { { arm_hold(child_docs, deferred); } - if next_is_tag && !tag_space_defers && !deferred.held { + // A held wrap never reaches the bare `line`: the hold requires a flowing + // predecessor, and a block element does not flow, so `tag_space_wraps` already + // routes it to the deferral below (where the follower reads `deferred.held`). + debug_assert!(!(deferred.held && next_is_tag) || tag_space_wraps); + if next_is_tag && !tag_space_wraps { child_docs.push(d.line()); } else { // Defer the separator to the next sibling, which leads with it. NOT only "the // next inline element": every follower this arm does not emit for reads the // flag, and each answers with the [`LeadBoundary`] its own kind calls for — the - // wrap for an inline element or component that owns a fill, a bare `line` for a - // run-ending comment / `{@debug}`. A follower whose arm ignored the flag simply - // deleted the space, which is why the reader set has to stay total. + // per-width wrap for an inline element, component, tag, comment, `{@debug}` or + // control-flow block, a bare `line` only for a declaration that owns its line. + // A follower whose arm ignored the flag simply deleted the space, which is why + // the reader set has to stay total. deferred.trimmed = true; } return; @@ -445,8 +445,9 @@ impl<'a> Printer<'a> { // content, so removing its break cannot touch the element's own BOUNDARY newlines, // which are what the multiline decision reads. (The period-2 cycle that argument rules // out is real where it does reach the boundary — the next pass would take the inline - // arm, whose `next_is_tag` case emits a bare `line`, all-or-nothing with the - // already-broken parent group, and split the run apart again.) Both spellings converge, + // arm, and any separator the two arms answered differently, as the tag case once was + // with a bare `line` there, would split the run apart again; today both arms defer + // every follower alike, so that route is closed twice over.) Both spellings converge, // for element and tag siblings alike, and `authoring:audit` is the standing guard. // `elements/inline_content_spaced_tags_tail_long` reaches the SAME interior through the // non-multiline arm above (a width-broken element), which is what keeps one prose run @@ -495,21 +496,40 @@ impl<'a> Printer<'a> { child_docs.push(d.hardline()); } child_docs.push(d.hardline()); - } else if next_is_tag && tag_space_defers { - // A space (or a flowing newline, re-spelled above) before a TAG in a run that holds a - // content text. `trim_to_collapsible` above covers only a next - // inline *element*, so without this arm the boundary would fall to the bare `line` - // below — which resolves all-or-nothing with the parent group, and the parent is - // already broken whenever the fragment is multiline. The whole run would then hard- - // break while the one boundary owned by a content text's fill flowed: the mixed - // layout this rule exists to remove, just relocated from elements to tags. Deferring - // to the next sibling gives the tag the same per-width `group([line, tag])` an inline - // element gets, so the run reflows as one. + } else if tag_space_wraps { + // A space (or a flowing newline, re-spelled above) before a TAG. + // `trim_to_collapsible` above covers only a next inline *element*, so without + // this arm the boundary would fall to the bare `line` below — which resolves + // all-or-nothing with the parent group, and the parent is already broken whenever + // the fragment is multiline: every tag would take its own line however the author + // spaced them, a hold turned into a forced break, and in a prose run the one + // boundary owned by a content text's fill would flow while the rest hard-broke. + // Deferring to the next sibling gives the tag the same per-width `group([line, tag])` + // an inline element gets, so the run packs as one fill (`inline_tag_pair_space`, + // `inline_content_spaced_tags_pack_long`). // - // Gated on `tag_space_defers` — NOT on `next_is_tag` alone. A plain authored space - // before a tag keeps the bare `line`: its neighbour may be a **comment**, whose own - // line is authorship (`` `{expr}` must not weld — `root_expressions_spaced`), - // and the flow predicate is exactly what excludes it. + // Gated on `tag_space_wraps` rather than `next_is_tag` alone for the BLOCK ELEMENT + // predecessor only: its `handle_block_child` already supplies the break, and a wrap + // would strand a leading space after it (`block_before_inline`'s tag twin, + // `
block1
{expr}` in `inline_tag_pair_space_bounded`). A comment, a + // `
` or a control-flow block BEFORE the tag does not gate it: the tag keeps + // its authored space there exactly as an inline element or component does + // (` ` and ` {expr}` answer alike), and the comment's own + // line is what the NEWLINE spelling holds. + deferred.trimmed = true; + } else if next_bounds_run && !prev_is_block_el { + // A space before a follower that ENDS the inline run — a comment, a `{@debug}`, a + // control-flow block — is that follower's own per-width wrap too, exactly as it is + // after TEXT (the fill's `line`): the follower's arm reads the flag and wraps + // ([`LeadBoundary::Spaced`]), and a block that renders multiline breaks that group + // and drops to a fresh line whole. The bare `line` this arm used to emit here broke + // the space with the container — `a ` splitting where + // `text1 ` hugged — the FOLLOWER-side twin of the tag gate above, and half + // of a two-arm disagreement with the inline arm's `Spaced` lead + // (`inline_sibling_space_before_bounding`; prettier keeps that split). A comment's + // line is authorship, and it is the NEWLINE spelling that holds it (the hardline arm + // above). The block-element predecessor keeps the bare `line` for the same reason a + // tag's does. deferred.trimmed = true; } else { child_docs.push(d.line()); diff --git a/docs/conformance_prettier_svelte.md b/docs/conformance_prettier_svelte.md index d39a81bc5..5e4fb52b4 100644 --- a/docs/conformance_prettier_svelte.md +++ b/docs/conformance_prettier_svelte.md @@ -49,7 +49,7 @@ the parser keeps the wider set deliberately. See - Inline content (text) — ◆design_choice — [inline_content_text_wrap](../tests/fixtures/svelte/elements/inline_content_text_wrap_prettier_divergence/), [text_non_breaking_whitespace](../tests/fixtures/svelte/elements/text_non_breaking_whitespace_prettier_divergence/) — an inline element's wrapping text content lays out **block-style** (both tags intact, content on its own indented line, collapsing inline when it fits); prettier pre-breaks the opening tag. See [§Svelte: Inline content block-style](#svelte-inline-content-block-style). - Inline content (expression) — ◆design_choice — [inline_content_hug_long](../tests/fixtures/svelte/elements/inline_content_hug_long_prettier_divergence/) — breakable-expression content also lays out **block-style** (uniform with text/element content), where prettier dangles. See [§Svelte: Inline content block-style](#svelte-inline-content-block-style). - Inline content (line-continuation string) — ◆design_choice — [string_line_continuation_content](../tests/fixtures/svelte/expressions/string_line_continuation_content_prettier_divergence/) — a line-continuation string literal's mandatory newline is the **third** block-style trigger (a leaf's forced newline, beside width and structure): the element goes block-style with the leaf un-broken, where prettier dangles the tag delimiters around it. See [§Svelte: Inline content block-style](#svelte-inline-content-block-style). -- Inline content (spaced expression tags) — ◆design_choice — [inline_content_spaced_tags_long](../tests/fixtures/svelte/elements/inline_content_spaced_tags_long_prettier_divergence/) — space-separated expression tags (`{a} {b}`) each take their own line once the element goes block-style, where prettier dangles the compact authoring and lets it run past printWidth. See [§Svelte: Inline content block-style](#svelte-inline-content-block-style). +- Inline content (spaced expression tags) — ◆design_choice — [inline_content_spaced_tags_long](../tests/fixtures/svelte/elements/inline_content_spaced_tags_long_prettier_divergence/), [inline_content_spaced_tags_pack_long](../tests/fixtures/svelte/elements/inline_content_spaced_tags_pack_long_prettier_divergence/) — space-separated expression tags (`{a} {b}`) pack per width on the block-style content line, breaking only before the tag that would cross print width, where prettier splits them one per line once the element is multiline and dangles the compact authoring past printWidth. See [§Svelte: Inline content block-style](#svelte-inline-content-block-style). - Block multiline attrs hug — ◆print_width — [block_multiline_attrs_content_hug](../tests/fixtures/svelte/elements/block_multiline_attrs_content_hug_prettier_divergence/) - Fill expr travel boundary — ◆print_width — [fill_expr_travel_boundary_long](../tests/fixtures/svelte/elements/fill_expr_travel_boundary_long_prettier_divergence/) (the continuation line after a traveled expression tag: tsv breaks before the word that would cross 100, prettier packs it and overflows) - Spaced wide tag travels — ◆design_choice — [fill_spaced_tag_travel_long](../tests/fixtures/svelte/elements/fill_spaced_tag_travel_long_prettier_divergence/) (the rule's own 100/101 boundary), [fill_expr_travel_continuation_long](../tests/fixtures/svelte/elements/fill_expr_travel_continuation_long_prettier_divergence/), [fill_expr_travel_middle_long](../tests/fixtures/svelte/elements/fill_expr_travel_middle_long_prettier_divergence/), [fill_expr_travel_middle_before_long](../tests/fixtures/svelte/elements/fill_expr_travel_middle_before_long_prettier_divergence/) — a spaced `{expr}` tag whose expression cannot fit flat after the text starts on the fresh line (collapsing flat there when it fits, breaking internally when not) rather than opening mid-line; prettier's boundary measurement stops at the expression's first internal break, so it opens the tag mid-line, a stable form pinned as `prettier_variant_midline` in each fixture. See [§Print Width Philosophy](./conformance_prettier.md#print-width-philosophy). @@ -278,7 +278,7 @@ Rule 2 is why content-boundary whitespace is **render-free**, so the injected bl Because it is render-free, that whitespace does **not select the tags' layout**. Wherever the content goes multiline, the rule holds without exception: it holds whether the content breaks **structurally** (a block child, a control-flow block) or by **width**, and it holds for a boundary the author *hugged* (zero whitespace) exactly as for one they spaced or broke. So the authorings of one document — `text
…`, ` text
…`, and the newline form — converge on one form. Prettier lets the boundary decide, so it keeps a *different* stable form for each ([content_boundary_convergence](../tests/fixtures/svelte/components/content_boundary_convergence_prettier_divergence/) is one document with three prettier forms and one tsv form), and its space form is additionally **non-idempotent** (the space is emitted at line-start, where the next parse reads it as indentation and drops it). This is the authoring-independence property `authoring_audit` checks — it mutates each content boundary across hug ↔ space ↔ newline and requires the authorings to converge, with one sanctioned remainder: for content that fits, a newline on **both** boundaries is the Tier-2 air request and holds the multiline form beside the inline one ([inline_boundary_air](../tests/fixtures/svelte/elements/inline_boundary_air/)) — the audit reports that split dual-stable, and a clean run means no render-free **spelling** picks a layout and nothing is non-idempotent. -The corollary is that whitespace *inside* the content — a **separator between two siblings** — is render-significant and therefore does still decide layout. Whitespace between two adjacent expression tags (`{a} {b}`) is a collapsible break: a space while the element fits, a newline once it goes block-style, so each tag takes its own line ([inline_content_spaced_tags_long](../tests/fixtures/svelte/elements/inline_content_spaced_tags_long_prettier_divergence/)); a text run between them flows as one fill and keeps them on the content line. The boundary carries no signal and must not select the layout; the separator carries one and must. +The corollary is that whitespace *inside* the content — a **separator between two siblings** — is render-significant and therefore does still decide layout. Whitespace between two adjacent expression tags (`{a} {b}`) is a fill boundary: a glued pair is never split, and a spaced pair packs per width on the block-style content line, breaking only before the tag that would cross print width — never one tag per line because the container broke ([inline_content_spaced_tags_long](../tests/fixtures/svelte/elements/inline_content_spaced_tags_long_prettier_divergence/), [inline_content_spaced_tags_pack_long](../tests/fixtures/svelte/elements/inline_content_spaced_tags_pack_long_prettier_divergence/); prettier splits every such pair once the element is multiline). A text run between them flows as one fill the same way. The boundary carries no signal and must not select the layout; the separator's presence carries one and must — its spelling is the flow rule's question below. **But it is the separator's *presence* that carries the signal, not its *spelling*.** A separator's **existence** decides layout — a *glued* boundary is never split, since breaking there would inject a rendered space. Its **form** does not: Svelte 5 collapses an inter-sibling run to one whitespace, so a space and a newline there render identically, and `text1⏎inline1⏎text2` is the same document as `text1 inline1 text2`. tsv therefore flows an inline sibling isolated by authored newlines back onto the content line, converging those authorings; prettier lets each boundary decide and holds a stable form for each. This is what turns the large majority of `authoring_audit`'s `content-leading` / `content-trailing` sites from `diverge (dual-stable)` to `converge`; the remainder are shapes the exclusions below deliberately keep. @@ -290,11 +290,11 @@ Five things are **not** reshaped, because none is a mere spelling difference. A **The comment exclusion is held on each SIDE independently, and it is where this section's convergence deliberately stops rather than a divergence from prettier.** "Keeps its authored line" is the symmetric reading, and it is the weaker one: the newline *after* a comment holds the following run off the comment's line exactly as the newline *before* it holds the comment off the preceding run's, so a one-sided authoring is a fixed point on that side alone — `text1 ⏎text2` and `text1⏎ text2` both stay, at any width, beside the both-sided form — and beside the form with a newline on neither side, which stays hugged, because the rule HOLDS an authored line rather than FORCING one around a comment (that fourth authoring is the pair's null control: it varies the same dimension, so without it an implementation that always broke both sides would satisfy the other three). Prettier holds all four too, so there is nothing to catalog against it here; the exclusion's whole cost is that these authorings do not converge with each other, which is the price [§Comment Position Philosophy](./conformance_prettier.md#comment-position-philosophy) is already paid for. ⚠️ **It is comment-specific, and the discriminating control has to hold the SITE fixed** — an element or a tag in the identical position flows (`text1 a⏎text2` collapses). Two *other* gates in this rule suppress flow for every sibling kind at once (the prose gate below, and the reflowable-fill suppression the paragraph after it, which closes this rule by making the element multiline for a reason it does not read), so an element control run in a shape either one already catches keeps its lines regardless of what sits there, and reads back as "the newline rule is not about comments" when it is. — [inline_separator_comment_newline](../tests/fixtures/svelte/elements/inline_separator_comment_newline/) pins all four comment authorings, and is deliberately **not** a divergence fixture: prettier holds each one too, so there is nothing for an `output_prettier` to claim. The element twins of the one-sided pair are [inline_sibling_newline_flow](../tests/fixtures/svelte/elements/inline_sibling_newline_flow_prettier_divergence/)'s `prettier_variant_newline_after` / `prettier_variant_newline_before`, which converge on its input — that pair beside these four is the whole control. ⚠️ **The exclusion is scoped to the comment's OWN two boundaries: it BOUNDS the run, it does not STERILIZE one.** The runs a comment separates keep flowing on their own — `text1 a text2⏎⏎text3 b text4` converges from every isolated authoring of those spans, with only the comment's two newlines held. Nothing distinguishes that from the wider reading ("a run holding a comment keeps every line") unless a control puts a flowing run *beside* the comment and re-authors it: a control whose run is already hugged in every file is a fixed point under either rule and cannot separate them, which is why [inline_sibling_newline_flow](../tests/fixtures/svelte/elements/inline_sibling_newline_flow_prettier_divergence/)'s comment-boundary control is re-authored in all three `prettier_variant_*` files rather than held. The same is true of every other run-bounding sibling (that fixture's block-sibling control carries the parallel pin), so the property belongs to run boundaries in general and not to comments — what is comment-specific is only the *holding*. The practical consequence is that a comment tsv has broken onto its own line for **width** ([fill_break_before_comment_spaced_long](../tests/fixtures/svelte/elements/fill_break_before_comment_spaced_long_prettier_divergence/)) reaches a second fixed point when the run after it was authored on the next line — the same document, differing only in whether that run shares the comment's line. -**The rule's boundary is the presence of a `fill` to reflow into, not the shape of the separator node.** Flowing means reflowing a run per width, and a run reflows only where there is prose for a `fill` to pack. So the rule reaches a whitespace-only separator standing *between two non-text siblings* — `text1⏎a` and `ab` converge alike — **when that separator's inline run holds prose, which is at least two words**, and not otherwise. A run of pure elements or tags (``) keeps its authored lines: with no prose its newlines are the only structure the author has, collapsing them packs independent siblings onto one line, and on a short run the collapse cascades into the parent element's own hug decision — an outright idempotency break (`` re-hugs on the second pass once its children fold). **A run holding a single word is a label, not prose, and keeps its lines the same way** — `⏎Delete⏎{n}`, `hue:⏎`, `⏎private`, `chars⏎{n}`, `{@render icon()}⏎text1` are a caption beside its icon, a field beside its unit, a label beside its value, and a fill needs a phrase to reflow into; prettier holds every such shape too, so the hold is agreement. Only the run's TEXT counts: an expression tag renders as a value, and a word beside one is a label like any other. The count is **run-level and never node-local**: at a content text's edge run the node beside the sibling is often the one-word tail a previous wrap left behind (`…x⏎prop.`), which a node-local test would hold alone on its line — the accretion the flow rule exists to heal, run in reverse — where the run's total sees the sentence it belongs to, so a one-word node ending a real sentence flows with it. The cliff is two words (`Remember me` still packs), where label and prose genuinely blur, and the count is the most words any **one** text node carries, never a sum over the run: words in two different text nodes are separated by a sibling, and that separation is the author's, so two one-word captions in one run (`text1⏎⏎text2⏎`, an icon-and-caption list) are two labels and hold — a sum packs every such list the moment it holds two entries. The cost is a sentence spelled entirely as one-word fragments between siblings (`text1⏎inline1⏎text2`), which holds; real prose has a two-word node somewhere in its run. The cliff's own cost is the mirror: one two-word caption in a list of one-word captions is prose, and packs its neighbours with it (`⏎text1⏎⏎text2 text3` → one line) — pinned as a control so it is recorded, not discovered. The gate is asked identically at all three of the rule's sites — the whitespace-only separator and a content text's leading and trailing runs — and **it is a hold, never a forced break**: it decides whether an authored newline may reflow, and says nothing about an authored space. So a one-word run's space spelling stays inline everywhere — beside an element, a void element, a component, a render/html tag, inside a content text, and between two **tags**, where the whitespace-only separator keeps deferring to the per-width group as a prose run's does: the space-spelled tag separator reads the weaker run gate, *any* word in the run, and only a wordless `{a} {b}` keeps the bare `line` that breaks with the container (the tag-separator rule above). Prettier's `line` between two tags breaks with the container whatever the run holds, so `text1 {a} {b}` in a multiline container is a divergence of the same family as the prose tag pair below — tsv packs where prettier splits. — [inline_sibling_newline_label_hold](../tests/fixtures/svelte/elements/inline_sibling_newline_label_hold_prettier_divergence/) (every label shape held, the caption list and the fragmented sentence among them; the two-word cliff, the one-word sentence tail, the phrase's reach across a whole run and the cliff in a list flowing as `prettier_variant_newline`; `variant_space` the null control), [inline_sibling_newline_label_hold_tag_pair_space](../tests/fixtures/svelte/elements/inline_sibling_newline_label_hold_tag_pair_space_prettier_divergence/) (the space-spelled tag pair before and after a word, and at the root; the prose-free pair as the control that still splits), and [inline_sibling_newline_label_hold_long](../tests/fixtures/svelte/elements/inline_sibling_newline_label_hold_long_prettier_divergence/) (the hold at the print-width boundary: the space spelling stays at 100 and wraps at 101 onto the newline spelling, which is then held — the one-way ratchet the hold accepts, prettier's too, though prettier tolerates the 101-char line itself; the wrap is the Elements catalog's fill-boundary divergence). What counts as a word is the fill's own item split over the SOURCE BYTES — an NBSP-joined pair is one word, and so is an entity-encoded space (`text1 text2` is one unbreakable fill item under both formatters, so the count must not promise a seam the fill has not got; the render-side question, whether a node is a separator in content's clothing, reads the decoded text, and the two axes are deliberately distinct), punctuation alone is a word, a hyphenated pair is one, a word glued to a tag is one — and the cliff is the same at a void element (` text1 text2`, the checkbox-and-caption shape): [inline_sibling_newline_prose_words](../tests/fixtures/svelte/elements/inline_sibling_newline_prose_words_prettier_divergence/). The container is not an axis on the holding side either — the root, an inline element, a list item, a table cell, a control-flow block body and a snippet body hold a one-word run alike: [inline_sibling_newline_label_hold_container](../tests/fixtures/svelte/elements/inline_sibling_newline_label_hold_container_prettier_divergence/). A run is bounded by whatever owns its own line — a block element, a control-flow block, a comment, a `
` — and by an authored blank line **wherever the parser put it**: in a whitespace-only node between two siblings, or folded into the edge whitespace of the content text beside it, so that one document does not count two ways by that accident. The count is per run, so a fragment's prose does not license flow in a neighbouring run that has none — a two-word run on one side of a boundary flows right up to it, and the one-word run on the other holds: [inline_sibling_newline_run_bounded](../tests/fixtures/svelte/elements/inline_sibling_newline_run_bounded_prettier_divergence/) (every boundary kind, the blank in both spellings, prose on one side and a label on the other in each, the prose half flowing as `prettier_variant_newline`; and the unbounded twin, where the label flows with the prose). An authored blank INSIDE one text node is not a boundary — a run is a partition of nodes — and the fill collapses it under both formatters, so the sibling newlines beside such a node flow with its prose where prettier holds them: [inline_sibling_newline_interior_blank](../tests/fixtures/svelte/elements/inline_sibling_newline_interior_blank_prettier_divergence/). Bounding is not sterilizing, though: a run that *does* hold prose still flows right up against the bounding sibling, and the two spellings of that ("its own boundaries are held" vs "every line near it is held") are only separated by a control that re-authors the neighbouring run — see the comment-boundary note above. Two mechanics keep this sound and are load-bearing. First, a flowing single newline takes the **space arm verbatim** rather than a parallel one: emitting a different collapsible form for the newline than for the space it claims to equal makes the formatter write a newline that its next pass re-reads as flowable and collapses. Second, the boundary must defer to the next sibling for a **tag** as well as an element — a bare `line` in the child list resolves all-or-nothing with the parent group, and a multiline fragment's parent group is always broken, so without that the one boundary owned by a content text's `fill` would flow while the rest of the same run hard-broke. That deferral is gated on the flow predicate and not on "next is a tag", because a plain authored space before a tag may have a **comment** on its other side, whose line is authorship. Note `authoring_audit` is blind to the tag half: both spellings agree there whichever form the rule picks — on the flowed form now, on the hard-broken one under a narrower gate — so it scores them as converging either way. — [inline_sibling_newline_flow](../tests/fixtures/svelte/elements/inline_sibling_newline_flow_prettier_divergence/) (an inline element, a component and all three tag kinds — expression, render and `{@html}`, one predicate arm each — plus all five controls) and [expressions/angle_escaped](../tests/fixtures/svelte/expressions/angle_escaped_prettier_divergence/) (the tag-separator case, where prettier's plain `line` holds each tag on its authored line), and the shapes the rule reaches across the suite: [fill_text_inline_collapse](../tests/fixtures/svelte/elements/fill_text_inline_collapse_prettier_divergence/), [root_text_newline_inline](../tests/fixtures/svelte/elements/root_text_newline_inline_prettier_divergence/), [block_text_multiline_collapse](../tests/fixtures/svelte/elements/block_text_multiline_collapse_prettier_divergence/), [inline_attrs_multiline_content](../tests/fixtures/svelte/elements/inline_attrs_multiline_content_prettier_divergence/), [components/root_with_text](../tests/fixtures/svelte/components/root_with_text_prettier_divergence/). **The CONTAINER holding the run is not an axis either**, and no fixture above varies it to say so — nearly all put the flowing run in a block container, and the one inline container that carries one ([inline_attrs_multiline_content](../tests/fixtures/svelte/elements/inline_attrs_multiline_content_prettier_divergence/)) reaches multiline past a 99-char attribute line, confounding the kind with the attribute wrap that fixture exists to test. An inline element reaches the multiline form by a *different boundary rule* — both-or-neither, rather than the block's leading boundary alone — so it is the kind where a container-keyed rule would show, and the run lays out identically once there: [container_kind_newline_flow](../tests/fixtures/svelte/elements/container_kind_newline_flow_prettier_divergence/) holds one run fixed and varies the container across all four kinds — `