Support format html event attributes (#17909)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: fisker <lionkay@gmail.com>
This commit is contained in:
authored and GitHub committed 2025-09-24 01:29:56 +08:00
1 parent b7b2b0d434
commit 72cfa18a42
15 files changed
+291 -38

No files matched your search

+26
View File
@@ -0,0 +1,26 @@
#### Format inline event handler (#17909 by @kovsu)
<!-- prettier-ignore -->
```html
<!-- Input -->
<button
type="button"
onclick="console .log( 'Hello, this is my old-fashioned event handler!')"
>Press me</button>
<!-- Prettier stable -->
<button
type="button"
onclick="console .log( 'Hello, this is my old-fashioned event handler!')"
>
Press me
</button>
<!-- Prettier main -->
<button
type="button"
onclick="console.log('Hello, this is my old-fashioned event handler!')"
>
Press me
</button>
```
+1
View File
@@ -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",
+2
View File
@@ -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,
+7 -5
View File
@@ -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;
+27
View File
@@ -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;
+12 -8
View File
@@ -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 };
+22 -12
View File
@@ -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 };
+19 -10
View File
@@ -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,
@@ -537,6 +537,98 @@ printWidth: 80
================================================================================
`;
exports[`event-attributes.html format 1`] = `
====================================options=====================================
parsers: ["html"]
printWidth: 80
| printWidth
=====================================input======================================
<button onclick="alert( '1')">click me!</button>
<div onclick="handleClick( event )" onmouseover="showTooltip( this )" onmouseout="hideTooltip( )">Hover me</div>
<img onerror='handleError( this )' onload="imageLoaded( event )" />
<button onclick="(event) => { console.log('clicked'); handleClick( event ); }">Modern JS</button>
<div onclick="processData( { id: 123, name: 'test' }, [ 1, 2, 3 ] )">Data</div>
<button onclick="api.user.get( 123 ).then( data => updateUI( data ) )">API Call</button>
<input onfocus="highlightField( this )" onblur="validateField( this.value )" />
<select onchange="filterResults( this.value, this.selectedIndex )">
<option>Option 1</option>
</select>
<button onclick="
alert(1); alert(2);
">Click</button>
<button onclick="
return false
">Click</button>
<button onclick="
console.log(arguments)
">Click</button>
<button onclick="
console.log(onclick)
">Click</button>
=====================================output=====================================
<button onclick="alert('1')">click me!</button>
<div
onclick="handleClick(event)"
onmouseover="showTooltip(this)"
onmouseout="hideTooltip()"
>
Hover me
</div>
<img onerror="handleError(this)" onload="imageLoaded(event)" />
<button
onclick="
(event) => {
console.log('clicked');
handleClick(event);
}
"
>
Modern JS
</button>
<div onclick="processData({ id: 123, name: 'test' }, [1, 2, 3])">Data</div>
<button onclick="api.user.get(123).then((data) => updateUI(data))">
API Call
</button>
<input onfocus="highlightField(this)" onblur="validateField(this.value)" />
<select onchange="filterResults(this.value, this.selectedIndex)">
<option>Option 1</option>
</select>
<button
onclick="
alert(1);
alert(2);
"
>
Click
</button>
<button onclick="return false;">Click</button>
<button onclick="console.log(arguments)">Click</button>
<button onclick="console.log(onclick)">Click</button>
================================================================================
`;
exports[`single-quotes.html format 1`] = `
====================================options=====================================
parsers: ["html"]
@@ -0,0 +1,32 @@
<button onclick="alert( '1')">click me!</button>
<div onclick="handleClick( event )" onmouseover="showTooltip( this )" onmouseout="hideTooltip( )">Hover me</div>
<img onerror='handleError( this )' onload="imageLoaded( event )" />
<button onclick="(event) => { console.log('clicked'); handleClick( event ); }">Modern JS</button>
<div onclick="processData( { id: 123, name: 'test' }, [ 1, 2, 3 ] )">Data</div>
<button onclick="api.user.get( 123 ).then( data => updateUI( data ) )">API Call</button>
<input onfocus="highlightField( this )" onblur="validateField( this.value )" />
<select onchange="filterResults( this.value, this.selectedIndex )">
<option>Option 1</option>
</select>
<button onclick="
alert(1); alert(2);
">Click</button>
<button onclick="
return false
">Click</button>
<button onclick="
console.log(arguments)
">Click</button>
<button onclick="
console.log(onclick)
">Click</button>
@@ -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======================================
<button
onclick="[foo, bar].forEach((fn) => void fn())"
></button>
<button
onclick="[foo, bar].forEach((fn) => void fn()); alert(1)"
></button>
=====================================output=====================================
<button onclick="[foo, bar].forEach((fn) => void fn())"></button>
<button
onclick="
;[foo, bar].forEach((fn) => void fn())
alert(1)
"
></button>
================================================================================
`;
@@ -0,0 +1,6 @@
<button
onclick="[foo, bar].forEach((fn) => void fn())"
></button>
<button
onclick="[foo, bar].forEach((fn) => void fn()); alert(1)"
></button>
@@ -0,0 +1 @@
runFormatTest(import.meta, ["html"], { semi: false });
@@ -656,21 +656,27 @@ printWidth: 1
<menu>
<li>
<button
onclick="copy()"
onclick="
copy()
"
>
Copy
</button>
</li>
<li>
<button
onclick="cut()"
onclick="
cut()
"
>
Cut
</button>
</li>
<li>
<button
onclick="paste()"
onclick="
paste()
"
>
Paste
</button>
+8
View File
@@ -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"