From b0356389f5215ba60babae06bd180af94d7a5ce4 Mon Sep 17 00:00:00 2001 From: Manuel Serret Date: Sun, 18 May 2025 10:51:49 +0200 Subject: [PATCH 01/13] fix: formatting for text nodes --- .../src/template/root.test.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/svelte-ast-print/src/template/root.test.ts b/packages/svelte-ast-print/src/template/root.test.ts index 7393b880..da9859b9 100644 --- a/packages/svelte-ast-print/src/template/root.test.ts +++ b/packages/svelte-ast-print/src/template/root.test.ts @@ -320,6 +320,32 @@ I am good. hello world" `); }); + + it("it prints new line after text node without parent", ({ expect }) => { + const code = ` + hello +

world

+ `; + const node = parse_and_extract(code, "Root"); + expect(printRoot(node).code).toMatchInlineSnapshot(`"hello +

world

"`); + }); + + it("it prints no new line after inline elments", ({ expect }) => { + const code = ` +

hello world!

+

hello world!

+ `; + const node = parse_and_extract(code, "Root"); + expect(printRoot(node).code).toMatchInlineSnapshot(` + "

+ hello world! +

+

+ hello world! +

" + `); + }); }); describe(printScript, () => { From 2bdb4b6cf1565dde851052a8bb62b4e3e969bb09 Mon Sep 17 00:00:00 2001 From: Manuel <30698007+manuel3108@users.noreply.github.com> Date: Sun, 18 May 2025 10:54:26 +0200 Subject: [PATCH 02/13] Update packages/svelte-ast-print/src/template/root.test.ts Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Signed-off-by: Manuel <30698007+manuel3108@users.noreply.github.com> --- packages/svelte-ast-print/src/template/root.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte-ast-print/src/template/root.test.ts b/packages/svelte-ast-print/src/template/root.test.ts index da9859b9..44f16148 100644 --- a/packages/svelte-ast-print/src/template/root.test.ts +++ b/packages/svelte-ast-print/src/template/root.test.ts @@ -331,7 +331,7 @@ I am good.

world

"`); }); - it("it prints no new line after inline elments", ({ expect }) => { + it("it prints no new line after inline elements", ({ expect }) => { const code = `

hello world!

hello world!

