Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
### Added

- Add language-aware class extraction for HTML, Svelte, Django, Jinja, Twig,
Liquid, Handlebars, ERB, EJS, PHP, Blade, Lit, and Ruby
Liquid, Handlebars, ERB, EJS, PHP, Blade, Lit, Ruby, JSX, and TSX
- Parse JSX and TSX with Winnow so only directly quoted `class` and `className`
attributes in real JSX elements are sorted
- Add `--language` to override source-language inference and
`--stdin-filename` to infer the language of standard input
- Preserve template expressions as opaque boundaries while independently
Expand Down Expand Up @@ -44,6 +46,8 @@
- Recognized markup languages only sort literal `class` and `className`
attributes in actual tags. Class-like text in comments, raw-text elements,
program strings, and dynamic or namespaced attributes is no longer rewritten
- `SourceLanguage` now includes a `Jsx` variant used for both JSX and TSX.
Downstream exhaustive matches must handle it

## [0.25.2] - 2026-07-08

Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,11 @@ Provide a filename when sorting standard input so RustyWind can infer its templa

- `cat Component.svelte | rustywind --stdin --stdin-filename Component.svelte`

Supported language profiles are HTML, Svelte, Astro, Django, Jinja, Twig, Liquid, Handlebars, ERB,
EJS, PHP, Blade, Lit, and Ruby. Astro frontmatter, expressions, dynamic class attributes, and
`class:list` directives are preserved while static quoted class attributes are sorted. Files with
Supported language profiles are HTML, JSX/TSX, Svelte, Astro, Django, Jinja, Twig, Liquid,
Handlebars, ERB, EJS, PHP, Blade, Lit, and Ruby. JSX and TSX program text, comments, and dynamic
attributes are preserved while directly quoted `class` and `className` attributes are sorted.
Astro frontmatter, expressions, dynamic class attributes, and `class:list` directives are
preserved while static quoted class attributes are sorted. Files with
unrecognized extensions use conservative legacy-compatible extraction: simple static class
attributes are sorted, while template-looking attributes remain unchanged. Because a plain `.html`
extension does not identify its template engine, attributes with template syntax are left unchanged
Expand Down Expand Up @@ -160,7 +162,7 @@ Options:
-l, --language <LANGUAGE>
Source language to use for all inputs, overriding filename inference

[possible values: html, svelte, astro, django, jinja, twig, liquid, handlebars, erb, ejs, php, blade, lit, ruby]
[possible values: html, svelte, astro, jsx, tsx, django, jinja, twig, liquid, handlebars, erb, ejs, php, blade, lit, ruby]

-f, --stdin-filename <PATH>
Filename used to infer the source language of stdin
Expand Down
12 changes: 12 additions & 0 deletions rustywind-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,18 @@ mod tests {
assert!(matches!(cli.language, Some(CliSourceLanguage::Svelte)));
}

#[test]
fn jsx_and_tsx_cli_values_share_the_jsx_profile() {
for language in ["jsx", "tsx"] {
let cli = Cli::try_parse_from(["rustywind", "--language", language, "input.js"])
.expect("JSX language alias should parse");
assert!(matches!(
cli.language,
Some(CliSourceLanguage::Jsx | CliSourceLanguage::Tsx)
));
}
}

