mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(formatter): quote keys added in an HTML event handler with single quotes (#28380)
Regardless of the location, the attribute value must be quoted by single quote.
This commit is contained in:
1 parent
2e7a629be8
commit
e738daede1
5 files changed
+38
-12
No files matched your search
@@ -269,6 +269,21 @@ describe("Embedded languages", () => {
|
||||
expect(result.code).toMatchSnapshot();
|
||||
});
|
||||
|
||||
// js-in-html: a quote the formatter adds (a key under `quoteProps: consistent`) is `'`, as every string there,
|
||||
// so the attribute's `"` needs no `"`
|
||||
it("should add single quotes to keys in an event handler attribute", async () => {
|
||||
const input = `\
|
||||
<button onclick="f({a: 1, 'b-c': 2})">x</button>
|
||||
<button onclick="f({1: 1, 'b-c': 2})">x</button>
|
||||
`;
|
||||
const result = await format("index.html", input, { quoteProps: "consistent" });
|
||||
expect(result.errors).toStrictEqual([]);
|
||||
expect(result.code).toBe(`\
|
||||
<button onclick="f({ 'a': 1, 'b-c': 2 })">x</button>
|
||||
<button onclick="f({ '1': 1, 'b-c': 2 })">x</button>
|
||||
`);
|
||||
});
|
||||
|
||||
// (css-in-html)-in-js: For now, HTML is handled by Prettier, so CSS is also handled by Prettier too.
|
||||
// The CSS `Doc` carries a trailing space after `prop: ` that only materializes when the value fits flat;
|
||||
// when the value breaks, it must not leak (the Doc→IR conversion maps it to a pending space, dropped at the break).
|
||||
|
||||
@@ -3,7 +3,11 @@ use oxc_formatter_core::{FormatElement, SourceText};
|
||||
use oxc_span::{GetSpan, SourceType, Span};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::{JsEmbeddedIn, options::JsFormatOptions, utils::assignment_like::AssignmentLikeLayout};
|
||||
use crate::{
|
||||
JsEmbeddedIn,
|
||||
options::{JsFormatOptions, QuoteStyle},
|
||||
utils::assignment_like::AssignmentLikeLayout,
|
||||
};
|
||||
|
||||
use super::Comments;
|
||||
|
||||
@@ -254,6 +258,13 @@ impl<'ast> JsFormatContext<'ast> {
|
||||
self.embedded_in == Some(JsEmbeddedIn::HtmlEventHandler)
|
||||
}
|
||||
|
||||
/// The quote a string prints in (Prettier's `printString`), the formatter's own too (a quoted property key):
|
||||
/// the configured one, but `'` in an HTML attribute.
|
||||
/// A JSX attribute and a directive have their own (`FormatLiteralStringToken`).
|
||||
pub(crate) fn string_quote_style(&self) -> QuoteStyle {
|
||||
if self.in_html_attribute() { QuoteStyle::Single } else { self.options.quote_style }
|
||||
}
|
||||
|
||||
/// Push a Tailwind context entry onto the stack.
|
||||
/// Call this when entering a JSXAttribute or CallExpression with Tailwind class context.
|
||||
pub fn push_tailwind_context(&mut self, entry: TailwindContextEntry) {
|
||||
|
||||
@@ -183,6 +183,9 @@ pub fn format_fragment<'a>(
|
||||
// NOTE: Since this options is just the preferred quote (not a forced),
|
||||
// this may not be enough for all cases.
|
||||
// But it seems fine for almost all cases, so leave it for now.
|
||||
// TODO: When Vue formats natively, share the HTML event handler's rule instead
|
||||
// (`JsFormatContext::string_quote_style`, forced `'` as Prettier's `__isInHtmlAttribute`):
|
||||
// a string holding more `'` than `"` prints `"` here, which the attribute escapes as `"`.
|
||||
let options = JsFormatOptions { quote_style: QuoteStyle::Single, ..options };
|
||||
|
||||
// A js-in-xxx fragment never owns file envelopes (BOM / front matter)
|
||||
|
||||
@@ -144,7 +144,7 @@ impl<'a> FormatWrite<'a> for AstNode<'_, 'a, IdentifierName<'a>> {
|
||||
| AstNodes::TSEnumMember(_)
|
||||
);
|
||||
if is_property_key_parent && f.context().is_quote_needed() {
|
||||
let quote_str = f.options().quote_style.as_str();
|
||||
let quote_str = f.context().string_quote_style().as_str();
|
||||
write!(f, [quote_str, text, quote_str]);
|
||||
} else {
|
||||
write!(f, text);
|
||||
@@ -1223,7 +1223,7 @@ impl<'a> FormatWrite<'a> for AstNode<'_, 'a, NumericLiteral<'a>> {
|
||||
};
|
||||
|
||||
if should_quote {
|
||||
let quote_str = f.options().quote_style.as_str();
|
||||
let quote_str = f.context().string_quote_style().as_str();
|
||||
write!(f, [quote_str, text(f.allocator().alloc_str(&formatted)), quote_str]);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -47,15 +47,12 @@ impl<'a> FormatLiteralStringToken<'a> {
|
||||
let source_type = f.context().source_type();
|
||||
|
||||
// In an HTML attribute, `'` always (Prettier's `__isInHtmlAttribute`), but a directive as usual
|
||||
let in_html_attribute = !self.jsx
|
||||
&& self.parent_kind != StringLiteralParentKind::Directive
|
||||
&& f.context().in_html_attribute();
|
||||
let chosen_quote_style = if in_html_attribute {
|
||||
QuoteStyle::Single
|
||||
} else if self.jsx {
|
||||
options.jsx_quote_style
|
||||
let (chosen_quote_style, force_chosen_quote) = if self.jsx {
|
||||
(options.jsx_quote_style, false)
|
||||
} else if self.parent_kind == StringLiteralParentKind::Directive {
|
||||
(options.quote_style, false)
|
||||
} else {
|
||||
options.quote_style
|
||||
(f.context().string_quote_style(), f.context().in_html_attribute())
|
||||
};
|
||||
|
||||
let is_quote_needed = match options.quote_properties {
|
||||
@@ -67,7 +64,7 @@ impl<'a> FormatLiteralStringToken<'a> {
|
||||
let string_cleaner = LiteralStringNormalizer {
|
||||
token: *self,
|
||||
chosen_quote_style,
|
||||
force_chosen_quote: in_html_attribute,
|
||||
force_chosen_quote,
|
||||
is_quote_needed,
|
||||
allocator: f.allocator(),
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user