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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on: pull_request

jobs:
test:
name: Test
name: Test (default)
runs-on: ubuntu-latest

steps:
Expand All @@ -19,6 +19,42 @@ jobs:
- name: Run tests
run: npm test

test-svelte-5:
name: Test (Svelte 5)
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
cache: npm
- name: Install dependencies
run: npm ci
- name: Override Svelte version to 5
run: npm install svelte@^5 --no-save
- name: Run tests with Svelte 5
run: npm test

test-prettier-latest:
name: Test (Prettier 3.x latest)
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
cache: npm
- name: Install dependencies
run: npm ci
- name: Override Prettier to latest 3.x
run: npm install prettier@^3 --no-save
- name: Run tests with latest Prettier 3.x
run: npm test

lint:
name: Lint
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# prettier-plugin-svelte changelog

## 3.5.0

- (feat) Svelte 5: print attribute comments

## 3.4.1

- (fix) externalize all prettier imports
Expand Down
88 changes: 86 additions & 2 deletions src/embed.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Doc, doc, FastPath, Options } from 'prettier';
import { Doc, doc, FastPath, Options, util } from 'prettier';
import { getText } from './lib/getText';
import { snippedTagContentAttribute } from './lib/snipTagContent';
import { isBracketSameLine, ParserOptions } from './options';
Expand All @@ -8,6 +8,7 @@ import { isASTNode, printWithPrependedAttributeLine } from './print/helpers';
import {
assignCommentsToNodes,
getAttributeTextValue,
getChildren,
getLeadingComment,
isIgnoreDirective,
isInsideQuotedAttribute,
Expand All @@ -19,7 +20,15 @@ import {
isTypeScript,
printRaw,
} from './print/node-helpers';
import { BaseNode, CommentNode, ElementNode, Node, ScriptNode, StyleNode } from './print/nodes';
import {
ASTNode,
BaseNode,
CommentNode,
ElementNode,
Node,
ScriptNode,
StyleNode,
} from './print/nodes';
import { extractAttributes } from './lib/extractAttributes';
import { base64ToString } from './base64-string';

Expand Down Expand Up @@ -58,6 +67,7 @@ export function embed(path: FastPath, _options: Options) {

if (isASTNode(node)) {
assignCommentsToNodes(node);
attachAttributeComments(node);
if (node.module) {
node.module.type = 'Script';
node.module.attributes = extractAttributes(getText(node.module, options));
Expand Down Expand Up @@ -433,3 +443,77 @@ function printJS(
part.removeParentheses = options.removeParentheses;
part.surroundWithSoftline = options.surroundWithSoftline;
}

/**
* Walk the AST and use `_comments` (stashed by the parser) to attach
* attribute-level comments to their neighbouring attribute nodes via
* Prettier's `util.addLeadingComment` / `util.addTrailingComment`.
*/
function attachAttributeComments(ast: ASTNode): void {
const comments: any[] | undefined = ast._comments;
if (!comments || comments.length === 0) return;

// Index comments by start position for fast lookup
const commentsByStart = new Map<number, any>();
for (const c of comments) {
commentsByStart.set(c.start, c);
}

walkAndAttach(ast.html, commentsByStart);
}

function walkAndAttach(node: Node, commentsByStart: Map<number, any>): void {
if (!node || typeof node !== 'object') return;

if ('attributes' in node && Array.isArray(node.attributes) && node.attributes.length > 0) {
const attrs = node.attributes;

// Check gap before first attribute (between tag name and first attr)
const tagNameEnd = node.start + 2;
attachCommentsInRange(tagNameEnd, attrs[0].start, null, attrs[0], commentsByStart);

// Check gaps between consecutive attributes
for (let i = 0; i < attrs.length - 1; i++) {
attachCommentsInRange(
attrs[i].end,
attrs[i + 1].start,
attrs[i],
attrs[i + 1],
commentsByStart,
);
}
}

// Recurse into children and block branches
for (const child of getChildren(node)) {
walkAndAttach(child, commentsByStart);
}

if ((node.type === 'IfBlock' || node.type === 'EachBlock') && node.else) {
walkAndAttach(node.else, commentsByStart);
}
if (node.type === 'AwaitBlock') {
if (node.pending) walkAndAttach(node.pending, commentsByStart);
if (node.then) walkAndAttach(node.then, commentsByStart);
if (node.catch) walkAndAttach(node.catch, commentsByStart);
}
}

function attachCommentsInRange(
rangeStart: number,
rangeEnd: number,
precedingAttr: any | null,
followingAttr: any | null,
commentsByStart: Map<number, any>,
): void {
for (const [start, comment] of commentsByStart) {
if (start >= rangeStart && comment.end <= rangeEnd) {
if (followingAttr) {
util.addLeadingComment(followingAttr, comment);
} else if (precedingAttr) {
util.addTrailingComment(precedingAttr, comment);
}
commentsByStart.delete(start);
}
}
}
27 changes: 25 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,20 @@ export const parsers: Record<string, Parser> = {
}
}

return <ASTNode>{ ..._parse(text), __isRoot: true };
const root = _parse(text) as Record<string, any>;
(root as ASTNode).__isRoot = true;

// TODO this will need to be done once we switch to the modern parser output:
// Prettier does a sanity check on ast.comments after printing
// to verify all comments were printed. Since the comments array
// includes script/style comments already handled by embedded
// parsers, we stash the full array on _comments and remove
// comments so Prettier doesn't try to process them itself.
// We then manually attach attribute comments in embed().
// (root as ASTNode)._comments = root.comments;
// delete root.comments;

return root;
} catch (err: any) {
if (err.start != null && err.end != null) {
// Prettier expects error objects to have loc.start and loc.end fields.
Expand Down Expand Up @@ -111,8 +124,18 @@ export const printers: Record<string, Printer> = {
'svelte-ast': {
print,
embed,
// @ts-expect-error Prettier's type definitions are wrong
// @ts-expect-error Prettier's type definitions don't include getVisitorKeys
getVisitorKeys,
isBlockComment(comment: any) {
return comment.type === 'Block';
},
printComment(commentPath: any) {
const comment = commentPath.getValue();
if (comment.type === 'Line') {
return '//' + comment.value.replace(/\r$/, '');
}
return '/*' + comment.value + '*/';
},
},
};

Expand Down
4 changes: 3 additions & 1 deletion src/print/nodes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Node as ESTreeNode } from 'estree';
import { Node as ESTreeNode, Comment } from 'estree';

export interface BaseNode {
start: number;
Expand Down Expand Up @@ -394,6 +394,8 @@ export interface ASTNode {
js?: ScriptNode;
instance?: ScriptNode;
module?: ScriptNode;
/** JS-style comments (line and block) stashed from the Svelte parser's comments array */
_comments?: Comment[];
/**
* This is not actually part of the Svelte parser output,
* but we add it afterwards to make sure we can distinguish
Expand Down
5 changes: 3 additions & 2 deletions test/printer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import * as SveltePlugin from '../../src';
const isSvelte5Plus = Number(VERSION.split('.')[0]) >= 5;

let files = readdirSync('test/printer/samples').filter(
(name) => name.endsWith('.html') || name.endsWith('.md'),
(name) =>
name.endsWith('.html') || name.endsWith('.md') || (isSvelte5Plus && name.endsWith('.skip')),
);
const formattingDirsHaveOnly = readdirSync('test/formatting/samples').some((d) =>
d.endsWith('.only'),
Expand All @@ -21,7 +22,7 @@ if (process.env.CI && hasOnly) {
}

for (const file of files) {
const ending = file.split('.').pop();
const ending = file.split('.').at(file.endsWith('.skip') ? -2 : -1);
const input = readFileSync(`test/printer/samples/${file}`, 'utf-8').replace(/\r?\n/g, '\n');
const options = readOptions(
`test/printer/samples/${file.replace('.only', '').replace(`.${ending}`, '.options.json')}`,
Expand Down
126 changes: 126 additions & 0 deletions test/printer/samples/attribute-comment.html.skip
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<script>
// comment in script
let foo = true;
let items = [1, 2, 3];
let promise = Promise.resolve("done!");
let keyValue = "unique-key";
</script>

<div
// hello
attribute="value"
/* world */
another-attribute="value"
>
this works
<span
// span comment
title="nested"
/* after title */
data-extra="info"
>
Nested span works
</span>
</div>

<Component
// hello
attribute="value"
/* world */
another-attribute="value"
>
this works
<div
// nested hello
class="nested"
/* another nested */
id="nested"
>
Component with nested div
</div>
</Component>

{#if true}
<div
// hello
attribute="value"
/* world */
another-attribute="value"
>
this works
</div>
{:else}
<Component
// hello
attribute="value"
/* world */
another-attribute="value"
>
this works
</Component>
{/if}

{#await promise}
<div
// awaiting
class="promise-pending"
/* waiting */
data-state="pending"
>
waiting for promise...
</div>
{:then result}
<span
// promise resolved
class="promise-done"
/* after class */
data-state="done"
>
Promise resolved: {result}
</span>
{:catch error}
<div
// promise error
class="promise-error"
/* error style */
data-state="error"
>
Error: {error}
</div>
{/await}

{#each items as item, idx (item)}
<div
// each block
class="item-class"
/* another in each */
data-index={idx}
>
Item: {item}
<span
// nested each
foo="bar"
/* world */
baz="qux">Nested in each</span
>
</div>
{/each}

{#key keyValue}
<div
// key block
foo="bar"
/* key comment */
bar="baz"
>
Keyed content: {keyValue}
<div
// key-nested
x="y"
/* nested key block */
z="w"
>
Nested in key
</div>
</div>
{/key}
2 changes: 1 addition & 1 deletion test/printer/samples/svelte-html-element.html.skip
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<svelte:html lang={language} />
<!--<svelte:html lang={language} />-->