mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(formatter): don't sort templates in nested non-Tailwind calls
- Hide the Tailwind context with a `None` stack entry instead of a `disabled` flag,
so templates and strings in their `${}` honor it like strings did
- Note the divergence from prettier-plugin-tailwindcss#426 in AGENTS.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
255c5cca26
commit
b65809dcde
9 files changed
+94
-81
No files matched your search
@@ -697,7 +697,8 @@ const f = clsx({ kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk
|
|||||||
const result = await format("test.ts", input, options);
|
const result = await format("test.ts", input, options);
|
||||||
|
|
||||||
// The line breaks are collapsed, so the template is not hugged as multiline,
|
// The line breaks are collapsed, so the template is not hugged as multiline,
|
||||||
// and the collapsed key and call arguments are short.
|
// and the collapsed key and Tailwind call arguments are short.
|
||||||
|
// `foo()` is not a Tailwind function, so its argument is kept and measured as is.
|
||||||
expect(result.code).toMatchInlineSnapshot(`
|
expect(result.code).toMatchInlineSnapshot(`
|
||||||
"const a = clsx(
|
"const a = clsx(
|
||||||
\`flex items-center justify-between p-4 \${active ? "bg-white" : "bg-black"} text-black hover:bg-gray-100\`,
|
\`flex items-center justify-between p-4 \${active ? "bg-white" : "bg-black"} text-black hover:bg-gray-100\`,
|
||||||
@@ -714,8 +715,9 @@ const f = clsx({ kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk
|
|||||||
const eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee =
|
const eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee =
|
||||||
clsx(\`flex p-4\`);
|
clsx(\`flex p-4\`);
|
||||||
const f = clsx({
|
const f = clsx({
|
||||||
kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk:
|
kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk: foo(
|
||||||
foo(\`flex p-4\`),
|
\`p-4 flex\`,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
"
|
"
|
||||||
`);
|
`);
|
||||||
@@ -1005,6 +1007,37 @@ const f = clsx({ kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk
|
|||||||
expect(result.errors).toStrictEqual([]);
|
expect(result.errors).toStrictEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should NOT sort template literals inside nested non-Tailwind call expressions", async () => {
|
||||||
|
const input = `const a = cn(x.includes(\`a b\`) && "p-4 flex");
|
||||||
|
const b = cn(x.includes(\`\${"a b"} c\`));
|
||||||
|
const C = <div className={x.split(\`a b\`).length ? "p-4 flex" : ""} />;
|
||||||
|
const D = <div className={foo(\`p-4
|
||||||
|
flex\`)} />;
|
||||||
|
`;
|
||||||
|
const options = { experimentalTailwindcss: { functions: ["cn"] } };
|
||||||
|
|
||||||
|
const result = await format("test.tsx", input, options);
|
||||||
|
|
||||||
|
// Like strings, the templates (and strings inside them) are kept as is
|
||||||
|
expect(result.code).toMatchInlineSnapshot(`
|
||||||
|
"const a = cn(x.includes(\`a b\`) && "flex p-4");
|
||||||
|
const b = cn(x.includes(\`\${"a b"} c\`));
|
||||||
|
const C = <div className={x.split(\`a b\`).length ? "flex p-4" : ""} />;
|
||||||
|
const D = (
|
||||||
|
<div
|
||||||
|
className={foo(\`p-4
|
||||||
|
flex\`)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
expect(result.errors).toStrictEqual([]);
|
||||||
|
|
||||||
|
// Formatting the output again should not change it.
|
||||||
|
const second = await format("test.tsx", result.code, options);
|
||||||
|
expect(second.code).toBe(result.code);
|
||||||
|
});
|
||||||
|
|
||||||
it("should preserve escape sequences in nested call expressions", async () => {
|
it("should preserve escape sequences in nested call expressions", async () => {
|
||||||
// Various escape sequences should be preserved in nested calls
|
// Various escape sequences should be preserved in nested calls
|
||||||
const input = `const A = <div className={clsx(
|
const input = `const A = <div className={clsx(
|
||||||
|
|||||||
@@ -54,9 +54,12 @@ After changing AST shapes or the generators, regenerate with `just ast`, never h
|
|||||||
|
|
||||||
### Sort Tailwind CSS
|
### Sort Tailwind CSS
|
||||||
|
|
||||||
- Backed by `prettier-plugin-tailwindcss`
|
- Backed by `prettier-plugin-tailwindcss`, but not fully compatible
|
||||||
- Classes are collected during IR construction and sorted in one batch when the IR is stringified
|
- Classes are collected during IR construction and sorted in one batch when the IR is stringified
|
||||||
- Requires the session's `TailwindSorter` service (the sort itself is delegated to the host)
|
- Requires the session's `TailwindSorter` service (the sort itself is delegated to the host)
|
||||||
|
- Covered by the Oxfmt API tests (`apps/oxfmt/test/api/sort_tailwindcss.test.ts`), since only the host sorts
|
||||||
|
- Deliberately unlike the plugin, nothing in the arguments of a nested non-Tailwind call is sorted
|
||||||
|
(tailwindlabs/prettier-plugin-tailwindcss#426): `cn(x.includes("a b"))` keeps `"a b"`, which the plugin rewrites
|
||||||
|
|
||||||
### Embedded language formatting
|
### Embedded language formatting
|
||||||
|
|
||||||
|
|||||||
@@ -31,11 +31,6 @@ pub struct TailwindContextEntry {
|
|||||||
/// Whether this is the last quasi in a template literal.
|
/// Whether this is the last quasi in a template literal.
|
||||||
/// Used for template element boundary detection.
|
/// Used for template element boundary detection.
|
||||||
pub is_last_quasi: bool,
|
pub is_last_quasi: bool,
|
||||||
/// Whether Tailwind sorting is disabled in this context.
|
|
||||||
/// Used to prevent sorting strings inside nested non-Tailwind call expressions.
|
|
||||||
/// For example, in `classNames("a", x.includes("\n") ? "b" : "c")`, the `"\n"`
|
|
||||||
/// inside `includes()` should NOT be sorted as a Tailwind class.
|
|
||||||
pub disabled: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TailwindContextEntry {
|
impl TailwindContextEntry {
|
||||||
@@ -48,7 +43,6 @@ impl TailwindContextEntry {
|
|||||||
quasi_after_has_leading_ws: true, // Default: can collapse
|
quasi_after_has_leading_ws: true, // Default: can collapse
|
||||||
is_first_quasi: true,
|
is_first_quasi: true,
|
||||||
is_last_quasi: true,
|
is_last_quasi: true,
|
||||||
disabled: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +60,6 @@ impl TailwindContextEntry {
|
|||||||
quasi_after_has_leading_ws,
|
quasi_after_has_leading_ws,
|
||||||
is_first_quasi: true,
|
is_first_quasi: true,
|
||||||
is_last_quasi: true,
|
is_last_quasi: true,
|
||||||
disabled: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,9 +93,9 @@ pub struct JsFormatContext<'ast> {
|
|||||||
/// structures (e.g., `{ a: { "b-c": 1 } }` where only the inner object needs quoted keys).
|
/// structures (e.g., `{ a: { "b-c": 1 } }` where only the inner object needs quoted keys).
|
||||||
quote_needed_stack: Vec<bool>,
|
quote_needed_stack: Vec<bool>,
|
||||||
|
|
||||||
/// Stack tracking whether we're inside a Tailwind class context.
|
/// Stack tracking whether we're inside a Tailwind class context, read by [`Self::tailwind_context`].
|
||||||
/// When non-empty, StringLiterals should be sorted as Tailwind classes.
|
/// - `None` hides the context below it, inside a nested non-Tailwind call
|
||||||
tailwind_context_stack: Vec<TailwindContextEntry>,
|
tailwind_context_stack: Vec<Option<TailwindContextEntry>>,
|
||||||
|
|
||||||
/// Set only for an embedded program (see [`crate::format_to_ir`]):
|
/// Set only for an embedded program (see [`crate::format_to_ir`]):
|
||||||
/// its sole statement prints without its semicolon.
|
/// its sole statement prints without its semicolon.
|
||||||
@@ -268,7 +261,13 @@ impl<'ast> JsFormatContext<'ast> {
|
|||||||
/// Push a Tailwind context entry onto the stack.
|
/// Push a Tailwind context entry onto the stack.
|
||||||
/// Call this when entering a JSXAttribute or CallExpression with Tailwind class context.
|
/// Call this when entering a JSXAttribute or CallExpression with Tailwind class context.
|
||||||
pub fn push_tailwind_context(&mut self, entry: TailwindContextEntry) {
|
pub fn push_tailwind_context(&mut self, entry: TailwindContextEntry) {
|
||||||
self.tailwind_context_stack.push(entry);
|
self.tailwind_context_stack.push(Some(entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hide the current Tailwind context until the matching [`Self::pop_tailwind_context`].
|
||||||
|
/// Call this when entering a nested non-Tailwind CallExpression.
|
||||||
|
pub fn hide_tailwind_context(&mut self) {
|
||||||
|
self.tailwind_context_stack.push(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pop a Tailwind context entry from the stack.
|
/// Pop a Tailwind context entry from the stack.
|
||||||
@@ -278,13 +277,9 @@ impl<'ast> JsFormatContext<'ast> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current Tailwind context, if any.
|
/// Get the current Tailwind context, if any.
|
||||||
/// Returns `Some` if we're inside a Tailwind class context (JSXAttribute or CallExpression).
|
/// Returns `Some` if we're inside a Tailwind class context (JSXAttribute or CallExpression),
|
||||||
|
/// and not inside a nested non-Tailwind call (see [`Self::hide_tailwind_context`]).
|
||||||
pub fn tailwind_context(&self) -> Option<&TailwindContextEntry> {
|
pub fn tailwind_context(&self) -> Option<&TailwindContextEntry> {
|
||||||
self.tailwind_context_stack.last()
|
self.tailwind_context_stack.last().and_then(Option::as_ref)
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a mutable reference to the current Tailwind context, if any.
|
|
||||||
pub fn tailwind_context_mut(&mut self) -> Option<&mut TailwindContextEntry> {
|
|
||||||
self.tailwind_context_stack.last_mut()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,18 +26,14 @@ impl<'a> FormatWrite<'a> for AstNode<'_, 'a, CallExpression<'a>> {
|
|||||||
// Check if this is a Tailwind function call (e.g., clsx, cn, tw)
|
// Check if this is a Tailwind function call (e.g., clsx, cn, tw)
|
||||||
let tailwind_ctx_to_push = tailwind_context_for_callee(&self.callee, f);
|
let tailwind_ctx_to_push = tailwind_context_for_callee(&self.callee, f);
|
||||||
|
|
||||||
// For nested non-Tailwind calls inside a Tailwind context, disable sorting
|
// For nested non-Tailwind calls inside a Tailwind context, hide the context
|
||||||
// to prevent sorting strings inside the nested call's arguments.
|
// to prevent sorting strings and templates inside the nested call's arguments.
|
||||||
// (e.g., `classNames("a", x.includes("\n") ? "b" : "c")` - don't sort "\n")
|
// (e.g., `classNames("a", x.includes("\n") ? "b" : "c")` - don't sort "\n")
|
||||||
let was_disabled = if tailwind_ctx_to_push.is_none()
|
let hides_tailwind_ctx =
|
||||||
&& let Some(ctx) = f.context_mut().tailwind_context_mut()
|
tailwind_ctx_to_push.is_none() && f.context().tailwind_context().is_some();
|
||||||
{
|
if hides_tailwind_ctx {
|
||||||
let was = ctx.disabled;
|
f.context_mut().hide_tailwind_context();
|
||||||
ctx.disabled = true;
|
}
|
||||||
Some(was)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// NOTE: Decided before this call pushes its Tailwind context, and `MemberChain` pushes none,
|
// NOTE: Decided before this call pushes its Tailwind context, and `MemberChain` pushes none,
|
||||||
// so a Tailwind call with a multiline template takes the branch below, which sorts it
|
// so a Tailwind call with a multiline template takes the branch below, which sorts it
|
||||||
@@ -77,11 +73,8 @@ impl<'a> FormatWrite<'a> for AstNode<'_, 'a, CallExpression<'a>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore the previous disabled state
|
if hides_tailwind_ctx {
|
||||||
if let Some(was) = was_disabled
|
f.context_mut().pop_tailwind_context();
|
||||||
&& let Some(ctx) = f.context_mut().tailwind_context_mut()
|
|
||||||
{
|
|
||||||
ctx.disabled = was;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ use crate::{
|
|||||||
write_trailing_comments_before,
|
write_trailing_comments_before,
|
||||||
},
|
},
|
||||||
string::{FormatLiteralStringToken, StringLiteralParentKind},
|
string::{FormatLiteralStringToken, StringLiteralParentKind},
|
||||||
tailwindcss::{tailwind_context_for_string_literal, write_tailwind_string_literal},
|
tailwindcss::{tailwind_context_for_class_list, write_tailwind_string_literal},
|
||||||
typecast::is_cast_target,
|
typecast::is_cast_target,
|
||||||
},
|
},
|
||||||
write,
|
write,
|
||||||
@@ -1246,7 +1246,7 @@ impl<'a> FormatWrite<'a> for AstNode<'_, 'a, StringLiteral<'a>> {
|
|||||||
fn write(&self, f: &mut JsFormatter<'_, 'a>) {
|
fn write(&self, f: &mut JsFormatter<'_, 'a>) {
|
||||||
// Check if we're in a Tailwind context via stack (O(1) lookup)
|
// Check if we're in a Tailwind context via stack (O(1) lookup)
|
||||||
// This handles nested string literals inside JSXAttribute/CallExpression values
|
// This handles nested string literals inside JSXAttribute/CallExpression values
|
||||||
if let Some(ctx) = tailwind_context_for_string_literal(self, f) {
|
if let Some(ctx) = tailwind_context_for_class_list(self, f) {
|
||||||
// We're inside a Tailwind context - sort this string literal as Tailwind classes
|
// We're inside a Tailwind context - sort this string literal as Tailwind classes
|
||||||
write_tailwind_string_literal(self, ctx, f);
|
write_tailwind_string_literal(self, ctx, f);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ use crate::{
|
|||||||
call_expression::is_test_each_pattern,
|
call_expression::is_test_each_pattern,
|
||||||
expression::is_member_expression_without_chain_wrappers,
|
expression::is_member_expression_without_chain_wrappers,
|
||||||
format_node_without_trailing_comments::FormatNodeWithoutTrailingComments,
|
format_node_without_trailing_comments::FormatNodeWithoutTrailingComments,
|
||||||
tailwindcss::{tailwind_context_for_callee, write_tailwind_template_element},
|
tailwindcss::{
|
||||||
|
tailwind_context_for_callee, tailwind_context_for_class_list,
|
||||||
|
write_tailwind_template_element,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
write,
|
write,
|
||||||
};
|
};
|
||||||
@@ -110,16 +113,9 @@ impl<'a> FormatWrite<'a> for AstNode<'_, 'a, TaggedTemplateExpression<'a>> {
|
|||||||
|
|
||||||
impl<'a> FormatWrite<'a> for AstNode<'_, 'a, TemplateElement<'a>> {
|
impl<'a> FormatWrite<'a> for AstNode<'_, 'a, TemplateElement<'a>> {
|
||||||
fn write(&self, f: &mut JsFormatter<'_, 'a>) {
|
fn write(&self, f: &mut JsFormatter<'_, 'a>) {
|
||||||
let source = f.source_text().text_for(self);
|
|
||||||
|
|
||||||
// Check if we're in a Tailwind context via the stack
|
// Check if we're in a Tailwind context via the stack
|
||||||
// (handles JSXAttribute, CallExpression, TaggedTemplateExpression, and nested contexts)
|
// (handles JSXAttribute, CallExpression, TaggedTemplateExpression, and nested contexts)
|
||||||
let tailwind_ctx = f.context().tailwind_context().copied().filter(|_| {
|
if let Some(ctx) = tailwind_context_for_class_list(self, f) {
|
||||||
// No whitespace means only one class, so no need to sort
|
|
||||||
source.as_bytes().iter().any(|&b| b.is_ascii_whitespace())
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(ctx) = tailwind_ctx {
|
|
||||||
write_tailwind_template_element(self, ctx, f);
|
write_tailwind_template_element(self, ctx, f);
|
||||||
} else {
|
} else {
|
||||||
write!(f, text(self.value.raw.as_str()));
|
write!(f, text(self.value.raw.as_str()));
|
||||||
|
|||||||
@@ -21,10 +21,7 @@ use crate::{
|
|||||||
format_node_without_trailing_comments::FormatNodeWithoutTrailingComments,
|
format_node_without_trailing_comments::FormatNodeWithoutTrailingComments,
|
||||||
member_chain::is_member_call_chain,
|
member_chain::is_member_call_chain,
|
||||||
object::{format_property_key, write_member_name},
|
object::{format_property_key, write_member_name},
|
||||||
tailwindcss::{
|
tailwindcss::{collapsed_class_list_width, tailwind_context_for_callee},
|
||||||
collapsed_class_list_width, tailwind_collapses_template_line_breaks,
|
|
||||||
tailwind_context_for_callee,
|
|
||||||
},
|
|
||||||
typecast::is_cast_target,
|
typecast::is_cast_target,
|
||||||
},
|
},
|
||||||
write,
|
write,
|
||||||
@@ -1251,6 +1248,11 @@ fn is_short_argument(
|
|||||||
callee: &Expression,
|
callee: &Expression,
|
||||||
f: &JsFormatter,
|
f: &JsFormatter,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
|
// Decided by `callee` alone: a Tailwind one pushes its context, a non-Tailwind one hides an outer one
|
||||||
|
// (except for a call printed as a `MemberChain`, which pushes none)
|
||||||
|
let collapses_tailwind_whitespace =
|
||||||
|
|| tailwind_context_for_callee(callee, f).is_some_and(|ctx| !ctx.preserve_whitespace);
|
||||||
|
|
||||||
match argument {
|
match argument {
|
||||||
Expression::Identifier(identifier) => identifier.name.width() <= threshold as usize,
|
Expression::Identifier(identifier) => identifier.name.width() <= threshold as usize,
|
||||||
Expression::UnaryExpression(unary_expression) => {
|
Expression::UnaryExpression(unary_expression) => {
|
||||||
@@ -1265,9 +1267,7 @@ fn is_short_argument(
|
|||||||
);
|
);
|
||||||
let text = formatter.clean_text(f);
|
let text = formatter.clean_text(f);
|
||||||
|
|
||||||
let width = if tailwind_context_for_callee(callee, f)
|
let width = if collapses_tailwind_whitespace() {
|
||||||
.is_some_and(|ctx| !ctx.preserve_whitespace)
|
|
||||||
{
|
|
||||||
// Opening and closing quotes, and the classes
|
// Opening and closing quotes, and the classes
|
||||||
2 + collapsed_class_list_width(&text[1..text.len() - 1])
|
2 + collapsed_class_list_width(&text[1..text.len() - 1])
|
||||||
} else {
|
} else {
|
||||||
@@ -1281,7 +1281,7 @@ fn is_short_argument(
|
|||||||
// Prettier: https://github.com/prettier/prettier/blob/a043ac0d733c4d53f980aa73807a63fc914f23bd/src/language-js/print/assignment.js#L402-L405
|
// Prettier: https://github.com/prettier/prettier/blob/a043ac0d733c4d53f980aa73807a63fc914f23bd/src/language-js/print/assignment.js#L402-L405
|
||||||
literal.quasis.len() == 1 && {
|
literal.quasis.len() == 1 && {
|
||||||
let raw = literal.quasis[0].value.raw;
|
let raw = literal.quasis[0].value.raw;
|
||||||
if tailwind_collapses_template_line_breaks(Some(callee), f) {
|
if collapses_tailwind_whitespace() {
|
||||||
collapsed_class_list_width(&raw) <= threshold as usize
|
collapsed_class_list_width(&raw) <= threshold as usize
|
||||||
} else {
|
} else {
|
||||||
raw.width() <= threshold as usize && !raw.contains('\n')
|
raw.width() <= threshold as usize && !raw.contains('\n')
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use crate::{
|
|||||||
formatter::{JsFormatter, JsFormatterExt as _},
|
formatter::{JsFormatter, JsFormatterExt as _},
|
||||||
utils::{
|
utils::{
|
||||||
string::{FormatLiteralStringToken, StringLiteralParentKind},
|
string::{FormatLiteralStringToken, StringLiteralParentKind},
|
||||||
tailwindcss::{tailwind_context_for_string_literal, write_tailwind_string_literal},
|
tailwindcss::{tailwind_context_for_class_list, write_tailwind_string_literal},
|
||||||
},
|
},
|
||||||
write,
|
write,
|
||||||
};
|
};
|
||||||
@@ -26,7 +26,7 @@ pub fn format_property_key<'a>(
|
|||||||
|
|
||||||
// Check if we're in a Tailwind context and the key is a string literal with multiple classes
|
// Check if we're in a Tailwind context and the key is a string literal with multiple classes
|
||||||
if let AstNodeKind::StringLiteral(string) = &key.as_kind() {
|
if let AstNodeKind::StringLiteral(string) = &key.as_kind() {
|
||||||
if let Some(ctx) = tailwind_context_for_string_literal(string, f) {
|
if let Some(ctx) = tailwind_context_for_class_list(string, f) {
|
||||||
// Reuse the existing Tailwind string literal writer
|
// Reuse the existing Tailwind string literal writer
|
||||||
write_tailwind_string_literal(string, ctx, f);
|
write_tailwind_string_literal(string, ctx, f);
|
||||||
return;
|
return;
|
||||||
@@ -57,7 +57,7 @@ pub fn write_member_name<'a>(
|
|||||||
f: &mut JsFormatter<'_, 'a>,
|
f: &mut JsFormatter<'_, 'a>,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
if let AstNodeKind::StringLiteral(string) = &key.as_kind() {
|
if let AstNodeKind::StringLiteral(string) = &key.as_kind() {
|
||||||
if let Some(ctx) = tailwind_context_for_string_literal(string, f) {
|
if let Some(ctx) = tailwind_context_for_class_list(string, f) {
|
||||||
// Reuse the existing Tailwind string literal writer
|
// Reuse the existing Tailwind string literal writer
|
||||||
string.format_leading_comments(f);
|
string.format_leading_comments(f);
|
||||||
let width = write_tailwind_string_literal(string, ctx, f);
|
let width = write_tailwind_string_literal(string, ctx, f);
|
||||||
|
|||||||
@@ -26,25 +26,19 @@ use super::string::{FormatLiteralStringToken, StringLiteralParentKind};
|
|||||||
// Detection Functions
|
// Detection Functions
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// Returns the Tailwind context if we should sort classes in the given string literal.
|
/// Returns the Tailwind context if we should sort classes in the given string literal or template element.
|
||||||
///
|
///
|
||||||
/// Returns `Some(ctx)` when:
|
/// Returns `Some(ctx)` when:
|
||||||
/// - We're inside a Tailwind function call context
|
/// - We're inside a Tailwind class context (see [`JsFormatContext::tailwind_context`])
|
||||||
/// - The context is not disabled (e.g., not inside a nested non-Tailwind call)
|
/// - The source contains whitespace (no whitespace means only one class, so no need to sort)
|
||||||
/// - The string contains whitespace (indicating multiple classes to sort)
|
pub fn tailwind_context_for_class_list(
|
||||||
pub fn tailwind_context_for_string_literal<'a>(
|
node: &impl GetSpan,
|
||||||
string: &AstNode<'_, 'a, StringLiteral<'a>>,
|
f: &JsFormatter<'_, '_>,
|
||||||
f: &JsFormatter<'_, 'a>,
|
|
||||||
) -> Option<TailwindContextEntry> {
|
) -> Option<TailwindContextEntry> {
|
||||||
f.context().tailwind_context().copied().filter(|ctx| {
|
f.context()
|
||||||
let text = f.source_text().text_for(string);
|
.tailwind_context()
|
||||||
|
.copied()
|
||||||
if ctx.disabled {
|
.filter(|_| f.source_text().text_for(node).bytes().any(|b| b.is_ascii_whitespace()))
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
text.as_bytes().iter().any(|&b| b.is_ascii_whitespace())
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if a JSX attribute is a Tailwind class attribute.
|
/// Checks if a JSX attribute is a Tailwind class attribute.
|
||||||
@@ -84,17 +78,16 @@ pub fn tailwind_context_for_jsx_attribute(
|
|||||||
.map(|opts| TailwindContextEntry::new(opts.preserve_whitespace))
|
.map(|opts| TailwindContextEntry::new(opts.preserve_whitespace))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether a template literal printed at this point has its whitespace, including line breaks, collapsed.
|
/// Whether a template literal printed at this point (under `tag` when tagged) has its line breaks collapsed.
|
||||||
/// - `callee` is the tag of a tagged template, or the callee of the call it's an argument of
|
/// - Reads what `TemplateElement` reads: the current Tailwind context, or the one a Tailwind `tag` pushes
|
||||||
/// - Reads what `TemplateElement` reads: the current Tailwind context, or the one a Tailwind `callee` pushes
|
|
||||||
pub fn tailwind_collapses_template_line_breaks(
|
pub fn tailwind_collapses_template_line_breaks(
|
||||||
callee: Option<&Expression<'_>>,
|
tag: Option<&Expression<'_>>,
|
||||||
f: &JsFormatter<'_, '_>,
|
f: &JsFormatter<'_, '_>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
f.context()
|
f.context()
|
||||||
.tailwind_context()
|
.tailwind_context()
|
||||||
.copied()
|
.copied()
|
||||||
.or_else(|| callee.and_then(|callee| tailwind_context_for_callee(callee, f)))
|
.or_else(|| tag.and_then(|tag| tailwind_context_for_callee(tag, f)))
|
||||||
.is_some_and(|ctx| !ctx.preserve_whitespace)
|
.is_some_and(|ctx| !ctx.preserve_whitespace)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user