#[test]
fn parses_stdin_filename_for_language_inference() {
let cli = Cli::try_parse_from(["rustywind", "--stdin", "-f", "components/card.blade.php"])
Expand Down
13 changes: 13 additions & 0 deletions rustywind-cli/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ pub enum CliSourceLanguage {
Html,
Svelte,
Astro,
Jsx,
Tsx,
Django,
Jinja,
Twig,
Expand All @@ -76,6 +78,7 @@ impl From<CliSourceLanguage> for SourceLanguage {
CliSourceLanguage::Html => Self::Html,
CliSourceLanguage::Svelte => Self::Svelte,
CliSourceLanguage::Astro => Self::Astro,
CliSourceLanguage::Jsx | CliSourceLanguage::Tsx => Self::Jsx,
CliSourceLanguage::Django => Self::Django,
CliSourceLanguage::Jinja => Self::Jinja,
CliSourceLanguage::Twig => Self::Twig,
Expand Down Expand Up @@ -290,4 +293,14 @@ mod tests {
);
assert_eq!(resolve_source_language(None, None), SourceLanguage::Unknown);
}

#[test]
fn jsx_and_tsx_filenames_share_the_jsx_profile() {
for path in ["component.jsx", "component.tsx"] {
assert_eq!(
resolve_source_language(None, Some(Path::new(path))),
SourceLanguage::Jsx
);
}
}
}
4 changes: 4 additions & 0 deletions rustywind-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

- Add first-class Astro parsing for static quoted class attributes while preserving frontmatter,
expressions, `class:list`, raw content, and component semantics.
- Add first-class Winnow parsing for directly quoted JSX and TSX `class` and `className`
attributes while preserving host-language program text and dynamic attributes.
- Add `-p`, `-l`, and `-f` aliases for `--tailwind-prefix`, `--language`, and
`--stdin-filename`.

Expand All @@ -19,6 +21,8 @@
### Breaking changes

- `SourceLanguage` now includes an `Astro` variant. Downstream exhaustive matches must handle it.
- `SourceLanguage` now includes a `Jsx` variant shared by `.jsx` and `.tsx` sources. Downstream
exhaustive matches must handle it.
- The default extractor for known markup languages only sorts actual quoted `class` and `className`
attributes. Class-looking text in comments, raw elements, non-markup program expressions, and
other attributes is no longer rewritten.
Expand Down
7 changes: 4 additions & 3 deletions rustywind-core/src/app.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use std::{borrow::Cow, fmt, ops::Range};

use crate::{
attribute_parser::{ClassAttribute, class_attributes},
class_wrapping::ClassWrapping,
consts::{VARIANT_SEARCHER, VARIANTS},
hybrid_sorter::HybridSorter,
markup_parser::{ClassAttribute, class_attributes, is_attribute_name_character},
markup_parser::is_attribute_name_character,
sorter::{FinderRegex, Sorter},
source::{
ClassValueAnalysis, SourceDocument, StaticRuns, analyze_class_value, is_plain_class_list,
Expand Down Expand Up @@ -199,7 +200,7 @@ impl RustyWind {
/// Checks whether a source document contains a sortable static class run
pub fn has_classes(&self, document: SourceDocument<'_>) -> bool {
if matches!(self.regex, FinderRegex::DefaultRegex)
&& document.language().markup_profile().is_some()
&& document.language().attribute_parser_profile().is_some()
{
return class_attributes(document).is_some_and(|attributes| {
attributes
Expand All @@ -219,7 +220,7 @@ impl RustyWind {
/// deduplication never crosses an expression boundary
pub fn sort_document<'a>(&self, document: SourceDocument<'a>) -> Cow<'a, str> {
if matches!(self.regex, FinderRegex::DefaultRegex)
&& document.language().markup_profile().is_some()
&& document.language().attribute_parser_profile().is_some()
{
return self.sort_structured_document(document);
}
Expand Down
28 changes: 28 additions & 0 deletions rustywind-core/src/attribute_parser.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use std::ops::Range;

use crate::{
jsx_parser, markup_parser,
source::{AttributeParserProfile, SourceDocument},
};

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ClassAttribute(Range<usize>);

impl ClassAttribute {
pub(crate) const fn new(value: Range<usize>) -> Self {
Self(value)
}

pub(crate) fn value_range(&self) -> Range<usize> {
self.0.clone()
}
}

pub(crate) fn class_attributes(document: SourceDocument<'_>) -> Option<Vec<ClassAttribute>> {
match document.language().attribute_parser_profile()? {
AttributeParserProfile::Markup(profile) => {
markup_parser::class_attributes(document.text(), profile)
}
AttributeParserProfile::Jsx => jsx_parser::class_attributes(document.text()),
}
}
Loading
Loading