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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cold-mangos-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte-ast-print": patch
---

fix: formatting for text nodes
1 change: 0 additions & 1 deletion internals/test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
},
"dependencies": {
"@types/estree": "catalog:",
"dedent": "^1.5.0",
"svelte": "catalog:",
"zimmerframe": "catalog:"
}
Expand Down
5 changes: 2 additions & 3 deletions internals/test/src/svelte.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import dedent from "dedent";
import type * as JS from "estree";
import * as compiler from "svelte/compiler";
import { type Context, walk } from "zimmerframe";

type Node = compiler.AST.SvelteNode | compiler.AST.Root | compiler.AST.Script | JS.Node;

export function parse_and_extract<N extends Node>(code: string, name: N["type"]): N {
const parsed = parse<N>(dedent(code));
const parsed = parse<N>(code);
return extract(parsed, name);
}

function parse<N extends Node>(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<N extends Node>(parsed: N, name: N["type"]): N {
Expand Down
12 changes: 0 additions & 12 deletions packages/svelte-ast-print/src/_internal/fragment.ts

This file was deleted.

25 changes: 25 additions & 0 deletions packages/svelte-ast-print/src/_internal/root.ts
Original file line number Diff line number Diff line change
@@ -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;
}
1 change: 1 addition & 0 deletions packages/svelte-ast-print/src/_internal/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export function isSvelteOnlyNode(n: JS.BaseNode | SV.BaseNode): n is SvelteOnlyN
"Root",
"Script",
// Tag
"AttachTag",
"ExpressionTag",
"HtmlTag",
"ConstTag",
Expand Down
11 changes: 0 additions & 11 deletions packages/svelte-ast-print/src/_internal/template/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
86 changes: 7 additions & 79 deletions packages/svelte-ast-print/src/_internal/template/element-like.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -123,12 +81,7 @@ export function print_maybe_self_closing_el<N extends SV.ElementLike>(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;
}
Expand Down Expand Up @@ -175,36 +128,11 @@ export function print_non_self_closing_el<N extends SV.ElementLike>(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;
}

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,
Expand Down
111 changes: 61 additions & 50 deletions packages/svelte-ast-print/src/fragment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,81 +7,88 @@ import { printFragment } from "./fragment.ts";
describe(printFragment, () => {
it("it prints correctly fragment code", ({ expect }) => {
const code = `
<h1 {@attach tooltip(content)}>Shopping list</h1>
<ul>
{#each items as item}
<li>{item.name} x {item.qty}</li>
{/each}
</ul>
<h1 {@attach tooltip(content)}>Shopping list</h1>

<canvas
width={32}
height={32}
{@attach (canvas) => {
const context = canvas.getContext('2d');
<ul>
{#each items as item}
<li>{item.name} x {item.qty}</li>
{/each}
</ul>

$effect(() => {
context.fillStyle = color;
context.fillRect(0, 0, canvas.width, canvas.height);
});
}}
></canvas>
<canvas
width={32}
height={32}
{@attach (canvas) => {
const context = canvas.getContext('2d');

<div class="mb-6">
<Label for="large-input" class="block mb-2">Large input</Label>
<Input id="large-input" size="lg" placeholder="Large input" />
</div>
$effect(() => {
context.fillStyle = color;
context.fillRect(0, 0, canvas.width, canvas.height);
});
}}
></canvas>

{#if porridge.temperature > 100}
<p>too hot!</p>
{:else if 80 > porridge.temperature}
<p>too cold!</p>
{:else}
<p>just right!</p>
{/if}
<div class="mb-6">
<Label for="large-input" class="block mb-2">Large input</Label>
<Input id="large-input" size="lg" placeholder="Large input" />
</div>

{#await promise}
<!-- promise is pending -->
<p>waiting for the promise to resolve...</p>
{:then value}
<!-- promise was fulfilled or not a Promise -->
<p>The value is {value}</p>
{:catch error}
<!-- promise was rejected -->
<p>Something went wrong: {error.message}</p>
{/await}
{#if porridge.temperature > 100}
<p>too hot!</p>
{:else if 80 > porridge.temperature}
<p>too cold!</p>
{:else}
<p>just right!</p>
{/if}

{#key value}
<div transition:fade>{value}</div>
{/key}
{#await promise}
<!-- promise is pending -->
<p>waiting for the promise to resolve...</p>
{:then value}
<!-- promise was fulfilled or not a Promise -->
<p>The value is {value}</p>
{:catch error}
<!-- promise was rejected -->
<p>Something went wrong: {error.message}</p>
{/await}

{#key value}
<div transition:fade>{value}</div>
{/key}
`;
const node = parse_and_extract<AST.Fragment>(code, "Fragment");
expect(printFragment(node).code).toMatchInlineSnapshot(`
"<h1 {@attach tooltip(content)}>Shopping list</h1>
"
<h1 {@attach tooltip(content)}>Shopping list</h1>

<ul>
{#each items as item}
<li>{item.name} x {item.qty}</li>
{/each}
</ul>

<canvas width={32} height={32} {@attach (canvas) => {
const context = canvas.getContext('2d');

$effect(() => {
context.fillStyle = color;
context.fillRect(0, 0, canvas.width, canvas.height);
});
}} />
}}></canvas>

<div class="mb-6">
<Label for="large-input" class="block mb-2">Large input</Label>
<Input id="large-input" size="lg" placeholder="Large input" />
</div>

{#if porridge.temperature > 100}
<p>too hot!</p>
{:else if 80 > porridge.temperature}
<p>too cold!</p>
{:else}
<p>just right!</p>
{/if}

{#await promise}
<!-- promise is pending -->
<p>waiting for the promise to resolve...</p>
Expand All @@ -92,6 +99,7 @@ describe(printFragment, () => {
<!-- promise was rejected -->
<p>Something went wrong: {error.message}</p>
{/await}

{#key value}
<div transition:fade>{value}</div>
{/key}"
Expand All @@ -100,17 +108,20 @@ describe(printFragment, () => {

it("it prints correctly fragment code with typescript syntax", ({ expect }) => {
const code = `
<script lang="ts">
//
</script>
<script lang="ts">
//
</script>

{#snippet template({ children, ...args }: Args<typeof Story>, context: StoryContext<typeof Story>)}
<Button {...args}>{children}</Button>
{/snippet}
{#snippet template({ children, ...args }: Args<typeof Story>, context: StoryContext<typeof Story>)}
<Button {...args}>{children}</Button>
{/snippet}
`;
const node = parse_and_extract<AST.Fragment>(code, "Fragment");
expect(printFragment(node).code).toMatchInlineSnapshot(`
"{#snippet template({ children, ...args }: Args<typeof Story>, context: StoryContext<typeof Story>)}
"


{#snippet template({ children, ...args }: Args<typeof Story>, context: StoryContext<typeof Story>)}
<Button {...args}>{children}</Button>
{/snippet}"
`);
Expand Down
Loading
Loading