diff --git a/Cargo.toml b/Cargo.toml
index 42443b9..88736b8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,7 +20,7 @@ documentation = "https://codebreather.com/oyelowo"
[workspace.dependencies]
-tw-macro = { path = "tw-macro" }
+twust = { path = "tw-macro" }
# tailwind = { path = "tailwind" }
proc-macro2 = "1.0.66"
diff --git a/README.md b/README.md
index a50c1ef..eb97e13 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,13 @@
-# `tw-macro`
+# `twust`
-A powerful Rust macro to validate TailwindCSS class names at compile-time.
+Twust is a powerful static checker in rust for TailwindCSS class names at
+compile-time.
-
-
-
+
## Table of Contents
@@ -25,18 +25,18 @@ A powerful Rust macro to validate TailwindCSS class names at compile-time.
## Overview
-`tw-macro` is a Rust procedural macro that provides compile-time validation for
-TailwindCSS class names. Leveraging the power of Rust's macro system, `tw-macro`
+`twust` is a Rust procedural macro that provides compile-time validation for
+TailwindCSS class names. Leveraging the power of Rust's macro system, `twust`
ensures that you only use valid TailwindCSS class names, preventing runtime
errors and promoting a more robust development experience.
## Installation
-Add `tw-macro` to your `Cargo.toml`:
+Add `twust` to your `Cargo.toml`:
```toml
[dependencies]
-tw-macro = "0.1.0"
+twust = "0.1.0"
```
## Usage
@@ -92,24 +92,24 @@ applications. However, its flexibility can also lead to potential pitfalls:
## Solution
-`tw-macro` addresses these challenges by offering:
+`twust` addresses these challenges by offering:
- **Compile-time Validation:** By checking the validity of TailwindCSS class
- names at compile time, `tw-macro` prevents invalid class names from making
+ names at compile time, `twust` prevents invalid class names from making
their way into the production code.
-- **Seamless Integration:** As a Rust macro, `tw-macro` integrates seamlessly
+- **Seamless Integration:** As a Rust macro, `twust` integrates seamlessly
into your Rust workflow, offering immediate feedback without the need for
external tools or manual validation.
-- **Plugin Support:** With tw-macro, you can easily integrate popular plugins
+- **Plugin Support:** With twust, you can easily integrate popular plugins
like daisyui by merely specifying them as a feature, ensuring a consistent and
extended development experience.
- **Effortless Code Reusability:** The ability to copy-paste and reuse your
TailwindCSS code without any manual mappings or transformations. Just wrap
your code with the macro, and you're set.
-- **Optimized Builds:** By ensuring only valid class names are used, `tw-macro`
+- **Optimized Builds:** By ensuring only valid class names are used, `twust`
helps in reducing the unnecessary bloat in the final CSS bundle.
## Features
@@ -168,16 +168,16 @@ nonexistent classes and code completion for available classes.
adds an external dependency, which might not be suitable for all projects,
especially those that want to minimize their dependency tree.
-### Approach with `tw-macro`
+### Approach with `twust`
-Our solution with `tw-macro` offers a more streamlined and integrated approach:
+Our solution with `twust` offers a more streamlined and integrated approach:
- **Simpler Setup:** Just add the macro to your project and start using it. No
need for external tools or additional configuration steps.
- **Real-time Validation:** Instead of generating static Rust code from
- TailwindCSS, `tw-macro` validates class names in real-time during the
+ TailwindCSS, `twust` validates class names in real-time during the
compilation process.
-- **No External Dependencies:** `tw-macro` is self-contained, meaning you don't
+- **No External Dependencies:** `twust` is self-contained, meaning you don't
need any external tools like the `tailwindcss` CLI.
- **Extensive Coverage:** We support all standard TailwindCSS class names,
@@ -231,5 +231,5 @@ to contribute to the code, please open an issue or pull request.
## License
-`tw-macro` is licensed under the MIT license. See the `LICENSE` file for
+`twust` is licensed under the MIT license. See the `LICENSE` file for
details.
diff --git a/examples/leptos-demo/Cargo.toml b/examples/leptos-demo/Cargo.toml
index a4c1671..0b0dfba 100644
--- a/examples/leptos-demo/Cargo.toml
+++ b/examples/leptos-demo/Cargo.toml
@@ -11,7 +11,7 @@ leptos_meta = { version = "0.5.0-beta2", features = ["csr", "nightly"] }
leptos_router = { version = "0.5.0-beta2", features = ["csr", "nightly"] }
log = "0.4"
gloo-net = { version = "0.2", features = ["http"] }
-tw-macro = { git = "https://github.com/oyelowo/tailwind-rust", features = ["daisyui"]}
+twust = { git = "https://github.com/oyelowo/twust", features = ["daisyui"] }
# dependecies for client (enable when csr or hydrate set)
diff --git a/examples/leptos-demo/src/app.rs b/examples/leptos-demo/src/app.rs
index 5c596a7..3f21728 100644
--- a/examples/leptos-demo/src/app.rs
+++ b/examples/leptos-demo/src/app.rs
@@ -1,7 +1,7 @@
use leptos::*;
use leptos_meta::*;
use leptos_router::*;
-use tw_macro::tw;
+use twust::tw;
#[component]
pub fn App() -> impl IntoView {
diff --git a/sss.md b/sss.md
deleted file mode 100644
index 36c36bd..0000000
--- a/sss.md
+++ /dev/null
@@ -1,1181 +0,0 @@
-Create a PR for these changes:
-
-Skip to content
-Oyelowo
-/
-tailwind-rust
-
-Type / to search
-
-Code
-Issues
-1
-Pull requests
-Actions
-Projects
-Wiki
-Security
-Insights
-Settings
-Open a pull request
-Create a new pull request by comparing changes across two branches. If you need to, you can also .
-
-...
-
- Able to merge. These branches can be automatically merged.
-@Oyelowo
-5 use a more robust parsing for tailwind classes
-
-
-Leave a comment
-No file chosen
-Attach files by dragging & dropping, selecting or pasting them.
-Remember, contributions to this repository should follow our GitHub Community Guidelines.
-Reviewers
-No reviews—at least 1 approving review is required.
-Assignees
-No one—
-Labels
-None yet
-Projects
-None yet
-Milestone
-No milestone
-Development
-Use Closing keywords in the description to automatically close issues
-
-Helpful resources
-GitHub Community Guidelines
- 1 contributor
- Commits 44
- Files changed 8
-Showing with 836 additions and 283 deletions.
- 1 change: 1 addition & 0 deletions1
-Cargo.toml
-@@ -26,6 +26,7 @@ tw-macro = { path = "tw-macro" }
-proc-macro2 = "1.0.66"
-quote = "1.0.33"
-syn = "2.0.29"
-nom = "7.1.3"
-static_assertions = "1.1.0"
-serde = { version = "1.0.188", features = ["derive"] }
-serde_json = "1.0.105"
- 15 changes: 10 additions & 5 deletions15
-tailwind/src/lib.rs
-@@ -82,9 +82,9 @@ fn _unsupported_media_query() {}
-///
-/// ```compile_fail
-/// use tw_macro::tw;
-/// tw!("px-[45]");
-/// tw!("px-45]");
-/// ```
-fn _missing_unit_after_arbitrary_value() {}
-fn _malformed_arbitrary_value() {}
-
-/// Invalid group usage.
-///
-@@ -166,10 +166,11 @@ fn _happy_paths() {
- let _classnames = tw!("text-blue-600/[.07]");
-
- // tw!("[something]");
- let _classnames = tw!("px-[-45px]");
- let _classnames = tw!("px-[45.43px]");
- let _classnames = tw!("px-[-45cm]");
- let _classnames = tw!("px-[-45rem]");
- let _classnames = tw!("px-[-45em]");
- let _classnames = tw!("px-[45em]");
- let _classnames = tw!("px-[-45%]");
- let _classnames = tw!("px-[-45in]");
- let _classnames = tw!("px-[-45vh]");
-@@ -178,20 +179,24 @@ fn _happy_paths() {
- let _classnames = tw!("px-[-45vmax]");
- let _classnames = tw!("px-[-45mm]");
- let _classnames = tw!("px-[-45pc]");
- let _classnames = tw!("px-[0px]");
- let _classnames = tw!("px-[0]");
- let _classnames = tw!("px-[45px]");
- let _classnames = tw!("px-[45cm]");
- let _classnames = tw!("px-[45rem]");
- let _classnames = tw!("px-[45em]");
- tw!("bg-taxvhiti");
-
- // let _classnames = tw!("px-[45em]");
- let _classnames = tw!("px-[45%]");
- let _classnames = tw!("px-[45in]");
- let _classnames = tw!("px-[45vh]");
- let _classnames = tw!("px-[45vw]");
- let _classnames = tw!("px-[45vmin]");
- let _classnames = tw!("px-[45vmax]");
- let _classnames = tw!("px-[45mm]");
- let _classnames = tw!("px-[45.5mm]");
- let _classnames = tw!("px-[45pc]");
- let _classnames = tw!("py-[0]");
- let _classnames = tw!("px-[45pc]");
- let _classnames = tw!("-px-[45pc]");
- let _classnames = tw!("hover:[mask-type:alpha]");
- let _classnames = tw!(
- 116 changes: 116 additions & 0 deletions116
-tailwind/src/main.rs
- 1 change: 1 addition & 0 deletions1
-tw-macro/Cargo.toml
- 958 changes: 680 additions & 278 deletions958
-tw-macro/src/lib.rs
-@@ -4,116 +4,31 @@
- * Copyright (c) 2023 Oyelowo Oyedayo
- * Licensed under the MIT license
- */
-
-use nom::{
- branch::alt,
- bytes::complete::{tag, take_until, take_while1},
- character::complete::{digit1, multispace0, multispace1},
- combinator::{all_consuming, not, opt, recognize},
- multi::separated_list0,
- number,
- sequence::{preceded, tuple},
- IResult,
-};
-use syn::{parse_macro_input, LitStr};
-mod config;
-mod plugins;
-mod tailwind;
-use tailwind::{
- lengthy::LENGTHY, modifiers::get_modifiers, tailwind_config::CustomisableClasses,
- valid_baseclass_names::VALID_BASECLASS_NAMES,
- colorful::COLORFUL_BASECLASSES, lengthy::LENGTHY, modifiers::get_modifiers,
- tailwind_config::CustomisableClasses, valid_baseclass_names::VALID_BASECLASS_NAMES,
-};
-
-use config::{get_classes, noconfig::UNCONFIGURABLE, read_tailwind_config};
-use proc_macro::TokenStream;
-use regex::{self, Regex};
-use tailwind::signable::SIGNABLES;
-// use tailwindcss_core::parser::{Extractor, ExtractorOptions};
-
-#[proc_macro]
-pub fn tw(raw_input: TokenStream) -> TokenStream {
- let r_input = raw_input.clone();
- let input = parse_macro_input!(r_input as LitStr);
- let (modifiers, valid_class_names) = match setup(&input) {
- Ok(value) => value,
- Err(value) => {
- return syn::Error::new_spanned(input, value)
- .to_compile_error()
- .into()
- }
- };
-
- for word in input.value().split_whitespace() {
- let (last_word_signed, last_word_unsigned) = get_last_word_types(word);
-
- // modifiers e.g hover: in
- // hover:[mask-type:alpha]
- let is_valid_arb_prop = is_valid_arb_prop(word, &modifiers);
-
- let is_valid_class =
- is_valid_class(is_valid_arb_prop, &valid_class_names, last_word_unsigned);
-
- let (base_classname, arbitrary_value_with_bracket) =
- last_word_unsigned.split_once("-[").unwrap_or_default();
-
- let is_valid_negative_baseclass = is_valid_negative_baseclass(
- &valid_class_names,
- last_word_unsigned,
- last_word_signed,
- is_valid_arb_prop,
- );
-
- let prefix_is_valid_tailwind_keyword = VALID_BASECLASS_NAMES.contains(&base_classname);
- let is_arbitrary_value =
- prefix_is_valid_tailwind_keyword && arbitrary_value_with_bracket.ends_with(']');
-
- let arbitrary_value = arbitrary_value_with_bracket.trim_end_matches(']');
- let is_lengthy_class = LENGTHY.contains(&base_classname);
- let is_valid_length = is_arbitrary_value
- && is_lengthy_class
- && (is_valid_length(arbitrary_value) || is_valid_calc(arbitrary_value));
-
- let has_arb_variant = has_arb_variant(word);
-
- let is_valid_opacity = is_valid_opacity(last_word_unsigned, &valid_class_names);
-
- if (is_valid_class && is_valid_modifier(word, &modifiers))
- || is_valid_negative_baseclass
- || (!is_lengthy_class && is_arbitrary_value)
- || is_valid_length
- || is_valid_arb_prop
- || has_arb_variant
- || is_valid_opacity
- || is_valid_group_classname(last_word_unsigned)
- || is_validate_modifier_or_group(word, &modifiers, &valid_class_names)
- {
- // if check_word(word, false).is_empty() {
- // return syn::Error::new_spanned(input, format!("Invalid string: {}", word))
- // .to_compile_error()
- // .into();
- // }
- } else {
- return syn::Error::new_spanned(input, format!("Invalid string: {word}"))
- .to_compile_error()
- .into();
- }
- }
-
- raw_input
-}
-
-// fn check_word(input: &str, loose: bool) -> Vec<&str> {
-// Extractor::unique_ord(
-// input.as_bytes(),
-// ExtractorOptions {
-// preserve_spaces_in_arbitrary: loose,
-// },
-// )
-// .into_iter()
-// .map(|s| unsafe { std::str::from_utf8_unchecked(s) })
-// .collect()
-// }
-
-fn is_valid_length(value: &str) -> bool {
- let re = regex::Regex::new(r"^(-?\d+(\.?\d+)?(px|em|rem|%|cm|mm|in|pt|pc|vh|vw|vmin|vmax)|0)$")
- .expect("Invalid regex");
- re.is_match(value)
-}
-
-fn is_valid_calc(value: &str) -> bool {
- let re = regex::Regex::new(r"^calc\([^)]+\)$").expect("Invalid regex");
- re.is_match(value)
-}
-
-fn setup(input: &LitStr) -> Result<(Vec, Vec), TokenStream> {
- let config = &(match read_tailwind_config() {
- Ok(config) => config,
-@@ -146,199 +61,686 @@ fn setup(input: &LitStr) -> Result<(Vec, Vec), TokenStream> {
- Ok((modifiers, valid_class_names))
-}
-
-fn get_last_word_types(word: &str) -> (&str, &str) {
- let modifiers_and_class = word.split(':');
-fn get_classes_straight() -> Vec {
- get_classes(&read_tailwind_config().unwrap())
-}
-
-fn is_valid_classname(class_name: &str) -> bool {
- get_classes_straight().contains(&class_name.to_string())
-}
-
-fn is_valid_modifier(modifier: &str) -> bool {
- get_modifiers(&read_tailwind_config().unwrap()).contains(&modifier.to_string())
-}
-
-fn parse_predefined_tw_classname(input: &str) -> IResult<&str, ()> {
- let (input, class_name) = recognize(|i| {
- // Considering a Tailwind class consists of alphanumeric, dashes, and slash
- nom::bytes::complete::is_a(
- "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-./",
- )(i)
- })(input)?;
-
- let is_signable = SIGNABLES.iter().any(|s| {
- class_name
- .strip_prefix('-')
- .unwrap_or(class_name)
- .starts_with(s)
- });
-
- if is_signable && is_valid_classname(class_name.strip_prefix('-').unwrap_or(class_name))
- || !is_signable && is_valid_classname(class_name)
- {
- Ok((input, ()))
- } else {
- Err(nom::Err::Error(nom::error::Error::new(
- input,
- nom::error::ErrorKind::Tag,
- )))
- }
-}
-
-fn is_ident_char(c: char) -> bool {
- c.is_alphanumeric() || c == '_' || c == '-'
-}
-
-fn is_lengthy_classname(class_name: &str) -> bool {
- LENGTHY.contains(&class_name.strip_prefix('-').unwrap_or(class_name))
-}
-
-// Custom number parser that handles optional decimals and signs, and scientific notation
-fn float_strict(input: &str) -> IResult<&str, f64> {
- let (input, number) = recognize(tuple((
- opt(alt((tag("-"), tag("+")))),
- digit1,
- opt(preceded(tag("."), digit1)),
- opt(tuple((
- alt((tag("e"), tag("E"))),
- opt(alt((tag("-"), tag("+")))),
- digit1,
- ))),
- )))(input)?;
-
- let float_val: f64 = number.parse().unwrap();
- Ok((input, float_val))
-}
-
-fn parse_length_unit(input: &str) -> IResult<&str, String> {
- let (input, number) = float_strict(input)?;
- let (input, unit) = {
- // px|em|rem|%|cm|mm|in|pt|pc|vh|vw|vmin|vmax
- alt((
- tag("px"),
- tag("em"),
- tag("rem"),
- tag("%"),
- tag("cm"),
- tag("mm"),
- tag("in"),
- tag("pt"),
- tag("pc"),
- tag("vh"),
- tag("vw"),
- tag("vmin"),
- tag("vmax"),
- // TODO: Should i allow unitless values? Would need something like this in caller
- // location if so:
- // let (input, _) = alt((parse_length_unit, parse_number))(input)?;
- tag(""),
- ))
- }(input)?;
- Ok((input, format!("{}{}", number, unit)))
-}
-
- // let is_arbitrary_property = word.starts_with('[') && word.ends_with(']');
- let last_word_signed = modifiers_and_class.clone().last().unwrap_or_default();
- let last_word_unsigned = last_word_signed
- .strip_prefix('-')
- .unwrap_or(last_word_signed);
-// text-[22px]
-fn lengthy_arbitrary_classname(input: &str) -> IResult<&str, ()> {
- let (input, class_name) = take_until("-[")(input)?;
- let (input, _) = if is_lengthy_classname(class_name) {
- Ok((input, ()))
- } else {
- Err(nom::Err::Error(nom::error::Error::new(
- input,
- nom::error::ErrorKind::Tag,
- )))
- }?;
-
- // arbitrary value
- let (input, _) = tag("-")(input)?;
- let (input, _) = tag("[")(input)?;
- // is number
- let (input, _) = parse_length_unit(input)?;
- let (input, _) = tag("]")(input)?;
- Ok((input, ()))
-}
-
- (last_word_signed, last_word_unsigned)
-// #bada55
-fn parse_hex_color(input: &str) -> IResult<&str, String> {
- let (input, _) = tag("#")(input)?;
- let (input, color) = take_while1(|c: char| c.is_ascii_hexdigit())(input)?;
- let (input, _) = if color.chars().count() == 3 || color.chars().count() == 6 {
- Ok((input, ()))
- } else {
- Err(nom::Err::Error(nom::error::Error::new(
- input,
- nom::error::ErrorKind::Tag,
- )))
- }?;
- let color = format!("#{}", color);
- Ok((input, color))
-}
-
-fn is_valid_modifier(word: &str, modifiers: &[String]) -> bool {
- let modifiers_and_class = word.split(':');
- let modifiers_from_word = modifiers_and_class
- .clone()
- .take(modifiers_and_class.count() - 1)
- .collect::>();
- modifiers_from_word
-fn parse_u8(input: &str) -> IResult<&str, u8> {
- let (input, num) = number::complete::double(input)?;
- let input = match num as u32 {
- 0..=255 => input,
- _ => {
- return Err(nom::Err::Error(nom::error::Error::new(
- input,
- nom::error::ErrorKind::Tag,
- )))
- }
- };
- Ok((input, num as u8))
-}
-
-// rgb(255, 255, 255) rgb(255_255_255)
-fn parse_rgb_color(input: &str) -> IResult<&str, String> {
- let (input, _) = tag("rgb(")(input)?;
- let (input, r) = parse_u8(input)?;
- let (input, _) = alt((tag(","), tag("_")))(input)?;
- let (input, g) = parse_u8(input)?;
- let (input, _) = alt((tag(","), tag("_")))(input)?;
- let (input, b) = parse_u8(input)?;
- let (input, _) = tag(")")(input)?;
- let color = format!("rgb({}, {}, {})", r, g, b);
- Ok((input, color))
-}
-
-// rgba(255, 255, 255, 0.5) rgba(255_255_255_0.5)
-fn parse_rgba_color(input: &str) -> IResult<&str, String> {
- let (input, _) = tag("rgba(")(input)?;
- let (input, r) = parse_u8(input)?;
- let (input, _) = alt((tag(","), tag("_")))(input)?;
- let (input, g) = parse_u8(input)?;
- let (input, _) = alt((tag(","), tag("_")))(input)?;
- let (input, b) = parse_u8(input)?;
- let (input, _) = alt((tag(","), tag("_")))(input)?;
- let (input, a) = number::complete::double(input)?;
- let (input, _) = tag(")")(input)?;
- let color = format!("rgba({}, {}, {}, {})", r, g, b, a);
- Ok((input, color))
-}
-
-fn is_colorful_baseclass(class_name: &str) -> bool {
- COLORFUL_BASECLASSES.contains(&class_name)
-}
-
-// text-[#bada55]
-fn colorful_arbitrary_baseclass(input: &str) -> IResult<&str, ()> {
- let (input, class_name) = take_until("-[")(input)?;
- let (input, _) = if is_colorful_baseclass(class_name) {
- Ok((input, ()))
- } else {
- Err(nom::Err::Error(nom::error::Error::new(
- input,
- nom::error::ErrorKind::Tag,
- )))
- }?;
-
- // arbitrary value
- let (input, _) = tag("-")(input)?;
- let (input, _) = tag("[")(input)?;
- let (input, _) = alt((parse_hex_color, parse_rgb_color, parse_rgba_color))(input)?;
- let (input, _) = tag("]")(input)?;
- Ok((input, ()))
-}
-
-// e.g: [mask-type:alpha]
-fn kv_pair_classname(input: &str) -> IResult<&str, ()> {
- let (input, _) = tag("[")(input)?;
- let (input, _) = take_while1(is_ident_char)(input)?;
- let (input, _) = tag(":")(input)?;
- let (input, _) = take_until("]")(input)?;
- let (input, _) = tag("]")(input)?;
- Ok((input, ()))
-}
-
-// before:content-['Festivus']
-fn arbitrary_content(input: &str) -> IResult<&str, ()> {
- let (input, _) = tag("content-['")(input)?;
- let (input, _) = take_until("']")(input)?;
- let (input, _) = tag("']")(input)?;
- Ok((input, ()))
-}
-
-// content-[>] content-[<]
-fn arbitrary_with_arrow(input: &str) -> IResult<&str, ()> {
- let (input, _) = take_while1(is_ident_char)(input)?;
- let (input, _) = tag("[")(input)?;
- let (input, _) = alt((tag(">"), tag("<")))(input)?;
- let (input, _) = take_until("]")(input)?;
- let (input, _) = tag("]")(input)?;
- Ok((input, ()))
-}
-
-// bg-black/25
-fn predefined_colorful_opacity(input: &str) -> IResult<&str, ()> {
- let input = if COLORFUL_BASECLASSES
- .iter()
- .all(|modifier| modifiers.contains(&modifier.to_string()))
-}
-
-fn is_valid_opacity(last_word_unsigned: &str, valid_class_names: &[String]) -> bool {
- let is_valid_opacity = {
- let (class_name, opacity_raw) = last_word_unsigned.split_once('/').unwrap_or_default();
- let opacity_arb = opacity_raw
- .trim_start_matches('[')
- .trim_end_matches(']')
- .parse::();
- let is_valid_number =
- opacity_arb.is_ok_and(|opacity_num| (0.0..=100.0).contains(&opacity_num));
- valid_class_names.contains(&class_name.to_string()) && is_valid_number
- .any(|cb| input.trim().starts_with(cb))
- {
- input
- } else {
- return Err(nom::Err::Error(nom::error::Error::new(
- input,
- nom::error::ErrorKind::Tag,
- )));
- };
- is_valid_opacity
-}
-
-fn has_arb_variant(word: &str) -> bool {
- // lg:[&:nth-child(3)]:hover:underline
- // [&_p]:mt-4
- // flex [@supports(display:grid)]:grid
- // [@media(any-hover:hover){&:hover}]:opacity-100
- let has_arb_variant = {
- // lg:[&:nth-child(3)]:hover:underline => :nth-child(3)
- // [&_p]:mt-4 => _p
- let mut ampersand_variant_selector =
- word.split("[@").last().unwrap_or_default().split("]:");
- let and_variant_selector = word.split("[&").last().unwrap_or_default().split("]:");
- let is_valid_arbitrary_variant_selector = ampersand_variant_selector.clone().count() >= 2
- && !ampersand_variant_selector
- .next()
- .unwrap_or_default()
- .is_empty();
- let is_valid_arbitrary_variant_queries = and_variant_selector.clone().count() >= 2
- && !and_variant_selector
- .clone()
- .last()
- .unwrap_or_default()
- .split("]:")
- .next()
- .unwrap_or_default()
- .is_empty();
- let is_query = word.starts_with("[@");
-
- is_valid_arbitrary_variant_selector || is_valid_arbitrary_variant_queries || is_query
- // &&
- // ((!is_query && !word.split("[&").next().unwrap_or_default().is_empty() && word.split(":[&").count() >= 2) || is_query)
- let (input, _) = take_while1(|char| is_ident_char(char) && char != '/')(input)?;
- // let (input, _) = take_until("/")(input)?;
- let (input, _) = tag("/")(input)?;
-
- let (input, num) = number::complete::double(input)?;
- let input = match num as u8 {
- 0..=100 => input,
- _ => {
- return Err(nom::Err::Error(nom::error::Error::new(
- input,
- nom::error::ErrorKind::Tag,
- )))
- }
- };
- has_arb_variant
-}
-
-fn is_valid_negative_baseclass(
- valid_class_names: &[String],
- last_word_unsigned: &str,
- last_word_signed: &str,
- is_valid_arb_prop: bool,
-) -> bool {
- let is_valid_negative_baseclass = {
- // tw!("-m-4 p-4 p-4");
- (valid_class_names.contains(&last_word_unsigned.to_string())
- && last_word_signed.starts_with('-')
- && SIGNABLES
- .iter()
- .any(|s| (last_word_unsigned.starts_with(s))))
- || (is_valid_arb_prop
- && last_word_signed.starts_with('-')
- && SIGNABLES.iter().any(|s| last_word_unsigned.starts_with(s)))
-
- Ok((input, ()))
-}
-
-// bg-black/[27] bg-black/[27%]
-fn arbitrary_opacity(input: &str) -> IResult<&str, ()> {
- let input = if COLORFUL_BASECLASSES
- .iter()
- .any(|cb| input.trim().starts_with(cb))
- {
- input
- } else {
- return Err(nom::Err::Error(nom::error::Error::new(
- input,
- nom::error::ErrorKind::Tag,
- )));
- };
- is_valid_negative_baseclass
-}
-
-fn is_valid_class(
- is_valid_arb_prop: bool,
- valid_class_names: &[String],
- last_word_unsigned: &str,
-) -> bool {
- !is_valid_arb_prop && valid_class_names.contains(&last_word_unsigned.to_string())
-}
-
-fn is_valid_arb_prop(word: &str, modifiers: &[String]) -> bool {
- // TODO: check the first and the last character are not open and close brackets
- // respectively i.e arbitrary property e.g [mask_type:aplha];
- // hover:[mask-type:alpha];
- let mut word_for_arb_prop = word.split(":[");
-
- word_for_arb_prop
- .next()
- // e.g for hover:[mask-type:alpha], this will be hover,
- // for [mask-type:alpha], this will be [mask-type:alpha]
- .is_some_and(|modifiers_or_full_arb_prop| {
- let is_arbitrary_property = modifiers_or_full_arb_prop.starts_with('[') && modifiers_or_full_arb_prop.ends_with(']');
-
- let is_valid = if is_arbitrary_property {
- modifiers_or_full_arb_prop.matches('[').count() == 1 &&
- modifiers_or_full_arb_prop.matches(']').count() == 1 &&
- modifiers_or_full_arb_prop
- .trim_start_matches('[')
- .trim_end_matches(']')
- .split(':')
- .count() == 2
- } else {
- // e.g mask-type:alpha] in hover:[mask-type:alpha]
- let full_arb_prop = word_for_arb_prop.next().unwrap_or_default();
- // e.g for single, hover in hover:[mask-type:alpha]
- // for multiple, hover:first:last, in hover:first:last:[mask-type:alpha]
- modifiers_or_full_arb_prop
- .split(':')
- .all(|modifier| modifiers.contains(&modifier.to_string())) &&
- full_arb_prop.matches(']').count() == 1 &&
- full_arb_prop
- .trim_end_matches(']')
- .split(':')
- .count() == 2
-
- };
- is_valid
- })
- ||
- // value e.g [mask-type:alpha] in hover:[mask-type:alpha]
- // potential addition checks(probably not a good idea. Imagine a new css property, we would
- // have to open a PR for every new or esoteric css property.)
- word_for_arb_prop.next().is_some_and(|value| {
- value.ends_with(']')
- && value.split(':').count() == 2
- // We had already split by ":[", so there should be no "[" anymore
- && value.matches('[').count() == 0
- && value.matches(']').count() == 1
- })
-}
-
-fn is_valid_group_pattern(modifier: &str, valid_modifiers: &[String]) -> bool {
- let parts: Vec<&str> = modifier.split('/').collect();
- let group_modifier = parts[0];
- parts.len() == 2
- && valid_modifiers.contains(&group_modifier.to_string())
- && group_modifier.starts_with("group")
-}
-
-// tw!("group/edit invisible hover:bg-slate-200 group-hover/item:visible");
-// tw!("group-[:nth-of-type(3)_&]:block group-hover/edit:text-gray-700 group-[:nth-of-type(3)_&]:block");
-fn is_validate_modifier_or_group(
- word: &str,
- valid_modifiers: &[String],
- valid_class_names: &[String],
-) -> bool {
- let valid_arb_group = word.split(':').collect::>();
- let modifiers = &valid_arb_group[..valid_arb_group.len() - 1];
- let last_word = valid_arb_group.last().unwrap_or(&"");
- let is_valid_last_word =
- is_valid_string(last_word) && valid_class_names.contains(&last_word.to_string());
-
- for modifier in modifiers {
- if modifier.starts_with("group") {
- if !is_valid_group_pattern(modifier, valid_modifiers) && is_valid_last_word {
- return false;
- }
- } else if !valid_modifiers.contains(&modifier.to_string()) && is_valid_last_word {
- return false;
- let (input, _) = take_while1(|char| is_ident_char(char) && char != '/')(input)?;
- let (input, _) = tag("/")(input)?;
- let (input, _) = tag("[")(input)?;
- // 0-100 integer
- let (input, num) = number::complete::double(input)?;
- let input = match num as u8 {
- 0..=100 => input,
- _ => {
- return Err(nom::Err::Error(nom::error::Error::new(
- input,
- nom::error::ErrorKind::Tag,
- )))
- }
- };
- let (input, _) = opt(tag("%"))(input)?;
- let (input, _) = tag("]")(input)?;
- Ok((input, ()))
-}
-
-// bg-[url('/img/down-arrow.svg')]
-fn bg_arbitrary_url(input: &str) -> IResult<&str, ()> {
- // prefixed by baseclass
- let input = if COLORFUL_BASECLASSES
- .iter()
- .any(|cb| input.trim().starts_with(cb))
- {
- input
- } else {
- return Err(nom::Err::Error(nom::error::Error::new(
- input,
- nom::error::ErrorKind::Tag,
- )));
- };
- let (input, _) = take_while1(|char| is_ident_char(char) && char != '[')(input)?;
- let (input, _) = tag("[")(input)?;
- let (input, _) = tag("url('")(input)?;
- let (input, _) = take_until("')")(input)?;
- let (input, _) = tag("')")(input)?;
- let (input, _) = tag("]")(input)?;
- Ok((input, ()))
-}
-
-// grid-cols-[fit-content(theme(spacing.32))]
-fn arbitrary_css_value(input: &str) -> IResult<&str, ()> {
- // is prefixed by valid base class
- // take until -[
- let (input, base_class) = take_until("-[")(input)?;
- let input = if VALID_BASECLASS_NAMES
- .iter()
- .any(|cb| base_class.trim().eq(*cb))
- {
- input
- } else {
- return Err(nom::Err::Error(nom::error::Error::new(
- base_class,
- nom::error::ErrorKind::Tag,
- )));
- };
- let (input, _) = tag("-[")(input)?;
- let (input, _) = not(alt((
- tag("--"),
- tag("var(--"),
- // :var(--
- )))(input)?;
- let (input, _) = take_while1(|char| is_ident_char(char) && char != '(')(input)?;
- let (input, _) = tag("(")(input)?;
- let (input, _) = take_until(")]")(input)?;
-
- // allow anything inthe brackets
- let (input, _) = take_until("]")(input)?;
- let (input, _) = tag("]")(input)?;
- Ok((input, ()))
-}
-
-// bg-[--my-color]
-fn arbitrary_css_var(input: &str) -> IResult<&str, ()> {
- // is prefixed by valid base class
- let input = if VALID_BASECLASS_NAMES
- .iter()
- .any(|cb| input.trim().starts_with(cb))
- {
- input
- } else {
- return Err(nom::Err::Error(nom::error::Error::new(
- input,
- nom::error::ErrorKind::Tag,
- )));
- };
- let (input, _) = take_while1(|char| is_ident_char(char) && char != '[')(input)?;
- let (input, _) = tag("[")(input)?;
- let (input, _) = tag("--")(input)?;
- let (input, _) = take_while1(|char| is_ident_char(char) && char != ']')(input)?;
- let (input, _) = tag("]")(input)?;
- Ok((input, ()))
-}
-// text-[var(--my-var)]
-fn arbitrary_css_var2(input: &str) -> IResult<&str, ()> {
- // is prefixed by valid base class
- let input = if VALID_BASECLASS_NAMES
- .iter()
- .any(|cb| input.trim().starts_with(cb))
- {
- input
- } else {
- return Err(nom::Err::Error(nom::error::Error::new(
- input,
- nom::error::ErrorKind::Tag,
- )));
- };
- let (input, _) = take_while1(|char| is_ident_char(char) && char != '[')(input)?;
- let (input, _) = tag("[")(input)?;
- let (input, _) = tag("var(--")(input)?;
- let (input, _) = take_while1(|char| is_ident_char(char) && char != ')')(input)?;
- let (input, _) = tag(")]")(input)?;
- Ok((input, ()))
-}
-
-// text-[length:var(--my-var)]
-fn arbitrary_css_var3(input: &str) -> IResult<&str, ()> {
- // is prefixed by valid base class
- let input = if VALID_BASECLASS_NAMES
- .iter()
- .any(|cb| input.trim().starts_with(cb))
- {
- input
- } else {
- return Err(nom::Err::Error(nom::error::Error::new(
- input,
- nom::error::ErrorKind::Tag,
- )));
- };
- let (input, _) = take_while1(|char| is_ident_char(char) && char != '[')(input)?;
- let (input, _) = tag("[")(input)?;
- let (input, _) = take_while1(|char| is_ident_char(char) && char != ':')(input)?;
- let (input, _) = tag(":")(input)?;
- let (input, _) = tag("var(--")(input)?;
- let (input, _) = take_while1(|char| is_ident_char(char) && char != ')')(input)?;
- let (input, _) = tag(")]")(input)?;
- Ok((input, ()))
-}
-
-// group/edit
-fn arbitrary_group_classname(input: &str) -> IResult<&str, ()> {
- let (input, _) = alt((tag("group"),))(input)?;
- let (input, _) = tag("/")(input)?;
- let (input, _) = take_while1(is_ident_char)(input)?;
- Ok((input, ()))
-}
-
-fn parse_single_tw_classname(input: &str) -> IResult<&str, ()> {
- alt((
- // bg-[url('/what_a_rush.png')]
- bg_arbitrary_url,
- // bg-black/25
- predefined_colorful_opacity,
- // group/edit
- arbitrary_group_classname,
- // bg-black/[27]
- arbitrary_opacity,
- // btn
- parse_predefined_tw_classname,
- // [mask-type:luminance] [mask-type:alpha]
- kv_pair_classname,
- // text-[22px]
- lengthy_arbitrary_classname,
- // text-[#bada55]
- colorful_arbitrary_baseclass,
- // before:content-['Festivus']
- arbitrary_content,
- // content-[>] content-[<]
- arbitrary_with_arrow,
- // bg-[--my-color]
- arbitrary_css_var,
- // text-[var(--my-var)]
- arbitrary_css_var2,
- // text-[length:var(--my-var)]
- arbitrary_css_var3,
- // grid-cols-[fit-content(theme(spacing.32))]
- arbitrary_css_value,
- ))(input)
-}
-
-// hover:underline
-fn predefined_modifier(input: &str) -> IResult<&str, ()> {
- let (input, modifier) = recognize(|i| {
- // Assuming a Tailwind class consists of alphanumeric, dashes, and colons
- nom::bytes::complete::is_a(
- "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-",
- )(i)
- })(input)?;
-
- if is_valid_modifier(modifier) {
- Ok((input, ()))
- } else {
- Err(nom::Err::Error(nom::error::Error::new(
- input,
- nom::error::ErrorKind::Tag,
- )))
- }
-}
-
-// predefined special modifiers e.g peer-checked:p-4 group-hover:visible
-fn predefined_special_modifier(input: &str) -> IResult<&str, ()> {
- let (input, _) = alt((
- // peer-checked:p-4
- tuple((tag("peer-"), predefined_modifier)),
- // group-hover:visible
- tuple((tag("group-"), predefined_modifier)),
- ))(input)?;
- Ok((input, ()))
-}
-
-// [&:nth-child(3)]:underline
-// [&_p]:mt-4
-fn arbitrary_front_selector_modifier(input: &str) -> IResult<&str, ()> {
- let (input, _) = tag("[&")(input)?;
- let (input, _) = take_until("]")(input)?;
- let (input, _) = tag("]")(input)?;
- Ok((input, ()))
-}
-
-// group-[:nth-of-type(3)_&]:block
-fn arbitrary_back_selector_modifier(input: &str) -> IResult<&str, ()> {
- let (input, _) = take_while1(|char| is_ident_char(char) && char != '[')(input)?;
- let (input, _) = tag("-[")(input)?;
- let (input, _) = take_until("&]")(input)?;
- let (input, _) = tag("&]")(input)?;
- Ok((input, ()))
-}
-
-// [@supports(display:grid)]:grid
-fn arbitrary_at_supports_rule_modifier(input: &str) -> IResult<&str, ()> {
- let (input, _) = tag("[@supports(")(input)?;
- let (input, _) = take_until(")")(input)?;
- let (input, _) = tag(")]")(input)?;
- Ok((input, ()))
-}
-
-// [@media(any-hover:hover){&:hover}]:opacity-100
-fn arbitrary_at_media_rule_modifier(input: &str) -> IResult<&str, ()> {
- // starts with [@media and ends with ]
- let (input, _) = tag("[@media(")(input)?;
- let (input, _) = take_until("]")(input)?;
- let (input, _) = tag("]")(input)?;
- Ok((input, ()))
-}
-
-// group/edit invisible hover:bg-slate-200 group-hover/item:visible
-fn group_peer_modifier(input: &str) -> IResult<&str, ()> {
- let (input, _) = alt((
- tuple((tag("group-"), predefined_modifier)),
- // https://tailwindcss.com/docs/hover-focus-and-other-states#differentiating-peers
- // peer-checked/published:text-sky-500
- tuple((tag("peer-"), predefined_modifier)),
- ))(input)?;
- let (input, _) = tag("/")(input)?;
- let (input, _) = take_while1(is_ident_char)(input)?;
- Ok((input, ()))
-}
-
-// hidden group-[.is-published]:block
-// group-[:nth-of-type(3)_&]:block
-// peer-[.is-dirty]:peer-required:block hidden
-// hidden peer-[:nth-of-type(3)_&]:block
-fn group_modifier_selector(input: &str) -> IResult<&str, ()> {
- let (input, _) = alt((tag("group"), tag("peer")))(input)?;
- let (input, _) = tag("-[")(input)?;
- let (input, _) = take_until("]")(input)?;
- let (input, _) = tag("]")(input)?;
- Ok((input, ()))
-}
-
- is_valid_last_word
-// supports-[backdrop-filter]
-fn supports_arbitrary(input: &str) -> IResult<&str, ()> {
- let (input, _) = tag("supports-[")(input)?;
- let (input, _) = take_until("]")(input)?;
- let (input, _) = tag("]")(input)?;
- Ok((input, ()))
-}
-
-fn is_valid_group_classname(class_name: &str) -> bool {
- !class_name.contains(':')
- && !class_name.contains('[')
- && !class_name.contains(']')
- && class_name.starts_with("group/")
-// aria-[sort=ascending]:bg-[url('/img/down-arrow.svg')]
-// aria-[sort=descending]:bg-[url('/img/up-arrow.svg')]
-fn aria_arbitrary(input: &str) -> IResult<&str, ()> {
- let (input, _) = opt(tag("group-"))(input)?;
- let (input, _) = tag("aria-[")(input)?;
- let (input, _) = take_while1(is_ident_char)(input)?;
- let (input, _) = tag("=")(input)?;
- let (input, _) = take_while1(is_ident_char)(input)?;
- let (input, _) = tag("]")(input)?;
- Ok((input, ()))
-}
-
-fn is_valid_string(s: &str) -> bool {
- // Matches strings that contain only alphanumeric characters, underscores, and hyphens.
- let re = Regex::new(r"^[a-zA-Z0-9_-]*$").expect("Invalid regex");
- re.is_match(s) && !s.is_empty()
-// data-[size=large]:p-8
-fn data_arbitrary(input: &str) -> IResult<&str, ()> {
- let (input, _) = tag("data-[")(input)?;
- let (input, _) = take_while1(is_ident_char)(input)?;
- let (input, _) = tag("=")(input)?;
- let (input, _) = take_while1(is_ident_char)(input)?;
- let (input, _) = tag("]")(input)?;
- Ok((input, ()))
-}
-
-// min-[320px]:text-center max-[600px]:bg-sky-300
-fn min_max_arbitrary_modifier(input: &str) -> IResult<&str, ()> {
- let (input, _) = alt((tag("min-"), tag("max-")))(input)?;
- let (input, _) = tag("[")(input)?;
- let (input, _) = parse_length_unit(input)?;
- let (input, _) = tag("]")(input)?;
- Ok((input, ()))
-}
-
-fn modifier(input: &str) -> IResult<&str, ()> {
- alt((
- group_modifier_selector,
- group_peer_modifier,
- predefined_special_modifier,
- arbitrary_front_selector_modifier,
- arbitrary_back_selector_modifier,
- arbitrary_at_supports_rule_modifier,
- arbitrary_at_media_rule_modifier,
- predefined_modifier,
- supports_arbitrary,
- aria_arbitrary,
- data_arbitrary,
- min_max_arbitrary_modifier,
- ))(input)
-}
-
-fn modifiers_chained(input: &str) -> IResult<&str, ()> {
- let (input, _modifiers) = separated_list0(tag(":"), modifier)(input)?;
- Ok((input, ()))
-}
-
-fn parse_tw_full_classname(input: &str) -> IResult<&str, Vec<&str>> {
- let (input, _class_names) = tuple((
- opt(tuple((modifiers_chained, tag(":")))),
- parse_single_tw_classname,
- ))(input)?;
-
- Ok((input, vec![]))
-}
-
-// Edge cases
-// [&:nth-child(3)]:underline
-// lg:[&:nth-child(3)]:hover:underline
-// [&_p]:mt-4
-// flex [@supports(display:grid)]:grid
-// [@media(any-hover:hover){&:hover}]:opacity-100
-// group/edit invisible hover:bg-slate-200 group-hover/item:visible
-// hidden group-[.is-published]:block
-// group-[:nth-of-type(3)_&]:block
-// peer-checked/published:text-sky-500
-// peer-[.is-dirty]:peer-required:block hidden
-// hidden peer-[:nth-of-type(3)_&]:block
-// after:content-['*'] after:ml-0.5 after:text-red-500 block text-sm font-medium text-slate-700
-// before:content-[''] before:block
-// bg-black/75 supports-[backdrop-filter]:bg-black/25 supports-[backdrop-filter]:backdrop-blur
-// aria-[sort=ascending]:bg-[url('/img/down-arrow.svg')] aria-[sort=descending]:bg-[url('/img/up-arrow.svg')]
-// group-aria-[sort=ascending]:rotate-0 group-aria-[sort=descending]:rotate-180
-// data-[size=large]:p-8
-// open:bg-white dark:open:bg-slate-900 open:ring-1 open:ring-black/5 dark:open:ring-white/10 open:shadow-lg p-6 rounded-lg
-// lg:[&:nth-child(3)]:hover:underline
-// min-[320px]:text-center max-[600px]:bg-sky-300
-// top-[117px] lg:top-[344px]
-// bg-[#bada55] text-[22px] before:content-['Festivus']
-// grid grid-cols-[fit-content(theme(spacing.32))]
-// bg-[--my-color]
-// [mask-type:luminance] hover:[mask-type:alpha]
-// [--scroll-offset:56px] lg:[--scroll-offset:44px]
-// lg:[&:nth-child(3)]:hover:underline
-// bg-[url('/what_a_rush.png')]
-// before:content-['hello\_world']
-// text-[22px]
-// text-[#bada55]
-// text-[var(--my-var)]
-// text-[length:var(--my-var)]
-// text-[color:var(--my-var)]
-fn parse_class_names(input: &str) -> IResult<&str, Vec<&str>> {
- let (input, _) = multispace0(input)?;
- let (input, _class_names) = separated_list0(multispace1, parse_tw_full_classname)(input)?;
- let (input, _) = multispace0(input)?;
-
- Ok((input, vec![]))
-}
-
-fn parse_top(input: &str) -> IResult<&str, Vec<&str>> {
- all_consuming(parse_class_names)(input)
-}
-
-#[proc_macro]
-pub fn tw(raw_input: TokenStream) -> TokenStream {
- let r_input = raw_input.clone();
- let input = parse_macro_input!(r_input as LitStr);
- let (_modifiers, _valid_class_names) = match setup(&input) {
- Ok(value) => value,
- Err(value) => {
- return syn::Error::new_spanned(input, value)
- .to_compile_error()
- .into()
- }
- };
- let full_classnames = input.value();
-
- let (input, _class_names) = match parse_top(&full_classnames) {
- Ok(value) => value,
- Err(value) => {
- return syn::Error::new_spanned(input, value)
- .to_compile_error()
- .into()
- }
- };
-
- quote::quote! {
- #input
- }
- .into()
-}
- 24 changes: 24 additions & 0 deletions24
-tw-macro/src/tailwind/colorful.rs
-@@ -0,0 +1,24 @@
-pub const COLORFUL_BASECLASSES: [&str; 22] = [
- "text",
- "bg",
- "border",
- "border-x",
- "border-y",
- "border-s",
- "border-e",
- "border-t",
- "border-r",
- "border-b",
- "border-l",
- "divide",
- "outline",
- "ring",
- "ring-offset",
- "shadow",
- "caret",
- "accent",
- "fill",
- "stroke",
- "placeholder",
- "decoration",
-];
- 1 change: 1 addition & 0 deletions1
-tw-macro/src/tailwind/mod.rs
-@@ -4,6 +4,7 @@
- * Copyright (c) 2023 Oyelowo Oyedayo
- * Licensed under the MIT license
- */
-pub mod colorful;
-pub mod default_classnames;
-pub mod lengthy;
-pub mod modifiers;
- 3 changes: 3 additions & 0 deletions3
-tw-macro/src/tailwind/signable.rs
-@@ -45,4 +45,7 @@ pub const SIGNABLES: [&str; 40] = [
- "grid-auto-columns",
- "z",
- "order",
- // "scroll-mx",
- // "scroll-my",
- // "scroll-m",
-];
-Footer
-© 2023 GitHub, Inc.
-Footer navigation
-Terms
-Privacy
-Security
-Status
-Docs
-Contact GitHub
-Pricing
-API
-Training
-Blog
-About
-@@ -4,116 +4,31 @@ * Copyright (c) 2023 Oyelowo Oyedayo * Licensed under the MIT license */ use nom::{ branch::alt, bytes::complete::{tag, take_until, take_while1}, character::complete::{digit1, multispace0, multispace1}, combinator::{all_consuming, not, opt, recognize}, multi::separated_list0, number, sequence::{preceded, tuple}, IResult, }; use syn::{parse_macro_input, LitStr}; mod config; mod plugins; mod tailwind; use tailwind::{ lengthy::LENGTHY, modifiers::get_modifiers, tailwind_config::CustomisableClasses, valid_baseclass_names::VALID_BASECLASS_NAMES, colorful::COLORFUL_BASECLASSES, lengthy::LENGTHY, modifiers::get_modifiers, tailwind_config::CustomisableClasses, valid_baseclass_names::VALID_BASECLASS_NAMES, }; use config::{get_classes, noconfig::UNCONFIGURABLE, read_tailwind_config}; use proc_macro::TokenStream; use regex::{self, Regex}; use tailwind::signable::SIGNABLES; // use tailwindcss_core::parser::{Extractor, ExtractorOptions}; #[proc_macro] pub fn tw(raw_input: TokenStream) -> TokenStream { let r_input = raw_input.clone(); let input = parse_macro_input!(r_input as LitStr); let (modifiers, valid_class_names) = match setup(&input) { Ok(value) => value, Err(value) => { return syn::Error::new_spanned(input, value) .to_compile_error() .into() } }; for word in input.value().split_whitespace() { let (last_word_signed, last_word_unsigned) = get_last_word_types(word); // modifiers e.g hover: in // hover:[mask-type:alpha] let is_valid_arb_prop = is_valid_arb_prop(word, &modifiers); let is_valid_class = is_valid_class(is_valid_arb_prop, &valid_class_names, last_word_unsigned); let (base_classname, arbitrary_value_with_bracket) = last_word_unsigned.split_once("-[").unwrap_or_default(); let is_valid_negative_baseclass = is_valid_negative_baseclass( &valid_class_names, last_word_unsigned, last_word_signed, is_valid_arb_prop, ); let prefix_is_valid_tailwind_keyword = VALID_BASECLASS_NAMES.contains(&base_classname); let is_arbitrary_value = prefix_is_valid_tailwind_keyword && arbitrary_value_with_bracket.ends_with(']'); let arbitrary_value = arbitrary_value_with_bracket.trim_end_matches(']'); let is_lengthy_class = LENGTHY.contains(&base_classname); let is_valid_length = is_arbitrary_value && is_lengthy_class && (is_valid_length(arbitrary_value) || is_valid_calc(arbitrary_value)); let has_arb_variant = has_arb_variant(word); let is_valid_opacity = is_valid_opacity(last_word_unsigned, &valid_class_names); if (is_valid_class && is_valid_modifier(word, &modifiers)) || is_valid_negative_baseclass || (!is_lengthy_class && is_arbitrary_value) || is_valid_length || is_valid_arb_prop || has_arb_variant || is_valid_opacity || is_valid_group_classname(last_word_unsigned) || is_validate_modifier_or_group(word, &modifiers, &valid_class_names) { // if check_word(word, false).is_empty() { // return syn::Error::new_spanned(input, format!("Invalid string: {}", word)) // .to_compile_error() // .into(); // } } else { return syn::Error::new_spanned(input, format!("Invalid string: {word}")) .to_compile_error() .into(); } } raw_input } // fn check_word(input: &str, loose: bool) -> Vec<&str> { // Extractor::unique_ord( // input.as_bytes(), // ExtractorOptions { // preserve_spaces_in_arbitrary: loose, // }, // ) // .into_iter() // .map(|s| unsafe { std::str::from_utf8_unchecked(s) }) // .collect() // } fn is_valid_length(value: &str) -> bool { let re = regex::Regex::new(r"^(-?\d+(\.?\d+)?(px|em|rem|%|cm|mm|in|pt|pc|vh|vw|vmin|vmax)|0)$") .expect("Invalid regex"); re.is_match(value) } fn is_valid_calc(value: &str) -> bool { let re = regex::Regex::new(r"^calc\([^)]+\)$").expect("Invalid regex"); re.is_match(value) } fn setup(input: &LitStr) -> Result<(Vec, Vec), TokenStream> { let config = &(match read_tailwind_config() { Ok(config) => config, @@ -146,199 +61,686 @@ fn setup(input: &LitStr) -> Result<(Vec, Vec), TokenStream> { Ok((modifiers, valid_class_names)) } fn get_last_word_types(word: &str) -> (&str, &str) { let modifiers_and_class = word.split(':'); fn get_classes_straight() -> Vec { get_classes(&read_tailwind_config().unwrap()) } fn is_valid_classname(class_name: &str) -> bool { get_classes_straight().contains(&class_name.to_string()) } fn is_valid_modifier(modifier: &str) -> bool { get_modifiers(&read_tailwind_config().unwrap()).contains(&modifier.to_string()) } fn parse_predefined_tw_classname(input: &str) -> IResult<&str, ()> { let (input, class_name) = recognize(|i| { // Considering a Tailwind class consists of alphanumeric, dashes, and slash nom::bytes::complete::is_a( "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-./", )(i) })(input)?; let is_signable = SIGNABLES.iter().any(|s| { class_name .strip_prefix('-') .unwrap_or(class_name) .starts_with(s) }); if is_signable && is_valid_classname(class_name.strip_prefix('-').unwrap_or(class_name)) || !is_signable && is_valid_classname(class_name) { Ok((input, ())) } else { Err(nom::Err::Error(nom::error::Error::new( input, nom::error::ErrorKind::Tag, ))) } } fn is_ident_char(c: char) -> bool { c.is_alphanumeric() || c == '_' || c == '-' } fn is_lengthy_classname(class_name: &str) -> bool { LENGTHY.contains(&class_name.strip_prefix('-').unwrap_or(class_name)) } // Custom number parser that handles optional decimals and signs, and scientific notation fn float_strict(input: &str) -> IResult<&str, f64> { let (input, number) = recognize(tuple(( opt(alt((tag("-"), tag("+")))), digit1, opt(preceded(tag("."), digit1)), opt(tuple(( alt((tag("e"), tag("E"))), opt(alt((tag("-"), tag("+")))), digit1, ))), )))(input)?; let float_val: f64 = number.parse().unwrap(); Ok((input, float_val)) } fn parse_length_unit(input: &str) -> IResult<&str, String> { let (input, number) = float_strict(input)?; let (input, unit) = { // px|em|rem|%|cm|mm|in|pt|pc|vh|vw|vmin|vmax alt(( tag("px"), tag("em"), tag("rem"), tag("%"), tag("cm"), tag("mm"), tag("in"), tag("pt"), tag("pc"), tag("vh"), tag("vw"), tag("vmin"), tag("vmax"), // TODO: Should i allow unitless values? Would need something like this in caller // location if so: // let (input, _) = alt((parse_length_unit, parse_number))(input)?; tag(""), )) }(input)?; Ok((input, format!("{}{}", number, unit))) } // let is_arbitrary_property = word.starts_with('[') && word.ends_with(']'); let last_word_signed = modifiers_and_class.clone().last().unwrap_or_default(); let last_word_unsigned = last_word_signed .strip_prefix('-') .unwrap_or(last_word_signed); // text-[22px] fn lengthy_arbitrary_classname(input: &str) -> IResult<&str, ()> { let (input, class_name) = take_until("-[")(input)?; let (input, _) = if is_lengthy_classname(class_name) { Ok((input, ())) } else { Err(nom::Err::Error(nom::error::Error::new( input, nom::error::ErrorKind::Tag, ))) }?; // arbitrary value let (input, _) = tag("-")(input)?; let (input, _) = tag("[")(input)?; // is number let (input, _) = parse_length_unit(input)?; let (input, _) = tag("]")(input)?; Ok((input, ())) } (last_word_signed, last_word_unsigned) // #bada55 fn parse_hex_color(input: &str) -> IResult<&str, String> { let (input, _) = tag("#")(input)?; let (input, color) = take_while1(|c: char| c.is_ascii_hexdigit())(input)?; let (input, _) = if color.chars().count() == 3 || color.chars().count() == 6 { Ok((input, ())) } else { Err(nom::Err::Error(nom::error::Error::new( input, nom::error::ErrorKind::Tag, ))) }?; let color = format!("#{}", color); Ok((input, color)) } fn is_valid_modifier(word: &str, modifiers: &[String]) -> bool { let modifiers_and_class = word.split(':'); let modifiers_from_word = modifiers_and_class .clone() .take(modifiers_and_class.count() - 1) .collect::>(); modifiers_from_word fn parse_u8(input: &str) -> IResult<&str, u8> { let (input, num) = number::complete::double(input)?; let input = match num as u32 { 0..=255 => input, _ => { return Err(nom::Err::Error(nom::error::Error::new( input, nom::error::ErrorKind::Tag, ))) } }; Ok((input, num as u8)) } // rgb(255, 255, 255) rgb(255_255_255) fn parse_rgb_color(input: &str) -> IResult<&str, String> { let (input, _) = tag("rgb(")(input)?; let (input, r) = parse_u8(input)?; let (input, _) = alt((tag(","), tag("_")))(input)?; let (input, g) = parse_u8(input)?; let (input, _) = alt((tag(","), tag("_")))(input)?; let (input, b) = parse_u8(input)?; let (input, _) = tag(")")(input)?; let color = format!("rgb({}, {}, {})", r, g, b); Ok((input, color)) } // rgba(255, 255, 255, 0.5) rgba(255_255_255_0.5) fn parse_rgba_color(input: &str) -> IResult<&str, String> { let (input, _) = tag("rgba(")(input)?; let (input, r) = parse_u8(input)?; let (input, _) = alt((tag(","), tag("_")))(input)?; let (input, g) = parse_u8(input)?; let (input, _) = alt((tag(","), tag("_")))(input)?; let (input, b) = parse_u8(input)?; let (input, _) = alt((tag(","), tag("_")))(input)?; let (input, a) = number::complete::double(input)?; let (input, _) = tag(")")(input)?; let color = format!("rgba({}, {}, {}, {})", r, g, b, a); Ok((input, color)) } fn is_colorful_baseclass(class_name: &str) -> bool { COLORFUL_BASECLASSES.contains(&class_name) } // text-[#bada55] fn colorful_arbitrary_baseclass(input: &str) -> IResult<&str, ()> { let (input, class_name) = take_until("-[")(input)?; let (input, _) = if is_colorful_baseclass(class_name) { Ok((input, ())) } else { Err(nom::Err::Error(nom::error::Error::new( input, nom::error::ErrorKind::Tag, ))) }?; // arbitrary value let (input, _) = tag("-")(input)?; let (input, _) = tag("[")(input)?; let (input, _) = alt((parse_hex_color, parse_rgb_color, parse_rgba_color))(input)?; let (input, _) = tag("]")(input)?; Ok((input, ())) } // e.g: [mask-type:alpha] fn kv_pair_classname(input: &str) -> IResult<&str, ()> { let (input, _) = tag("[")(input)?; let (input, _) = take_while1(is_ident_char)(input)?; let (input, _) = tag(":")(input)?; let (input, _) = take_until("]")(input)?; let (input, _) = tag("]")(input)?; Ok((input, ())) } // before:content-['Festivus'] fn arbitrary_content(input: &str) -> IResult<&str, ()> { let (input, _) = tag("content-['")(input)?; let (input, _) = take_until("']")(input)?; let (input, _) = tag("']")(input)?; Ok((input, ())) } // content-[>] content-[<] fn arbitrary_with_arrow(input: &str) -> IResult<&str, ()> { let (input, _) = take_while1(is_ident_char)(input)?; let (input, _) = tag("[")(input)?; let (input, _) = alt((tag(">"), tag("<")))(input)?; let (input, _) = take_until("]")(input)?; let (input, _) = tag("]")(input)?; Ok((input, ())) } // bg-black/25 fn predefined_colorful_opacity(input: &str) -> IResult<&str, ()> { let input = if COLORFUL_BASECLASSES .iter() .all(|modifier| modifiers.contains(&modifier.to_string())) } fn is_valid_opacity(last_word_unsigned: &str, valid_class_names: &[String]) -> bool { let is_valid_opacity = { let (class_name, opacity_raw) = last_word_unsigned.split_once('/').unwrap_or_default(); let opacity_arb = opacity_raw .trim_start_matches('[') .trim_end_matches(']') .parse::(); let is_valid_number = opacity_arb.is_ok_and(|opacity_num| (0.0..=100.0).contains(&opacity_num)); valid_class_names.contains(&class_name.to_string()) && is_valid_number .any(|cb| input.trim().starts_with(cb)) { input } else { return Err(nom::Err::Error(nom::error::Error::new( input, nom::error::ErrorKind::Tag, ))); }; is_valid_opacity } fn has_arb_variant(word: &str) -> bool { // lg:[&:nth-child(3)]:hover:underline // [&_p]:mt-4 // flex [@supports(display:grid)]:grid // [@media(any-hover:hover){&:hover}]:opacity-100 let has_arb_variant = { // lg:[&:nth-child(3)]:hover:underline => :nth-child(3) // [&_p]:mt-4 => _p let mut ampersand_variant_selector = word.split("[@").last().unwrap_or_default().split("]:"); let and_variant_selector = word.split("[&").last().unwrap_or_default().split("]:"); let is_valid_arbitrary_variant_selector = ampersand_variant_selector.clone().count() >= 2 && !ampersand_variant_selector .next() .unwrap_or_default() .is_empty(); let is_valid_arbitrary_variant_queries = and_variant_selector.clone().count() >= 2 && !and_variant_selector .clone() .last() .unwrap_or_default() .split("]:") .next() .unwrap_or_default() .is_empty(); let is_query = word.starts_with("[@"); is_valid_arbitrary_variant_selector || is_valid_arbitrary_variant_queries || is_query // && // ((!is_query && !word.split("[&").next().unwrap_or_default().is_empty() && word.split(":[&").count() >= 2) || is_query) let (input, _) = take_while1(|char| is_ident_char(char) && char != '/')(input)?; // let (input, _) = take_until("/")(input)?; let (input, _) = tag("/")(input)?; let (input, num) = number::complete::double(input)?; let input = match num as u8 { 0..=100 => input, _ => { return Err(nom::Err::Error(nom::error::Error::new( input, nom::error::ErrorKind::Tag, ))) } }; has_arb_variant } fn is_valid_negative_baseclass( valid_class_names: &[String], last_word_unsigned: &str, last_word_signed: &str, is_valid_arb_prop: bool, ) -> bool { let is_valid_negative_baseclass = { // tw!("-m-4 p-4 p-4"); (valid_class_names.contains(&last_word_unsigned.to_string()) && last_word_signed.starts_with('-') && SIGNABLES .iter() .any(|s| (last_word_unsigned.starts_with(s)))) || (is_valid_arb_prop && last_word_signed.starts_with('-') && SIGNABLES.iter().any(|s| last_word_unsigned.starts_with(s))) Ok((input, ())) } // bg-black/[27] bg-black/[27%] fn arbitrary_opacity(input: &str) -> IResult<&str, ()> { let input = if COLORFUL_BASECLASSES .iter() .any(|cb| input.trim().starts_with(cb)) { input } else { return Err(nom::Err::Error(nom::error::Error::new( input, nom::error::ErrorKind::Tag, ))); }; is_valid_negative_baseclass } fn is_valid_class( is_valid_arb_prop: bool, valid_class_names: &[String], last_word_unsigned: &str, ) -> bool { !is_valid_arb_prop && valid_class_names.contains(&last_word_unsigned.to_string()) } fn is_valid_arb_prop(word: &str, modifiers: &[String]) -> bool { // TODO: check the first and the last character are not open and close brackets // respectively i.e arbitrary property e.g [mask_type:aplha]; // hover:[mask-type:alpha]; let mut word_for_arb_prop = word.split(":["); word_for_arb_prop .next() // e.g for hover:[mask-type:alpha], this will be hover, // for [mask-type:alpha], this will be [mask-type:alpha] .is_some_and(|modifiers_or_full_arb_prop| { let is_arbitrary_property = modifiers_or_full_arb_prop.starts_with('[') && modifiers_or_full_arb_prop.ends_with(']'); let is_valid = if is_arbitrary_property { modifiers_or_full_arb_prop.matches('[').count() == 1 && modifiers_or_full_arb_prop.matches(']').count() == 1 && modifiers_or_full_arb_prop .trim_start_matches('[') .trim_end_matches(']') .split(':') .count() == 2 } else { // e.g mask-type:alpha] in hover:[mask-type:alpha] let full_arb_prop = word_for_arb_prop.next().unwrap_or_default(); // e.g for single, hover in hover:[mask-type:alpha] // for multiple, hover:first:last, in hover:first:last:[mask-type:alpha] modifiers_or_full_arb_prop .split(':') .all(|modifier| modifiers.contains(&modifier.to_string())) && full_arb_prop.matches(']').count() == 1 && full_arb_prop .trim_end_matches(']') .split(':') .count() == 2 }; is_valid }) || // value e.g [mask-type:alpha] in hover:[mask-type:alpha] // potential addition checks(probably not a good idea. Imagine a new css property, we would // have to open a PR for every new or esoteric css property.) word_for_arb_prop.next().is_some_and(|value| { value.ends_with(']') && value.split(':').count() == 2 // We had already split by ":[", so there should be no "[" anymore && value.matches('[').count() == 0 && value.matches(']').count() == 1 }) } fn is_valid_group_pattern(modifier: &str, valid_modifiers: &[String]) -> bool { let parts: Vec<&str> = modifier.split('/').collect(); let group_modifier = parts[0]; parts.len() == 2 && valid_modifiers.contains(&group_modifier.to_string()) && group_modifier.starts_with("group") } // tw!("group/edit invisible hover:bg-slate-200 group-hover/item:visible"); // tw!("group-[:nth-of-type(3)_&]:block group-hover/edit:text-gray-700 group-[:nth-of-type(3)_&]:block"); fn is_validate_modifier_or_group( word: &str, valid_modifiers: &[String], valid_class_names: &[String], ) -> bool { let valid_arb_group = word.split(':').collect::>(); let modifiers = &valid_arb_group[..valid_arb_group.len() - 1]; let last_word = valid_arb_group.last().unwrap_or(&""); let is_valid_last_word = is_valid_string(last_word) && valid_class_names.contains(&last_word.to_string()); for modifier in modifiers { if modifier.starts_with("group") { if !is_valid_group_pattern(modifier, valid_modifiers) && is_valid_last_word { return false; } } else if !valid_modifiers.contains(&modifier.to_string()) && is_valid_last_word { return false; let (input, _) = take_while1(|char| is_ident_char(char) && char != '/')(input)?; let (input, _) = tag("/")(input)?; let (input, _) = tag("[")(input)?; // 0-100 integer let (input, num) = number::complete::double(input)?; let input = match num as u8 { 0..=100 => input, _ => { return Err(nom::Err::Error(nom::error::Error::new( input, nom::error::ErrorKind::Tag, ))) } }; let (input, _) = opt(tag("%"))(input)?; let (input, _) = tag("]")(input)?; Ok((input, ())) } // bg-[url('/img/down-arrow.svg')] fn bg_arbitrary_url(input: &str) -> IResult<&str, ()> { // prefixed by baseclass let input = if COLORFUL_BASECLASSES .iter() .any(|cb| input.trim().starts_with(cb)) { input } else { return Err(nom::Err::Error(nom::error::Error::new( input, nom::error::ErrorKind::Tag, ))); }; let (input, _) = take_while1(|char| is_ident_char(char) && char != '[')(input)?; let (input, _) = tag("[")(input)?; let (input, _) = tag("url('")(input)?; let (input, _) = take_until("')")(input)?; let (input, _) = tag("')")(input)?; let (input, _) = tag("]")(input)?; Ok((input, ())) } // grid-cols-[fit-content(theme(spacing.32))] fn arbitrary_css_value(input: &str) -> IResult<&str, ()> { // is prefixed by valid base class // take until -[ let (input, base_class) = take_until("-[")(input)?; let input = if VALID_BASECLASS_NAMES .iter() .any(|cb| base_class.trim().eq(*cb)) { input } else { return Err(nom::Err::Error(nom::error::Error::new( base_class, nom::error::ErrorKind::Tag, ))); }; let (input, _) = tag("-[")(input)?; let (input, _) = not(alt(( tag("--"), tag("var(--"), // :var(-- )))(input)?; let (input, _) = take_while1(|char| is_ident_char(char) && char != '(')(input)?; let (input, _) = tag("(")(input)?; let (input, _) = take_until(")]")(input)?; // allow anything inthe brackets let (input, _) = take_until("]")(input)?; let (input, _) = tag("]")(input)?; Ok((input, ())) } // bg-[--my-color] fn arbitrary_css_var(input: &str) -> IResult<&str, ()> { // is prefixed by valid base class let input = if VALID_BASECLASS_NAMES .iter() .any(|cb| input.trim().starts_with(cb)) { input } else { return Err(nom::Err::Error(nom::error::Error::new( input, nom::error::ErrorKind::Tag, ))); }; let (input, _) = take_while1(|char| is_ident_char(char) && char != '[')(input)?; let (input, _) = tag("[")(input)?; let (input, _) = tag("--")(input)?; let (input, _) = take_while1(|char| is_ident_char(char) && char != ']')(input)?; let (input, _) = tag("]")(input)?; Ok((input, ())) } // text-[var(--my-var)] fn arbitrary_css_var2(input: &str) -> IResult<&str, ()> { // is prefixed by valid base class let input = if VALID_BASECLASS_NAMES .iter() .any(|cb| input.trim().starts_with(cb)) { input } else { return Err(nom::Err::Error(nom::error::Error::new( input, nom::error::ErrorKind::Tag, ))); }; let (input, _) = take_while1(|char| is_ident_char(char) && char != '[')(input)?; let (input, _) = tag("[")(input)?; let (input, _) = tag("var(--")(input)?; let (input, _) = take_while1(|char| is_ident_char(char) && char != ')')(input)?; let (input, _) = tag(")]")(input)?; Ok((input, ())) } // text-[length:var(--my-var)] fn arbitrary_css_var3(input: &str) -> IResult<&str, ()> { // is prefixed by valid base class let input = if VALID_BASECLASS_NAMES .iter() .any(|cb| input.trim().starts_with(cb)) { input } else { return Err(nom::Err::Error(nom::error::Error::new( input, nom::error::ErrorKind::Tag, ))); }; let (input, _) = take_while1(|char| is_ident_char(char) && char != '[')(input)?; let (input, _) = tag("[")(input)?; let (input, _) = take_while1(|char| is_ident_char(char) && char != ':')(input)?; let (input, _) = tag(":")(input)?; let (input, _) = tag("var(--")(input)?; let (input, _) = take_while1(|char| is_ident_char(char) && char != ')')(input)?; let (input, _) = tag(")]")(input)?; Ok((input, ())) } // group/edit fn arbitrary_group_classname(input: &str) -> IResult<&str, ()> { let (input, _) = alt((tag("group"),))(input)?; let (input, _) = tag("/")(input)?; let (input, _) = take_while1(is_ident_char)(input)?; Ok((input, ())) } fn parse_single_tw_classname(input: &str) -> IResult<&str, ()> { alt(( // bg-[url('/what_a_rush.png')] bg_arbitrary_url, // bg-black/25 predefined_colorful_opacity, // group/edit arbitrary_group_classname, // bg-black/[27] arbitrary_opacity, // btn parse_predefined_tw_classname, // [mask-type:luminance] [mask-type:alpha] kv_pair_classname, // text-[22px] lengthy_arbitrary_classname, // text-[#bada55] colorful_arbitrary_baseclass, // before:content-['Festivus'] arbitrary_content, // content-[>] content-[<] arbitrary_with_arrow, // bg-[--my-color] arbitrary_css_var, // text-[var(--my-var)] arbitrary_css_var2, // text-[length:var(--my-var)] arbitrary_css_var3, // grid-cols-[fit-content(theme(spacing.32))] arbitrary_css_value, ))(input) } // hover:underline fn predefined_modifier(input: &str) -> IResult<&str, ()> { let (input, modifier) = recognize(|i| { // Assuming a Tailwind class consists of alphanumeric, dashes, and colons nom::bytes::complete::is_a( "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-", )(i) })(input)?; if is_valid_modifier(modifier) { Ok((input, ())) } else { Err(nom::Err::Error(nom::error::Error::new( input, nom::error::ErrorKind::Tag, ))) } } // predefined special modifiers e.g peer-checked:p-4 group-hover:visible fn predefined_special_modifier(input: &str) -> IResult<&str, ()> { let (input, _) = alt(( // peer-checked:p-4 tuple((tag("peer-"), predefined_modifier)), // group-hover:visible tuple((tag("group-"), predefined_modifier)), ))(input)?; Ok((input, ())) } // [&:nth-child(3)]:underline // [&_p]:mt-4 fn arbitrary_front_selector_modifier(input: &str) -> IResult<&str, ()> { let (input, _) = tag("[&")(input)?; let (input, _) = take_until("]")(input)?; let (input, _) = tag("]")(input)?; Ok((input, ())) } // group-[:nth-of-type(3)_&]:block fn arbitrary_back_selector_modifier(input: &str) -> IResult<&str, ()> { let (input, _) = take_while1(|char| is_ident_char(char) && char != '[')(input)?; let (input, _) = tag("-[")(input)?; let (input, _) = take_until("&]")(input)?; let (input, _) = tag("&]")(input)?; Ok((input, ())) } // [@supports(display:grid)]:grid fn arbitrary_at_supports_rule_modifier(input: &str) -> IResult<&str, ()> { let (input, _) = tag("[@supports(")(input)?; let (input, _) = take_until(")")(input)?; let (input, _) = tag(")]")(input)?; Ok((input, ())) } // [@media(any-hover:hover){&:hover}]:opacity-100 fn arbitrary_at_media_rule_modifier(input: &str) -> IResult<&str, ()> { // starts with [@media and ends with ] let (input, _) = tag("[@media(")(input)?; let (input, _) = take_until("]")(input)?; let (input, _) = tag("]")(input)?; Ok((input, ())) } // group/edit invisible hover:bg-slate-200 group-hover/item:visible fn group_peer_modifier(input: &str) -> IResult<&str, ()> { let (input, _) = alt(( tuple((tag("group-"), predefined_modifier)), // https://tailwindcss.com/docs/hover-focus-and-other-states#differentiating-peers // peer-checked/published:text-sky-500 tuple((tag("peer-"), predefined_modifier)), ))(input)?; let (input, _) = tag("/")(input)?; let (input, _) = take_while1(is_ident_char)(input)?; Ok((input, ())) } // hidden group-[.is-published]:block // group-[:nth-of-type(3)_&]:block // peer-[.is-dirty]:peer-required:block hidden // hidden peer-[:nth-of-type(3)_&]:block fn group_modifier_selector(input: &str) -> IResult<&str, ()> { let (input, _) = alt((tag("group"), tag("peer")))(input)?; let (input, _) = tag("-[")(input)?; let (input, _) = take_until("]")(input)?; let (input, _) = tag("]")(input)?; Ok((input, ())) } is_valid_last_word // supports-[backdrop-filter] fn supports_arbitrary(input: &str) -> IResult<&str, ()> { let (input, _) = tag("supports-[")(input)?; let (input, _) = take_until("]")(input)?; let (input, _) = tag("]")(input)?; Ok((input, ())) } fn is_valid_group_classname(class_name: &str) -> bool { !class_name.contains(':') && !class_name.contains('[') && !class_name.contains(']') && class_name.starts_with("group/") // aria-[sort=ascending]:bg-[url('/img/down-arrow.svg')] // aria-[sort=descending]:bg-[url('/img/up-arrow.svg')] fn aria_arbitrary(input: &str) -> IResult<&str, ()> { let (input, _) = opt(tag("group-"))(input)?; let (input, _) = tag("aria-[")(input)?; let (input, _) = take_while1(is_ident_char)(input)?; let (input, _) = tag("=")(input)?; let (input, _) = take_while1(is_ident_char)(input)?; let (input, _) = tag("]")(input)?; Ok((input, ())) } fn is_valid_string(s: &str) -> bool { // Matches strings that contain only alphanumeric characters, underscores, and hyphens. let re = Regex::new(r"^[a-zA-Z0-9_-]*$").expect("Invalid regex"); re.is_match(s) && !s.is_empty() // data-[size=large]:p-8 fn data_arbitrary(input: &str) -> IResult<&str, ()> { let (input, _) = tag("data-[")(input)?; let (input, _) = take_while1(is_ident_char)(input)?; let (input, _) = tag("=")(input)?; let (input, _) = take_while1(is_ident_char)(input)?; let (input, _) = tag("]")(input)?; Ok((input, ())) } // min-[320px]:text-center max-[600px]:bg-sky-300 fn min_max_arbitrary_modifier(input: &str) -> IResult<&str, ()> { let (input, _) = alt((tag("min-"), tag("max-")))(input)?; let (input, _) = tag("[")(input)?; let (input, _) = parse_length_unit(input)?; let (input, _) = tag("]")(input)?; Ok((input, ())) } fn modifier(input: &str) -> IResult<&str, ()> { alt(( group_modifier_selector, group_peer_modifier, predefined_special_modifier, arbitrary_front_selector_modifier, arbitrary_back_selector_modifier, arbitrary_at_supports_rule_modifier, arbitrary_at_media_rule_modifier, predefined_modifier, supports_arbitrary, aria_arbitrary, data_arbitrary, min_max_arbitrary_modifier, ))(input) } fn modifiers_chained(input: &str) -> IResult<&str, ()> { let (input, _modifiers) = separated_list0(tag(":"), modifier)(input)?; Ok((input, ())) } fn parse_tw_full_classname(input: &str) -> IResult<&str, Vec<&str>> { let (input, _class_names) = tuple(( opt(tuple((modifiers_chained, tag(":")))), parse_single_tw_classname, ))(input)?; Ok((input, vec![])) } // Edge cases // [&:nth-child(3)]:underline // lg:[&:nth-child(3)]:hover:underline // [&_p]:mt-4 // flex [@supports(display:grid)]:grid // [@media(any-hover:hover){&:hover}]:opacity-100 // group/edit invisible hover:bg-slate-200 group-hover/item:visible // hidden group-[.is-published]:block // group-[:nth-of-type(3)_&]:block // peer-checked/published:text-sky-500 // peer-[.is-dirty]:peer-required:block hidden // hidden peer-[:nth-of-type(3)_&]:block // after:content-['*'] after:ml-0.5 after:text-red-500 block text-sm font-medium text-slate-700 // before:content-[''] before:block // bg-black/75 supports-[backdrop-filter]:bg-black/25 supports-[backdrop-filter]:backdrop-blur // aria-[sort=ascending]:bg-[url('/img/down-arrow.svg')] aria-[sort=descending]:bg-[url('/img/up-arrow.svg')] // group-aria-[sort=ascending]:rotate-0 group-aria-[sort=descending]:rotate-180 // data-[size=large]:p-8 // open:bg-white dark:open:bg-slate-900 open:ring-1 open:ring-black/5 dark:open:ring-white/10 open:shadow-lg p-6 rounded-lg // lg:[&:nth-child(3)]:hover:underline // min-[320px]:text-center max-[600px]:bg-sky-300 // top-[117px] lg:top-[344px] // bg-[#bada55] text-[22px] before:content-['Festivus'] // grid grid-cols-[fit-content(theme(spacing.32))] // bg-[--my-color] // [mask-type:luminance] hover:[mask-type:alpha] // [--scroll-offset:56px] lg:[--scroll-offset:44px] // lg:[&:nth-child(3)]:hover:underline // bg-[url('/what_a_rush.png')] // before:content-['hello\_world'] // text-[22px] // text-[#bada55] // text-[var(--my-var)] // text-[length:var(--my-var)] // text-[color:var(--my-var)] fn parse_class_names(input: &str) -> IResult<&str, Vec<&str>> { let (input, _) = multispace0(input)?; let (input, _class_names) = separated_list0(multispace1, parse_tw_full_classname)(input)?; let (input, _) = multispace0(input)?; Ok((input, vec![])) } fn parse_top(input: &str) -> IResult<&str, Vec<&str>> { all_consuming(parse_class_names)(input) } #[proc_macro] pub fn tw(raw_input: TokenStream) -> TokenStream { let r_input = raw_input.clone(); let input = parse_macro_input!(r_input as LitStr); let (_modifiers, _valid_class_names) = match setup(&input) { Ok(value) => value, Err(value) => { return syn::Error::new_spanned(input, value) .to_compile_error() .into() } }; let full_classnames = input.value(); let (input, _class_names) = match parse_top(&full_classnames) { Ok(value) => value, Err(value) => { return syn::Error::new_spanned(input, value) .to_compile_error() .into() } }; quote::quote! { #input } .into() }
\ No newline at end of file
diff --git a/tailwind/Cargo.toml b/tailwind/Cargo.toml
index 9658225..2328c5c 100644
--- a/tailwind/Cargo.toml
+++ b/tailwind/Cargo.toml
@@ -9,7 +9,7 @@ documentation.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-tw-macro = { workspace = true, features = ["daisyui"] }
+twust = { workspace = true, features = ["daisyui"] }
serde = "1.0.188"
diff --git a/tailwind/src/lib.rs b/tailwind/src/lib.rs
index c6ddf90..a9326f3 100644
--- a/tailwind/src/lib.rs
+++ b/tailwind/src/lib.rs
@@ -4,7 +4,7 @@
* Copyright (c) 2023 Oyelowo Oyedayo
* Licensed under the MIT license
*/
-use tw_macro::tw;
+use twust::tw;
/// Invalid character in class name.
///
diff --git a/tailwind/src/main.rs b/tailwind/src/main.rs
index 93974db..4cbb02a 100644
--- a/tailwind/src/main.rs
+++ b/tailwind/src/main.rs
@@ -5,7 +5,7 @@
* Copyright (c) 2023 Oyelowo Oyedayo
* Licensed under the MIT license
*/
-use tw_macro::tw;
+use twust::tw;
fn main() {
let _ = tw!("btn btn");
diff --git a/tw-macro/Cargo.toml b/tw-macro/Cargo.toml
index e90da69..d1444ab 100644
--- a/tw-macro/Cargo.toml
+++ b/tw-macro/Cargo.toml
@@ -1,5 +1,5 @@
[package]
-name = "tw-macro"
+name = "twust"
version = { workspace = true }
edition = { workspace = true }
authors = { workspace = true }