From 579c09371635f8c239cc3f9e4d2d26608c7b9a4b Mon Sep 17 00:00:00 2001 From: Manuel Serret Date: Sun, 18 May 2025 10:55:06 +0200 Subject: [PATCH 03/13] changeset --- .changeset/cold-mangos-swim.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/cold-mangos-swim.md diff --git a/.changeset/cold-mangos-swim.md b/.changeset/cold-mangos-swim.md new file mode 100644 index 00000000..9daaf232 --- /dev/null +++ b/.changeset/cold-mangos-swim.md @@ -0,0 +1,5 @@ +--- +"svelte-ast-print": patch +--- + +fix: formatting for text nodes From bc849f120c3aab36510682b632917626153f6d6c Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Sun, 1 Jun 2025 17:54:54 +0800 Subject: [PATCH 04/13] chore(deps): Go away `dedent` and update `svelte` --- internals/test/package.json | 1 - internals/test/src/svelte.ts | 6 +++--- pnpm-lock.yaml | 15 +-------------- pnpm-workspace.yaml | 2 +- 4 files changed, 5 insertions(+), 19 deletions(-) diff --git a/internals/test/package.json b/internals/test/package.json index a50cd8c4..d8052286 100644 --- a/internals/test/package.json +++ b/internals/test/package.json @@ -11,7 +11,6 @@ }, "dependencies": { "@types/estree": "catalog:", - "dedent": "^1.5.0", "svelte": "catalog:", "zimmerframe": "catalog:" } diff --git a/internals/test/src/svelte.ts b/internals/test/src/svelte.ts index 16cd74d0..bfd8f2f8 100644 --- a/internals/test/src/svelte.ts +++ b/internals/test/src/svelte.ts @@ -1,4 +1,4 @@ -import dedent from "dedent"; +// import dedent from "dedent"; import type * as JS from "estree"; import * as compiler from "svelte/compiler"; import { type Context, walk } from "zimmerframe"; @@ -6,12 +6,12 @@ import { type Context, walk } from "zimmerframe"; type Node = compiler.AST.SvelteNode | compiler.AST.Root | compiler.AST.Script | JS.Node; export function parse_and_extract(code: string, name: N["type"]): N { - const parsed = parse(dedent(code)); + const parsed = parse(code); return extract(parsed, name); } function parse(code: string): N { - return compiler.parse(code, { modern: true }) as unknown as N; + return compiler.parse(code, { modern: true, loose: true }) as unknown as N; } function extract(parsed: N, name: N["type"]): N { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 78a0d0ec..2f260f28 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,7 @@ catalogs: specifier: ^1.4.0 version: 1.4.6 svelte: - specifier: ^5.31.0 + specifier: ^5.33.0 version: 5.33.10 typescript: specifier: ^5.8.3 @@ -77,9 +77,6 @@ importers: '@types/estree': specifier: 'catalog:' version: 1.0.7 - dedent: - specifier: ^1.5.0 - version: 1.5.3 svelte: specifier: 'catalog:' version: 5.33.10 @@ -818,14 +815,6 @@ packages: supports-color: optional: true - dedent@1.5.3: - resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} - peerDependencies: - babel-plugin-macros: ^3.1.0 - peerDependenciesMeta: - babel-plugin-macros: - optional: true - deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -2402,8 +2391,6 @@ snapshots: dependencies: ms: 2.1.3 - dedent@1.5.3: {} - deep-eql@5.0.2: {} deep-extend@0.6.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index efe4f2b1..970a66dd 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,6 +8,6 @@ packages: catalog: "@types/estree": "~1.0.0" "esrap": "^1.4.0" - "svelte": "^5.31.0" + "svelte": "^5.33.0" "typescript": "^5.8.3" "zimmerframe": "^1.1.0" From f6b3ad3ea99684a827b09a1b799070461523aeb2 Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Sun, 1 Jun 2025 17:57:44 +0800 Subject: [PATCH 05/13] fix(internal): Missing `AttachTag` --- packages/svelte-ast-print/src/_internal/shared.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/svelte-ast-print/src/_internal/shared.ts b/packages/svelte-ast-print/src/_internal/shared.ts index 91e877c1..52e7af28 100644 --- a/packages/svelte-ast-print/src/_internal/shared.ts +++ b/packages/svelte-ast-print/src/_internal/shared.ts @@ -68,6 +68,7 @@ export function isSvelteOnlyNode(n: JS.BaseNode | SV.BaseNode): n is SvelteOnlyN "Root", "Script", // Tag + "AttachTag", "ExpressionTag", "HtmlTag", "ConstTag", From dbd99f8bc2ca8f4660405e68d6c7353098572fc4 Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Sun, 1 Jun 2025 17:58:44 +0800 Subject: [PATCH 06/13] fix: auto-breaking on element-likes el --- .../src/_internal/fragment.ts | 12 - .../src/_internal/template/element-like.ts | 66 +--- .../src/template/element-like.test.ts | 325 +++++++++--------- 3 files changed, 172 insertions(+), 231 deletions(-) delete mode 100644 packages/svelte-ast-print/src/_internal/fragment.ts diff --git a/packages/svelte-ast-print/src/_internal/fragment.ts b/packages/svelte-ast-print/src/_internal/fragment.ts deleted file mode 100644 index 67c510de..00000000 --- a/packages/svelte-ast-print/src/_internal/fragment.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { AST as SV } from "svelte/compiler"; - -/** - * @internal - * @__NO_SIDE_EFFECTS__ - */ -export function has_frag_text_or_exp_tag_only(nodes: SV.Fragment["nodes"]): boolean { - for (const ch of nodes) { - if (ch.type !== "Text" && ch.type !== "ExpressionTag") return false; - } - return true; -} diff --git a/packages/svelte-ast-print/src/_internal/template/element-like.ts b/packages/svelte-ast-print/src/_internal/template/element-like.ts index db3babf9..51798d29 100644 --- a/packages/svelte-ast-print/src/_internal/template/element-like.ts +++ b/packages/svelte-ast-print/src/_internal/template/element-like.ts @@ -4,7 +4,6 @@ import { printFragment } from "../../fragment.ts"; import { printAttributeLike } from "../../template/attribute-like.ts"; import { printAttachTag } from "../../template/tag.ts"; import * as char from "../char.ts"; -import { has_frag_text_or_exp_tag_only } from "../fragment.ts"; import { HTMLClosingTag, HTMLOpeningTag, HTMLSelfClosingTag } from "../html.ts"; import type { PrintOptions } from "../option.ts"; import { type Result, State, Wrapper } from "../shared.ts"; @@ -31,59 +30,18 @@ const NATIVE_SELF_CLOSEABLE_ELS = new Set([ "wbr", ] as const); -/** - * @internal - * @__NO_SIDE_EFFECTS__ - */ -const NATIVE_INLINE_ELS = new Set([ - "a", - "abbr", - "b", - "bdo", - "bdi", - "br", - "cite", - "code", - "data", - "dfn", - "em", - "i", - "kbd", - "mark", - "q", - "rp", - "rt", - "ruby", - "s", - "samp", - "small", - "span", - "strong", - "sub", - "sup", - "time", - "u", - "var", - "wbr", - "button", - "input", - "label", - "select", - "textarea", -] as const); - /** * @internal * @__NO_SIDE_EFFECTS__ */ function is_el_self_closing(n: SV.ElementLike): boolean { - return ( - NATIVE_SELF_CLOSEABLE_ELS - // @ts-expect-error: WARN: `Set.prototype.has()` doesn't accept loose string - .has(n.name) || - // or if there's no "children" - n.fragment.nodes.length === 0 - ); + if (n.type === "RegularElement") + return ( + NATIVE_SELF_CLOSEABLE_ELS + // @ts-expect-error: WARN: `Set.prototype.has()` doesn't accept loose string + .has(n.name) + ); + return n.fragment.nodes.length === 0; } /** @@ -123,12 +81,7 @@ export function print_maybe_self_closing_el(params: { }); } st.add(opening); - const should_break = - // @ts-expect-error `Set.prototype.has()` doesn't accept loose string - !NATIVE_INLINE_ELS.has(n.name) && !has_frag_text_or_exp_tag_only(n.fragment.nodes); - if (should_break) st.break(+1); if (n.fragment) st.add(printFragment(n.fragment, opts)); - if (should_break) st.break(-1); st.add(new HTMLClosingTag("inline", n.name)); return st.result; } @@ -175,12 +128,7 @@ export function print_non_self_closing_el(params: { }); } st.add(opening); - const should_break = - // @ts-expect-error `Set.prototype.has()` doesn't accept loose string - !NATIVE_INLINE_ELS.has(n.name) && !has_frag_text_or_exp_tag_only(n.fragment.nodes); - if (should_break) st.break(+1); st.add(printFragment(n.fragment, opts)); - if (should_break) st.break(-1); st.add(new HTMLClosingTag("inline", n.name)); return st.result; } diff --git a/packages/svelte-ast-print/src/template/element-like.test.ts b/packages/svelte-ast-print/src/template/element-like.test.ts index 81ed074e..d6007583 100644 --- a/packages/svelte-ast-print/src/template/element-like.test.ts +++ b/packages/svelte-ast-print/src/template/element-like.test.ts @@ -24,12 +24,12 @@ describe(printElementLike, () => { describe(printComponent, () => { it("works on example component without children", ({ expect }) => { const code = ` - + `; const node = parse_and_extract(code, "Component"); expect(printComponent(node).code).toMatchInlineSnapshot( @@ -39,63 +39,67 @@ describe(printElementLike, () => { it("works on example component with children", ({ expect }) => { const code = ` - - - Flowbite Logo - Flowbite - - - - - - Home - - Mega menu - - - - {item.name} - - - Services - Products - Contact - - + + + Flowbite Logo + Flowbite + + + + + + Home + + Mega menu + + + + {item.name} + + + Services + Products + Contact + + `; const node = parse_and_extract(code, "Component"); expect(printComponent(node).code).toMatchInlineSnapshot(` - " - - Flowbite Logo - Flowbite - - - - Home - - Mega menu - - - {item.name} - - Services - Products - Contact - - " - `); + " + + Flowbite Logo + Flowbite + + + + + + Home + + Mega menu + + + + {item.name} + + + Services + Products + Contact + + " + `); }); }); describe(printRegularElement, () => { it("works on example component without children", ({ expect }) => { const code = ` - + `; const node = parse_and_extract(code, "RegularElement"); expect(printRegularElement(node).code).toMatchInlineSnapshot( @@ -105,7 +109,7 @@ describe(printElementLike, () => { it("works on example component with children", ({ expect }) => { const code = ` - + `; const node = parse_and_extract(code, "RegularElement"); expect(printRegularElement(node).code).toMatchInlineSnapshot(` @@ -115,7 +119,7 @@ describe(printElementLike, () => { it("prints class attribute with string containing expression tags correctly", ({ expect }) => { const code = ` - test +test `; const node = parse_and_extract(code, "RegularElement"); expect(printRegularElement(node).code).toMatchInlineSnapshot(`"test"`); @@ -125,7 +129,7 @@ describe(printElementLike, () => { describe(printSlotElement, () => { it("works on example component without children", ({ expect }) => { const code = ` - + `; const node = parse_and_extract(code, "SlotElement"); expect(printSlotElement(node).code).toMatchInlineSnapshot(`""`); @@ -133,10 +137,10 @@ describe(printElementLike, () => { it("works on example component with children", ({ expect }) => { const code = ` -
- No header was provided -

Some content between header and footer

-
+
+ No header was provided +

Some content between header and footer

