Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add tailwind support for attr values #122

Merged
merged 6 commits into from
Jun 13, 2024
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
562 changes: 239 additions & 323 deletions Cargo.lock

Large diffs are not rendered by default.

46 changes: 32 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
# leptosfmt

[![crates.io](https://img.shields.io/crates/v/leptosfmt.svg)](https://crates.io/crates/leptosfmt)
[![build](https://img.shields.io/github/actions/workflow/status/bram209/leptosfmt/ci.yml)](https://github.com/bram209/leptosfmt/actions/workflows/ci.yml?query=branch%3Amain)
[![security](https://img.shields.io/github/actions/workflow/status/bram209/leptosfmt/security-audit.yml?label=%F0%9F%9B%A1%EF%B8%8F%20security%20audit)](https://github.com/bram209/leptosfmt/actions/workflows/security-audit.yml?query=branch%3Amain)
[![discord](https://img.shields.io/discord/1031524867910148188?color=%237289DA&label=discord%20%23leptosfmt)](https://discord.gg/YdRAhS7eQB)



A formatter for the leptos view! macro

All notable changes are documented in: [CHANGELOG.md](./CHANGELOG.md)
Expand All @@ -27,29 +26,42 @@ Arguments:
[INPUT_PATTERNS]... A space separated list of file, directory or glob

Options:
-m, --max-width <MAX_WIDTH> Maximum width of each line
-t, --tab-spaces <TAB_SPACES> Number of spaces per tab
-c, --config-file <CONFIG_FILE> Configuration file
-s, --stdin Format stdin and write to stdout
-r, --rustfmt Format with rustfmt after formatting with leptosfmt (requires stdin)
--override-macro-names
<OVERRIDE_MACRO_NAMES>... Override formatted macro names
-q, --quiet
--check Check if the file is correctly formatted. Exit with code 1 if not
-h, --help Print help
-V, --version Print version
-m, --max-width <MAX_WIDTH>
Maximum width of each line
-t, --tab-spaces <TAB_SPACES>
Number of spaces per tab
-c, --config-file <CONFIG_FILE>
Configuration file
-s, --stdin
Format stdin and write to stdout
-r, --rustfmt
Format with rustfmt after formatting with leptosfmt (requires stdin)
--override-macro-names <OVERRIDE_MACRO_NAMES>...
Override formatted macro names
-e, --experimental-tailwind
Format attributes with tailwind
--tailwind-attr-names <TAILWIND_ATTR_NAMES>...
Override attributes to be formatted with tailwind [default: class]
-q, --quiet

--check
Check if the file is correctly formatted. Exit with code 1 if not
-h, --help
Print help
-V, --version
Print version
```

## Using with Rust Analyzer

You can set the `rust-analyzer.rustfmt.overrideCommand` setting.


```json
"rust-analyzer.rustfmt.overrideCommand": ["leptosfmt", "--stdin", "--rustfmt"]
```

And **you must** configure `rustfmt` to use the correct edition, place a `rustfmt.toml` file in the root of your project:

```toml
edition = "2021"
# (optional) other config...
Expand All @@ -58,6 +70,7 @@ edition = "2021"
> Note: For VSCode users, I recommend to use workpsace settings (CMD + shift + p -> Open workspace settings), so that you can only configure `leptosfmt` for workpsaces that are using leptos. For Neovim users, I recommend using [neoconf.nvim](https://github.com/folke/neoconf.nvim) for managing project-local LSP configuration.

## Configuration

You can configure all settings through a `leptosfmt.toml` file.

```toml
Expand All @@ -67,6 +80,11 @@ indentation_style = "Auto" # "Tabs", "Spaces" or "Auto"
newline_style = "Auto" # "Unix", "Windows" or "Auto"
attr_value_brace_style = "WhenRequired" # "Always", "AlwaysUnlessLit", "WhenRequired" or "Preserve"
macro_names = [ "leptos::view", "view" ] # Macro names which will be formatted

# Attribute values can be formatted by custom formatters
# Every attribute name may only select one formatter (this might change later on)
[attr_values]
class = "Tailwind" # "Tailwind" is the only attribute value formatter available for now
```

To see what each setting does, the see [configuration docs](./docs/configuration.md)
Expand Down
21 changes: 21 additions & 0 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ struct Args {
#[arg(long, num_args=1.., value_delimiter= ' ')]
override_macro_names: Option<Vec<String>>,

/// Format attributes with tailwind
#[arg(short, long, default_value = "false")]
experimental_tailwind: bool,

/// Override attributes to be formatted with tailwind
#[arg(long, num_args=1.., value_delimiter= ' ', default_value = "class")]
tailwind_attr_names: Vec<String>,

#[arg(
short,
long,
Expand Down Expand Up @@ -262,6 +270,19 @@ fn create_settings(args: &Args) -> anyhow::Result<FormatterSettings> {
if let Some(macro_names) = args.override_macro_names.to_owned() {
settings.macro_names = macro_names;
}

if args.experimental_tailwind {
settings.attr_values = args
.tailwind_attr_names
.iter()
.map(|attr_name| {
(
attr_name.to_owned(),
leptosfmt_formatter::ExpressionFormatter::Tailwind,
)
})
.collect();
}
Ok(settings)
}

Expand Down
13 changes: 7 additions & 6 deletions formatter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ leptosfmt-prettyplease.workspace = true
rstml = "0.11.2"
syn = { workspace = true }
proc-macro2 = { workspace = true }
thiserror = "1.0.40"
thiserror = "1.0.61"
crop = "0.3.0"
serde = { version = "1.0.163", features = ["derive"] }
quote = "1.0.26"
serde = { version = "1.0.203", features = ["derive"] }
quote = "1.0.36"
rustywind_core = "0.1.2"

[dev-dependencies]
indoc = "2.0.1"
insta = "1.28.0"
quote = "1.0.26"
indoc = "2.0.5"
insta = "1.39.0"
quote = "1.0.36"
24 changes: 17 additions & 7 deletions formatter/src/formatter/attribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use syn::{spanned::Spanned, Expr};

use crate::{formatter::Formatter, AttributeValueBraceStyle as Braces};

use super::ExpressionFormatter;

impl Formatter<'_> {
pub fn attribute(&mut self, attribute: &NodeAttribute) {
self.flush_comments(attribute.span().start().line - 1);
Expand All @@ -16,24 +18,32 @@ impl Formatter<'_> {
self.node_name(&attribute.key);

if let Some(value) = attribute.value() {
let formatter = self
.settings
.attr_values
.get(&attribute.key.to_string())
.copied();

self.printer.word("=");
self.attribute_value(value);
self.attribute_value(value, formatter);
}
}

fn attribute_value(&mut self, value: &Expr) {
fn attribute_value(&mut self, value: &Expr, formatter: Option<ExpressionFormatter>) {
match (self.settings.attr_value_brace_style, value) {
(Braces::Always, syn::Expr::Block(_)) => self.node_value_expr(value, false, false),
(Braces::Always, syn::Expr::Block(_)) => {
self.node_value_expr(value, false, false, formatter)
}
(Braces::AlwaysUnlessLit, syn::Expr::Block(_) | syn::Expr::Lit(_)) => {
self.node_value_expr(value, false, true)
self.node_value_expr(value, false, true, formatter)
}
(Braces::Always | Braces::AlwaysUnlessLit, _) => {
self.printer.word("{");
self.node_value_expr(value, false, false);
self.node_value_expr(value, false, false, formatter);
self.printer.word("}");
}
(Braces::WhenRequired, _) => self.node_value_expr(value, true, true),
(Braces::Preserve, _) => self.node_value_expr(value, false, false),
(Braces::WhenRequired, _) => self.node_value_expr(value, true, true, formatter),
(Braces::Preserve, _) => self.node_value_expr(value, false, false, formatter),
}
}
}
Expand Down
30 changes: 20 additions & 10 deletions formatter/src/formatter/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use syn::{spanned::Spanned, Block, Expr, ExprBlock, ExprLit, LitStr};

use crate::{formatter::Formatter, get_text_beween_spans, view_macro::ViewMacroFormatter};

use super::ExpressionFormatter;

fn trim_start_with_max(str: &str, max_chars: usize) -> &str {
let mut chars = 0;
str.trim_start_matches(|c: char| {
Expand Down Expand Up @@ -65,27 +67,31 @@ impl Formatter<'_> {
if unwrap_single_expr_blocks
|| (unwrap_single_lit_blocks && matches!(single_expr, syn::Expr::Lit(_)))
{
self.expr(single_expr);
self.expr(single_expr, None);
} else {
self.printer.word("{");
self.expr(single_expr);
self.expr(single_expr, None);
self.printer.word("}");
}
return;
}

self.expr(&Expr::Block(ExprBlock {
attrs: vec![],
label: None,
block: block.clone(),
}))
self.expr(
&Expr::Block(ExprBlock {
attrs: vec![],
label: None,
block: block.clone(),
}),
None,
)
}

pub fn node_value_expr(
&mut self,
value: &syn::Expr,
unwrap_single_expr_blocks: bool,
unwrap_single_lit_blocks: bool,
formatter: Option<ExpressionFormatter>,
) {
// if single line expression, format as '{expr}' instead of '{ expr }' (prettyplease inserts a space)
if let syn::Expr::Block(expr_block) = value {
Expand All @@ -98,18 +104,22 @@ impl Formatter<'_> {
}
}

self.expr(value)
self.expr(value, formatter)
}

fn expr(&mut self, expr: &syn::Expr) {
fn expr(&mut self, expr: &syn::Expr, formatter: Option<ExpressionFormatter>) {
let span = expr.span();
self.flush_comments(span.start().line - 1);
if let syn::Expr::Lit(ExprLit {
lit: syn::Lit::Str(lit_str),
..
}) = expr
{
self.literal_str(lit_str);
if let Some(formatter) = formatter {
formatter.format(self, lit_str.value())
} else {
self.literal_str(lit_str);
}
return;
}

Expand Down
31 changes: 25 additions & 6 deletions formatter/src/formatter/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::collections::HashMap;
use std::fmt::Debug;

use crop::Rope;

Expand All @@ -10,6 +11,7 @@ mod expr;
mod fragment;
mod mac;
mod node;
mod tailwind;

pub use mac::format_macro;
pub use mac::{ParentIndent, ViewMacro};
Expand Down Expand Up @@ -40,26 +42,42 @@ pub enum NewlineStyle {
Windows,
}

#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
pub enum ExpressionFormatter {
Tailwind,
}

impl ExpressionFormatter {
pub fn format(&self, formatter: &mut Formatter, value: String) {
match self {
Self::Tailwind => formatter.tailwind_expr(value),
}
}
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct FormatterSettings {
// Maximum width of each line
/// Maximum width of each line
pub max_width: usize,

// Number of spaces per tab
/// Number of spaces per tab
pub tab_spaces: usize,

// Determines indentation style (tabs or spaces)
/// Determines indentation style (tabs or spaces)
pub indentation_style: IndentationStyle,

// Determines line ending (unix or windows)
/// Determines line ending (unix or windows)
pub newline_style: NewlineStyle,

// Determines placement of braces around single expression attribute values
/// Determines placement of braces around single expression attribute values
pub attr_value_brace_style: AttributeValueBraceStyle,

// Determines macros to be formatted. Default: leptos::view, view
/// Determines macros to be formatted. Default: leptos::view, view
pub macro_names: Vec<String>,

/// Determines whether to format attribute values with a specific formatter (e.g. tailwind)
pub attr_values: HashMap<String, ExpressionFormatter>,
}

impl Default for FormatterSettings {
Expand All @@ -71,6 +89,7 @@ impl Default for FormatterSettings {
indentation_style: IndentationStyle::Auto,
newline_style: NewlineStyle::Auto,
macro_names: vec!["leptos::view".to_string(), "view".to_string()],
attr_values: HashMap::new(),
}
}
}
Expand Down
18 changes: 18 additions & 0 deletions formatter/src/formatter/tailwind.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use rustywind_core::sorter::{self, FinderRegex};

use crate::Formatter;

impl Formatter<'_> {
pub fn tailwind_expr(&mut self, attr_value: String) {
static OPTIONS: sorter::Options = sorter::Options {
regex: FinderRegex::DefaultRegex,
sorter: sorter::Sorter::DefaultSorter,
allow_duplicates: true,
};

let sorted = sorter::sort_classes(&attr_value, &OPTIONS);
self.printer.word("\"");
self.printer.word(sorted);
self.printer.word("\"");
}
}
32 changes: 31 additions & 1 deletion formatter/src/source_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ fn format_source(
mod tests {
use indoc::indoc;

use crate::IndentationStyle;
use crate::{ExpressionFormatter, IndentationStyle};

use super::*;

Expand Down Expand Up @@ -753,4 +753,34 @@ mod tests {

assert_eq!(result, expected);
}

#[test]
fn tailwind() {
let source = indoc! {r#"
view! {
<button class="text-white px-4 sm:px-8 py-2 sm:py-3 bg-sky-700 hover:bg-sky-800">Test</button>
<button class="some non tailwind classes">Test</button>
<button class="some mixed classes non tailwind classes text-white px-4 sm:px-8 py-2 sm:py-3">Test</button>
}"#};

let result = format_file_source(
source,
&FormatterSettings {
attr_values: [("class".to_string(), ExpressionFormatter::Tailwind)]
.into_iter()
.collect(),
..Default::default()
},
)
.unwrap();
insta::assert_snapshot!(result, @r###"
view! {
<button class="py-2 px-4 text-white sm:py-3 sm:px-8 bg-sky-700 hover:bg-sky-800">Test</button>
<button class="some non tailwind classes">Test</button>
<button class="py-2 px-4 text-white sm:py-3 sm:px-8 some mixed classes non tailwind classes">
Test
</button>
}
"###);
}
}
Loading
Loading