diff --git a/.changeset/angry-suns-call.md b/.changeset/angry-suns-call.md new file mode 100644 index 0000000000..c927c1df8f --- /dev/null +++ b/.changeset/angry-suns-call.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: allow comments in tags diff --git a/packages/svelte/src/compiler/legacy.js b/packages/svelte/src/compiler/legacy.js index b1bbcfcf74..459858fe13 100644 --- a/packages/svelte/src/compiler/legacy.js +++ b/packages/svelte/src/compiler/legacy.js @@ -101,7 +101,12 @@ export function convert(source, ast) { }, instance, module, - css: ast.css ? visit(ast.css) : undefined + css: ast.css ? visit(ast.css) : undefined, + // put it on _comments not comments because the latter is checked by prettier and then fails + // if we don't adjust stuff accordingly in our prettier plugin, and so it would be kind of an + // indirect breaking change for people updating their Svelte version but not their prettier plugin version. + // We can keep it as comments for the modern AST because the modern AST is not used in the plugin yet. + _comments: ast.comments?.length > 0 ? ast.comments : undefined }; }, AnimateDirective(node) { diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index 91d072c1f3..170edf7b8f 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -506,6 +506,15 @@ function read_static_attribute(parser) { * @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag | null} */ function read_attribute(parser) { + /** @type {AST.JSComment | null} */ + // eslint-disable-next-line no-useless-assignment -- it is, in fact, eslint that is useless + let comment = null; + + while ((comment = read_comment(parser))) { + parser.root.comments.push(comment); + parser.allow_whitespace(); + } + const start = parser.index; if (parser.eat('{')) { @@ -702,6 +711,50 @@ function read_attribute(parser) { return create_attribute(tag.name, tag.loc, start, end, value); } +/** + * @param {Parser} parser + * @returns {AST.JSComment | null} + */ +function read_comment(parser) { + const start = parser.index; + + if (parser.eat('//')) { + const value = parser.read_until(/\n/); + const end = parser.index; + + return { + type: 'Line', + start, + end, + value, + loc: { + start: locator(start), + end: locator(end) + } + }; + } + + if (parser.eat('/*')) { + const value = parser.read_until(/\*\//); + + parser.eat('*/'); + const end = parser.index; + + return { + type: 'Block', + start, + end, + value, + loc: { + start: locator(start), + end: locator(end) + } + }; + } + + return null; +} + /** * @param {string} name * @returns {any} diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index 5543b6b581..160aeb4345 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -18,15 +18,17 @@ const LINE_BREAK_THRESHOLD = 50; * @param {import('./types.js').Options | undefined} options */ export function print(ast, options = undefined) { + const comments = (ast.type === 'Root' && ast.comments) || []; + return esrap.print( ast, /** @type {Visitors} */ ({ ...ts({ - comments: ast.type === 'Root' ? ast.comments : [], + comments, getLeadingComments: options?.getLeadingComments, getTrailingComments: options?.getTrailingComments }), - ...svelte_visitors, + ...svelte_visitors(comments), ...css_visitors }) ); @@ -57,35 +59,72 @@ function block(context, node, allow_inline = false) { } /** + * @param {AST.BaseNode} node * @param {AST.BaseElement['attributes']} attributes * @param {Context} context + * @param {AST.JSComment[]} comments * @returns {boolean} true if attributes were formatted on multiple lines */ -function attributes(attributes, context) { +function attributes(node, attributes, context, comments) { if (attributes.length === 0) { return false; } - // Measure total width of all attributes when rendered inline - const child_context = context.new(); + let length = -1; - for (const attribute of attributes) { - child_context.write(' '); - child_context.visit(attribute); + let comment_index = comments.findIndex((comment) => comment.start > node.start); + + if (comment_index === -1) { + comment_index = comments.length; } - const multiline = child_context.measure() > LINE_BREAK_THRESHOLD; + const separator = context.new(); + + const children = attributes.map((attribute) => { + const child_context = context.new(); + + while (comment_index < comments.length) { + const comment = comments[comment_index]; + + if (comment.start < attribute.start) { + if (comment.type === 'Line') { + child_context.write('//' + comment.value); + child_context.newline(); + } else { + child_context.write('/*' + comment.value + '*/'); // TODO match indentation? + child_context.append(separator); + } + + comment_index += 1; + } else { + break; + } + } + + child_context.visit(attribute); + + length += child_context.measure() + 1; + + return child_context; + }); + + let multiline = context.multiline || length > LINE_BREAK_THRESHOLD; if (multiline) { + separator.newline(); context.indent(); - for (const attribute of attributes) { + for (const child of children) { context.newline(); - context.visit(attribute); + context.append(child); } context.dedent(); context.newline(); } else { - context.append(child_context); + separator.write(' '); + for (const child of children) { + context.write(' '); + context.append(child); + } } return multiline; @@ -94,8 +133,9 @@ function attributes(attributes, context) { /** * @param {AST.BaseElement} node * @param {Context} context + * @param {AST.JSComment[]} comments */ -function base_element(node, context) { +function base_element(node, context, comments) { const child_context = context.new(); child_context.write('<' + node.name); @@ -111,7 +151,7 @@ function base_element(node, context) { child_context.write('}'); } - const multiline_attributes = attributes(node.attributes, child_context); + const multiline_attributes = attributes(node, node.attributes, child_context, comments); const is_doctype_node = node.name.toLowerCase() === '!doctype'; const is_self_closing = is_void(node.name) || (node.type === 'Component' && node.fragment.nodes.length === 0); @@ -284,8 +324,11 @@ const css_visitors = { } }; -/** @type {Visitors} */ -const svelte_visitors = { +/** + * @param {AST.JSComment[]} comments + * @returns {Visitors} + */ +const svelte_visitors = (comments) => ({ Root(node, context) { if (node.options) { context.write(''); block(context, node.content); context.write(''); @@ -545,7 +588,7 @@ const svelte_visitors = { }, Component(node, context) { - base_element(node, context); + base_element(node, context, comments); }, ConstTag(node, context) { @@ -681,7 +724,7 @@ const svelte_visitors = { }, RegularElement(node, context) { - base_element(node, context); + base_element(node, context, comments); }, RenderTag(node, context) { @@ -691,7 +734,7 @@ const svelte_visitors = { }, SlotElement(node, context) { - base_element(node, context); + base_element(node, context, comments); }, SnippetBlock(node, context) { @@ -747,7 +790,7 @@ const svelte_visitors = { StyleSheet(node, context) { context.write(''); if (node.children.length > 0) { @@ -774,7 +817,7 @@ const svelte_visitors = { }, SvelteBoundary(node, context) { - base_element(node, context); + base_element(node, context, comments); }, SvelteComponent(node, context) { @@ -783,7 +826,7 @@ const svelte_visitors = { context.write(' this={'); context.visit(node.expression); context.write('}'); - attributes(node.attributes, context); + attributes(node, node.attributes, context, comments); if (node.fragment && node.fragment.nodes.length > 0) { context.write('>'); block(context, node.fragment, true); @@ -794,7 +837,7 @@ const svelte_visitors = { }, SvelteDocument(node, context) { - base_element(node, context); + base_element(node, context, comments); }, SvelteElement(node, context) { @@ -803,7 +846,7 @@ const svelte_visitors = { context.write('this={'); context.visit(node.tag); context.write('}'); - attributes(node.attributes, context); + attributes(node, node.attributes, context, comments); if (node.fragment && node.fragment.nodes.length > 0) { context.write('>'); @@ -815,19 +858,19 @@ const svelte_visitors = { }, SvelteFragment(node, context) { - base_element(node, context); + base_element(node, context, comments); }, SvelteHead(node, context) { - base_element(node, context); + base_element(node, context, comments); }, SvelteSelf(node, context) { - base_element(node, context); + base_element(node, context, comments); }, SvelteWindow(node, context) { - base_element(node, context); + base_element(node, context, comments); }, Text(node, context) { @@ -835,7 +878,7 @@ const svelte_visitors = { }, TitleElement(node, context) { - base_element(node, context); + base_element(node, context, comments); }, TransitionDirective(node, context) { @@ -865,4 +908,4 @@ const svelte_visitors = { context.write('}'); } } -}; +}); diff --git a/packages/svelte/tests/parser-legacy/samples/javascript-comments/output.json b/packages/svelte/tests/parser-legacy/samples/javascript-comments/output.json index 3775c48228..4d7cb2397f 100644 --- a/packages/svelte/tests/parser-legacy/samples/javascript-comments/output.json +++ b/packages/svelte/tests/parser-legacy/samples/javascript-comments/output.json @@ -880,5 +880,423 @@ ], "sourceType": "module" } - } + }, + "_comments": [ + { + "type": "Line", + "value": " a leading comment", + "start": 10, + "end": 30, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 21 + } + } + }, + { + "type": "Line", + "value": " a trailing comment", + "start": 45, + "end": 66, + "loc": { + "start": { + "line": 3, + "column": 14 + }, + "end": { + "line": 3, + "column": 35 + } + } + }, + { + "type": "Block", + "value": "* a comment ", + "start": 83, + "end": 99, + "loc": { + "start": { + "line": 6, + "column": 1 + }, + "end": { + "line": 6, + "column": 17 + } + } + }, + { + "type": "Line", + "value": " trailing", + "start": 125, + "end": 136, + "loc": { + "start": { + "line": 8, + "column": 7 + }, + "end": { + "line": 8, + "column": 18 + } + } + }, + { + "type": "Block", + "value": " leading comment 1 ", + "start": 139, + "end": 162, + "loc": { + "start": { + "line": 9, + "column": 2 + }, + "end": { + "line": 9, + "column": 25 + } + } + }, + { + "type": "Block", + "value": " leading comment 2 ", + "start": 165, + "end": 188, + "loc": { + "start": { + "line": 10, + "column": 2 + }, + "end": { + "line": 10, + "column": 25 + } + } + }, + { + "type": "Block", + "value": " leading comment 3 ", + "start": 191, + "end": 214, + "loc": { + "start": { + "line": 11, + "column": 2 + }, + "end": { + "line": 11, + "column": 25 + } + } + }, + { + "type": "Block", + "value": " trailing comment 1 ", + "start": 224, + "end": 248, + "loc": { + "start": { + "line": 13, + "column": 2 + }, + "end": { + "line": 13, + "column": 26 + } + } + }, + { + "type": "Block", + "value": " trailing comment 2 ", + "start": 251, + "end": 275, + "loc": { + "start": { + "line": 14, + "column": 2 + }, + "end": { + "line": 14, + "column": 26 + } + } + }, + { + "type": "Block", + "value": " trailing comment 3 ", + "start": 278, + "end": 302, + "loc": { + "start": { + "line": 15, + "column": 2 + }, + "end": { + "line": 15, + "column": 26 + } + } + }, + { + "type": "Line", + "value": " leading comment 1", + "start": 326, + "end": 346, + "loc": { + "start": { + "line": 19, + "column": 2 + }, + "end": { + "line": 19, + "column": 22 + } + } + }, + { + "type": "Line", + "value": " leading comment 2", + "start": 349, + "end": 369, + "loc": { + "start": { + "line": 20, + "column": 2 + }, + "end": { + "line": 20, + "column": 22 + } + } + }, + { + "type": "Line", + "value": " trailing comment 1", + "start": 375, + "end": 396, + "loc": { + "start": { + "line": 21, + "column": 5 + }, + "end": { + "line": 21, + "column": 26 + } + } + }, + { + "type": "Block", + "value": " trailing comment 2 ", + "start": 399, + "end": 423, + "loc": { + "start": { + "line": 22, + "column": 2 + }, + "end": { + "line": 22, + "column": 26 + } + } + }, + { + "type": "Line", + "value": " leading comment 1", + "start": 449, + "end": 469, + "loc": { + "start": { + "line": 26, + "column": 2 + }, + "end": { + "line": 26, + "column": 22 + } + } + }, + { + "type": "Line", + "value": " leading comment 2", + "start": 472, + "end": 492, + "loc": { + "start": { + "line": 27, + "column": 2 + }, + "end": { + "line": 27, + "column": 22 + } + } + }, + { + "type": "Line", + "value": " trailing comment 1", + "start": 501, + "end": 522, + "loc": { + "start": { + "line": 28, + "column": 8 + }, + "end": { + "line": 28, + "column": 29 + } + } + }, + { + "type": "Block", + "value": " trailing comment 2 ", + "start": 525, + "end": 549, + "loc": { + "start": { + "line": 29, + "column": 2 + }, + "end": { + "line": 29, + "column": 26 + } + } + }, + { + "type": "Line", + "value": " comment", + "start": 584, + "end": 594, + "loc": { + "start": { + "line": 34, + "column": 11 + }, + "end": { + "line": 34, + "column": 21 + } + } + }, + { + "type": "Block", + "value": " another comment ", + "start": 606, + "end": 627, + "loc": { + "start": { + "line": 36, + "column": 2 + }, + "end": { + "line": 36, + "column": 23 + } + } + }, + { + "type": "Line", + "value": " a trailing comment", + "start": 636, + "end": 657, + "loc": { + "start": { + "line": 37, + "column": 8 + }, + "end": { + "line": 37, + "column": 29 + } + } + }, + { + "type": "Block", + "value": " trailing block comment ", + "start": 660, + "end": 688, + "loc": { + "start": { + "line": 38, + "column": 2 + }, + "end": { + "line": 38, + "column": 30 + } + } + }, + { + "type": "Block", + "value": " leading block comment ", + "start": 696, + "end": 723, + "loc": { + "start": { + "line": 41, + "column": 1 + }, + "end": { + "line": 41, + "column": 28 + } + } + }, + { + "type": "Line", + "value": " leading line comment", + "start": 739, + "end": 762, + "loc": { + "start": { + "line": 43, + "column": 2 + }, + "end": { + "line": 43, + "column": 25 + } + } + }, + { + "type": "Line", + "value": " trailing line comment", + "start": 770, + "end": 794, + "loc": { + "start": { + "line": 44, + "column": 7 + }, + "end": { + "line": 44, + "column": 31 + } + } + }, + { + "type": "Block", + "value": " trailing block comment ", + "start": 796, + "end": 824, + "loc": { + "start": { + "line": 45, + "column": 1 + }, + "end": { + "line": 45, + "column": 29 + } + } + } + ] } diff --git a/packages/svelte/tests/parser-legacy/samples/script-comment-only/output.json b/packages/svelte/tests/parser-legacy/samples/script-comment-only/output.json index 0f7d0b8465..8ec0e0cd65 100644 --- a/packages/svelte/tests/parser-legacy/samples/script-comment-only/output.json +++ b/packages/svelte/tests/parser-legacy/samples/script-comment-only/output.json @@ -51,5 +51,23 @@ } ] } - } + }, + "_comments": [ + { + "type": "Line", + "value": " TODO write some code", + "start": 10, + "end": 33, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 24 + } + } + } + ] } diff --git a/packages/svelte/tests/parser-modern/samples/comment-in-tag/input.svelte b/packages/svelte/tests/parser-modern/samples/comment-in-tag/input.svelte new file mode 100644 index 0000000000..3070aeb178 --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/comment-in-tag/input.svelte @@ -0,0 +1,22 @@ +
+ + diff --git a/packages/svelte/tests/parser-modern/samples/comment-in-tag/output.json b/packages/svelte/tests/parser-modern/samples/comment-in-tag/output.json new file mode 100644 index 0000000000..45a57d3049 --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/comment-in-tag/output.json @@ -0,0 +1,286 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 261, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [ + { + "type": "RegularElement", + "start": 0, + "end": 199, + "name": "div", + "name_loc": { + "start": { + "line": 1, + "column": 1, + "character": 1 + }, + "end": { + "line": 1, + "column": 4, + "character": 4 + } + }, + "attributes": [ + { + "type": "Attribute", + "start": 6, + "end": 18, + "name": "data-one", + "name_loc": { + "start": { + "line": 2, + "column": 1, + "character": 6 + }, + "end": { + "line": 2, + "column": 9, + "character": 14 + } + }, + "value": [ + { + "start": 16, + "end": 17, + "type": "Text", + "raw": "1", + "data": "1" + } + ] + }, + { + "type": "Attribute", + "start": 47, + "end": 59, + "name": "data-two", + "name_loc": { + "start": { + "line": 4, + "column": 1, + "character": 47 + }, + "end": { + "line": 4, + "column": 9, + "character": 55 + } + }, + "value": [ + { + "start": 57, + "end": 58, + "type": "Text", + "raw": "2", + "data": "2" + } + ] + }, + { + "type": "Attribute", + "start": 177, + "end": 191, + "name": "data-three", + "name_loc": { + "start": { + "line": 19, + "column": 1, + "character": 177 + }, + "end": { + "line": 19, + "column": 11, + "character": 187 + } + }, + "value": [ + { + "start": 189, + "end": 190, + "type": "Text", + "raw": "3", + "data": "3" + } + ] + } + ], + "fragment": { + "type": "Fragment", + "nodes": [] + } + }, + { + "type": "Text", + "start": 199, + "end": 201, + "raw": "\n\n", + "data": "\n\n" + }, + { + "type": "RegularElement", + "start": 201, + "end": 261, + "name": "span", + "name_loc": { + "start": { + "line": 22, + "column": 1, + "character": 202 + }, + "end": { + "line": 22, + "column": 5, + "character": 206 + } + }, + "attributes": [ + { + "type": "Attribute", + "start": 241, + "end": 253, + "name": "data-one", + "name_loc": { + "start": { + "line": 22, + "column": 40, + "character": 241 + }, + "end": { + "line": 22, + "column": 48, + "character": 249 + } + }, + "value": [ + { + "start": 251, + "end": 252, + "type": "Text", + "raw": "1", + "data": "1" + } + ] + } + ], + "fragment": { + "type": "Fragment", + "nodes": [] + } + } + ] + }, + "options": null, + "comments": [ + { + "type": "Line", + "start": 20, + "end": 45, + "value": " this is a line comment", + "loc": { + "start": { + "line": 3, + "column": 1, + "character": 20 + }, + "end": { + "line": 3, + "column": 26, + "character": 45 + } + } + }, + { + "type": "Block", + "start": 61, + "end": 123, + "value": " this is a\n\t\tm\n\t\tu\n\t\tl\n\t\tt\n\t\ti\n\t\tl\n\t\ti\n\t\tn\n\t\te\n\t\tcomment\n\t", + "loc": { + "start": { + "line": 5, + "column": 1, + "character": 61 + }, + "end": { + "line": 16, + "column": 3, + "character": 123 + } + } + }, + { + "type": "Line", + "start": 125, + "end": 156, + "value": " oh look another line comment", + "loc": { + "start": { + "line": 17, + "column": 1, + "character": 125 + }, + "end": { + "line": 17, + "column": 32, + "character": 156 + } + } + }, + { + "type": "Line", + "start": 158, + "end": 175, + "value": " (two, in fact)", + "loc": { + "start": { + "line": 18, + "column": 1, + "character": 158 + }, + "end": { + "line": 18, + "column": 18, + "character": 175 + } + } + }, + { + "type": "Block", + "start": 207, + "end": 219, + "value": " inline ", + "loc": { + "start": { + "line": 22, + "column": 6, + "character": 207 + }, + "end": { + "line": 22, + "column": 18, + "character": 219 + } + } + }, + { + "type": "Block", + "start": 220, + "end": 240, + "value": " another inline ", + "loc": { + "start": { + "line": 22, + "column": 19, + "character": 220 + }, + "end": { + "line": 22, + "column": 39, + "character": 240 + } + } + } + ] +} diff --git a/packages/svelte/tests/parser-modern/test.ts b/packages/svelte/tests/parser-modern/test.ts index fb762e89e1..540a393c38 100644 --- a/packages/svelte/tests/parser-modern/test.ts +++ b/packages/svelte/tests/parser-modern/test.ts @@ -25,8 +25,6 @@ const { test, run } = suite(async (config, cwd) => { ) ); - delete actual.comments; - // run `UPDATE_SNAPSHOTS=true pnpm test parser` to update parser tests if (process.env.UPDATE_SNAPSHOTS) { fs.writeFileSync(`${cwd}/output.json`, JSON.stringify(actual, null, '\t') + '\n'); @@ -34,6 +32,11 @@ const { test, run } = suite(async (config, cwd) => { fs.writeFileSync(`${cwd}/_actual.json`, JSON.stringify(actual, null, '\t')); const expected = try_load_json(`${cwd}/output.json`); + + if (!expected.comments) { + delete actual.comments; + } + assert.deepEqual(actual, expected); } @@ -50,7 +53,9 @@ const { test, run } = suite(async (config, cwd) => { fs.writeFileSync(`${cwd}/_actual.svelte`, printed.code); - delete reparsed.comments; + if (!actual.comments) { + delete reparsed.comments; + } assert.deepEqual(clean(actual), clean(reparsed)); }