From af720efb42e4ed524dd8f641fd9ef6b11ba16844 Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Fri, 26 Jun 2026 19:43:04 +0800 Subject: [PATCH] Angular: Support comments in start tag (#19465) --- changelog_unreleased/angular/19465.md | 31 ++++ src/language-html/parse/parse-options.js | 2 + src/language-html/parse/parsers.js | 1 + src/language-html/parse/postprocess.js | 13 +- src/language-html/print-preprocess.js | 2 + src/language-html/print/start-tag-comment.js | 19 +++ src/language-html/print/tag.js | 57 ++++--- src/language-html/printer-html.js | 5 + src/language-html/visitor-keys.evaluate.js | 3 +- .../__snapshots__/format.test.js.snap | 144 ++++++++++++++++++ .../angular/start-tag-comments/format.test.js | 1 + .../start-tag-comments/merge-into-text.html | 11 ++ .../start-tag-comments.html | 45 ++++++ .../in-element-comments/format.test.js | 8 - .../__snapshots__/format.test.js.snap | 17 --- .../block-comment.html | 0 .../start-tag-comments/format.test.js | 1 + .../line-comment.html | 0 tests/unit/__snapshots__/visitor-keys.js.snap | 2 + 19 files changed, 315 insertions(+), 47 deletions(-) create mode 100644 changelog_unreleased/angular/19465.md create mode 100644 src/language-html/print/start-tag-comment.js create mode 100644 tests/format/angular/start-tag-comments/__snapshots__/format.test.js.snap create mode 100644 tests/format/angular/start-tag-comments/format.test.js create mode 100644 tests/format/angular/start-tag-comments/merge-into-text.html create mode 100644 tests/format/angular/start-tag-comments/start-tag-comments.html delete mode 100644 tests/format/html/_errors_/in-element-comments/format.test.js rename tests/format/html/_errors_/{in-element-comments => start-tag-comments}/__snapshots__/format.test.js.snap (80%) rename tests/format/html/_errors_/{in-element-comments => start-tag-comments}/block-comment.html (100%) create mode 100644 tests/format/html/_errors_/start-tag-comments/format.test.js rename tests/format/html/_errors_/{in-element-comments => start-tag-comments}/line-comment.html (100%) diff --git a/changelog_unreleased/angular/19465.md b/changelog_unreleased/angular/19465.md new file mode 100644 index 000000000..fd6685017 --- /dev/null +++ b/changelog_unreleased/angular/19465.md @@ -0,0 +1,31 @@ +#### Support comments in html element (#19465 by @fisker) + + +```html + +
+ + +
+ + +
+``` diff --git a/src/language-html/parse/parse-options.js b/src/language-html/parse/parse-options.js index 9a0a74af7..15ca553b1 100644 --- a/src/language-html/parse/parse-options.js +++ b/src/language-html/parse/parse-options.js @@ -51,6 +51,7 @@ function toAngularHtmlParserParseOptions(parseOptions) { const { canSelfClose, allowHtmComponentClosingTags, + allowStartTagComments, isTagNameCaseSensitive, shouldParseAsRawText, tokenizeAngularBlocks, @@ -60,6 +61,7 @@ function toAngularHtmlParserParseOptions(parseOptions) { return { canSelfClose, allowHtmComponentClosingTags, + allowStartTagComments, isTagNameCaseSensitive, getTagContentType: shouldParseAsRawText ? (...args) => diff --git a/src/language-html/parse/parsers.js b/src/language-html/parse/parsers.js index f1b6293bb..16afabb66 100644 --- a/src/language-html/parse/parsers.js +++ b/src/language-html/parse/parsers.js @@ -39,6 +39,7 @@ const angularParser = createParser({ name: "angular", tokenizeAngularBlocks: true, tokenizeAngularLetDeclaration: true, + allowStartTagComments: true, }); // Vue.js diff --git a/src/language-html/parse/postprocess.js b/src/language-html/parse/postprocess.js index f89e01c46..495e6ff50 100644 --- a/src/language-html/parse/postprocess.js +++ b/src/language-html/parse/postprocess.js @@ -36,6 +36,8 @@ class Visitor extends RecursiveVisitor { } function postprocess(rawAst, frontMatter, parseOptions, parseSubHtml) { + const isAngular = parseOptions.name === "angular"; + visitAll(new Visitor(), rawAst.children, { parseOptions }); if (frontMatter) { @@ -53,11 +55,16 @@ function postprocess(rawAst, frontMatter, parseOptions, parseSubHtml) { if (ieConditionalComment) { node.parent.replaceChild(node, ieConditionalComment); } + } else if (isAngular && node.kind === "element" && node.comments) { + node.startTagComments = node.comments; + delete node.comments; } - normalizeAngularControlFlowBlock(node); - normalizeAngularLetDeclaration(node); - normalizeAngularIcuExpression(node); + if (isAngular) { + normalizeAngularControlFlowBlock(node); + normalizeAngularLetDeclaration(node); + normalizeAngularIcuExpression(node); + } }); return ast; diff --git a/src/language-html/print-preprocess.js b/src/language-html/print-preprocess.js index 49e7ed99d..487b012b8 100644 --- a/src/language-html/print-preprocess.js +++ b/src/language-html/print-preprocess.js @@ -1,5 +1,6 @@ import { ParseSourceSpan } from "angular-html-parser"; import htmlWhitespace from "../utilities/html-whitespace.js"; +import isNonEmptyArray from "../utilities/is-non-empty-array.js"; import { canHaveInterpolation, getLeadingAndTrailingHtmlWhitespace, @@ -141,6 +142,7 @@ function mergeSimpleElementIntoText(ast /* , options */) { const isSimpleElement = (node) => node.kind === "element" && node.attrs.length === 0 && + !isNonEmptyArray(node.startTagComments) && node.children.length === 1 && node.firstChild.kind === "text" && !htmlWhitespace.hasWhitespaceCharacter(node.children[0].value) && diff --git a/src/language-html/print/start-tag-comment.js b/src/language-html/print/start-tag-comment.js new file mode 100644 index 000000000..ed43fae86 --- /dev/null +++ b/src/language-html/print/start-tag-comment.js @@ -0,0 +1,19 @@ +import { replaceEndOfLine } from "../../document/index.js"; + +// Based on JS comment print src/language-js/print/comment.js + +function printStartTagComment(path) { + const { + node: { value, type }, + } = path; + + if (type === "single") { + return `//${value.trimEnd()}`; + } + + // TODO: support indentable block comment + + return ["/*", replaceEndOfLine(value), "*/"]; +} + +export { printStartTagComment }; diff --git a/src/language-html/print/tag.js b/src/language-html/print/tag.js index 57968903d..26396f317 100644 --- a/src/language-html/print/tag.js +++ b/src/language-html/print/tag.js @@ -221,7 +221,9 @@ function needsToBorrowParentOpeningTagEndMarker(node) { function printAttributes(path, options, print) { const { node } = path; - if (!isNonEmptyArray(node.attrs)) { + const { attrs: attributes = [], startTagComments = [] } = node; + + if (attributes.length === 0 && startTagComments.length === 0) { return node.isSelfClosing ? /** *
@@ -242,34 +244,53 @@ function printAttributes(path, options, print) { ? (attribute) => ignoreAttributeData.includes(attribute.rawName) : () => false; - const printedAttributes = path.map( - ({ node: attribute }) => - hasPrettierIgnoreAttribute(attribute) - ? replaceEndOfLine( - options.originalText.slice(locStart(attribute), locEnd(attribute)), - ) - : print(), - "attrs", + const properties = ["attrs", "startTagComments"].filter((property) => + isNonEmptyArray(node[property]), ); + const printedAttributes = properties.flatMap((property) => + path.map( + ({ node }) => ({ + loc: locStart(node), + printed: + node.kind === "attribute" && hasPrettierIgnoreAttribute(node) + ? replaceEndOfLine( + options.originalText.slice(locStart(node), locEnd(node)), + ) + : print(), + }), + property, + ), + ); + + if (properties.length > 1) { + printedAttributes.sort((a, b) => a.loc - b.loc); + } const forceNotToBreakAttrContent = node.kind === "element" && node.fullName === "script" && - node.attrs.length === 1 && - node.attrs[0].fullName === "src" && - node.children.length === 0; - + attributes.length === 1 && + attributes[0].fullName === "src" && + node.children.length === 0 && + startTagComments.length === 0; + const shouldForceBreak = startTagComments.some( + (comment) => comment.type === "single", + ); const shouldPrintAttributePerLine = - options.singleAttributePerLine && - node.attrs.length > 1 && - !isVueSfcBlock(node, options); + shouldForceBreak || + (options.singleAttributePerLine && + attributes.length > 1 && + !isVueSfcBlock(node, options)); const attributeLine = shouldPrintAttributePerLine ? hardline : line; /** @type {Doc[]} */ const parts = [ indent([ - forceNotToBreakAttrContent ? " " : line, - join(attributeLine, printedAttributes), + forceNotToBreakAttrContent ? " " : shouldForceBreak ? hardline : line, + join( + attributeLine, + printedAttributes.map(({ printed }) => printed), + ), ]), ]; diff --git a/src/language-html/printer-html.js b/src/language-html/printer-html.js index d1caf98e1..bd5228d58 100644 --- a/src/language-html/printer-html.js +++ b/src/language-html/printer-html.js @@ -28,6 +28,7 @@ import { } from "./print/angular-icu-expression.js"; import { printChildren } from "./print/children.js"; import { printElement } from "./print/element.js"; +import { printStartTagComment } from "./print/start-tag-comment.js"; import { printClosingTagEnd, printClosingTagSuffix, @@ -150,6 +151,10 @@ function genericPrint(path, options, print) { quote, ]; } + + case "startTagComment": + return printStartTagComment(path); + case "frontMatter": // Handled in core case "cdata": // Transformed into `text` default: diff --git a/src/language-html/visitor-keys.evaluate.js b/src/language-html/visitor-keys.evaluate.js index cf036e223..d5ea4cbf6 100644 --- a/src/language-html/visitor-keys.evaluate.js +++ b/src/language-html/visitor-keys.evaluate.js @@ -2,7 +2,7 @@ import { generateReferenceSharedVisitorKeys } from "../utilities/visitor-keys.js const visitorKeys = generateReferenceSharedVisitorKeys({ root: ["children"], - element: ["attrs", "children"], + element: ["attrs", "startTagComments", "children"], ieConditionalComment: ["children"], ieConditionalStartComment: [], ieConditionalEndComment: [], @@ -11,6 +11,7 @@ const visitorKeys = generateReferenceSharedVisitorKeys({ docType: [], comment: [], attribute: [], + startTagComment: [], cdata: [], angularControlFlowBlock: ["children", "parameters"], angularControlFlowBlockParameters: ["children"], diff --git a/tests/format/angular/start-tag-comments/__snapshots__/format.test.js.snap b/tests/format/angular/start-tag-comments/__snapshots__/format.test.js.snap new file mode 100644 index 000000000..ca63f8d6a --- /dev/null +++ b/tests/format/angular/start-tag-comments/__snapshots__/format.test.js.snap @@ -0,0 +1,144 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`merge-into-text.html format 1`] = ` +====================================options===================================== +parsers: ["angular"] + printWidth: 80 (default) | +=====================================input====================================== +
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, + "seddoeiusmod". +
+ +
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, + "seddoeiusmod". +
+ +=====================================output===================================== +
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, + "seddoeiusmod". +
+ +
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, "seddoeiusmod". +
+ +================================================================================ +`; + +exports[`start-tag-comments.html format 1`] = ` +====================================options===================================== +parsers: ["angular"] + printWidth: 80 (default) | +=====================================input====================================== +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+
+
+ +
+
+
+ +=====================================output===================================== +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+
+
+
+
+ +
+
+
+
+
+ +================================================================================ +`; diff --git a/tests/format/angular/start-tag-comments/format.test.js b/tests/format/angular/start-tag-comments/format.test.js new file mode 100644 index 000000000..8cbc48b1a --- /dev/null +++ b/tests/format/angular/start-tag-comments/format.test.js @@ -0,0 +1 @@ +runFormatTest(import.meta, ["angular"]); diff --git a/tests/format/angular/start-tag-comments/merge-into-text.html b/tests/format/angular/start-tag-comments/merge-into-text.html new file mode 100644 index 000000000..d364a47b8 --- /dev/null +++ b/tests/format/angular/start-tag-comments/merge-into-text.html @@ -0,0 +1,11 @@ +
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, + "seddoeiusmod". +
+ +
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, + "seddoeiusmod". +
diff --git a/tests/format/angular/start-tag-comments/start-tag-comments.html b/tests/format/angular/start-tag-comments/start-tag-comments.html new file mode 100644 index 000000000..047d3e6cc --- /dev/null +++ b/tests/format/angular/start-tag-comments/start-tag-comments.html @@ -0,0 +1,45 @@ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+
+
+ +
+
+
diff --git a/tests/format/html/_errors_/in-element-comments/format.test.js b/tests/format/html/_errors_/in-element-comments/format.test.js deleted file mode 100644 index 2af3dd93d..000000000 --- a/tests/format/html/_errors_/in-element-comments/format.test.js +++ /dev/null @@ -1,8 +0,0 @@ -runFormatTest(import.meta, [ - "html", - "vue", - "lwc", - "mjml", - // https://github.com/prettier/prettier/issues/18974 - "angular", -]); diff --git a/tests/format/html/_errors_/in-element-comments/__snapshots__/format.test.js.snap b/tests/format/html/_errors_/start-tag-comments/__snapshots__/format.test.js.snap similarity index 80% rename from tests/format/html/_errors_/in-element-comments/__snapshots__/format.test.js.snap rename to tests/format/html/_errors_/start-tag-comments/__snapshots__/format.test.js.snap index d1aeb8d43..293894462 100644 --- a/tests/format/html/_errors_/in-element-comments/__snapshots__/format.test.js.snap +++ b/tests/format/html/_errors_/start-tag-comments/__snapshots__/format.test.js.snap @@ -1,13 +1,5 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`block-comment.html format[angular] 1`] = ` -"Opening tag "div" not terminated. (1:1) -> 1 |
- | ^^^^^^ - 2 | -Cause: Opening tag "div" not terminated." -`; - exports[`block-comment.html format[html] 1`] = ` "Opening tag "div" not terminated. (1:1) > 1 |
@@ -40,15 +32,6 @@ exports[`block-comment.html format[vue] 1`] = ` Cause: Opening tag "div" not terminated." `; -exports[`line-comment.html format[angular] 1`] = ` -"Opening tag "div" not terminated. (1:1) -> 1 |
- 3 | -Cause: Opening tag "div" not terminated." -`; - exports[`line-comment.html format[html] 1`] = ` "Opening tag "div" not terminated. (1:1) > 1 |