Skip to content

Commit

Permalink
feat: add tailwind support for attr values (#122)
Browse files Browse the repository at this point in the history
* feat: add tailwind support for attr values

* update prettyplease

* update docs

* fix docs

* cli arg 'requires' does not work together with 'default_value'

* quote sorted tailwind class names
  • Loading branch information
bram209 authored Jun 13, 2024
1 parent f4229d6 commit b9ce861
Show file tree
Hide file tree
Showing 10 changed files with 411 additions and 368 deletions.
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

0 comments on commit b9ce861

Please sign in to comment.