From 72cfa18a42e2d4cd3a41ad46c7742701b17c91c5 Mon Sep 17 00:00:00 2001 From: Konv Suu <2583695112@qq.com> Date: Wed, 24 Sep 2025 01:29:56 +0800 Subject: [PATCH] Support format html event attributes (#17909) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: fisker --- changelog_unreleased/html/17909.md | 26 ++++++ package.json | 1 + src/language-html/embed/attribute.js | 2 + src/language-html/embed/class-names.js | 12 ++- src/language-html/embed/event-handler.js | 27 ++++++ src/language-html/embed/style.js | 20 ++-- src/language-js/print/expression-statement.js | 34 ++++--- src/language-js/print/semicolon.js | 29 ++++-- .../__snapshots__/format.test.js.snap | 92 +++++++++++++++++++ .../html/attributes/event-attributes.html | 32 +++++++ .../no-semi/__snapshots__/format.test.js.snap | 27 ++++++ .../attributes/no-semi/event-attributes.html | 6 ++ .../html/attributes/no-semi/format.test.js | 1 + .../tags/__snapshots__/format.test.js.snap | 12 ++- yarn.lock | 8 ++ 15 files changed, 291 insertions(+), 38 deletions(-) create mode 100644 changelog_unreleased/html/17909.md create mode 100644 src/language-html/embed/event-handler.js create mode 100644 tests/format/html/attributes/event-attributes.html create mode 100644 tests/format/html/attributes/no-semi/__snapshots__/format.test.js.snap create mode 100644 tests/format/html/attributes/no-semi/event-attributes.html create mode 100644 tests/format/html/attributes/no-semi/format.test.js diff --git a/changelog_unreleased/html/17909.md b/changelog_unreleased/html/17909.md new file mode 100644 index 000000000..88c5a5bcb --- /dev/null +++ b/changelog_unreleased/html/17909.md @@ -0,0 +1,26 @@ +#### Format inline event handler (#17909 by @kovsu) + + +```html + + + + + + + + +``` diff --git a/package.json b/package.json index 412335db0..bc5b35d28 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@babel/types": "7.28.4", "@glimmer/syntax": "0.95.0", "@prettier/cli": "0.9.0", + "@prettier/html-event-attributes": "0.0.1", "@prettier/html-tags": "1.1.0", "@prettier/parse-srcset": "3.1.0", "@typescript-eslint/typescript-estree": "8.44.1", diff --git a/src/language-html/embed/attribute.js b/src/language-html/embed/attribute.js index 0adbe272b..718bfc8b6 100644 --- a/src/language-html/embed/attribute.js +++ b/src/language-html/embed/attribute.js @@ -2,6 +2,7 @@ import { group } from "../../document/builders.js"; import { mapDoc } from "../../document/utils.js"; import printAngularAttribute from "./angular-attributes.js"; import printClassNames from "./class-names.js"; +import printEventHandler from "./event-handler.js"; import printSrcset from "./srcset.js"; import { printStyleAttribute } from "./style.js"; import printVueAttribute from "./vue-attributes.js"; @@ -36,6 +37,7 @@ function printAttribute(path, options) { for (const getValuePrinter of [ printSrcset, printStyleAttribute, + printEventHandler, printClassNames, printVueAttribute, printAngularAttribute, diff --git a/src/language-html/embed/class-names.js b/src/language-html/embed/class-names.js index 029e26ced..31a3ebc4b 100644 --- a/src/language-html/embed/class-names.js +++ b/src/language-html/embed/class-names.js @@ -2,14 +2,16 @@ import { getUnescapedAttributeValue } from "../utils/index.js"; function printClassNames(path, options) { const { node } = path; - const value = getUnescapedAttributeValue(node); + if ( - node.fullName === "class" && - !options.parentParser && - !value.includes("{{") + options.parentParser || + node.fullName !== "class" || + node.value.includes("{{") ) { - return () => value.trim().split(/\s+/u).join(" "); + return; } + + return () => getUnescapedAttributeValue(node).trim().split(/\s+/u).join(" "); } export default printClassNames; diff --git a/src/language-html/embed/event-handler.js b/src/language-html/embed/event-handler.js new file mode 100644 index 000000000..f1fd53eb2 --- /dev/null +++ b/src/language-html/embed/event-handler.js @@ -0,0 +1,27 @@ +import htmlEventAttributesArray from "@prettier/html-event-attributes"; +import { getUnescapedAttributeValue } from "../utils/index.js"; +import { formatAttributeValue } from "./utils.js"; + +const htmlEventAttributes = new Set(htmlEventAttributesArray); + +function printEventHandler(path, options) { + const { node } = path; + + if ( + options.parentParser || + !htmlEventAttributes.has(node.fullName) || + node.value.includes("{{") + ) { + return; + } + + return (textToDoc) => + formatAttributeValue( + getUnescapedAttributeValue(node), + textToDoc, + { parser: "babel", __isHtmlInlineEventHandler: true }, + () => false, + ); +} + +export default printEventHandler; diff --git a/src/language-html/embed/style.js b/src/language-html/embed/style.js index 544a9ea0d..f07bee2c3 100644 --- a/src/language-html/embed/style.js +++ b/src/language-html/embed/style.js @@ -3,17 +3,21 @@ import { printExpand } from "./utils.js"; function printStyleAttribute(path, options) { const { node } = path; - const text = getUnescapedAttributeValue(path.node).trim(); if ( - node.fullName === "style" && - !options.parentParser && - !text.includes("{{") + node.fullName !== "style" || + options.parentParser || + node.value.includes("{{") ) { - return async (textToDoc) => - printExpand( - await textToDoc(text, { parser: "css", __isHTMLStyleAttribute: true }), - ); + return; } + + return async (textToDoc) => + printExpand( + await textToDoc(getUnescapedAttributeValue(node), { + parser: "css", + __isHTMLStyleAttribute: true, + }), + ); } export { printStyleAttribute }; diff --git a/src/language-js/print/expression-statement.js b/src/language-js/print/expression-statement.js index 97de0d7d7..aa0f9cad1 100644 --- a/src/language-js/print/expression-statement.js +++ b/src/language-js/print/expression-statement.js @@ -4,28 +4,38 @@ import { unwrapVueEventBindingTsNode, } from "../utils/vue-event-binding.js"; import { + isSingleHtmlEventHandlerExpressionStatement, isSingleJsxExpressionStatementInMarkdown, isSingleVueEventBindingExpressionStatement, } from "./semicolon.js"; -function printExpressionStatement(path, options, print) { - const parts = [print("expression")]; - +function shouldPrintSemicolon(path, options) { if (isSingleVueEventBindingExpressionStatement(path, options)) { const expression = unwrapVueEventBindingTsNode(path.node.expression); - if ( + return ( isVueEventBindingFunctionExpression(expression) || isVueEventBindingMemberExpression(expression) - ) { - parts.push(";"); - } - } else if (isSingleJsxExpressionStatementInMarkdown(path, options)) { - // Do not append semicolon after the only JSX element in a program - } else if (options.semi) { - parts.push(";"); + ); } - return parts; + if (!options.semi) { + return false; + } + + if ( + // Do not append semicolon after the only JSX element in a program + isSingleJsxExpressionStatementInMarkdown(path, options) || + // Do not append semicolon after the only HTML event binding expression in a program + isSingleHtmlEventHandlerExpressionStatement(path, options) + ) { + return false; + } + + return true; +} + +function printExpressionStatement(path, options, print) { + return [print("expression"), shouldPrintSemicolon(path, options) ? ";" : ""]; } export { printExpressionStatement }; diff --git a/src/language-js/print/semicolon.js b/src/language-js/print/semicolon.js index 76b1f0b7a..0771b436e 100644 --- a/src/language-js/print/semicolon.js +++ b/src/language-js/print/semicolon.js @@ -10,7 +10,8 @@ function shouldPrintLeadingSemicolon(path, options) { if ( options.semi || isSingleJsxExpressionStatementInMarkdown(path, options) || - isSingleVueEventBindingExpressionStatement(path, options) + isSingleVueEventBindingExpressionStatement(path, options) || + isSingleHtmlEventHandlerExpressionStatement(path, options) ) { return false; } @@ -89,27 +90,35 @@ function expressionNeedsASIProtection(path, options) { ); } -function isSingleJsxExpressionStatementInMarkdown({ node, parent }, options) { +const isSingleExpressionStatement = ({ node, parent }) => + node.type === "ExpressionStatement" && + parent.type === "Program" && + parent.body.length === 1; + +function isSingleJsxExpressionStatementInMarkdown(path, options) { return ( (options.parentParser === "markdown" || options.parentParser === "mdx") && - node.type === "ExpressionStatement" && - isJsxElement(node.expression) && - parent.type === "Program" && - parent.body.length === 1 + isSingleExpressionStatement(path) && + isJsxElement(path.node.expression) ); } -function isSingleVueEventBindingExpressionStatement({ node, parent }, options) { +function isSingleHtmlEventHandlerExpressionStatement(path, options) { + return ( + options.__isHtmlInlineEventHandler && isSingleExpressionStatement(path) + ); +} + +function isSingleVueEventBindingExpressionStatement(path, options) { return ( (options.parser === "__vue_event_binding" || options.parser === "__vue_ts_event_binding") && - node.type === "ExpressionStatement" && - parent.type === "Program" && - parent.body.length === 1 + isSingleExpressionStatement(path) ); } export { + isSingleHtmlEventHandlerExpressionStatement, isSingleJsxExpressionStatementInMarkdown, isSingleVueEventBindingExpressionStatement, shouldPrintLeadingSemicolon, diff --git a/tests/format/html/attributes/__snapshots__/format.test.js.snap b/tests/format/html/attributes/__snapshots__/format.test.js.snap index cb612b9f7..04f8fd966 100644 --- a/tests/format/html/attributes/__snapshots__/format.test.js.snap +++ b/tests/format/html/attributes/__snapshots__/format.test.js.snap @@ -537,6 +537,98 @@ printWidth: 80 ================================================================================ `; +exports[`event-attributes.html format 1`] = ` +====================================options===================================== +parsers: ["html"] +printWidth: 80 + | printWidth +=====================================input====================================== + + +
Hover me
+ + + + + +
Data
+ + + + + + + + + + + + + + +=====================================output===================================== + + +
+ Hover me +
+ + + + + +
Data
+ + + + + + + + + + + + + + +================================================================================ +`; + exports[`single-quotes.html format 1`] = ` ====================================options===================================== parsers: ["html"] diff --git a/tests/format/html/attributes/event-attributes.html b/tests/format/html/attributes/event-attributes.html new file mode 100644 index 000000000..a3d256a2b --- /dev/null +++ b/tests/format/html/attributes/event-attributes.html @@ -0,0 +1,32 @@ + + +
Hover me
+ + + + + +
Data
+ + + + + + + + + + + + + diff --git a/tests/format/html/attributes/no-semi/__snapshots__/format.test.js.snap b/tests/format/html/attributes/no-semi/__snapshots__/format.test.js.snap new file mode 100644 index 000000000..c522795d8 --- /dev/null +++ b/tests/format/html/attributes/no-semi/__snapshots__/format.test.js.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`event-attributes.html - {"semi":false} format 1`] = ` +====================================options===================================== +parsers: ["html"] +printWidth: 80 +semi: false + | printWidth +=====================================input====================================== + + + +=====================================output===================================== + + + +================================================================================ +`; diff --git a/tests/format/html/attributes/no-semi/event-attributes.html b/tests/format/html/attributes/no-semi/event-attributes.html new file mode 100644 index 000000000..51274fad0 --- /dev/null +++ b/tests/format/html/attributes/no-semi/event-attributes.html @@ -0,0 +1,6 @@ + + diff --git a/tests/format/html/attributes/no-semi/format.test.js b/tests/format/html/attributes/no-semi/format.test.js new file mode 100644 index 000000000..04fa2b56d --- /dev/null +++ b/tests/format/html/attributes/no-semi/format.test.js @@ -0,0 +1 @@ +runFormatTest(import.meta, ["html"], { semi: false }); diff --git a/tests/format/html/tags/__snapshots__/format.test.js.snap b/tests/format/html/tags/__snapshots__/format.test.js.snap index b2371761b..63b967b9e 100644 --- a/tests/format/html/tags/__snapshots__/format.test.js.snap +++ b/tests/format/html/tags/__snapshots__/format.test.js.snap @@ -656,21 +656,27 @@ printWidth: 1
  • diff --git a/yarn.lock b/yarn.lock index 775689a39..8e6b0fca6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2296,6 +2296,13 @@ __metadata: languageName: node linkType: hard +"@prettier/html-event-attributes@npm:0.0.1": + version: 0.0.1 + resolution: "@prettier/html-event-attributes@npm:0.0.1" + checksum: 10/28c88092b06bf5a0ed2b9e8e4b7abf55eff409e64e0ebc958dd115f8c06c0667c2cb2df1bd4c55d28df58d481eadc7465e7138566b91288cafefc4b5ea048eb9 + languageName: node + linkType: hard + "@prettier/html-tags@npm:1.1.0": version: 1.1.0 resolution: "@prettier/html-tags@npm:1.1.0" @@ -7536,6 +7543,7 @@ __metadata: "@eslint/js": "npm:9.35.0" "@glimmer/syntax": "npm:0.95.0" "@prettier/cli": "npm:0.9.0" + "@prettier/html-event-attributes": "npm:0.0.1" "@prettier/html-tags": "npm:1.1.0" "@prettier/parse-srcset": "npm:3.1.0" "@stylistic/eslint-plugin": "npm:5.3.1"