+
`; const node = parse_and_extract(code, "SlotElement"); expect(printSlotElement(node).code).toMatchInlineSnapshot( @@ -148,7 +152,7 @@ describe(printElementLike, () => { describe(printSvelteBody, () => { it("works when is valid (has no children)", ({ expect }) => { const code = ` - + `; const node = parse_and_extract(code, "SvelteBody"); expect(printSvelteBody(node).code).toMatchInlineSnapshot( @@ -160,23 +164,23 @@ describe(printElementLike, () => { describe(printSvelteBoundary, () => { it("works on basic example", ({ expect }) => { const code = ` - - - {#snippet failed(error, reset)} - - {/snippet} - + + + {#snippet failed(error, reset)} + + {/snippet} + `; const node = parse_and_extract(code, "SvelteBoundary"); expect(printSvelteBoundary(node).code).toMatchInlineSnapshot( ` - " - - {#snippet failed(error, reset)} - - {/snippet} - " - `, + " + + {#snippet failed(error, reset)} + + {/snippet} + " + `, ); }); }); @@ -184,7 +188,7 @@ describe(printElementLike, () => { describe(printSvelteComponent, () => { it("works when is valid (has no children)", ({ expect }) => { const code = ` - + `; const node = parse_and_extract(code, "SvelteComponent"); expect(printSvelteComponent(node).code).toMatchInlineSnapshot(`""`); @@ -194,7 +198,7 @@ describe(printElementLike, () => { describe(printSvelteDocument, () => { it("works when is valid (has no children)", ({ expect }) => { const code = ` - + `; const node = parse_and_extract(code, "SvelteDocument"); expect(printSvelteDocument(node).code).toMatchInlineSnapshot( @@ -206,7 +210,7 @@ describe(printElementLike, () => { describe(printSvelteElement, () => { it("works when is valid (has no children)", ({ expect }) => { const code = ` - Foo +Foo `; const node = parse_and_extract(code, "SvelteElement"); expect(printSvelteElement(node).code).toMatchInlineSnapshot( @@ -218,10 +222,10 @@ describe(printElementLike, () => { describe(printSvelteFragment, () => { it("works when is valid (has children)", ({ expect }) => { const code = ` - -

All rights reserved.

-

Copyright (c) 2019 Svelte Industries

-
+ +

All rights reserved.

+

Copyright (c) 2019 Svelte Industries

+
`; const node = parse_and_extract(code, "SvelteFragment"); expect(printSvelteFragment(node).code).toMatchInlineSnapshot(` @@ -236,10 +240,10 @@ describe(printElementLike, () => { describe(printSvelteHead, () => { it("works when is valid (has children)", ({ expect }) => { const code = ` - - Hello world! - - + + Hello world! + + `; const node = parse_and_extract(code, "SvelteHead"); expect(printSvelteHead(node).code).toMatchInlineSnapshot(` @@ -253,69 +257,70 @@ describe(printElementLike, () => { describe(printSvelteOptions, () => { it("works when is valid (has no children)", ({ expect }) => { + // TODO: Add support for mutliline const code = ` - { - // Extend the class so we can let it participate in HTML forms - return class extends customElementConstructor { - static formAssociated = true; - - constructor() { - super(); - this.attachedInternals = this.attachInternals(); - } - - // Add the function here, not below in the component so that - // it's always available, not just when the inner Svelte component - // is mounted - randomIndex() { - this.elementIndex = Math.random(); - } - }; - } - }} - /> + { + // Extend the class so we can let it participate in HTML forms + return class extends customElementConstructor { + static formAssociated = true; + + constructor() { + super(); + this.attachedInternals = this.attachInternals(); + } + + // Add the function here, not below in the component so that + // it's always available, not just when the inner Svelte component + // is mounted + randomIndex() { + this.elementIndex = Math.random(); + } + }; + } + }} +/> `; const node = parse_and_extract(code, "Root"); expect(node.options).toBeDefined(); if (node.options) { expect(printSvelteOptions(node.options).code).toMatchInlineSnapshot(` - " { - // Extend the class so we can let it participate in HTML forms - return class extends customElementConstructor { - static formAssociated = true; - - constructor() { - super(); - this.attachedInternals = this.attachInternals(); + " { + // Extend the class so we can let it participate in HTML forms + return class extends customElementConstructor { + static formAssociated = true; - // Add the function here, not below in the component so that - // it's always available, not just when the inner Svelte component - // is mounted - randomIndex() { - this.elementIndex = Math.random(); - } - }; - } - }} />" - `); + constructor() { + super(); + this.attachedInternals = this.attachInternals(); + } + + // Add the function here, not below in the component so that + // it's always available, not just when the inner Svelte component + // is mounted + randomIndex() { + this.elementIndex = Math.random(); + } + }; + } + }} />" + `); } }); }); @@ -323,12 +328,12 @@ describe(printElementLike, () => { describe(printSvelteSelf, () => { it("works when is valid (has no children)", ({ expect }) => { const code = ` - {#if count > 0} -

counting down... {count}

- - {:else} -

lift-off!

- {/if} +{#if count > 0} +

counting down... {count}

+ +{:else} +

lift-off!

+{/if} `; const node = parse_and_extract(code, "SvelteSelf"); expect(printSvelteSelf(node).code).toMatchInlineSnapshot(`""`); @@ -338,14 +343,14 @@ describe(printElementLike, () => { describe(printSvelteWindow, () => { it("works when is valid (has no children)", ({ expect }) => { const code = ` - + - + `; const node = parse_and_extract(code, "SvelteWindow"); expect(printSvelteWindow(node).code).toMatchInlineSnapshot( @@ -357,13 +362,13 @@ describe(printElementLike, () => { describe(printTitleElement, () => { it("works when is valid (has children)", ({ expect }) => { const code = ` - + - - Svelte is optimized for {reason} - + + Svelte is optimized for {reason} + `; const node = parse_and_extract(code, "TitleElement"); expect(printTitleElement(node).code).toMatchInlineSnapshot( From adf90209eeae9cb10f778895f637c50b243b4aa6 Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Sun, 1 Jun 2025 18:00:20 +0800 Subject: [PATCH 07/13] simplify attribute-like tests --- .../src/template/attribute-like.test.ts | 168 ++++-------------- 1 file changed, 35 insertions(+), 133 deletions(-) diff --git a/packages/svelte-ast-print/src/template/attribute-like.test.ts b/packages/svelte-ast-print/src/template/attribute-like.test.ts index 2a03cea0..9aad3192 100644 --- a/packages/svelte-ast-print/src/template/attribute-like.test.ts +++ b/packages/svelte-ast-print/src/template/attribute-like.test.ts @@ -19,73 +19,55 @@ import { describe(printAttributeLike, () => { describe(printAttribute, () => { it("correctly prints boolean value", ({ expect }) => { - const code = ` - - `; + const code = ``; const node = parse_and_extract(code, "Attribute"); expect(printAttribute(node).code).toMatchInlineSnapshot(`"required"`); }); it("correctly prints empty text expression value", ({ expect }) => { - const code = ` - - `; + const code = ``; const node = parse_and_extract(code, "Attribute"); expect(printAttribute(node).code).toMatchInlineSnapshot(`"required="""`); }); it("correctly prints text expression value with text", ({ expect }) => { - const code = ` -
- `; + const code = `
`; const node = parse_and_extract(code, "Attribute"); expect(printAttribute(node).code).toMatchInlineSnapshot(`"aria-label="this is a modal box""`); }); it("correctly prints booleanish expression tag value", ({ expect }) => { - const code = ` - + const code = ` `; const node = parse_and_extract(code, "OnDirective"); expect(printOnDirective(node).code).toMatchInlineSnapshot(`"on:click={() => count += 1}"`); }); it("works on with modifiers variant", ({ expect }) => { - const code = ` -
- ... -
+ const code = `
...
`; const node = parse_and_extract(code, "OnDirective"); expect(printOnDirective(node).code).toMatchInlineSnapshot(`"on:submit|preventDefault={handleSubmit}"`); @@ -235,33 +175,25 @@ describe(printAttributeLike, () => { describe(printStyleDirective, () => { it("works with expression tag value", ({ expect }) => { - const code = ` -
...
- `; + const code = `
...
`; const node = parse_and_extract(code, "StyleDirective"); expect(printStyleDirective(node).code).toMatchInlineSnapshot(`"style:color={myColor}"`); }); it("works with shorthand", ({ expect }) => { - const code = ` -
...
- `; + const code = `
...
`; const node = parse_and_extract(code, "StyleDirective"); expect(printStyleDirective(node).code).toMatchInlineSnapshot(`"style:color"`); }); it("works with text expression", ({ expect }) => { - const code = ` -
...
- `; + const code = `
...
`; const node = parse_and_extract(code, "StyleDirective"); expect(printStyleDirective(node).code).toMatchInlineSnapshot(`"style:color="red""`); }); it("works with modifiers and text expression", ({ expect }) => { - const code = ` -
...
- `; + const code = `
...
`; const node = parse_and_extract(code, "StyleDirective"); expect(printStyleDirective(node).code).toMatchInlineSnapshot(`"style:color|important="red""`); }); @@ -269,43 +201,25 @@ describe(printAttributeLike, () => { describe(printTransitionDirective, () => { it("works when using transition", ({ expect }) => { - const code = ` - {#if visible} -
scales in, scales out
- {/if} - `; + const code = `
scales in, scales out
`; const node = parse_and_extract(code, "TransitionDirective"); expect(printTransitionDirective(node).code).toMatchInlineSnapshot(`"transition:scale"`); }); it("works when using intro", ({ expect }) => { - const code = ` - {#if visible} -
flies in
- {/if} - `; + const code = `
flies in
`; const node = parse_and_extract(code, "TransitionDirective"); expect(printTransitionDirective(node).code).toMatchInlineSnapshot(`"in:fly"`); }); it("works when using outro", ({ expect }) => { - const code = ` - {#if visible} -
fades out
- {/if} - `; + const code = `
fades out
`; const node = parse_and_extract(code, "TransitionDirective"); expect(printTransitionDirective(node).code).toMatchInlineSnapshot(`"out:fade"`); }); it("works when using params", ({ expect }) => { - const code = ` - {#if visible} -

- Flies in and out -

- {/if} - `; + const code = `

`; const node = parse_and_extract(code, "TransitionDirective"); expect(printTransitionDirective(node).code).toMatchInlineSnapshot( `"transition:fly={{ y: 200, duration: 2000 }}"`, @@ -313,21 +227,13 @@ describe(printAttributeLike, () => { }); it("works when using modifiers", ({ expect }) => { - const code = ` - {#if visible} -

fades in and out when x or y change

- {/if} - `; + const code = `

fades in and out when x or y change

`; const node = parse_and_extract(code, "TransitionDirective"); expect(printTransitionDirective(node).code).toMatchInlineSnapshot(`"transition:fade|global"`); }); it("works when using modifiers and with params", ({ expect }) => { - const code = ` - {#if visible} -

fades in and out when x or y change

- {/if} - `; + const code = `

fades in and out when x or y change

`; const node = parse_and_extract(code, "TransitionDirective"); expect(printTransitionDirective(node).code).toMatchInlineSnapshot( `"transition:fade|local={{ y: 200, duration: 2000 }}"`, @@ -337,17 +243,13 @@ describe(printAttributeLike, () => { describe(printUseDirective, () => { it("works with parameters", ({ expect }) => { - const code = ` -
- `; + const code = `
`; const node = parse_and_extract(code, "UseDirective"); expect(printUseDirective(node).code).toMatchInlineSnapshot(`"use:foo={bar}"`); }); it("works on without parameters - shorthand", ({ expect }) => { - const code = ` -
- `; + const code = `
`; const node = parse_and_extract(code, "UseDirective"); expect(printUseDirective(node).code).toMatchInlineSnapshot(`"use:foo"`); }); From d70e21f8e011f450cb330eafd6a1bcf994a0725d Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Sun, 1 Jun 2025 18:05:28 +0800 Subject: [PATCH 08/13] fix: don't auto-break on blocks --- .../src/template/block.test.ts | 269 +++++++++--------- .../svelte-ast-print/src/template/block.ts | 23 +- 2 files changed, 135 insertions(+), 157 deletions(-) diff --git a/packages/svelte-ast-print/src/template/block.test.ts b/packages/svelte-ast-print/src/template/block.test.ts index 4ed9af02..c56d2959 100644 --- a/packages/svelte-ast-print/src/template/block.test.ts +++ b/packages/svelte-ast-print/src/template/block.test.ts @@ -15,13 +15,13 @@ describe(printBlock, () => { describe("AwaitBlock", () => { it("correctly prints standard example", ({ expect }) => { const code = ` - {#await promise} -

waiting for the promise to resolve...

- {:then value} -

The value is {value}

- {:catch error} -

Something went wrong: {error.message}

- {/await} +{#await promise} +

waiting for the promise to resolve...

+{:then value} +

The value is {value}

+{:catch error} +

Something went wrong: {error.message}

+{/await} `; const node = parse_and_extract(code, "AwaitBlock"); expect(printAwaitBlock(node).code).toMatchInlineSnapshot(` @@ -37,11 +37,11 @@ describe(printBlock, () => { it("correctly prints with omitted catch", ({ expect }) => { const code = ` - {#await promise} -

waiting for the promise to resolve...

- {:then value} -

The value is {value}

- {/await} +{#await promise} +

waiting for the promise to resolve...

+{:then value} +

The value is {value}

+{/await} `; const node = parse_and_extract(code, "AwaitBlock"); expect(printAwaitBlock(node).code).toMatchInlineSnapshot(` @@ -55,9 +55,9 @@ describe(printBlock, () => { it("correctly prints omitted initial block", ({ expect }) => { const code = ` - {#await promise then value} -

The value is {value}

- {/await} +{#await promise then value} +

The value is {value}

+{/await} `; const node = parse_and_extract(code, "AwaitBlock"); expect(printAwaitBlock(node).code).toMatchInlineSnapshot(` @@ -69,9 +69,9 @@ describe(printBlock, () => { it("correctly prints omitted then block", ({ expect }) => { const code = ` - {#await promise catch error} -

The error is {error}

- {/await} +{#await promise catch error} +

The error is {error}

+{/await} `; const node = parse_and_extract(code, "AwaitBlock"); expect(printAwaitBlock(node).code).toMatchInlineSnapshot(` @@ -83,11 +83,11 @@ describe(printBlock, () => { it("nested blocks works", ({ expect }) => { const code = ` - {#await promiseParent catch error} - {#await promiseChildren catch error} -

The error is {error}

- {/await} - {/await} +{#await promiseParent catch error} + {#await promiseChildren catch error} +

The error is {error}

+ {/await} +{/await} `; const node = parse_and_extract(code, "AwaitBlock"); expect(printAwaitBlock(node).code).toMatchInlineSnapshot(` @@ -103,9 +103,9 @@ describe(printBlock, () => { describe("EachBlock", () => { it("correctly prints simple example", ({ expect }) => { const code = ` - {#each items as item} -
  • {item.name} x {item.qty}
  • - {/each} +{#each items as item} +
  • {item.name} x {item.qty}
  • +{/each} `; const node = parse_and_extract(code, "EachBlock"); expect(printEachBlock(node).code).toMatchInlineSnapshot(` @@ -117,19 +117,17 @@ describe(printBlock, () => { it("supports without `as` item ", ({ expect }) => { const code = ` -
    - {#each { length: 8 }, rank} - {#each { length: 8 }, file} -
    - {/each} - {/each} -
    +{#each { length: 8 }, rank} + {#each { length: 8 }, file} +
    + {/each} +{/each} `; const node = parse_and_extract(code, "EachBlock"); expect(printEachBlock(node).code).toMatchInlineSnapshot(` "{#each { length: 8 }, rank} {#each { length: 8 }, file} -
    +
    {/each} {/each}" `); @@ -137,9 +135,9 @@ describe(printBlock, () => { it("correctly prints example with index", ({ expect }) => { const code = ` - {#each items as item, i} -
  • {i + 1}: {item.name} x {item.qty}
  • - {/each} +{#each items as item, i} +
  • {i + 1}: {item.name} x {item.qty}
  • +{/each} `; const node = parse_and_extract(code, "EachBlock"); expect(printEachBlock(node).code).toMatchInlineSnapshot(` @@ -151,9 +149,9 @@ describe(printBlock, () => { it("correctly prints example with index and keyed", ({ expect }) => { const code = ` - {#each items as item, i (item.id)} -
  • {i + 1}: {item.name} x {item.qty}
  • - {/each} +{#each items as item, i (item.id)} +
  • {i + 1}: {item.name} x {item.qty}
  • +{/each} `; const node = parse_and_extract(code, "EachBlock"); expect(printEachBlock(node).code).toMatchInlineSnapshot(` @@ -165,9 +163,9 @@ describe(printBlock, () => { it("works with destructuring object-like item", ({ expect }) => { const code = ` - {#each items as { id, item, qty }, i (id)} -
  • {i + 1}: {item.name} x {qty}
  • - {/each} +{#each items as { id, item, qty }, i (id)} +
  • {i + 1}: {item.name} x {qty}
  • +{/each} `; const node = parse_and_extract(code, "EachBlock"); expect(printEachBlock(node).code).toMatchInlineSnapshot(` @@ -179,12 +177,12 @@ describe(printBlock, () => { it("works with destructuring object-like item with rest pattern", ({ expect }) => { const code = ` - {#each objects as { id, ...rest }} -
  • - {id} - -
  • - {/each} +{#each objects as { id, ...rest }} +
  • + {id} + +
  • +{/each} `; const node = parse_and_extract(code, "EachBlock"); expect(printEachBlock(node).code).toMatchInlineSnapshot(` @@ -199,12 +197,12 @@ describe(printBlock, () => { it("works with destructuring array-like item and with rest pattern", ({ expect }) => { const code = ` - {#each items as [id, ...rest]} -
  • - {id} - -
  • - {/each} +{#each items as [id, ...rest]} +
  • + {id} + +
  • +{/each} `; const node = parse_and_extract(code, "EachBlock"); expect(printEachBlock(node).code).toMatchInlineSnapshot(` @@ -219,11 +217,11 @@ describe(printBlock, () => { it("works with else clause for empty lists", ({ expect }) => { const code = ` - {#each todos as todo} -

    {todo.text}

    - {:else} -

    No tasks today!

    - {/each} +{#each todos as todo} +

    {todo.text}

    +{:else} +

    No tasks today!

    +{/each} `; const node = parse_and_extract(code, "EachBlock"); expect(printEachBlock(node).code).toMatchInlineSnapshot(` @@ -237,15 +235,15 @@ describe(printBlock, () => { it("nested blocks works", ({ expect }) => { const code = ` - {#each todos as todo} - {#each todo.subtasks as subtask} -

    {subtask.test}

    - {:else} -

    No subtasks available!

    - {/each} - {:else} -

    No tasks today!

    - {/each} +{#each todos as todo} + {#each todo.subtasks as subtask} +

    {subtask.test}

    + {:else} +

    No subtasks available!

    + {/each} +{:else} +

    No tasks today!

    +{/each} `; const node = parse_and_extract(code, "EachBlock"); expect(printEachBlock(node).code).toMatchInlineSnapshot(` @@ -265,9 +263,9 @@ describe(printBlock, () => { describe("IfBlock", () => { it("correctly prints simple {#if} block", ({ expect }) => { const code = ` - {#if test} - simple if - {/if} +{#if test} + simple if +{/if} `; const node = parse_and_extract(code, "IfBlock"); expect(printIfBlock(node).code).toMatchInlineSnapshot(` @@ -279,11 +277,11 @@ describe(printBlock, () => { it("correctly prints {#if} block with {:else}", ({ expect }) => { const code = ` - {#if test} - if body - {:else} - else body - {/if} +{#if test} + if body +{:else} + else body +{/if} `; const node = parse_and_extract(code, "IfBlock"); expect(printIfBlock(node).code).toMatchInlineSnapshot(` @@ -297,11 +295,11 @@ describe(printBlock, () => { it("correctly prints {#if} block with {:else if}", ({ expect }) => { const code = ` - {#if test1} - if body - {:else if test2} - else if body - {/if} +{#if test1} + if body +{:else if test2} + else if body +{/if} `; const node = parse_and_extract(code, "IfBlock"); expect(printIfBlock(node).code).toMatchInlineSnapshot(` @@ -315,13 +313,13 @@ describe(printBlock, () => { it("correctly prints {#if} block with {:else if} and {:else}", ({ expect }) => { const code = ` - {#if test1} - if body - {:else if test2} - else if body - {:else} - else body - {/if} +{#if test1} + if body +{:else if test2} + else if body +{:else} + else body +{/if} `; const node = parse_and_extract(code, "IfBlock"); expect(printIfBlock(node).code).toMatchInlineSnapshot(` @@ -339,15 +337,15 @@ describe(printBlock, () => { expect, }) => { const code = ` - {#if test1} - if body - {:else if test2} - else if body1 - {:else if test3} - else if body2 - {:else} - else body - {/if} +{#if test1} + if body +{:else if test2} + else if body1 +{:else if test3} + else if body2 +{:else} + else body +{/if} `; const node = parse_and_extract(code, "IfBlock"); expect(printIfBlock(node).code).toMatchInlineSnapshot(` @@ -362,17 +360,18 @@ describe(printBlock, () => { {/if}" `); }); + it("correctly prints {#if} block with multiple {:else if} and {:else} - case with text only", ({ expect }) => { const code = ` - {#if test1} - if body - {:else if test2} - 1else if body - {:else if test3} - 2else if body - {:else} - else body - {/if} +{#if test1} + if body +{:else if test2} + 1else if body +{:else if test3} + 2else if body +{:else} + else body +{/if} `; const node = parse_and_extract(code, "IfBlock"); expect(printIfBlock(node).code).toMatchInlineSnapshot(` @@ -392,9 +391,9 @@ describe(printBlock, () => { describe("KeyBlock", () => { it("correctly prints the block where expression tag is used", ({ expect }) => { const code = ` - {#key value} -
    {value}
    - {/key} +{#key value} +
    {value}
    +{/key} `; const node = parse_and_extract(code, "KeyBlock"); expect(printKeyBlock(node).code).toMatchInlineSnapshot(` @@ -406,9 +405,9 @@ describe(printBlock, () => { it("correctly prints the block where no key expression is used", ({ expect }) => { const code = ` - {#key value} - - {/key} +{#key value} + +{/key} `; const node = parse_and_extract(code, "KeyBlock"); expect(printKeyBlock(node).code).toMatchInlineSnapshot(` @@ -422,9 +421,9 @@ describe(printBlock, () => { describe("SnippetBlock", () => { it("work for a simple template", ({ expect }) => { const code = ` - {#snippet hello(name, message)} -

    hello {name}! {message}!

    - {/snippet} +{#snippet hello(name, message)} +

    hello {name}! {message}!

    +{/snippet} `; const node = parse_and_extract(code, "SnippetBlock"); expect(printSnippetBlock(node).code).toMatchInlineSnapshot(` @@ -436,17 +435,17 @@ describe(printBlock, () => { it("works with deeply nested children", ({ expect }) => { const code = ` - {#snippet figure(image)} -
    - {image.caption} -
    {image.caption}
    -
    - {/snippet} +{#snippet figure(image)} +
    + {image.caption} +
    {image.caption}
    +
    +{/snippet} `; const node = parse_and_extract(code, "SnippetBlock"); expect(printSnippetBlock(node).code).toMatchInlineSnapshot(` @@ -461,11 +460,11 @@ describe(printBlock, () => { it("works with nested snippet", ({ expect }) => { const code = ` - {#snippet parent(message)} - {#snippet children(name)} -

    hello {name}! {message}!

    - {/snippet} - {/snippet} +{#snippet parent(message)} + {#snippet children(name)} +

    hello {name}! {message}!

    + {/snippet} +{/snippet} `; const node = parse_and_extract(code, "SnippetBlock"); expect(printSnippetBlock(node).code).toMatchInlineSnapshot(` @@ -479,9 +478,9 @@ describe(printBlock, () => { it("works with snippet containing more than one param", ({ expect }) => { const code = ` - {#snippet test(param1, param2)} -

    {param1} + {param2}

    - {/snippet} +{#snippet test(param1, param2)} +

    {param1} + {param2}

    +{/snippet} `; const node = parse_and_extract(code, "SnippetBlock"); expect(printSnippetBlock(node).code).toMatchInlineSnapshot(` diff --git a/packages/svelte-ast-print/src/template/block.ts b/packages/svelte-ast-print/src/template/block.ts index aaf2da93..f97934c5 100644 --- a/packages/svelte-ast-print/src/template/block.ts +++ b/packages/svelte-ast-print/src/template/block.ts @@ -68,26 +68,18 @@ export function printAwaitBlock(n: SV.AwaitBlock, opts: Partial = if (n.then && !n.pending) opening.insert(char.SPACE, "then", n.value && [char.SPACE, print_js(n.value, st.opts)]); if (n.catch && !n.pending) opening.insert(char.SPACE, "catch", n.error && [char.SPACE, print_js(n.error, st.opts)]); st.add(opening); - if (n.pending) { - st.break(+1); - st.add(printFragment(n.pending, opts)); - st.break(-1); - } + if (n.pending) st.add(printFragment(n.pending, opts)); if (n.then) { if (n.value && n.pending) { st.add(new MidBlock("inline", "then", n.value && [char.SPACE, print_js(n.value, st.opts)])); } - st.break(+1); st.add(printFragment(n.then, opts)); - st.break(-1); } if (n.catch) { if (n.error && n.pending) { st.add(new MidBlock("inline", "catch", n.error && [char.SPACE, print_js(n.error, st.opts)])); } - st.break(+1); st.add(printFragment(n.catch, opts)); - st.break(-1); } st.add(new ClosingBlock("inline", name)); return st.result; @@ -139,7 +131,6 @@ export function printEachBlock(n: SV.EachBlock, opts: Partial = {} const st = State.get(n, opts); st.add( new OpeningBlock( - // "inline", name, char.SPACE, @@ -149,14 +140,10 @@ export function printEachBlock(n: SV.EachBlock, opts: Partial = {} n.key && [char.SPACE, new RoundBrackets("inline", print_js(n.key, st.opts))], ), ); - st.break(+1); st.add(printFragment(n.body, opts)); - st.break(-1); if (n.fallback) { st.add(new MidBlock("inline", "else")); - st.break(+1); st.add(printFragment(n.fallback, opts)); - st.break(-1); } st.add(new ClosingBlock("inline", name)); return st.result; @@ -201,17 +188,13 @@ export function printIfBlock(n: SV.IfBlock, opts: Partial = {}): R } else { st.add(new MidBlock("inline", "else if", char.SPACE, print_js(n.test, st.opts))); } - st.break(+1); st.add(printFragment(n.consequent, opts)); - st.break(-1); const alternate_if_block = get_if_block_alternate(n.alternate); if (n.alternate) { if (alternate_if_block) st.add(printIfBlock(alternate_if_block, opts)); else { st.add(new MidBlock("inline", "else")); - st.break(+1); st.add(printFragment(n.alternate, opts)); - st.break(-1); } } if (!alternate_if_block) st.add(new ClosingBlock("inline", name)); @@ -233,9 +216,7 @@ export function printKeyBlock(n: SV.KeyBlock, opts: Partial = {}): const name = "key"; const st = State.get(n, opts); st.add(new OpeningBlock("inline", name, char.SPACE, print_js(n.expression, st.opts))); - st.break(+1); st.add(printFragment(n.fragment, opts)); - st.break(-1); st.add(new ClosingBlock("inline", name)); return st.result; } @@ -269,9 +250,7 @@ export function printSnippetBlock(n: SV.SnippetBlock, opts: Partial Date: Sun, 1 Jun 2025 18:05:55 +0800 Subject: [PATCH 09/13] simplify html tests --- .../src/template/html.test.ts | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/packages/svelte-ast-print/src/template/html.test.ts b/packages/svelte-ast-print/src/template/html.test.ts index 66f21025..df0d80e1 100644 --- a/packages/svelte-ast-print/src/template/html.test.ts +++ b/packages/svelte-ast-print/src/template/html.test.ts @@ -7,26 +7,20 @@ import { printComment, printHTMLNode, printText } from "./html.ts"; describe(printHTMLNode, () => { describe(printComment, () => { it("prints correctly a single line comment from random code", ({ expect }) => { - const code = ` - {#each boxes as box} - - {@const area = box.width * box.height} - {box.width} * {box.height} = {area} - {/each} - `; + const code = ``; const node = parse_and_extract(code, "Comment"); expect(printComment(node).code).toMatchInlineSnapshot(`""`); }); it("supports multiple line comment", ({ expect }) => { const code = ` - + `; const node = parse_and_extract(code, "Comment"); expect(printComment(node).code).toMatchInlineSnapshot( @@ -45,9 +39,7 @@ describe(printHTMLNode, () => { describe(printText, () => { it("prints correctly a random text comment from random code", ({ expect }) => { - const code = ` - Catch me if you can - `; + const code = `Catch me if you can`; const node = parse_and_extract(code, "Text"); expect(printText(node).code).toMatchInlineSnapshot(`"Catch me if you can"`); }); From dfe9e640fa702890859e644588b2be6b32300d61 Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Sun, 1 Jun 2025 18:06:22 +0800 Subject: [PATCH 10/13] simplify tag tests --- .../svelte-ast-print/src/template/tag.test.ts | 69 +++++-------------- 1 file changed, 18 insertions(+), 51 deletions(-) diff --git a/packages/svelte-ast-print/src/template/tag.test.ts b/packages/svelte-ast-print/src/template/tag.test.ts index cb8c0482..befe10a2 100644 --- a/packages/svelte-ast-print/src/template/tag.test.ts +++ b/packages/svelte-ast-print/src/template/tag.test.ts @@ -52,37 +52,31 @@ describe(printTag, () => { describe(printAttachTag, () => { it("prints correctly when is a reference", ({ expect }) => { - const code = ` -
    ...
    - `; + const code = `
    ...
    `; const node = parse_and_extract(code, "AttachTag"); expect(printAttachTag(node).code).toMatchInlineSnapshot(`"{@attach myAttachment}"`); }); it("prints correctly when is function call", ({ expect }) => { - const code = ` - - `; + const code = ``; const node = parse_and_extract(code, "AttachTag"); expect(printAttachTag(node).code).toMatchInlineSnapshot(`"{@attach tooltip(content)}"`); }); it("prints correctly when is an inline attachment", ({ expect }) => { const code = ` - { - const context = canvas.getContext('2d'); - - $effect(() => { - context.fillStyle = color; - context.fillRect(0, 0, canvas.width, canvas.height); - }); - }} - > + { + const context = canvas.getContext('2d'); + + $effect(() => { + context.fillStyle = color; + context.fillRect(0, 0, canvas.width, canvas.height); + }); + }} +> `; const node = parse_and_extract(code, "AttachTag"); expect(printAttachTag(node).code).toMatchInlineSnapshot( @@ -102,12 +96,7 @@ describe(printAttachTag, () => { describe(printConstTag, () => { it("prints correctly when used as direct child of allowed tags ", ({ expect }) => { - const code = ` - {#each boxes as box} - {@const area = box.width * box.height} - {box.width} * {box.height} = {area} - {/each} - `; + const code = `{@const area = box.width * box.height}`; const node = parse_and_extract(code, "ConstTag"); expect(printConstTag(node).code).toMatchInlineSnapshot(`"{@const area = box.width * box.height}"`); }); @@ -115,18 +104,7 @@ describe(printConstTag, () => { describe(printDebugTag, () => { it("prints correctly when used as direct child of allowed tags ", ({ expect }) => { - const code = ` - - - {@debug user} - -

    Hello {user.firstname}!

    - `; + const code = `{@debug user}`; const node = parse_and_extract(code, "DebugTag"); expect(printDebugTag(node).code).toMatchInlineSnapshot(`"{@debug user}"`); }); @@ -154,12 +132,7 @@ describe(printExpressionTag, () => { describe(printHtmlTag, () => { it("prints correctly when used in an example case", ({ expect }) => { - const code = ` -
    -

    {post.title}

    - {@html post.content} -
    - `; + const code = `{@html post.content}`; const node = parse_and_extract(code, "HtmlTag"); expect(printHtmlTag(node).code).toMatchInlineSnapshot(`"{@html post.content}"`); }); @@ -167,13 +140,7 @@ describe(printHtmlTag, () => { describe(printRenderTag, () => { it("prints correctly when used in an example case", ({ expect }) => { - const code = ` - {#snippet hello(name)} -

    hello {name}! {message}!

    - {/snippet} - - {@render hello('alice')} - `; + const code = `{@render hello('alice')}`; const node = parse_and_extract(code, "RenderTag"); expect(printRenderTag(node).code).toMatchInlineSnapshot(`"{@render hello('alice')}"`); }); From a6d6211ee531226ada4385f063598631b22745f9 Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Sun, 1 Jun 2025 18:12:10 +0800 Subject: [PATCH 11/13] fix auto-indentation in fragment --- .../src/_internal/template/block.ts | 11 -- .../src/_internal/template/element-like.ts | 20 ---- .../svelte-ast-print/src/fragment.test.ts | 111 ++++++++++-------- packages/svelte-ast-print/src/fragment.ts | 54 +++------ 4 files changed, 79 insertions(+), 117 deletions(-) diff --git a/packages/svelte-ast-print/src/_internal/template/block.ts b/packages/svelte-ast-print/src/_internal/template/block.ts index 2823c824..40bbabb6 100644 --- a/packages/svelte-ast-print/src/_internal/template/block.ts +++ b/packages/svelte-ast-print/src/_internal/template/block.ts @@ -37,14 +37,3 @@ export class ClosingBlock extends Wrapper { export function get_if_block_alternate(n: SV.IfBlock["alternate"]) { return n?.nodes.find((n) => n.type === "IfBlock"); } - -export function isBlock(n: SV.BaseNode): n is SV.Block { - return new Set([ - // - "AwaitBlock", - "EachBlock", - "IfBlock", - "KeyBlock", - "SnippetBlock", - ]).has(n.type); -} diff --git a/packages/svelte-ast-print/src/_internal/template/element-like.ts b/packages/svelte-ast-print/src/_internal/template/element-like.ts index 51798d29..fc51bd32 100644 --- a/packages/svelte-ast-print/src/_internal/template/element-like.ts +++ b/packages/svelte-ast-print/src/_internal/template/element-like.ts @@ -133,26 +133,6 @@ export function print_non_self_closing_el(params: { return st.result; } -export function isElementLike(n: SV.BaseNode): n is SV.ElementLike { - return new Set([ - "Component", - "TitleElement", - "SlotElement", - "RegularElement", - "SvelteBody", - "SvelteBoundary", - "SvelteComponent", - "SvelteDocument", - "SvelteElement", - "SvelteFragment", - "SvelteHead", - "SvelteOptionsRaw", - "SvelteSelf", - "SvelteWindow", - "SvelteBoundary", - ]).has(n.type); -} - function print_element_like_attributes({ attributes, tag, diff --git a/packages/svelte-ast-print/src/fragment.test.ts b/packages/svelte-ast-print/src/fragment.test.ts index 56baf1e8..5d575531 100644 --- a/packages/svelte-ast-print/src/fragment.test.ts +++ b/packages/svelte-ast-print/src/fragment.test.ts @@ -7,62 +7,66 @@ import { printFragment } from "./fragment.ts"; describe(printFragment, () => { it("it prints correctly fragment code", ({ expect }) => { const code = ` -

    Shopping list

    -
      - {#each items as item} -
    • {item.name} x {item.qty}
    • - {/each} -
    +

    Shopping list

    - { - const context = canvas.getContext('2d'); +
      + {#each items as item} +
    • {item.name} x {item.qty}
    • + {/each} +
    - $effect(() => { - context.fillStyle = color; - context.fillRect(0, 0, canvas.width, canvas.height); - }); - }} - >
    + { + const context = canvas.getContext('2d'); -
    - - -
    + $effect(() => { + context.fillStyle = color; + context.fillRect(0, 0, canvas.width, canvas.height); + }); + }} +>
    - {#if porridge.temperature > 100} -

    too hot!

    - {:else if 80 > porridge.temperature} -

    too cold!

    - {:else} -

    just right!

    - {/if} +
    + + +
    - {#await promise} - -

    waiting for the promise to resolve...

    - {:then value} - -

    The value is {value}

    - {:catch error} - -

    Something went wrong: {error.message}

    - {/await} +{#if porridge.temperature > 100} +

    too hot!

    +{:else if 80 > porridge.temperature} +

    too cold!

    +{:else} +

    just right!

    +{/if} - {#key value} -
    {value}
    - {/key} +{#await promise} + +

    waiting for the promise to resolve...

    +{:then value} + +

    The value is {value}

    +{:catch error} + +

    Something went wrong: {error.message}

    +{/await} + +{#key value} +
    {value}
    +{/key} `; const node = parse_and_extract(code, "Fragment"); expect(printFragment(node).code).toMatchInlineSnapshot(` - "

    Shopping list

    + " +

    Shopping list

    +
      {#each items as item}
    • {item.name} x {item.qty}
    • {/each}
    + { const context = canvas.getContext('2d'); @@ -70,11 +74,13 @@ describe(printFragment, () => { context.fillStyle = color; context.fillRect(0, 0, canvas.width, canvas.height); }); - }} /> + }}> +
    + {#if porridge.temperature > 100}

    too hot!

    {:else if 80 > porridge.temperature} @@ -82,6 +88,7 @@ describe(printFragment, () => { {:else}

    just right!

    {/if} + {#await promise}

    waiting for the promise to resolve...

    @@ -92,6 +99,7 @@ describe(printFragment, () => {

    Something went wrong: {error.message}

    {/await} + {#key value}
    {value}
    {/key}" @@ -100,17 +108,20 @@ describe(printFragment, () => { it("it prints correctly fragment code with typescript syntax", ({ expect }) => { const code = ` - + - {#snippet template({ children, ...args }: Args, context: StoryContext)} - - {/snippet} +{#snippet template({ children, ...args }: Args, context: StoryContext)} + +{/snippet} `; const node = parse_and_extract(code, "Fragment"); expect(printFragment(node).code).toMatchInlineSnapshot(` - "{#snippet template({ children, ...args }: Args, context: StoryContext)} + " + + + {#snippet template({ children, ...args }: Args, context: StoryContext)} {/snippet}" `); diff --git a/packages/svelte-ast-print/src/fragment.ts b/packages/svelte-ast-print/src/fragment.ts index 09d59414..14b81e19 100644 --- a/packages/svelte-ast-print/src/fragment.ts +++ b/packages/svelte-ast-print/src/fragment.ts @@ -6,8 +6,6 @@ import type { AST as SV } from "svelte/compiler"; import type { PrintOptions } from "./_internal/option.ts"; import { type Result, State } from "./_internal/shared.ts"; -import { isBlock } from "./_internal/template/block.ts"; -import { isElementLike } from "./_internal/template/element-like.ts"; import { printBlock } from "./template/block.ts"; import { printElementLike } from "./template/element-like.ts"; import { printHTMLNode } from "./template/html.ts"; @@ -19,41 +17,16 @@ import { printTag } from "./template/tag.ts"; */ export function printFragment(n: SV.Fragment, opts: Partial = {}): Result { const st = State.get(n, opts); - /** @type {SV.Fragment["nodes"]} */ - let nodes: SV.Fragment["nodes"] = []; - for (const [idx, ch] of n.nodes.entries()) { - const prev = n.nodes[idx - 1]; - const next = n.nodes[idx + 1]; - if (ch.type === "Text") { - if (ch.raw === " " && (prev?.type === "ExpressionTag" || next?.type === "ExpressionTag")) { - nodes.push(ch); - continue; - } - if (!(/^(?: {1,}|\t|\n)*$/.test(ch.raw) || /^(?: {2,}|\t|\n)*$/.test(ch.raw))) { - // biome-ignore format: Prettier - // prettier-ignore - ch.raw = ch.raw - .replace(/^[\n\t]+/, "") - .replace(/[\n\t]+$/, ""); - nodes.push(ch); - } - continue; - } - nodes.push(ch); - } - for (const [idx, ch] of nodes.entries()) { - const prev = nodes[idx - 1]; - if (prev && (isBlock(prev) || prev.type === "Comment" || isElementLike(prev))) { - st.break(); - } - // biome-ignore format: Prettier - // prettier-ignore - switch (ch.type) { + for (const node of n.nodes) { + switch (node.type) { case "AwaitBlock": case "KeyBlock": case "EachBlock": case "IfBlock": - case "SnippetBlock": st.add(printBlock(ch, opts)); break; + case "SnippetBlock": { + st.add(printBlock(node, opts)); + break; + } case "Component": case "RegularElement": case "SlotElement": @@ -67,14 +40,23 @@ export function printFragment(n: SV.Fragment, opts: Partial = {}): case "SvelteOptions": case "SvelteSelf": case "SvelteWindow": - case "TitleElement": st.add(printElementLike(ch, opts)); break; + case "TitleElement": { + st.add(printElementLike(node, opts)); + break; + } case "ConstTag": case "DebugTag": case "ExpressionTag": case "HtmlTag": - case "RenderTag": st.add(printTag(ch, opts));break; + case "RenderTag": { + st.add(printTag(node, opts)); + break; + } case "Comment": - case "Text": st.add(printHTMLNode(ch, opts)); break; + case "Text": { + st.add(printHTMLNode(node, opts)); + break; + } } } return st.result; From f728ca859e5c3990353af9f93713491b5ce7e7c8 Mon Sep 17 00:00:00 2001 From: Mateusz Kadlubowski Date: Sun, 1 Jun 2025 18:15:51 +0800 Subject: [PATCH 12/13] fix indentation issues in root --- .../svelte-ast-print/src/_internal/root.ts | 25 + packages/svelte-ast-print/src/lib.test.ts | 4 +- .../src/template/root.test.ts | 480 +++++++++--------- .../svelte-ast-print/src/template/root.ts | 60 ++- 4 files changed, 314 insertions(+), 255 deletions(-) create mode 100644 packages/svelte-ast-print/src/_internal/root.ts diff --git a/packages/svelte-ast-print/src/_internal/root.ts b/packages/svelte-ast-print/src/_internal/root.ts new file mode 100644 index 00000000..48d30a7e --- /dev/null +++ b/packages/svelte-ast-print/src/_internal/root.ts @@ -0,0 +1,25 @@ +import type { AST as SV } from "svelte/compiler"; + +export function clean_whitespace_in_fragment(n: SV.Fragment): SV.Fragment { + while (n.nodes.length > 0) { + const first = n.nodes[0]; + if (first?.type !== "Text") break; + if (!/^[\s]+$/.test(first.data)) break; + n.nodes.shift(); + } + while (n.nodes.length > 0) { + const last = n.nodes[n.nodes.length - 1]; + if (last?.type !== "Text") break; + if (!/^[\s]+$/.test(last.data)) break; + n.nodes.pop(); + } + if (n.nodes[0]?.type === "Text") { + const content = n.nodes[0].data.replace(/^[\s]+(?=\w)/, ""); + n.nodes[0] = { + ...n.nodes[0], + data: content, + raw: content, + }; + } + return n; +} diff --git a/packages/svelte-ast-print/src/lib.test.ts b/packages/svelte-ast-print/src/lib.test.ts index 545c9ccd..d34e26f8 100644 --- a/packages/svelte-ast-print/src/lib.test.ts +++ b/packages/svelte-ast-print/src/lib.test.ts @@ -88,9 +88,7 @@ describe(printSvelte, () => { console.log('asd'); -
    -

    Asd

    -
    +

    Asd

    + + + + +
    +

    todos

    + +
      + {#each todos as todo} +
    • + + +
    • + {/each} +
    + +

    {remaining} remaining

    + + + +
    + + `; const node = parse_and_extract(code, "Root"); expect(printRoot(node).code).toMatchInlineSnapshot( @@ -103,6 +107,7 @@ describe(printRoot, () => {

    todos

    +
      {#each todos as todo}
    • @@ -111,9 +116,15 @@ describe(printRoot, () => {
    • {/each}
    +

    {remaining} remaining

    - - + + +
    + `; const node = parse_and_extract(code, "StyleSheet"); expect(printCSSStyleSheet(node).code).toMatchInlineSnapshot(` - "" + "" `); }); it("it prints correctly advanced styles", ({ expect }) => { const code = ` - + `; const node = parse_and_extract(code, "StyleSheet"); expect(printCSSStyleSheet(node).code).toMatchInlineSnapshot(` diff --git a/packages/svelte-ast-print/src/template/root.ts b/packages/svelte-ast-print/src/template/root.ts index c379049f..c8224cc5 100644 --- a/packages/svelte-ast-print/src/template/root.ts +++ b/packages/svelte-ast-print/src/template/root.ts @@ -9,6 +9,7 @@ import * as char from "../_internal/char.ts"; import { HTMLClosingTag, HTMLOpeningTag } from "../_internal/html.ts"; import { print_js } from "../_internal/js.ts"; import type { PrintOptions } from "../_internal/option.ts"; +import { clean_whitespace_in_fragment } from "../_internal/root.ts"; import { type Result, State } from "../_internal/shared.ts"; import { printCSSAtrule, printCSSRule } from "../css/rule.ts"; import { printFragment } from "../fragment.ts"; @@ -27,38 +28,57 @@ export function printRoot(n: SV.Root, opts: Partial = {}): Result< st.break(); }; for (const [idx, curr_name] of st.opts.order.entries()) { + const prev_name = st.opts.order[idx - 1]; + const is_prev_fragment = prev_name === "fragment"; + const is_last_fragment_node_whitespace = (() => { + const last_node = n.fragment.nodes[n.fragment.nodes.length]; + if (!last_node || last_node.type !== "Text") return false; + return /^[\s].*$/.test(last_node.data); + })(); + switch (curr_name) { case "options": { if (n.options) { - if (had_previous_node) insert_two_line_breaks(); + if (had_previous_node || (is_prev_fragment && !is_last_fragment_node_whitespace)) { + insert_two_line_breaks(); + } st.add(printSvelteOptions(n.options, opts)); } break; } - case "instance": { - if (n.instance) { - if (had_previous_node) insert_two_line_breaks(); - st.add(printScript(n.instance, opts)); - } - break; - } case "module": { if (n.module) { - if (had_previous_node) insert_two_line_breaks(); + if (had_previous_node || (is_prev_fragment && !is_last_fragment_node_whitespace)) { + insert_two_line_breaks(); + } st.add(printScript(n.module, opts)); } break; } + case "instance": { + if (n.instance) { + if (had_previous_node || (is_prev_fragment && !is_last_fragment_node_whitespace)) { + insert_two_line_breaks(); + } + st.add(printScript(n.instance, opts)); + } + break; + } case "fragment": { - if (n.fragment.nodes.length) { - if (had_previous_node) insert_two_line_breaks(); + if (n.fragment.nodes.length > 0) { + clean_whitespace_in_fragment(n.fragment); + if (had_previous_node || (is_prev_fragment && !is_last_fragment_node_whitespace)) { + insert_two_line_breaks(); + } st.add(printFragment(n.fragment, opts)); } break; } case "css": { if (n.css) { - if (had_previous_node) insert_two_line_breaks(); + if (had_previous_node || (is_prev_fragment && !is_last_fragment_node_whitespace)) { + insert_two_line_breaks(); + } st.add(printCSSStyleSheet(n.css, opts)); } break; @@ -102,11 +122,12 @@ export function printCSSStyleSheet(n: SV.CSS.StyleSheet, opts: Partial 0) { + st.break(+1); + for (const [idx, ch] of n.children.entries()) { + // biome-ignore format: Prettier + // prettier-ignore + switch (ch.type) { case "Atrule": { st.add(printCSSAtrule(ch, opts)); break; @@ -116,9 +137,10 @@ export function printCSSStyleSheet(n: SV.CSS.StyleSheet, opts: Partial Date: Sun, 1 Jun 2025 18:23:57 +0800 Subject: [PATCH 13/13] remove commented out `dedent` import --- internals/test/src/svelte.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/internals/test/src/svelte.ts b/internals/test/src/svelte.ts index bfd8f2f8..ce2c8d6c 100644 --- a/internals/test/src/svelte.ts +++ b/internals/test/src/svelte.ts @@ -1,4 +1,3 @@ -// import dedent from "dedent"; import type * as JS from "estree"; import * as compiler from "svelte/compiler"; import { type Context, walk } from "zimmerframe";