diff --git a/examples/benchmarks/src/benchmarks.rs b/examples/benchmarks/src/benchmarks.rs index e393675..4d49782 100644 --- a/examples/benchmarks/src/benchmarks.rs +++ b/examples/benchmarks/src/benchmarks.rs @@ -1,17 +1,37 @@ -use stylist::ast::Sheet; -use stylist::Style; +use stylist::{ + ast::{sheet, Sheet}, + Style, +}; use crate::utils::now; pub fn bench_parse_simple() -> f64 { let start_time = now(); - for _ in 0..1_000_000 { + for _ in 0..10_000_000 { let _sheet: Sheet = "color:red;".parse().expect("Failed to parse stylesheet."); } now() - start_time } +pub fn bench_macro_simple() -> f64 { + let start_time = now(); + for _ in 0..10_000_000 { + let _sheet: Sheet = sheet!("color:red;"); + } + + now() - start_time +} + +pub fn bench_macro_inline_simple() -> f64 { + let start_time = now(); + for _ in 0..10_000_000 { + let _sheet: Sheet = sheet!(color:red;); + } + + now() - start_time +} + pub fn bench_parse_simple_no_cache() -> f64 { let start_time = now(); for i in 0..100_000 { @@ -25,8 +45,42 @@ pub fn bench_parse_simple_no_cache() -> f64 { pub fn bench_parse_complex() -> f64 { let start_time = now(); - for _ in 0..100_000 { - let _sheet: Sheet = r#" + for i in 0..1_000_000 { + let _sheet: Sheet = format!( + r#" + color:red; + + .class-name-a {{ + background: red; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + }} + + @media screen and (max-width: {i}px;) {{ + font-size: 0.9rem; + + .class-name-b {{ + flex-direction: row; + }} + }} + "#, + i = i / 1000 + ) + .parse() + .expect("Failed to parse stylesheet."); + } + + now() - start_time +} + +pub fn bench_macro_complex() -> f64 { + let start_time = now(); + for i in 0..1_000_000 { + let _sheet: Sheet = sheet!( + r#" color:red; .class-name-a { @@ -38,16 +92,44 @@ pub fn bench_parse_complex() -> f64 { align-items: center; } - @media screen and (max-width: 500px;) { + @media screen and (max-width: ${i}px;) { font-size: 0.9rem; .class-name-b { flex-direction: row; } } - "# - .parse() - .expect("Failed to parse stylesheet."); + "#, + i = (i / 1000).to_string() + ); + } + + now() - start_time +} + +pub fn bench_macro_inline_complex() -> f64 { + let start_time = now(); + for i in 0..1_000_000 { + let _sheet: Sheet = sheet!( + color: red; + + .class-name-a { + background: red; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + } + + @media screen and (max-width: ${i / 1000}px;) { + font-size: 0.9rem; + + .class-name-b { + flex-direction: row; + } + } + ); } now() - start_time diff --git a/examples/benchmarks/src/main.rs b/examples/benchmarks/src/main.rs index a45d011..6528469 100644 --- a/examples/benchmarks/src/main.rs +++ b/examples/benchmarks/src/main.rs @@ -1,6 +1,5 @@ use gloo::timers::callback::Timeout; -use stylist::yew::Global; -use stylist::{StyleSource, YieldStyle}; +use stylist::{yew::Global, StyleSource, YieldStyle}; use yew::prelude::*; use log::Level; @@ -22,8 +21,12 @@ static GLOBAL_STYLE: &str = r#" pub enum BenchMsg { ParseSimpleFinish(f64), + MacroSimpleFinish(f64), + MacroInlineSimpleFinish(f64), ParseSimpleNoCacheFinish(f64), ParseComplexFinish(f64), + MacroComplexFinish(f64), + MacroInlineComplexFinish(f64), ParseComplexNoCacheFinish(f64), CachedLookupFinish(f64), CachedLookupBigSheetFinish(f64), @@ -35,8 +38,12 @@ pub struct Benchmarks { finished: bool, parse_simple: Option, + macro_simple: Option, + macro_inline_simple: Option, parse_simple_no_cache: Option, parse_complex: Option, + macro_complex: Option, + macro_inline_complex: Option, parse_complex_no_cache: Option, cached_lookup: Option, @@ -54,8 +61,12 @@ impl Component for Benchmarks { link, finished: false, parse_simple: None, + macro_simple: None, + macro_inline_simple: None, parse_simple_no_cache: None, parse_complex: None, + macro_complex: None, + macro_inline_complex: None, parse_complex_no_cache: None, cached_lookup: None, cached_lookup_big_sheet: None, @@ -76,6 +87,22 @@ impl Component for Benchmarks { match msg { BenchMsg::ParseSimpleFinish(m) => { self.parse_simple = Some(m); + let cb = self + .link + .callback(|_| BenchMsg::MacroSimpleFinish(benchmarks::bench_macro_simple())); + + Timeout::new(100, move || cb.emit(())).forget(); + } + BenchMsg::MacroSimpleFinish(m) => { + self.macro_simple = Some(m); + let cb = self.link.callback(|_| { + BenchMsg::MacroInlineSimpleFinish(benchmarks::bench_macro_inline_simple()) + }); + + Timeout::new(100, move || cb.emit(())).forget(); + } + BenchMsg::MacroInlineSimpleFinish(m) => { + self.macro_inline_simple = Some(m); let cb = self.link.callback(|_| { BenchMsg::ParseSimpleNoCacheFinish(benchmarks::bench_parse_simple_no_cache()) }); @@ -94,6 +121,24 @@ impl Component for Benchmarks { BenchMsg::ParseComplexFinish(m) => { self.parse_complex = Some(m); + let cb = self + .link + .callback(|_| BenchMsg::MacroComplexFinish(benchmarks::bench_macro_complex())); + + Timeout::new(100, move || cb.emit(())).forget(); + } + BenchMsg::MacroComplexFinish(m) => { + self.macro_complex = Some(m); + + let cb = self.link.callback(|_| { + BenchMsg::MacroInlineComplexFinish(benchmarks::bench_macro_inline_complex()) + }); + + Timeout::new(100, move || cb.emit(())).forget(); + } + BenchMsg::MacroInlineComplexFinish(m) => { + self.macro_inline_complex = Some(m); + let cb = self.link.callback(|_| { BenchMsg::ParseComplexNoCacheFinish(benchmarks::bench_parse_complex_no_cache()) }); @@ -165,17 +210,33 @@ impl Component for Benchmarks { - {"Parse Simple (1,000,000 iterations): "} + {"Parse Simple (10,000,000 iterations): "} {self.parse_simple.map(|m| {format!("{:.0}ms", m)}).unwrap_or_else(|| "".to_string())} + + {"Macro Simple (10,000,000 iterations): "} + {self.macro_simple.map(|m| {format!("{:.0}ms", m)}).unwrap_or_else(|| "".to_string())} + + + {"Macro Inline Simple (10,000,000 iterations): "} + {self.macro_inline_simple.map(|m| {format!("{:.0}ms", m)}).unwrap_or_else(|| "".to_string())} + {"Parse Simple, No Cache (100,000 iterations): "} {self.parse_simple_no_cache.map(|m| {format!("{:.0}ms", m)}).unwrap_or_else(|| "".to_string())} - {"Parse Complex (100,000 iterations): "} + {"Parse Complex (1,000,000 iterations): "} {self.parse_complex.map(|m| {format!("{:.0}ms", m)}).unwrap_or_else(|| "".to_string())} + + {"Macro Complex (1,000,000 iterations): "} + {self.macro_complex.map(|m| {format!("{:.0}ms", m)}).unwrap_or_else(|| "".to_string())} + + + {"Macro Inline Complex (1,000,000 iterations): "} + {self.macro_inline_complex.map(|m| {format!("{:.0}ms", m)}).unwrap_or_else(|| "".to_string())} + {"Parse Complex, No Cache (100,000 iterations): "} {self.parse_complex_no_cache.map(|m| {format!("{:.0}ms", m)}).unwrap_or_else(|| "".to_string())} diff --git a/packages/stylist-core/src/ast/mod.rs b/packages/stylist-core/src/ast/mod.rs index ae8c3c2..393ffa6 100644 --- a/packages/stylist-core/src/ast/mod.rs +++ b/packages/stylist-core/src/ast/mod.rs @@ -39,7 +39,7 @@ mod tests { .into(), }), ScopeContent::Block(Block { - condition: vec![".inner".into()].into(), + condition: vec![vec![".inner".into()].into()].into(), style_attributes: vec![StyleAttribute { key: "background-color".into(), value: vec!["red".into()].into(), @@ -96,7 +96,7 @@ width: 200px; .into(), }), RuleContent::Block(Block { - condition: vec![".inner".into()].into(), + condition: vec![vec![".inner".into()].into()].into(), style_attributes: vec![StyleAttribute { key: "background-color".into(), value: vec!["red".into()].into(), diff --git a/packages/stylist-core/src/ast/selector.rs b/packages/stylist-core/src/ast/selector.rs index b7934d6..2a482a9 100644 --- a/packages/stylist-core/src/ast/selector.rs +++ b/packages/stylist-core/src/ast/selector.rs @@ -1,5 +1,4 @@ -use std::borrow::Cow; -use std::fmt; +use std::{borrow::Cow, fmt}; use super::{StringFragment, ToStyleStr}; use crate::Result; @@ -57,10 +56,10 @@ impl ToStyleStr for Selector { } } -impl>> From for Selector { +impl>> From for Selector { fn from(s: T) -> Self { Self { - fragments: vec![s.into().into()].into(), + fragments: s.into(), } } } @@ -71,7 +70,7 @@ mod tests { #[test] fn test_selector_gen_simple() -> Result<()> { - let s: Selector = ".abc".into(); + let s: Selector = vec![".abc".into()].into(); assert_eq!( s.to_style_str(Some("stylist-abcdefgh"))?, @@ -83,7 +82,7 @@ mod tests { #[test] fn test_selector_pseduo() -> Result<()> { - let s: Selector = ":hover".into(); + let s: Selector = vec![":hover".into()].into(); assert_eq!( s.to_style_str(Some("stylist-abcdefgh"))?, @@ -95,7 +94,7 @@ mod tests { #[test] fn test_selector_root_pseduo() -> Result<()> { - let s: Selector = ":root.big".into(); + let s: Selector = vec![":root.big".into()].into(); assert_eq!( s.to_style_str(Some("stylist-abcdefgh"))?, @@ -107,7 +106,7 @@ mod tests { #[test] fn test_selector_gen_current() -> Result<()> { - let s: Selector = "&.big".into(); + let s: Selector = vec!["&.big".into()].into(); assert_eq!( s.to_style_str(Some("stylist-abcdefgh"))?, diff --git a/packages/stylist-core/src/parser.rs b/packages/stylist-core/src/parser.rs index 11b1858..1bfe66f 100644 --- a/packages/stylist-core/src/parser.rs +++ b/packages/stylist-core/src/parser.rs @@ -1,9 +1,11 @@ use std::borrow::Cow; -use crate::ast::{ - Block, Rule, RuleContent, ScopeContent, Selector, Sheet, StringFragment, StyleAttribute, +use crate::{ + ast::{ + Block, Rule, RuleContent, ScopeContent, Selector, Sheet, StringFragment, StyleAttribute, + }, + Error, Result, }; -use crate::{Error, Result}; use nom::{ branch::alt, bytes::complete::{is_not, tag, take_while, take_while1}, @@ -344,7 +346,7 @@ impl Parser { Self::string, recognize(Self::interpolation), )))), - |p: &str| p.trim().to_owned().into(), + |p: &str| vec![p.trim().to_owned().into()].into(), )), )(i); @@ -725,7 +727,7 @@ mod tests { .into(), }), ScopeContent::Block(Block { - condition: vec![".nested".into()].into(), + condition: vec![vec![".nested".into()].into()].into(), style_attributes: vec![ StyleAttribute { key: "background-color".into(), @@ -772,7 +774,8 @@ mod tests { .into(), }), ScopeContent::Block(Block { - condition: vec![r#"[placeholder="someone@example.com"]"#.into()].into(), + condition: vec![vec![r#"[placeholder="someone@example.com"]"#.into()].into()] + .into(), style_attributes: vec![ StyleAttribute { key: "background-color".into(), @@ -801,7 +804,7 @@ mod tests { let parsed = Parser::parse(test_str).expect("Failed to Parse Style"); let expected = Sheet::from(vec![ScopeContent::Block(Block { - condition: vec![r#"[placeholder="\" {}"]"#.into()].into(), + condition: vec![vec![r#"[placeholder="\" {}"]"#.into()].into()].into(), style_attributes: vec![ StyleAttribute { key: "background-color".into(), @@ -827,7 +830,7 @@ mod tests { let parsed = Parser::parse(test_str).expect("Failed to Parse Style"); let expected = Sheet::from(vec![ScopeContent::Block(Block { - condition: vec!["&:hover".into()].into(), + condition: vec![vec!["&:hover".into()].into()].into(), style_attributes: vec![StyleAttribute { key: "background-color".into(), value: vec!["#d0d0d9".into()].into(), @@ -915,7 +918,7 @@ mod tests { .into(), }), ScopeContent::Block(Block { - condition: vec![".some-class2".into()].into(), + condition: vec![vec![".some-class2".into()].into()].into(), style_attributes: vec![StyleAttribute { key: "color".into(), value: vec!["yellow".into()].into(), @@ -947,7 +950,7 @@ mod tests { let expected = Sheet::from(vec![ ScopeContent::Block(Block { - condition: vec!["div".into(), "span".into()].into(), + condition: vec![vec!["div".into()].into(), vec!["span".into()].into()].into(), style_attributes: vec![StyleAttribute { key: "color".into(), value: vec!["yellow".into()].into(), @@ -955,7 +958,7 @@ mod tests { .into(), }), ScopeContent::Block(Block { - condition: vec!["&".into(), "& input".into()].into(), + condition: vec![vec!["&".into()].into(), vec!["& input".into()].into()].into(), style_attributes: vec![StyleAttribute { key: "color".into(), value: vec!["pink".into()].into(), @@ -1041,10 +1044,13 @@ mod tests { #[test] fn test_selectors_list_2() { init(); - assert_eq!(Parser::selector("&").map(|m| m.1), Ok("&".into())); + assert_eq!( + Parser::selector("&").map(|m| m.1), + Ok(vec!["&".into()].into()) + ); assert_eq!( Parser::selector("& input").map(|m| m.1), - Ok("& input".into()) + Ok(vec!["& input".into()].into()) ); } @@ -1070,7 +1076,11 @@ mod tests { .into(), }), ScopeContent::Block(Block { - condition: vec![".nested".into(), "${var_a}".into()].into(), + condition: vec![ + vec![".nested".into()].into(), + vec!["${var_a}".into()].into(), + ] + .into(), style_attributes: vec![ StyleAttribute { key: "background-color".into(), @@ -1094,7 +1104,7 @@ mod tests { let parsed = Parser::parse(test_str).expect("Failed to Parse Style"); let expected = Sheet::from(vec![ScopeContent::Block(Block { - condition: vec![".nested".into()].into(), + condition: vec![vec![".nested".into()].into()].into(), style_attributes: vec![].into(), })]); assert_eq!(parsed, expected); @@ -1153,7 +1163,8 @@ mod tests { .into(), }), ScopeContent::Block(Block { - condition: vec!["span".into(), "${sel_div}".into()].into(), + condition: vec![vec!["span".into()].into(), vec!["${sel_div}".into()].into()] + .into(), style_attributes: vec![StyleAttribute { key: "background-color".into(), value: vec!["blue".into()].into(), @@ -1161,7 +1172,7 @@ mod tests { .into(), }), ScopeContent::Block(Block { - condition: vec![":not(${sel_root})".into()].into(), + condition: vec![vec![":not(${sel_root})".into()].into()].into(), style_attributes: vec![StyleAttribute { key: "background-color".into(), value: vec!["black".into()].into(), @@ -1219,7 +1230,8 @@ mod tests { .into(), }), ScopeContent::Block(Block { - condition: vec!["span".into(), "${sel_div}".into()].into(), + condition: vec![vec!["span".into()].into(), vec!["${sel_div}".into()].into()] + .into(), style_attributes: vec![StyleAttribute { key: "background-color".into(), value: vec!["blue".into()].into(), @@ -1227,7 +1239,7 @@ mod tests { .into(), }), ScopeContent::Block(Block { - condition: vec![":not(${sel_root})".into()].into(), + condition: vec![vec![":not(${sel_root})".into()].into()].into(), style_attributes: vec![StyleAttribute { key: "background-color".into(), value: vec!["black".into()].into(), @@ -1281,7 +1293,8 @@ mod tests { .into(), }), ScopeContent::Block(Block { - condition: vec!["span".into(), "${sel_div}".into()].into(), + condition: vec![vec!["span".into()].into(), vec!["${sel_div}".into()].into()] + .into(), style_attributes: vec![StyleAttribute { key: "background-color".into(), value: vec!["blue".into()].into(), @@ -1289,7 +1302,7 @@ mod tests { .into(), }), ScopeContent::Block(Block { - condition: vec![":not(${sel_root})".into()].into(), + condition: vec![vec![":not(${sel_root})".into()].into()].into(), style_attributes: vec![StyleAttribute { key: "background-color".into(), value: vec!["black".into()].into(), diff --git a/packages/stylist-macros/Cargo.toml b/packages/stylist-macros/Cargo.toml index 5e8e17f..1aa49f7 100644 --- a/packages/stylist-macros/Cargo.toml +++ b/packages/stylist-macros/Cargo.toml @@ -6,6 +6,7 @@ license = "MIT" repository = "https://github.com/futursolo/stylist-rs" authors = [ "Kaede Hoshiakwa ", + "Martin Molzer ", ] description = "Stylist is a CSS-in-Rust styling solution for WebAssembly Applications." keywords = [ @@ -30,7 +31,9 @@ proc-macro-error = "1.0.4" proc-macro2 = "1.0.28" quote = "1.0.9" nom = "7.0.0" +syn = { version = "1.0", features = ["full", "extra-traits"] } +itertools = "0.10" +log = "0.4.14" [dev-dependencies] -log = "0.4.14" env_logger = "0.9.0" diff --git a/packages/stylist-macros/src/inline/component_value/function_token.rs b/packages/stylist-macros/src/inline/component_value/function_token.rs new file mode 100644 index 0000000..3f76ddb --- /dev/null +++ b/packages/stylist-macros/src/inline/component_value/function_token.rs @@ -0,0 +1,57 @@ +use super::{super::css_ident::CssIdent, ComponentValue}; +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::{ + parenthesized, + parse::{Parse, ParseBuffer, Result as ParseResult}, + token, +}; + +// Css-syntax parses like +// v----v function token +// foobar( arg1 , arg2 ) +// ---- - ---- ^-- closing bracket +// arguments +// +// while this parses like +// +// v-------------------v function token +// foobar( arg1 , arg2 ) +// ---- - ---- +// arguments +// +// This should not lead to noticable differences since we make no effort to +// do the insane special handling of 'url()' functions that's in the +// css spec +#[derive(Debug, Clone)] +pub struct FunctionToken { + pub(super) name: CssIdent, + pub(super) paren: token::Paren, + pub(super) args: Vec, +} + +impl ToTokens for FunctionToken { + fn to_tokens(&self, toks: &mut TokenStream) { + self.name.to_tokens(toks); + self.paren.surround(toks, |toks| { + for c in self.args.iter() { + c.to_tokens(toks); + } + }); + } +} + +impl Parse for FunctionToken { + fn parse(input: &ParseBuffer) -> ParseResult { + Self::parse_with_name(input.parse()?, input) + } +} + +impl FunctionToken { + pub(super) fn parse_with_name(name: CssIdent, input: &ParseBuffer) -> ParseResult { + let inner; + let paren = parenthesized!(inner in input); + let args = ComponentValue::parse_multiple(&inner)?; + Ok(Self { name, paren, args }) + } +} diff --git a/packages/stylist-macros/src/inline/component_value/interpolated_expression.rs b/packages/stylist-macros/src/inline/component_value/interpolated_expression.rs new file mode 100644 index 0000000..6f85c25 --- /dev/null +++ b/packages/stylist-macros/src/inline/component_value/interpolated_expression.rs @@ -0,0 +1,44 @@ +use super::super::output::OutputFragment; +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::{ + braced, + parse::{Parse, ParseBuffer, Result as ParseResult}, + token, Expr, +}; + +#[derive(Debug, Clone)] +pub struct InterpolatedExpression { + dollar: token::Dollar, + braces: token::Brace, + expr: Box, +} + +impl ToTokens for InterpolatedExpression { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.dollar.to_tokens(tokens); + self.braces.surround(tokens, |toks| { + self.expr.to_tokens(toks); + }); + } +} + +impl Parse for InterpolatedExpression { + fn parse(input: &ParseBuffer) -> ParseResult { + let dollar = input.parse()?; + let inner; + let braces = braced!(inner in input); + let expr = Box::new(inner.parse()?); + Ok(InterpolatedExpression { + dollar, + braces, + expr, + }) + } +} + +impl InterpolatedExpression { + pub fn to_output_fragment(&self) -> OutputFragment { + (&*self.expr).into() + } +} diff --git a/packages/stylist-macros/src/inline/component_value/mod.rs b/packages/stylist-macros/src/inline/component_value/mod.rs new file mode 100644 index 0000000..0261381 --- /dev/null +++ b/packages/stylist-macros/src/inline/component_value/mod.rs @@ -0,0 +1,202 @@ +//! The most prominent token in the css spec is called "component values". +//! You can think of this as being either a block, a function or a preserved (atomic) token. +//! +//! This guides our inline parser as follows: +//! - first re-tokenize the TokenStream into a stream of ComponentValues. For this step see +//! also [`ComponentValueStream`]. +//! - parse and verify the component values into blocks, @-rules and attributes. +//! +//! In general, only a parse error in the first step should be fatal and panic immediately, +//! while a parse error in the second step can recover and display a small precise error location +//! to the user, then continue parsing the rest of the input. +use super::{css_ident::CssIdent, output::OutputFragment}; +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::{ + parse::{Error as ParseError, Parse, ParseBuffer, Result as ParseResult}, + token, Lit, +}; + +mod preserved_token; +pub use preserved_token::PreservedToken; +mod simple_block; +pub use simple_block::SimpleBlock; +mod function_token; +pub use function_token::FunctionToken; +mod stream; +pub use stream::ComponentValueStream; +mod interpolated_expression; +pub use interpolated_expression::InterpolatedExpression; + +#[derive(Debug, Clone)] +pub enum ComponentValue { + Function(FunctionToken), + Token(PreservedToken), + Block(SimpleBlock), + Expr(InterpolatedExpression), +} + +impl ToTokens for ComponentValue { + fn to_tokens(&self, toks: &mut TokenStream) { + match self { + Self::Block(b) => b.to_tokens(toks), + Self::Function(f) => f.to_tokens(toks), + Self::Token(t) => t.to_tokens(toks), + Self::Expr(e) => e.to_tokens(toks), + } + } +} + +impl Parse for ComponentValue { + fn parse(input: &ParseBuffer) -> ParseResult { + let is_group = + input.peek(token::Brace) || input.peek(token::Bracket) || input.peek(token::Paren); + if is_group { + Ok(Self::Block(input.parse()?)) + } else if input.peek(token::Dollar) && input.peek2(token::Brace) { + Ok(Self::Expr(input.parse()?)) + } else if !CssIdent::peek(input) { + Ok(Self::Token(input.parse()?)) + } else { + let ident = input.parse()?; + if input.peek(token::Paren) { + Ok(Self::Function(FunctionToken::parse_with_name( + ident, input, + )?)) + } else { + Ok(Self::Token(PreservedToken::Ident(ident))) + } + } + } +} + +impl ComponentValue { + fn parse_multiple(input: &ParseBuffer) -> ParseResult> { + ComponentValueStream::from(input).collect() + } +} + +impl ComponentValue { + // clippy is of course right, it's just making the code less readable for negligable + // performance gains + #[allow(clippy::vec_init_then_push)] + pub fn to_output_fragments(&self) -> Vec { + match self { + Self::Token(token) => { + vec![token.clone().into()] + } + Self::Expr(expr) => vec![expr.to_output_fragment()], + Self::Block(SimpleBlock::Bracketed { contents, .. }) => { + let mut output = vec![]; + output.push('['.into()); + for c in contents { + output.extend(c.to_output_fragments()); + } + output.push(']'.into()); + output + } + Self::Block(SimpleBlock::Paren { contents, .. }) => { + let mut output = vec![]; + output.push('('.into()); + for c in contents { + output.extend(c.to_output_fragments()); + } + output.push(')'.into()); + output + } + Self::Function(FunctionToken { name, args, .. }) => { + let mut output = vec![]; + output.push(name.clone().into()); + output.push('('.into()); + for c in args { + output.extend(c.to_output_fragments()); + } + output.push(')'.into()); + output + } + Self::Block(SimpleBlock::Braced { .. }) => { + // this kind of block is not supposed to appear in @-rule preludes, block qualifiers + // or attribute values and as such should not get emitted + unreachable!("braced blocks should not get reified"); + } + } + } + + // Overly simplified parsing of a css attribute + #[must_use = "validation errors should not be discarded"] + pub fn validate_attribute_token(&self) -> Vec { + match self { + Self::Expr(_) + | Self::Token(PreservedToken::Ident(_)) + | Self::Token(PreservedToken::Literal(_)) => vec![], + Self::Function(FunctionToken { args, .. }) => args + .iter() + .flat_map(|a| a.validate_attribute_token()) + .collect(), + Self::Block(_) => { + let error = ParseError::new_spanned( + self, + concat!( + "expected a valid part of an attribute, got a block. ", + "Did you mean to write `${..}` to interpolate an expression?" + ), + ); + vec![error] + } + Self::Token(PreservedToken::Punct(p)) => { + if !"-/%:,#".contains(p.as_char()) { + vec![ParseError::new_spanned( + self, + "expected a valid part of an attribute", + )] + } else { + vec![] + } + } + } + } + + // Overly simplified version of parsing a css selector :) + pub fn validate_selector_token(&self) -> ParseResult> { + match self { + Self::Expr(_) | Self::Function(_) | Self::Token(PreservedToken::Ident(_)) => Ok(vec![]), + Self::Block(SimpleBlock::Bracketed { contents, .. }) => { + let mut collected = vec![]; + for e in contents.iter().map(|e| e.validate_selector_token()) { + collected.extend(e?); + } + Ok(collected) + } + Self::Block(_) => Ok(vec![ParseError::new_spanned( + self, + "expected a valid part of a scope qualifier, not a block", + )]), + Self::Token(PreservedToken::Literal(l)) => { + let syn_lit = Lit::new(l.clone()); + if !matches!(syn_lit, Lit::Str(_)) { + Ok(vec![ParseError::new_spanned( + self, + "only string literals are allowed in selectors", + )]) + } else { + Ok(vec![]) + } + } + Self::Token(PreservedToken::Punct(p)) => { + if p.as_char() == ';' { + Err(ParseError::new_spanned( + self, + "unexpected ';' in selector, did you mean to write an attribute?", + )) + } else if !"&>+~|$*=^#.:,".contains(p.as_char()) { + Ok(vec![ParseError::new_spanned( + self, + "unexpected punctuation in selector", + )]) + } else { + Ok(vec![]) + } + } + } + } +} diff --git a/packages/stylist-macros/src/inline/component_value/preserved_token.rs b/packages/stylist-macros/src/inline/component_value/preserved_token.rs new file mode 100644 index 0000000..2d1f56d --- /dev/null +++ b/packages/stylist-macros/src/inline/component_value/preserved_token.rs @@ -0,0 +1,45 @@ +use super::super::css_ident::CssIdent; +use proc_macro2::{Literal, Punct, TokenStream}; +use quote::ToTokens; +use syn::parse::{Parse, ParseBuffer, Result as ParseResult}; + +#[derive(Debug, Clone)] +pub enum PreservedToken { + Punct(Punct), + Literal(Literal), + Ident(CssIdent), +} + +impl ToTokens for PreservedToken { + fn to_tokens(&self, toks: &mut TokenStream) { + match self { + Self::Ident(i) => i.to_tokens(toks), + Self::Literal(i) => i.to_tokens(toks), + Self::Punct(i) => i.to_tokens(toks), + } + } +} + +impl Parse for PreservedToken { + fn parse(input: &ParseBuffer) -> ParseResult { + if CssIdent::peek(input) { + Ok(Self::Ident(input.parse()?)) + } else if input.cursor().punct().is_some() { + Ok(Self::Punct(input.parse()?)) + } else if input.cursor().literal().is_some() { + Ok(Self::Literal(input.parse()?)) + } else { + Err(input.error("Expected a css identifier, punctuation or a literal")) + } + } +} + +impl PreservedToken { + pub fn to_output_string(&self) -> String { + match self { + Self::Ident(i) => i.to_output_string(), + Self::Literal(l) => format!("{}", l), + Self::Punct(p) => format!("{}", p.as_char()), + } + } +} diff --git a/packages/stylist-macros/src/inline/component_value/simple_block.rs b/packages/stylist-macros/src/inline/component_value/simple_block.rs new file mode 100644 index 0000000..f97ba3b --- /dev/null +++ b/packages/stylist-macros/src/inline/component_value/simple_block.rs @@ -0,0 +1,70 @@ +use super::ComponentValue; +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::{ + braced, bracketed, parenthesized, + parse::{Parse, ParseBuffer, Result as ParseResult}, + token, +}; + +#[derive(Debug, Clone)] +pub enum SimpleBlock { + Braced { + brace: token::Brace, + contents: Vec, + }, + Bracketed { + bracket: token::Bracket, + contents: Vec, + }, + Paren { + paren: token::Paren, + contents: Vec, + }, +} + +impl ToTokens for SimpleBlock { + fn to_tokens(&self, toks: &mut TokenStream) { + match self { + Self::Braced { brace, contents } => brace.surround(toks, |toks| { + for c in contents.iter() { + c.to_tokens(toks); + } + }), + Self::Bracketed { bracket, contents } => bracket.surround(toks, |toks| { + for c in contents.iter() { + c.to_tokens(toks); + } + }), + Self::Paren { paren, contents } => paren.surround(toks, |toks| { + for c in contents.iter() { + c.to_tokens(toks); + } + }), + } + } +} + +impl Parse for SimpleBlock { + fn parse(input: &ParseBuffer) -> ParseResult { + let lookahead = input.lookahead1(); + if lookahead.peek(token::Brace) { + let inside; + let brace = braced!(inside in input); + let contents = ComponentValue::parse_multiple(&inside)?; + Ok(Self::Braced { brace, contents }) + } else if lookahead.peek(token::Bracket) { + let inside; + let bracket = bracketed!(inside in input); + let contents = ComponentValue::parse_multiple(&inside)?; + Ok(Self::Bracketed { bracket, contents }) + } else if lookahead.peek(token::Paren) { + let inside; + let paren = parenthesized!(inside in input); + let contents = ComponentValue::parse_multiple(&inside)?; + Ok(Self::Paren { paren, contents }) + } else { + Err(lookahead.error()) + } + } +} diff --git a/packages/stylist-macros/src/inline/component_value/stream.rs b/packages/stylist-macros/src/inline/component_value/stream.rs new file mode 100644 index 0000000..823e505 --- /dev/null +++ b/packages/stylist-macros/src/inline/component_value/stream.rs @@ -0,0 +1,38 @@ +use super::ComponentValue; +use std::ops::Deref; +use syn::parse::{ParseBuffer, Result as ParseResult}; + +// Implements an iterator over parsed component values instead of rust tokens. +#[derive(Debug)] +pub struct ComponentValueStream<'a> { + input: &'a ParseBuffer<'a>, +} + +impl<'a> From<&'a ParseBuffer<'a>> for ComponentValueStream<'a> { + fn from(input: &'a ParseBuffer<'a>) -> Self { + Self { input } + } +} + +impl<'a> From> for &'a ParseBuffer<'a> { + fn from(stream: ComponentValueStream<'a>) -> Self { + stream.input + } +} + +impl<'a> Deref for ComponentValueStream<'a> { + type Target = ParseBuffer<'a>; + fn deref(&self) -> &Self::Target { + self.input + } +} + +impl<'a> Iterator for ComponentValueStream<'a> { + type Item = ParseResult; + fn next(&mut self) -> Option { + if self.input.is_empty() { + return None; + } + Some(self.input.parse()) + } +} diff --git a/packages/stylist-macros/src/inline/css_ident.rs b/packages/stylist-macros/src/inline/css_ident.rs new file mode 100644 index 0000000..df3a245 --- /dev/null +++ b/packages/stylist-macros/src/inline/css_ident.rs @@ -0,0 +1,113 @@ +use proc_macro2::{Punct, Spacing, TokenStream}; +use quote::ToTokens; +use std::fmt::{Display, Formatter}; +use syn::{ + ext::IdentExt, + parse::{Parse, ParseBuffer, Result as ParseResult}, + token, Ident, +}; + +syn::custom_punctuation!(DoubleSub, --); + +#[derive(Debug, Clone)] +pub enum IdentPart { + Dash(Punct), + Ident(Ident), +} + +#[derive(Debug, Clone)] +pub struct CssIdent { + parts: Vec, +} + +impl IdentPart { + pub fn peek(lookahead: &ParseBuffer, accept_dash: bool, accept_ident: bool) -> bool { + let peek_dash = accept_dash && (lookahead.peek(token::Sub) || lookahead.peek(DoubleSub)); + let peek_ident = accept_ident && lookahead.peek(Ident::peek_any); + peek_dash || peek_ident + } +} + +impl Display for CssIdent { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + let name = self.to_output_string(); + f.write_str(&name) + } +} + +impl CssIdent { + pub fn peek(lookahead: &ParseBuffer) -> bool { + if lookahead.peek(token::Sub) { + // A single dash is not an identifier + lookahead.peek2(token::Sub) || lookahead.peek2(Ident::peek_any) + } else { + IdentPart::peek(lookahead, true, true) + } + } + + pub fn to_output_string(&self) -> String { + self.parts + .iter() + .map(|p| match p { + IdentPart::Dash(_) => "-".into(), + IdentPart::Ident(t) => format!("{}", t.unraw()), + }) + .collect() + } +} + +impl IdentPart { + fn parse_part( + input: &ParseBuffer, + accept_dash: bool, + accept_ident: bool, + ) -> ParseResult { + debug_assert!(accept_dash || accept_ident); + let lookahead = input.lookahead1(); + if accept_dash && (lookahead.peek(token::Sub) || lookahead.peek(DoubleSub)) { + let dash = input.parse::()?; + debug_assert!(dash.as_char() == '-', "expected a - character"); + Ok(IdentPart::Dash(dash)) + } else if accept_ident && lookahead.peek(Ident::peek_any) { + Ok(IdentPart::Ident(input.call(Ident::parse_any)?)) + } else { + Err(lookahead.error()) + } + } +} + +impl Parse for CssIdent { + fn parse(input: &ParseBuffer) -> ParseResult { + let mut parts = vec![IdentPart::parse_part(input, true, true)?]; + loop { + let (joins_dash, joins_idents) = match parts.last().unwrap() { + // Dashes always join identifiers, and only over dashes if jointly spaced + IdentPart::Dash(s) => (s.spacing() == Spacing::Joint, true), + // Identifiers join dashes, but never other dashes + IdentPart::Ident(_) => (true, false), + }; + if !IdentPart::peek(input, joins_dash, joins_idents) { + break; + } + parts.push(IdentPart::parse_part(input, joins_dash, joins_idents)?); + } + Ok(Self { parts }) + } +} + +impl ToTokens for IdentPart { + fn to_tokens(&self, toks: &mut TokenStream) { + match self { + Self::Dash(d) => d.to_tokens(toks), + Self::Ident(i) => i.to_tokens(toks), + } + } +} + +impl ToTokens for CssIdent { + fn to_tokens(&self, toks: &mut TokenStream) { + for p in self.parts.iter() { + p.to_tokens(toks); + } + } +} diff --git a/packages/stylist-macros/src/inline/mod.rs b/packages/stylist-macros/src/inline/mod.rs new file mode 100644 index 0000000..79cd0f8 --- /dev/null +++ b/packages/stylist-macros/src/inline/mod.rs @@ -0,0 +1,34 @@ +mod component_value; +mod css_ident; + +mod output; +mod parse; + +use log::debug; +use output::{AllowedUsage, Reify}; +use parse::CssRootNode; +use proc_macro2::TokenStream; +use quote::quote; + +pub fn macro_fn(input: TokenStream) -> TokenStream { + let root = match syn::parse2::(input) { + Ok(parsed) => parsed, + Err(failed) => return failed.to_compile_error(), + }; + debug!("Parsed as: {:?}", root); + + let (quoted_sheet, allowed_usage) = root.into_output().into_context_aware_tokens().into_value(); + if AllowedUsage::Static <= allowed_usage { + quote! { { + use ::stylist::vendor::once_cell::sync::Lazy; + + static SHEET_REF: Lazy<::stylist::ast::Sheet> = Lazy::new( + || #quoted_sheet + ); + + SHEET_REF.clone() + } } + } else { + quoted_sheet + } +} diff --git a/packages/stylist-macros/src/inline/output/block.rs b/packages/stylist-macros/src/inline/output/block.rs new file mode 100644 index 0000000..f7b8315 --- /dev/null +++ b/packages/stylist-macros/src/inline/output/block.rs @@ -0,0 +1,26 @@ +use super::{ContextRecorder, IntoCowVecTokens, OutputAttribute, OutputQualifier, Reify}; +use proc_macro2::TokenStream; +use quote::quote; + +pub struct OutputQualifiedRule { + pub qualifier: OutputQualifier, + pub attributes: Vec, +} + +impl Reify for OutputQualifiedRule { + fn into_token_stream(self, ctx: &mut ContextRecorder) -> TokenStream { + let Self { + qualifier, + attributes, + } = self; + let qualifier = qualifier.into_token_stream(ctx); + let attributes = attributes.into_cow_vec_tokens(ctx); + + quote! { + ::stylist::ast::Block { + condition: #qualifier, + style_attributes: #attributes, + } + } + } +} diff --git a/packages/stylist-macros/src/inline/output/context_recorder.rs b/packages/stylist-macros/src/inline/output/context_recorder.rs new file mode 100644 index 0000000..5ed1812 --- /dev/null +++ b/packages/stylist-macros/src/inline/output/context_recorder.rs @@ -0,0 +1,49 @@ +//! This module implements a type abstractly tracking in what kind of expression context +//! an item appears. This information is leverage to provide improved performance and +//! static caching of parts of the generated output. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum AllowedUsage { + // ``` + // let width = 500; + // style! { width: ${width}; } + // // ^^^^^^ dynamic expression, can't wrap style in Lazy + // ``` + Dynamic, + // ``` + // style! { width: 500px; } + // // ------------- everything is static, do wrap style in Lazy + // ``` + Static, + // TODO: we can probably avoid a few allocations if we track which parts + // of the ast can be constructed statically (with const methods), which is + // even stronger than constructing it in the global context in a Lazy. + // Should you decide to implement this, keep in mind to change Self::MAX + // and adjust the generation of cow-vec tokens. Also check the usages of + // MaybeStatic::statick if they can be upgraded to Const. + // Const, +} + +pub struct ContextRecorder { + usage: AllowedUsage, +} + +impl Default for ContextRecorder { + fn default() -> Self { + Self { + usage: AllowedUsage::Static, + } + } +} + +impl ContextRecorder { + // Record the usage of a dynamic expression + pub fn uses_dynamic_argument(&mut self) { + self.usage = self.usage.min(AllowedUsage::Dynamic) + } + pub fn merge_with(&mut self, other: &ContextRecorder) { + self.usage = self.usage.min(other.usage) + } + pub fn usage(&self) -> AllowedUsage { + self.usage + } +} diff --git a/packages/stylist-macros/src/inline/output/maybe_static.rs b/packages/stylist-macros/src/inline/output/maybe_static.rs new file mode 100644 index 0000000..f595a1c --- /dev/null +++ b/packages/stylist-macros/src/inline/output/maybe_static.rs @@ -0,0 +1,92 @@ +use super::{AllowedUsage, ContextRecorder, Reify}; +use proc_macro2::TokenStream; +use quote::quote; +use std::iter::FromIterator; + +pub trait IntoCowVecTokens: IntoIterator +where + Self::Item: Reify, +{ + // Get a TokenStream of an expression of type Cow<'_, [typ]>, containing + // as elements the values formed by the expressions in this stream. + // Depending on the context in which the expression can be expanded, + // uses either Cow::Owned or Cow::Borrowed (currently always Cow::Owned). + fn into_cow_vec_tokens(self, ctx: &mut ContextRecorder) -> TokenStream; +} + +impl IntoCowVecTokens for I +where + I: IntoIterator, + I::Item: Reify, +{ + fn into_cow_vec_tokens(self, ctx: &mut ContextRecorder) -> TokenStream { + self.into_iter() + .map(|e| e.into_context_aware_tokens()) + .collect::>() + .into_cow_vec_tokens_impl(ctx) + } +} + +// Used e.g. to decide whether a sheet can be statically cached in a Lazy or must be +// created everytime anew. +pub struct MaybeStatic { + value: T, + context: ContextRecorder, +} + +impl MaybeStatic { + pub fn in_context(value: T, context: ContextRecorder) -> Self { + Self { value, context } + } + pub fn into_value(self) -> (T, AllowedUsage) { + (self.value, self.context.usage()) + } +} + +impl MaybeStatic> { + // Get a TokenStream of an expression of type Cow<'_, [typ]>, containing + // as elements the expressions form from the Vec<_> in this MaybeStatic. + // Depending on the context in which the expression can be expanded, + // uses either Cow::Owned or Cow::Borrowed (currently always Cow::Owned). + fn into_cow_vec_tokens_impl(self, ctx: &mut ContextRecorder) -> TokenStream { + ctx.merge_with(&self.context); + let contents = self.value; + quote! { + ::std::vec![ + #( #contents, )* + ].into() + } + } +} + +// compare to the implementation of Result: FromIterator + Extend +struct StaticShun<'a, I> { + iter: I, + context: &'a mut ContextRecorder, +} + +impl<'a, T, I: Iterator>> Iterator for StaticShun<'a, I> { + type Item = T; + + fn next(&mut self) -> Option { + match self.iter.next() { + None => None, + Some(MaybeStatic { value, context }) => { + self.context.merge_with(&context); + Some(value) + } + } + } +} + +impl> FromIterator> for MaybeStatic { + fn from_iter>>(iter: I) -> Self { + let mut context = Default::default(); + let v = StaticShun { + iter: iter.into_iter(), + context: &mut context, + } + .collect(); + MaybeStatic::in_context(v, context) + } +} diff --git a/packages/stylist-macros/src/inline/output/mod.rs b/packages/stylist-macros/src/inline/output/mod.rs new file mode 100644 index 0000000..d1d23a8 --- /dev/null +++ b/packages/stylist-macros/src/inline/output/mod.rs @@ -0,0 +1,45 @@ +//! This module intentionally mirrors stylist_core::ast in structure and +//! is responsible for transforming finished macro outputs into the TokenStream +//! emitted by the different macros. +use proc_macro2::TokenStream; + +mod sheet; +pub use sheet::OutputSheet; +mod rule; +pub use rule::OutputAtRule; +mod block; +pub use block::OutputQualifiedRule; +mod selector; +pub use selector::OutputQualifier; +mod scope_content; +pub use scope_content::OutputScopeContent; +mod rule_content; +pub use rule_content::OutputRuleContent; +mod style_attr; +pub use style_attr::OutputAttribute; +mod str_frag; +pub use str_frag::{fragment_coalesce, fragment_spacing, OutputFragment}; + +mod context_recorder; +pub use context_recorder::{AllowedUsage, ContextRecorder}; +mod maybe_static; +pub use maybe_static::{IntoCowVecTokens, MaybeStatic}; + +/// Reify a structure into an expression of a specific type. +pub trait Reify { + fn into_token_stream(self, ctx: &mut ContextRecorder) -> TokenStream; + fn into_context_aware_tokens(self) -> MaybeStatic + where + Self: Sized, + { + let mut ctx = Default::default(); + let value = self.into_token_stream(&mut ctx); + MaybeStatic::in_context(value, ctx) + } +} + +impl Reify for syn::Error { + fn into_token_stream(self, _: &mut ContextRecorder) -> TokenStream { + self.into_compile_error() + } +} diff --git a/packages/stylist-macros/src/inline/output/rule.rs b/packages/stylist-macros/src/inline/output/rule.rs new file mode 100644 index 0000000..e9cd347 --- /dev/null +++ b/packages/stylist-macros/src/inline/output/rule.rs @@ -0,0 +1,49 @@ +use super::{ + super::{component_value::ComponentValue, css_ident::CssIdent}, + fragment_coalesce, fragment_spacing, ContextRecorder, IntoCowVecTokens, OutputFragment, + OutputRuleContent, Reify, +}; +use crate::spacing_iterator::SpacedIterator; +use itertools::Itertools; +use proc_macro2::TokenStream; +use quote::quote; +use std::iter::once; +use syn::parse::Error as ParseError; + +pub struct OutputAtRule { + pub name: CssIdent, + pub prelude: Vec, + pub contents: Vec, + pub errors: Vec, +} + +impl Reify for OutputAtRule { + fn into_token_stream(self, ctx: &mut ContextRecorder) -> TokenStream { + let Self { + name, + prelude, + contents, + errors, + } = self; + + let at_name = OutputFragment::Str(format!("@{} ", name.to_output_string())); + let prelude_parts = prelude + .into_iter() + .flat_map(|p| p.to_output_fragments()) + .spaced_with(fragment_spacing) + .coalesce(fragment_coalesce); + let condition = once(at_name).chain(prelude_parts).into_cow_vec_tokens(ctx); + let content = contents.into_cow_vec_tokens(ctx); + let errors = errors.into_iter().map(|e| e.into_compile_error()); + + quote! { + ::stylist::ast::Rule { + condition: { + #( #errors )* + #condition + }, + content: #content, + } + } + } +} diff --git a/packages/stylist-macros/src/inline/output/rule_content.rs b/packages/stylist-macros/src/inline/output/rule_content.rs new file mode 100644 index 0000000..9a6f0b1 --- /dev/null +++ b/packages/stylist-macros/src/inline/output/rule_content.rs @@ -0,0 +1,26 @@ +use super::{ContextRecorder, OutputAtRule, OutputQualifiedRule, Reify}; +use proc_macro2::TokenStream; +use quote::quote; +use syn::Error as ParseError; + +pub enum OutputRuleContent { + AtRule(OutputAtRule), + Block(OutputQualifiedRule), + Err(ParseError), +} + +impl Reify for OutputRuleContent { + fn into_token_stream(self, ctx: &mut ContextRecorder) -> TokenStream { + match self { + Self::AtRule(rule) => { + let block_tokens = rule.into_token_stream(ctx); + quote! { ::stylist::ast::RuleContent::Rule(::std::boxed::Box::new(#block_tokens)) } + } + Self::Block(block) => { + let block_tokens = block.into_token_stream(ctx); + quote! { ::stylist::ast::RuleContent::Block(#block_tokens) } + } + Self::Err(err) => err.into_token_stream(ctx), + } + } +} diff --git a/packages/stylist-macros/src/inline/output/scope_content.rs b/packages/stylist-macros/src/inline/output/scope_content.rs new file mode 100644 index 0000000..75a8229 --- /dev/null +++ b/packages/stylist-macros/src/inline/output/scope_content.rs @@ -0,0 +1,26 @@ +use super::{ContextRecorder, OutputAtRule, OutputQualifiedRule, Reify}; +use proc_macro2::TokenStream; +use quote::quote; +use syn::Error as ParseError; + +pub enum OutputScopeContent { + AtRule(OutputAtRule), + Block(OutputQualifiedRule), + Err(ParseError), +} + +impl Reify for OutputScopeContent { + fn into_token_stream(self, ctx: &mut ContextRecorder) -> TokenStream { + match self { + Self::AtRule(rule) => { + let block_tokens = rule.into_token_stream(ctx); + quote! { ::stylist::ast::ScopeContent::Rule(#block_tokens) } + } + Self::Block(block) => { + let block_tokens = block.into_token_stream(ctx); + quote! { ::stylist::ast::ScopeContent::Block(#block_tokens) } + } + Self::Err(err) => err.into_token_stream(ctx), + } + } +} diff --git a/packages/stylist-macros/src/inline/output/selector.rs b/packages/stylist-macros/src/inline/output/selector.rs new file mode 100644 index 0000000..eac88cc --- /dev/null +++ b/packages/stylist-macros/src/inline/output/selector.rs @@ -0,0 +1,75 @@ +use super::{ + super::component_value::{ComponentValue, PreservedToken}, + fragment_coalesce, fragment_spacing, ContextRecorder, IntoCowVecTokens, Reify, +}; +use crate::spacing_iterator::SpacedIterator; +use itertools::Itertools; +use proc_macro2::TokenStream; +use quote::quote; +use syn::parse::Error as ParseError; + +struct OutputSelector { + selectors: Vec, +} + +impl Reify for OutputSelector { + fn into_token_stream(self, ctx: &mut ContextRecorder) -> TokenStream { + let parts = self + .selectors + .into_iter() + // reify the individual parts + .flat_map(|p| p.to_output_fragments()) + // space them correctly + .spaced_with(fragment_spacing) + // optimize successive (string) literals + .coalesce(fragment_coalesce) + .into_cow_vec_tokens(ctx); + quote! { + ::stylist::ast::Selector { + fragments: #parts, + } + } + } +} + +#[derive(Clone)] +pub struct OutputQualifier { + pub selectors: Vec, + pub errors: Vec, +} + +impl Reify for OutputQualifier { + fn into_token_stream(self, ctx: &mut ContextRecorder) -> TokenStream { + fn is_not_comma(q: &ComponentValue) -> bool { + !matches!(q, ComponentValue::Token(PreservedToken::Punct(ref p)) if p.as_char() == ',') + } + + let Self { + selectors, errors, .. + } = self; + + let selectors = selectors + .into_iter() + .peekable() + .batching(|it| { + // Return if no items left + it.peek()?; + // Take until the next comma + let selector_parts = it.peeking_take_while(is_not_comma); + let selector = OutputSelector { + selectors: selector_parts.collect(), + }; + it.next(); // Consume the comma + Some(selector) + }) + .into_cow_vec_tokens(ctx); + let errors = errors.into_iter().map(|e| e.into_compile_error()); + + quote! { + { + #( #errors )* + #selectors + } + } + } +} diff --git a/packages/stylist-macros/src/inline/output/sheet.rs b/packages/stylist-macros/src/inline/output/sheet.rs new file mode 100644 index 0000000..760cf73 --- /dev/null +++ b/packages/stylist-macros/src/inline/output/sheet.rs @@ -0,0 +1,22 @@ +use super::{ContextRecorder, IntoCowVecTokens, OutputScopeContent, Reify}; +use proc_macro2::TokenStream; +use quote::quote; + +pub struct OutputSheet { + pub contents: Vec, +} + +impl Reify for OutputSheet { + fn into_token_stream(self, ctx: &mut ContextRecorder) -> TokenStream { + let Self { contents } = self; + let contents = contents.into_cow_vec_tokens(ctx); + + quote! { + { + use ::std::convert::{From, Into}; + use ::stylist::ast::Sheet; + >::from(#contents) + } + } + } +} diff --git a/packages/stylist-macros/src/inline/output/str_frag.rs b/packages/stylist-macros/src/inline/output/str_frag.rs new file mode 100644 index 0000000..dbf639b --- /dev/null +++ b/packages/stylist-macros/src/inline/output/str_frag.rs @@ -0,0 +1,133 @@ +use super::{ + super::{component_value::PreservedToken, css_ident::CssIdent}, + ContextRecorder, Reify, +}; +use proc_macro2::{Delimiter, Span, TokenStream}; +use quote::{quote, quote_spanned}; +use syn::{spanned::Spanned, Expr, ExprLit, Lit, LitStr}; + +#[derive(Debug, Clone)] +pub enum OutputFragment { + Raw(TokenStream), + Token(PreservedToken), + Str(String), + Delimiter(Delimiter, /*start:*/ bool), +} + +impl From for OutputFragment { + fn from(c: char) -> Self { + match c { + '{' => Self::Delimiter(Delimiter::Brace, true), + '}' => Self::Delimiter(Delimiter::Brace, false), + '[' => Self::Delimiter(Delimiter::Bracket, true), + ']' => Self::Delimiter(Delimiter::Bracket, false), + '(' => Self::Delimiter(Delimiter::Parenthesis, true), + ')' => Self::Delimiter(Delimiter::Parenthesis, false), + ' ' => Self::Str(" ".into()), + _ => unreachable!("Delimiter {} not recognized", c), + } + } +} + +impl From for OutputFragment { + fn from(t: TokenStream) -> Self { + Self::Raw(t) + } +} + +impl From for OutputFragment { + fn from(t: PreservedToken) -> Self { + Self::Token(t) + } +} + +impl From for OutputFragment { + fn from(i: CssIdent) -> Self { + PreservedToken::Ident(i).into() + } +} + +impl<'a> From<&'a Expr> for OutputFragment { + fn from(expr: &Expr) -> Self { + if let Expr::Lit(ExprLit { + lit: Lit::Str(ref litstr), + .. + }) = expr + { + return Self::Str(litstr.value()); + } + + // quote spanned here so that errors related to calling #ident_write_expr show correctly + Self::Raw(quote_spanned! {expr.span()=> + (&{ #expr } as &dyn ::std::fmt::Display).to_string().into() + }) + } +} + +impl OutputFragment { + fn str_for_delim(d: Delimiter, start: bool) -> &'static str { + match (d, start) { + (Delimiter::Brace, true) => "{", + (Delimiter::Brace, false) => "}", + (Delimiter::Bracket, true) => "[", + (Delimiter::Bracket, false) => "]", + (Delimiter::Parenthesis, true) => "(", + (Delimiter::Parenthesis, false) => ")", + (Delimiter::None, _) => unreachable!("only actual delimiters allowed"), + } + } + /// Return the string literal that will be quoted, or a full tokenstream + fn try_into_string(self) -> Result { + match self { + Self::Raw(t) => Err(t), + Self::Str(s) => Ok(s), + Self::Token(t) => Ok(t.to_output_string()), + Self::Delimiter(kind, start) => Ok(Self::str_for_delim(kind, start).into()), + } + } + fn as_str(&self) -> Option { + self.clone().try_into_string().ok() + } +} + +impl Reify for OutputFragment { + fn into_token_stream(self, ctx: &mut ContextRecorder) -> TokenStream { + match self.try_into_string() { + Err(t) => { + ctx.uses_dynamic_argument(); + t + } + Ok(lit) => { + let lit_str = LitStr::new(lit.as_ref(), Span::call_site()); + quote! { #lit_str.into() } + } + } + } +} + +pub fn fragment_spacing(l: &OutputFragment, r: &OutputFragment) -> Option { + use OutputFragment::*; + use PreservedToken::*; + let needs_spacing = matches!( + (l, r), + (Delimiter(_, false), Token(Ident(_))) + | ( + Token(Ident(_)) | Token(Literal(_)), + Token(Ident(_)) | Token(Literal(_)) + ) + ); + needs_spacing.then(|| ' '.into()) +} + +pub fn fragment_coalesce( + l: OutputFragment, + r: OutputFragment, +) -> Result { + match (l.as_str(), r.as_str()) { + (Some(lt), Some(rt)) => { + // Two successive string literals can be combined into a single one + Ok(OutputFragment::Str(format!("{}{}", lt, rt))) + } + _ => Err((l, r)), + } +} diff --git a/packages/stylist-macros/src/inline/output/style_attr.rs b/packages/stylist-macros/src/inline/output/style_attr.rs new file mode 100644 index 0000000..2a5acbe --- /dev/null +++ b/packages/stylist-macros/src/inline/output/style_attr.rs @@ -0,0 +1,43 @@ +use super::{ + super::component_value::ComponentValue, fragment_coalesce, fragment_spacing, ContextRecorder, + IntoCowVecTokens, OutputFragment, Reify, +}; +use crate::spacing_iterator::SpacedIterator; +use itertools::Itertools; +use proc_macro2::TokenStream; +use quote::quote; +use syn::parse::Error as ParseError; + +pub struct OutputAttribute { + pub key: OutputFragment, + pub values: Vec, + pub errors: Vec, +} + +impl Reify for OutputAttribute { + fn into_token_stream(self, ctx: &mut ContextRecorder) -> TokenStream { + let Self { + key, + values, + errors, + } = self; + let errors = errors.into_iter().map(|e| e.into_compile_error()); + + let key = key.into_token_stream(ctx); + let value_parts = values + .iter() + .flat_map(|p| p.to_output_fragments()) + .spaced_with(fragment_spacing) + .coalesce(fragment_coalesce) + .into_cow_vec_tokens(ctx); + quote! { + ::stylist::ast::StyleAttribute { + key: #key, + value: { + #( #errors )* + #value_parts + }, + } + } + } +} diff --git a/packages/stylist-macros/src/inline/parse/attribute.rs b/packages/stylist-macros/src/inline/parse/attribute.rs new file mode 100644 index 0000000..423e1b5 --- /dev/null +++ b/packages/stylist-macros/src/inline/parse/attribute.rs @@ -0,0 +1,114 @@ +use super::super::{ + component_value::{ + ComponentValue, ComponentValueStream, InterpolatedExpression, PreservedToken, + }, + css_ident::CssIdent, + output::{OutputAttribute, OutputFragment}, +}; +use syn::{ + parse::{Error as ParseError, Parse, ParseBuffer, Result as ParseResult}, + spanned::Spanned, + token, +}; + +#[derive(Debug)] +pub enum CssAttributeName { + Identifier(CssIdent), + InjectedExpr(InterpolatedExpression), +} + +#[derive(Debug)] +pub struct CssAttributeValue { + values: Vec, + errors: Vec, +} + +#[derive(Debug)] +pub struct CssAttribute { + name: CssAttributeName, + colon: token::Colon, + value: CssAttributeValue, + terminator: token::Semi, +} + +impl Parse for CssAttribute { + fn parse(input: &ParseBuffer) -> ParseResult { + let mut component_iter = ComponentValueStream::from(input); + // Advance the real iterator + let name = component_iter + .next() + .ok_or_else(|| input.error("Attribute: unexpected end of input"))??; + let name_span = name.span(); + let name = name.maybe_to_attribute_name().ok_or_else(|| { + ParseError::new( + name_span, + "expected an identifier or interpolated expression", + ) + })?; + + let colon = input.parse()?; + let value = input.parse()?; + let terminator = input.parse()?; + Ok(CssAttribute { + name, + colon, + value, + terminator, + }) + } +} + +impl Parse for CssAttributeValue { + fn parse(input: &ParseBuffer) -> ParseResult { + let mut component_iter = ComponentValueStream::from(input); + let mut values = vec![]; + let mut errors = vec![]; + + loop { + // Consume all tokens till the next ';' + if input.peek(token::Semi) { + break; + } + let next_token = component_iter + .next() + .ok_or_else(|| input.error("AttributeValue: unexpected end of input"))??; + let token_errors = next_token.validate_attribute_token(); + if token_errors.is_empty() { + values.push(next_token); + } + errors.extend(token_errors); + } + Ok(Self { values, errors }) + } +} + +impl ComponentValue { + pub(super) fn maybe_to_attribute_name(self) -> Option { + match self { + ComponentValue::Token(PreservedToken::Ident(i)) => { + Some(CssAttributeName::Identifier(i)) + } + ComponentValue::Expr(expr) => Some(CssAttributeName::InjectedExpr(expr)), + _ => None, + } + } +} + +impl CssAttribute { + pub(super) fn into_output(self) -> OutputAttribute { + OutputAttribute { + key: self.name.into_output(), + values: self.value.values, + errors: self.value.errors, + } + } +} + +impl CssAttributeName { + fn into_output(self) -> OutputFragment { + match self { + Self::Identifier(name) => name.into(), + Self::InjectedExpr(expr) => expr.to_output_fragment(), + } + } +} diff --git a/packages/stylist-macros/src/inline/parse/block.rs b/packages/stylist-macros/src/inline/parse/block.rs new file mode 100644 index 0000000..54649a5 --- /dev/null +++ b/packages/stylist-macros/src/inline/parse/block.rs @@ -0,0 +1,42 @@ +use super::{normalize_hierarchy_impl, CssBlockQualifier, CssScope, OutputSheetContent}; +use syn::parse::{Error as ParseError, Parse, ParseBuffer, Result as ParseResult}; + +#[derive(Debug)] +pub struct CssQualifiedRule { + qualifier: CssBlockQualifier, + scope: CssScope, +} + +impl Parse for CssQualifiedRule { + fn parse(input: &ParseBuffer) -> ParseResult { + let qualifier = input.parse()?; + let scope = input.parse()?; + Ok(Self { qualifier, scope }) + } +} + +impl CssQualifiedRule { + pub(super) fn fold_in_context( + self, + ctx: CssBlockQualifier, + ) -> Box> { + let own_ctx = self.qualifier; + if !own_ctx.is_empty() && !ctx.is_empty() { + // TODO: figure out how to combine contexts + // !Warning!: simply duplicating the containing blocks will (if no special care is taken) + // also duplicate injected expressions, which will evaluate them multiple times, which can be + // unexpected and confusing to the user. + // !Warning!: when the qualifiers contain several selectors each, this can lead to an explosion + // of emitted blocks. Consider + // .one, .two, .three { .inner-one, .inner-two, .inner-three { background: ${injected_expr} } } + // Following emotion, this would expand to 9 blocks and evaluate `injected_expr` 9 times. + // A possibility would be collecting appearing expressions once up front and putting replacements + // into the blocks. + return Box::new(std::iter::once(OutputSheetContent::Error( + ParseError::new_spanned(own_ctx, "Can not nest qualified blocks (yet)"), + ))); + } + let relevant_ctx = if !own_ctx.is_empty() { own_ctx } else { ctx }; + Box::new(normalize_hierarchy_impl(relevant_ctx, self.scope.contents)) + } +} diff --git a/packages/stylist-macros/src/inline/parse/mod.rs b/packages/stylist-macros/src/inline/parse/mod.rs new file mode 100644 index 0000000..210d921 --- /dev/null +++ b/packages/stylist-macros/src/inline/parse/mod.rs @@ -0,0 +1,106 @@ +use super::output::{OutputAtRule, OutputAttribute, OutputQualifiedRule}; +use itertools::Itertools; +use syn::parse::Error as ParseError; + +mod root; +pub use root::CssRootNode; +mod scope; +pub use scope::CssScope; +mod scope_content; +pub use scope_content::CssScopeContent; +mod block; +pub use block::CssQualifiedRule; +mod qualifier; +pub use qualifier::CssBlockQualifier; +mod rule; +pub use rule::CssAtRule; +mod attribute; +pub use attribute::{CssAttribute, CssAttributeName, CssAttributeValue}; + +/// We want to normalize the input a bit. For that, we want to pretend that e.g. +/// the sample input +/// +/// ```css +/// outer-attribute: some; +/// foo-bar: zet; +/// @media print { +/// .nested { +/// only-in-print: foo; +/// } +/// and-always: red; +/// } +/// ``` +/// +/// gets processed as if written in the (more verbose) shallowly nested style: +/// +/// ```css +/// { +/// outer-attribute: some; +/// foo-bar: zet; +/// } +/// @media print { +/// .nested { +/// only-in-print: foo; +/// } +/// { +/// and-always: red; +/// } +/// } +/// ``` +/// +/// Errors in nested items are reported as spanned TokenStreams. +/// +fn normalize_scope_hierarchy<'it>( + it: impl 'it + IntoIterator, +) -> impl 'it + Iterator { + normalize_hierarchy_impl(Default::default(), it) +} + +enum OutputSheetContent { + AtRule(OutputAtRule), + QualifiedRule(OutputQualifiedRule), + Error(ParseError), +} + +// Collect attributes into blocks, also flatten and lift nested blocks. +fn normalize_hierarchy_impl<'it>( + context: CssBlockQualifier, + it: impl 'it + IntoIterator, +) -> impl 'it + Iterator { + let qualifier = context.clone().into_output(); + + // Helper enum appearing in intermediate step + enum ScopeItem { + Attributes(Vec), + AtRule(CssAtRule), + Block(CssQualifiedRule), + } + it.into_iter() + .map(|c| match c { + CssScopeContent::Attribute(a) => ScopeItem::Attributes(vec![a.into_output()]), + CssScopeContent::AtRule(r) => ScopeItem::AtRule(r), + CssScopeContent::Nested(b) => ScopeItem::Block(b), + }) + // collect runs of attributes together into a single item + .coalesce(|l, r| match (l, r) { + (ScopeItem::Attributes(mut ls), ScopeItem::Attributes(rs)) => { + ls.extend(rs); + Ok(ScopeItem::Attributes(ls)) + } + (l, r) => Err((l, r)), + }) + .flat_map(move |w| match w { + ScopeItem::Attributes(attributes) => { + let result = OutputSheetContent::QualifiedRule(OutputQualifiedRule { + qualifier: qualifier.clone(), + attributes, + }); + Box::new(std::iter::once(result)) + } + ScopeItem::AtRule(r) => { + let result = r.fold_in_context(context.clone()); + Box::new(std::iter::once(result)) + } + ScopeItem::Block(b) => b.fold_in_context(context.clone()), + }) +} diff --git a/packages/stylist-macros/src/inline/parse/qualifier.rs b/packages/stylist-macros/src/inline/parse/qualifier.rs new file mode 100644 index 0000000..28eb143 --- /dev/null +++ b/packages/stylist-macros/src/inline/parse/qualifier.rs @@ -0,0 +1,74 @@ +use super::super::{ + component_value::{ComponentValue, ComponentValueStream}, + output::OutputQualifier, +}; + +use proc_macro2::TokenStream; +use quote::ToTokens; + +use syn::{ + parse::{Error as ParseError, Parse, ParseBuffer, Result as ParseResult}, + token, +}; + +#[derive(Debug, Clone)] +pub struct CssBlockQualifier { + qualifiers: Vec, + qualifier_errors: Vec, +} + +impl Parse for CssBlockQualifier { + fn parse(input: &ParseBuffer) -> ParseResult { + let mut component_iter = ComponentValueStream::from(input); + let mut qualifiers = vec![]; + let mut qualifier_errors = vec![]; + loop { + // Consume all tokens till the next '{'-block + if input.peek(token::Brace) { + break; + } + let next_token = component_iter + .next() + .ok_or_else(|| input.error("ScopeQualifier: unexpected end of input"))??; + let token_errors = next_token.validate_selector_token()?; + if token_errors.is_empty() { + qualifiers.push(next_token); + } + qualifier_errors.extend(token_errors); + } + Ok(Self { + qualifiers, + qualifier_errors, + }) + } +} + +impl ToTokens for CssBlockQualifier { + fn to_tokens(&self, tokens: &mut TokenStream) { + for q in self.qualifiers.iter() { + q.to_tokens(tokens); + } + } +} + +impl Default for CssBlockQualifier { + fn default() -> Self { + Self { + qualifiers: vec![], + qualifier_errors: vec![], + } + } +} + +impl CssBlockQualifier { + pub fn is_empty(&self) -> bool { + self.qualifiers.is_empty() + } + + pub fn into_output(self) -> OutputQualifier { + OutputQualifier { + selectors: self.qualifiers, + errors: self.qualifier_errors, + } + } +} diff --git a/packages/stylist-macros/src/inline/parse/root.rs b/packages/stylist-macros/src/inline/parse/root.rs new file mode 100644 index 0000000..4bdba5f --- /dev/null +++ b/packages/stylist-macros/src/inline/parse/root.rs @@ -0,0 +1,30 @@ +use super::{ + super::output::{OutputScopeContent, OutputSheet}, + normalize_scope_hierarchy, CssScopeContent, OutputSheetContent, +}; +use syn::parse::{Parse, ParseBuffer, Result as ParseResult}; + +#[derive(Debug)] +pub struct CssRootNode { + root_contents: Vec, +} + +impl Parse for CssRootNode { + fn parse(input: &ParseBuffer) -> ParseResult { + let root_contents = CssScopeContent::consume_list_of_rules(input)?; + Ok(Self { root_contents }) + } +} + +impl CssRootNode { + pub fn into_output(self) -> OutputSheet { + let contents = normalize_scope_hierarchy(self.root_contents) + .map(|c| match c { + OutputSheetContent::QualifiedRule(block) => OutputScopeContent::Block(block), + OutputSheetContent::AtRule(rule) => OutputScopeContent::AtRule(rule), + OutputSheetContent::Error(err) => OutputScopeContent::Err(err), + }) + .collect(); + OutputSheet { contents } + } +} diff --git a/packages/stylist-macros/src/inline/parse/rule.rs b/packages/stylist-macros/src/inline/parse/rule.rs new file mode 100644 index 0000000..39e66e3 --- /dev/null +++ b/packages/stylist-macros/src/inline/parse/rule.rs @@ -0,0 +1,121 @@ +use super::{ + super::{ + component_value::{ComponentValue, ComponentValueStream}, + css_ident::CssIdent, + output::{OutputAtRule, OutputRuleContent}, + }, + normalize_hierarchy_impl, CssBlockQualifier, CssScope, OutputSheetContent, +}; +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::{ + parse::{Error as ParseError, Parse, ParseBuffer, Result as ParseResult}, + token, +}; + +#[derive(Debug)] +pub enum CssAtRuleContent { + Scope(CssScope), + Empty(token::Semi), +} + +#[derive(Debug)] +pub struct CssAtRule { + at: token::At, + name: CssIdent, + prelude: Vec, + contents: CssAtRuleContent, + errors: Vec, +} + +impl Parse for CssAtRule { + fn parse(input: &ParseBuffer) -> ParseResult { + let at = input.parse()?; + let name = input.parse::()?; + + // Consume all tokens till the next ';' or the next block + let mut component_iter = ComponentValueStream::from(input); + let mut prelude = vec![]; + let mut errors = vec![]; + + // Recognize the type of @-rule + // TODO: be sensitive to this detected type when validating the prelude and contained attributes + if !["media", "supports"].contains(&name.to_output_string().as_str()) { + errors.push(ParseError::new_spanned( + &name, + format!("@-rule '{}' not supported", name), + )); + } + + let contents = loop { + if input.peek(token::Semi) { + let semi = input.parse()?; + break CssAtRuleContent::Empty(semi); + } + if input.peek(token::Brace) { + let scope = input.parse()?; + break CssAtRuleContent::Scope(scope); + } + let next_token = component_iter + .next() + .ok_or_else(|| input.error("AtRule: unexpected end of input"))??; + prelude.push(next_token); + }; + + Ok(Self { + at, + name, + prelude, + contents, + errors, + }) + } +} + +impl CssAtRule { + pub(super) fn fold_in_context(self, ctx: CssBlockQualifier) -> OutputSheetContent { + if !ctx.is_empty() { + return OutputSheetContent::Error(ParseError::new_spanned( + self.prelude_span(), + "Can not nest @-rules (yet)", + )); + } + let contents = match self.contents { + CssAtRuleContent::Empty(_) => Vec::new(), + CssAtRuleContent::Scope(scope) => normalize_hierarchy_impl(ctx, scope.contents) + .map(|c| match c { + OutputSheetContent::AtRule(rule) => OutputRuleContent::AtRule(rule), + OutputSheetContent::QualifiedRule(block) => OutputRuleContent::Block(block), + OutputSheetContent::Error(err) => OutputRuleContent::Err(err), + }) + .collect(), + }; + OutputSheetContent::AtRule(OutputAtRule { + name: self.name, + prelude: self.prelude, + contents, + errors: self.errors, + }) + } +} + +impl CssAtRule { + fn prelude_span(&self) -> PreludeSpan { + PreludeSpan { rule: self } + } +} + +struct PreludeSpan<'a> { + rule: &'a CssAtRule, +} + +impl<'a> ToTokens for PreludeSpan<'a> { + fn to_tokens(&self, toks: &mut TokenStream) { + let rule = self.rule; + rule.at.to_tokens(toks); + rule.name.to_tokens(toks); + for c in rule.prelude.iter() { + c.to_tokens(toks); + } + } +} diff --git a/packages/stylist-macros/src/inline/parse/scope.rs b/packages/stylist-macros/src/inline/parse/scope.rs new file mode 100644 index 0000000..33f81f1 --- /dev/null +++ b/packages/stylist-macros/src/inline/parse/scope.rs @@ -0,0 +1,21 @@ +use super::CssScopeContent; +use syn::{ + braced, + parse::{Parse, ParseBuffer, Result as ParseResult}, + token, +}; + +#[derive(Debug)] +pub struct CssScope { + brace: token::Brace, + pub contents: Vec, +} + +impl Parse for CssScope { + fn parse(input: &ParseBuffer) -> ParseResult { + let inner; + let brace = braced!(inner in input); + let contents = CssScopeContent::consume_list_of_rules(&inner)?; + Ok(Self { brace, contents }) + } +} diff --git a/packages/stylist-macros/src/inline/parse/scope_content.rs b/packages/stylist-macros/src/inline/parse/scope_content.rs new file mode 100644 index 0000000..c0e7bea --- /dev/null +++ b/packages/stylist-macros/src/inline/parse/scope_content.rs @@ -0,0 +1,60 @@ +use super::{ + super::component_value::{ComponentValue, ComponentValueStream, PreservedToken}, + CssAtRule, CssAttribute, CssQualifiedRule, +}; +use itertools::Itertools; +use syn::parse::{Parse, ParseBuffer, Result as ParseResult}; + +#[derive(Debug)] +pub enum CssScopeContent { + Attribute(CssAttribute), + AtRule(CssAtRule), + Nested(CssQualifiedRule), +} + +impl Parse for CssScopeContent { + fn parse(input: &ParseBuffer) -> ParseResult { + // Fork the stream. Peeking a component value might still consume tokens from the stream! + let forked_input = input.fork(); + let mut component_peek = ComponentValueStream::from(&forked_input).multipeek(); + let next_input = component_peek + .peek() + .cloned() + .ok_or_else(|| forked_input.error("Scope: unexpected end of input"))??; + // Steps roughly follow Css-Syntax-Level 3, §5.4.4: Consume a list of declarations + // Allows for directly nested attributes though + // At-rule first + if let ComponentValue::Token(PreservedToken::Punct(ref p)) = next_input { + if p.as_char() == '@' { + let atrule = input.parse()?; + return Ok(Self::AtRule(atrule)); + } + } + // If it starts with an , it might be an attribute. + if next_input.maybe_to_attribute_name().is_some() { + // peek another token to see if it's colon + let maybe_colon = component_peek.peek(); + if let Some(Ok(ComponentValue::Token(PreservedToken::Punct(p)))) = maybe_colon { + if p.as_char() == ':' { + let attr = input.parse()?; + return Ok(Self::Attribute(attr)); + } + } + } + // It isn't. All that's left now is that it's a qualified rule. + let rule = input.parse()?; + Ok(Self::Nested(rule)) + } +} + +impl CssScopeContent { + // §5.4.1: Consume a list of rules + pub fn consume_list_of_rules(input: &ParseBuffer) -> ParseResult> { + let mut contents = Vec::new(); + while !input.is_empty() { + // Not handled: + contents.push(input.parse()?); + } + Ok(contents) + } +} diff --git a/packages/stylist-macros/src/lib.rs b/packages/stylist-macros/src/lib.rs index 97eec03..79d7afc 100644 --- a/packages/stylist-macros/src/lib.rs +++ b/packages/stylist-macros/src/lib.rs @@ -1,7 +1,7 @@ #![deny(clippy::all)] -#![deny(missing_debug_implementations)] #![deny(unsafe_code)] #![deny(non_snake_case)] +#![deny(missing_debug_implementations)] #![deny(clippy::cognitive_complexity)] #![cfg_attr(documenting, feature(doc_cfg))] #![cfg_attr(any(releasing, not(debug_assertions)), deny(dead_code, unused_imports))] @@ -9,13 +9,14 @@ use proc_macro::TokenStream; use proc_macro_error::proc_macro_error; -mod argument; +mod inline; +mod literal; + mod css; -mod fstring; mod global_style; mod sheet; +mod spacing_iterator; mod style; -mod to_tokens_with_args; #[proc_macro] #[proc_macro_error] diff --git a/packages/stylist-macros/src/argument.rs b/packages/stylist-macros/src/literal/argument.rs similarity index 100% rename from packages/stylist-macros/src/argument.rs rename to packages/stylist-macros/src/literal/argument.rs diff --git a/packages/stylist-macros/src/fstring.rs b/packages/stylist-macros/src/literal/fstring.rs similarity index 100% rename from packages/stylist-macros/src/fstring.rs rename to packages/stylist-macros/src/literal/fstring.rs diff --git a/packages/stylist-macros/src/literal/mod.rs b/packages/stylist-macros/src/literal/mod.rs new file mode 100644 index 0000000..26dba63 --- /dev/null +++ b/packages/stylist-macros/src/literal/mod.rs @@ -0,0 +1,131 @@ +use proc_macro2::{TokenStream, TokenTree}; + +use std::collections::{HashMap, HashSet}; + +use litrs::StringLit; +use proc_macro_error::{abort, abort_call_site}; +use std::convert::TryFrom; + +use stylist_core::ast::Sheet; + +mod argument; +mod fstring; +mod to_tokens_with_args; + +use argument::Argument; +use to_tokens_with_args::ToTokensWithArgs; + +pub(crate) fn macro_fn(input: TokenStream) -> TokenStream { + let mut tokens = input.into_iter(); + + let first_token = match tokens.next() { + Some(m) => m, + None => abort_call_site!("expected at least one argument"), + }; + + let s_literal = match StringLit::try_from(first_token.clone()) { + Ok(m) => m, + Err(e) => return e.to_compile_error2(), + }; + + let sheet: Sheet = match s_literal.value().parse() { + Ok(m) => m, + + Err(e) => abort!(first_token, "{}", e.to_string()), + }; + + let mut args = HashMap::new(); + + let is_comma = |t: &TokenTree| -> bool { + match t { + TokenTree::Punct(m) => m.as_char() == ',', + _ => false, + } + }; + + let is_equal = |t: &TokenTree| -> bool { + match t { + TokenTree::Punct(m) => m.as_char() == '=', + _ => false, + } + }; + + let mut comma_read = false; + + 'outer: loop { + if !comma_read { + match tokens.next() { + Some(m) => { + if !is_comma(&m) { + abort!(m, "expected ',', got: {}", m) + } + } + None => break 'outer, + }; + } + + let name_token = match tokens.next() { + Some(m) => m, + None => break 'outer, + }; + + let name_ident = match name_token { + TokenTree::Ident(ref m) => m, + _ => abort!(name_token, "expected ident, got: {}", name_token), + }; + + let name = name_ident.to_string(); + + let mut arg = Argument { + name, + name_token: name_ident.clone(), + tokens: TokenStream::new(), + }; + + if !tokens.next().map(|m| is_equal(&m)).unwrap_or(false) { + abort!( + name_token, + "expected = at the end of this ident, only named arguments are allowed at this moment"; + hint = format!("try: {name} = {name}", name = arg.name), + ); + } + + 'inner: loop { + let next_token = match tokens.next() { + Some(m) => m, + None => { + if args.insert(arg.name.clone(), arg).is_some() { + abort!(name_token, "duplicate named argument"); + } + break 'outer; + } + }; + + if is_comma(&next_token) { + if args.insert(arg.name.clone(), arg).is_some() { + abort!(name_token, "duplicate named argument"); + } + comma_read = true; + break 'inner; + } + + arg.tokens.extend(TokenStream::from(next_token)); + } + } + + let mut args_used = HashSet::with_capacity(args.len()); + + let stream = sheet.to_tokens_with_args(&args, &mut args_used); + + for (k, v) in args.iter() { + if !args_used.contains(k) { + abort!( + v.name_token, + "argument {} is not used, arguments must be used", + k + ); + } + } + + stream +} diff --git a/packages/stylist-macros/src/to_tokens_with_args.rs b/packages/stylist-macros/src/literal/to_tokens_with_args.rs similarity index 97% rename from packages/stylist-macros/src/to_tokens_with_args.rs rename to packages/stylist-macros/src/literal/to_tokens_with_args.rs index 71ec52d..698082b 100644 --- a/packages/stylist-macros/src/to_tokens_with_args.rs +++ b/packages/stylist-macros/src/literal/to_tokens_with_args.rs @@ -7,8 +7,8 @@ use quote::quote; use stylist_core::ast::*; -use crate::argument::Argument; -use crate::fstring; +use super::argument::Argument; +use super::fstring; pub(crate) trait ToTokensWithArgs { fn to_tokens_with_args( @@ -152,7 +152,7 @@ impl ToTokensWithArgs for StringFragment { let current_tokens = quote! { ::stylist::ast::StringFragment { - inner: { #arg_tokens }.to_string().into(), + inner: (&{ #arg_tokens } as &dyn ::std::fmt::Display).to_string().into(), }, }; diff --git a/packages/stylist-macros/src/sheet.rs b/packages/stylist-macros/src/sheet.rs index d261c3f..3abb8b9 100644 --- a/packages/stylist-macros/src/sheet.rs +++ b/packages/stylist-macros/src/sheet.rs @@ -1,127 +1,8 @@ use proc_macro2::{TokenStream, TokenTree}; - -use std::collections::{HashMap, HashSet}; - -use litrs::StringLit; -use proc_macro_error::{abort, abort_call_site}; -use std::convert::TryFrom; - -use stylist_core::ast::Sheet; - -use crate::argument::Argument; -use crate::to_tokens_with_args::ToTokensWithArgs; - pub(crate) fn macro_fn(input: TokenStream) -> TokenStream { - let mut tokens = input.into_iter(); - - let first_token = match tokens.next() { - Some(m) => m, - None => abort_call_site!("expected at least one argument"), - }; - - let s_literal = match StringLit::try_from(first_token.clone()) { - Ok(m) => m, - Err(e) => return e.to_compile_error2(), - }; - - let sheet: Sheet = match s_literal.value().parse() { - Ok(m) => m, - - Err(e) => abort!(first_token, "{}", e.to_string()), - }; - - let mut args = HashMap::new(); - - let is_comma = |t: &TokenTree| -> bool { - match t { - TokenTree::Punct(m) => m.as_char() == ',', - _ => false, - } - }; - - let is_equal = |t: &TokenTree| -> bool { - match t { - TokenTree::Punct(m) => m.as_char() == '=', - _ => false, - } - }; - - let mut comma_read = false; - - 'outer: loop { - if !comma_read { - match tokens.next() { - Some(m) => { - if !is_comma(&m) { - abort!(m, "expected ',', got: {}", m) - } - } - None => break 'outer, - }; - } - - let name_token = match tokens.next() { - Some(m) => m, - None => break 'outer, - }; - - let name_ident = match name_token { - TokenTree::Ident(ref m) => m, - _ => abort!(name_token, "expected ident, got: {}", name_token), - }; - - let name = name_ident.to_string(); - - let mut arg = Argument { - name, - name_token: name_ident.clone(), - tokens: TokenStream::new(), - }; - - if !tokens.next().map(|m| is_equal(&m)).unwrap_or(false) { - abort!( - name_token, - "expected = at the end of this ident, only named arguments are allowed at this moment"; - hint = format!("try: {name} = {name}", name = arg.name), - ); - } - - 'inner: loop { - let next_token = match tokens.next() { - Some(m) => m, - None => { - if args.insert(arg.name.clone(), arg).is_some() { - abort!(name_token, "duplicate named argument"); - } - break 'outer; - } - }; - - if is_comma(&next_token) { - if args.insert(arg.name.clone(), arg).is_some() { - abort!(name_token, "duplicate named argument"); - } - comma_read = true; - break 'inner; - } - - arg.tokens.extend(TokenStream::from(next_token)); - } + if let Some(TokenTree::Literal(_)) = input.clone().into_iter().next() { + crate::literal::macro_fn(input) + } else { + crate::inline::macro_fn(input) } - - let mut args_used = HashSet::with_capacity(args.len()); - - let stream = sheet.to_tokens_with_args(&args, &mut args_used); - - for (k, v) in args.iter() { - if !args_used.contains(k) { - abort!( - v.name_token, - "argument {} is not used, arguments must be used", - k - ); - } - } - - stream } diff --git a/packages/stylist-macros/src/spacing_iterator.rs b/packages/stylist-macros/src/spacing_iterator.rs new file mode 100644 index 0000000..4237fdf --- /dev/null +++ b/packages/stylist-macros/src/spacing_iterator.rs @@ -0,0 +1,101 @@ +//! Provide an iterator inserting optional items between items + +#[test] +fn test_spacing_iterator() { + use SpacedIterator; + let it = (1..7).spaced_with(|l, _| (*l == 4).then(|| 2000)); + itertools::assert_equal(it, vec![1, 2, 3, 4, 2000, 5, 6]); +} + +pub trait SpacedIterator: Iterator { + /// Space a sequence of items by sometimes inserting another item. + fn spaced_with(self, spacer: F) -> Spacing + where + Self: Sized, + F: FnMut(&Self::Item, &Self::Item) -> Option, + { + Spacing { + it: self, + state: SpacingState::NotStarted, + spacer, + } + } +} + +impl SpacedIterator for I {} + +enum SpacingState { + NotStarted, + AtItemSpaced { item: I, spacing: I, next: I }, + AtItem { item: I, next: I }, + AtSpacing { spacing: I, next: I }, + AtEnd { item: I }, + Done, +} + +impl SpacingState { + fn maybe_spaced( + item: I, + next: Option, + spacer: &mut impl FnMut(&I, &I) -> Option, + ) -> Self { + match next { + None => Self::AtEnd { item }, + Some(next) => match spacer(&item, &next) { + None => Self::AtItem { item, next }, + Some(spacing) => Self::AtItemSpaced { + item, + spacing, + next, + }, + }, + } + } + + fn advance( + self, + it: &mut impl Iterator, + spacer: &mut impl FnMut(&I, &I) -> Option, + ) -> (Option, Self) { + use SpacingState::*; + match self { + NotStarted => match it.next() { + None => (None, Done), + Some(item) => Self::maybe_spaced(item, it.next(), spacer).advance(it, spacer), + }, + Done => (None, Done), + AtEnd { item } => (Some(item), Done), + AtSpacing { spacing, next } => { + (Some(spacing), Self::maybe_spaced(next, it.next(), spacer)) + } + AtItem { item, next } => (Some(item), Self::maybe_spaced(next, it.next(), spacer)), + AtItemSpaced { + item, + spacing, + next, + } => (Some(item), Self::AtSpacing { spacing, next }), + } + } +} + +pub struct Spacing { + it: I, + state: SpacingState, + spacer: F, +} + +impl Iterator for Spacing +where + It: Iterator, + F: FnMut(&It::Item, &It::Item) -> Option, +{ + type Item = It::Item; + + fn next(&mut self) -> Option { + // In case of panic in advance, drop the remaining items + let state = std::mem::replace(&mut self.state, SpacingState::Done); + let (next, new_state) = state.advance(&mut self.it, &mut self.spacer); + self.state = new_state; + next + } +} diff --git a/packages/stylist/Cargo.toml b/packages/stylist/Cargo.toml index bb5fa6d..696c910 100644 --- a/packages/stylist/Cargo.toml +++ b/packages/stylist/Cargo.toml @@ -49,6 +49,7 @@ features = [ [dev-dependencies] log = "0.4.14" env_logger = "0.9.0" +trybuild = "1.0" [features] random = ["rand", "getrandom"] diff --git a/packages/stylist/src/global_style.rs b/packages/stylist/src/global_style.rs index ca8451e..d257b0e 100644 --- a/packages/stylist/src/global_style.rs +++ b/packages/stylist/src/global_style.rs @@ -48,7 +48,9 @@ impl GlobalStyle { // We parse the style str again in debug mode to ensure that interpolated values are // not corrupting the stylesheet. #[cfg(all(debug_assertions, feature = "parser"))] - style_str.parse::()?; + style_str + .parse::() + .expect("debug: emitted style should parse"); let new_style = Self { inner: StyleContent { diff --git a/packages/stylist/src/lib.rs b/packages/stylist/src/lib.rs index f49f2d4..097f6d0 100644 --- a/packages/stylist/src/lib.rs +++ b/packages/stylist/src/lib.rs @@ -186,129 +186,18 @@ pub use style::Style; pub use style_src::StyleSource; pub use yield_style::YieldStyle; -/// A procedural macro that parses a string literal into a [`Style`]. -/// -/// This macro supports string interpolation, please see documentation of [`css!`] macro for -/// usage. -/// -/// # Example -/// -/// ``` -/// use stylist::style; -/// -/// // Returns a Style instance. -/// let style = style!("color: red;"); -/// ``` -#[doc(inline)] -#[cfg_attr(documenting, doc(cfg(feature = "macros")))] -#[cfg(feature = "macros")] -pub use stylist_macros::style; +#[cfg_attr(documenting, doc(cfg(feature = "yew_integration")))] +#[cfg(feature = "yew_integration")] +pub mod yew; -/// A procedural macro that parses a string literal into a [`GlobalStyle`]. -/// -/// This macro supports string interpolation, please see documentation of [`css!`] macro for -/// usage. -/// -/// # Example -/// -/// ``` -/// use stylist::global_style; -/// -/// // Returns a GlobalStyle instance. -/// let style = global_style!("color: red;"); -/// ``` -#[doc(inline)] #[cfg_attr(documenting, doc(cfg(feature = "macros")))] #[cfg(feature = "macros")] -pub use stylist_macros::global_style; +pub mod macros; -/// A procedural macro that parses a string literal into a [`StyleSource`]. -/// -/// # Example -/// -/// ``` -/// use stylist::css; -/// use stylist::yew::Global; -/// use yew::prelude::*; -/// -/// let rendered = html! {
}; -/// let rendered_global = html! {}; -/// ``` -/// -/// # String Interpolation -/// -/// This macro supports string interpolation on values of style attributes, selectors, `@supports` and `@media` rules. -/// -/// Interpolated strings are denoted with `${ident}` and any type that implements [`std::fmt::Display`] can be -/// used as value. Only named argument are supported at this moment. -/// -/// If you do need to output a `${` sequence, you may use `$${` to escape to a `${`. -/// -/// -/// ## Example -/// ```css -/// content: "$${}"; -/// ``` -/// -/// Will be turned into: -/// ```css -/// content: "${}"; -/// ``` -/// -/// ## Note: `$${` escape can only present where `${` is valid in the css stylesheet. -/// -/// Stylist currently does not check or escape the content of interpolated strings. It is possible -/// to pass invalid strings that would result in an invalid stylesheet. In debug mode, if feature `parser` is -/// enabled, Stylist will attempt to parse the stylesheet again after interpolated strings are -/// substituted with its actual value to check if the final stylesheet is invalid. -/// -/// ## Example -/// -/// ``` -/// use stylist::{Style, css}; -/// use yew::prelude::*; -/// -/// let s = css!( -/// r#" -/// color: ${color}; -/// -/// span, ${sel_div} { -/// background-color: blue; -/// } -/// -/// @media screen and ${breakpoint} { -/// display: flex; -/// } -/// "#, -/// color = "red", -/// sel_div = "div.selected", -/// breakpoint = "(max-width: 500px)", -/// ); -/// -/// let style = Style::new(s).expect("Failed to create style"); -/// -/// // Example Output: -/// // .stylist-fIEWv6EP { -/// // color: red; -/// // } -/// // stylist-fIEWv6EP span, .stylist-fIEWv6EP div.selected { -/// // background-color: blue; -/// // } -/// // @media screen and (max-width: 500px) { -/// // .stylist-fIEWv6EP { -/// // display: flex; -/// // } -/// // } -/// println!("{}", style.get_style_str()); -/// ``` -#[doc(inline)] #[cfg_attr(documenting, doc(cfg(feature = "macros")))] #[cfg(feature = "macros")] -pub use stylist_macros::css; - -#[cfg_attr(documenting, doc(cfg(feature = "yew_integration")))] -#[cfg(feature = "yew_integration")] -pub mod yew; +#[doc(no_inline)] +pub use macros::{css, global_style, style}; #[doc(inline)] pub use stylist_core::{Error, Result}; diff --git a/packages/stylist/src/macros.rs b/packages/stylist/src/macros.rs new file mode 100644 index 0000000..592d705 --- /dev/null +++ b/packages/stylist/src/macros.rs @@ -0,0 +1,183 @@ +//! Utility macros for writing (global) styles. +//! +//! This module contains also runtime support for the macros and documents their usage. There are two +//! syntaxes available: using [string interpolation] or [tokentree based]. +//! +//! # String Interpolation +//! +//! This macro supports string interpolation on values of style attributes, selectors, `@supports` and `@media` rules. +//! +//! Interpolated strings are denoted with `${ident}` and any type that implements [`Display`] can be +//! used as value. Only named argument are supported at this moment. +//! +//! If you do need to output a `${` sequence, you may use `$${` to escape to a `${`. +//! +//! +//! ## Example +//! ```css +//! content: "$${}"; +//! ``` +//! +//! Will be turned into: +//! ```css +//! content: "${}"; +//! ``` +//! +//! ## Note: `$${` escape can only present where `${` is valid in the css stylesheet. +//! +//! Stylist currently does not check or escape the content of interpolated strings. It is possible +//! to pass invalid strings that would result in an invalid stylesheet. In debug mode, if feature `parser` is +//! enabled, Stylist will attempt to parse the stylesheet again after interpolated strings are +//! substituted with its actual value to check if the final stylesheet is invalid. +//! +//! # Example +//! +//! ``` +//! use stylist::{Style, css}; +//! use yew::prelude::*; +//! +//! let s = css!( +//! r#" +//! color: ${color}; +//! +//! span, ${sel_div} { +//! background-color: blue; +//! } +//! +//! @media screen and ${breakpoint} { +//! display: flex; +//! } +//! "#, +//! color = "red", +//! sel_div = "div.selected", +//! breakpoint = "(max-width: 500px)", +//! ); +//! +//! let style = Style::new(s).expect("Failed to create style"); +//! +//! // Example Output: +//! // .stylist-fIEWv6EP { +//! // color: red; +//! // } +//! // stylist-fIEWv6EP span, .stylist-fIEWv6EP div.selected { +//! // background-color: blue; +//! // } +//! // @media screen and (max-width: 500px) { +//! // .stylist-fIEWv6EP { +//! // display: flex; +//! // } +//! // } +//! println!("{}", style.get_style_str()); +//! ``` +//! +//! # Tokentree Style +//! +//! The other possibility is oriented around reusing the rust tokenizer where possible instead of passing +//! a string literal to the macro. The hope is to have an improved programming experience by more precise +//! error locations and diagnostics. +//! +//! Like in string interpolation syntax, interpolated values are allowed in most places through the `${expr}` +//! syntax. In distinction, the braces contain a rust expression of any type implementing [`Display`] that +//! will be evaluated in the surrounding context. +//! +//! Due to the tokenizer there are some quirks with literals. For example `4em` would be tokenized as a +//! floating point literal with a missing exponent and a suffix of `m`. To work around this issue, use +//! string interpolation as in `${"4em"}`. Similarly, some color hash-tokens like `#44444e` as misinterpreted, +//! use the same workaround here: `${"#44444e"}`. +//! +//! # Example +//! +//! ``` +//! use stylist::{Style, css}; +//! use yew::prelude::*; +//! +//! let max_width_cuttoff = "500px"; +//! let primary_color = "red"; +//! let s = css!( +//! color: ${primary_color}; +//! +//! span, ${"div.selected"} { +//! background-color: blue; +//! } +//! +//! @media screen and (max-width: ${max_width_cuttoff}) { +//! display: flex; +//! } +//! ); +//! +//! let style = Style::new(s).expect("Failed to create style"); +//! +//! // Example Output: +//! // .stylist-fIEWv6EP { +//! // color: red; +//! // } +//! // stylist-fIEWv6EP span, .stylist-fIEWv6EP div.selected { +//! // background-color: blue; +//! // } +//! // @media screen and (max-width: 500px) { +//! // .stylist-fIEWv6EP { +//! // display: flex; +//! // } +//! // } +//! println!("{}", style.get_style_str()); +//! ``` +//! +//! [string interpolation]: #string-interpolation +//! [tokentree based]: #tokentree-style +//! [`Display`]: std::fmt::Display + +/// A procedural macro that parses a string literal into a [`Style`]. +/// +/// Please consult the documentation of the [`macros`] module for the supported syntax of this macro. +/// +/// # Example +/// +/// ``` +/// use stylist::style; +/// +/// // Returns a Style instance. +/// let style = style!("color: red;"); +/// ``` +/// +/// [`Style`]: crate::Style +/// [`macros`]: self +#[cfg_attr(documenting, doc(cfg(feature = "macros")))] +pub use stylist_macros::style; + +/// A procedural macro that parses a string literal into a [`GlobalStyle`]. +/// +/// Please consult the documentation of the [`macros`] module for the supported syntax of this macro. +/// +/// # Example +/// +/// ``` +/// use stylist::global_style; +/// +/// // Returns a GlobalStyle instance. +/// let style = global_style!("color: red;"); +/// ``` +/// +/// [`GlobalStyle`]: crate::GlobalStyle +/// [`macros`]: self +#[cfg_attr(documenting, doc(cfg(feature = "macros")))] +pub use stylist_macros::global_style; + +/// A procedural macro that parses a string literal into a [`StyleSource`]. +/// +/// Please consult the documentation of the [`macros`] module for the supported syntax of this macro. +/// +/// # Example +/// +/// ``` +/// use stylist::css; +/// use stylist::yew::Global; +/// use yew::prelude::*; +/// +/// let rendered = html! {
}; +/// let rendered_global = html! {}; +/// ``` +/// +/// [`StyleSource`]: crate::StyleSource +/// [`macros`]: self +#[cfg_attr(documenting, doc(cfg(feature = "macros")))] +pub use stylist_macros::css; diff --git a/packages/stylist/src/style.rs b/packages/stylist/src/style.rs index 1b2c231..98f3b31 100644 --- a/packages/stylist/src/style.rs +++ b/packages/stylist/src/style.rs @@ -178,7 +178,9 @@ impl Style { // We parse the style str again in debug mode to ensure that interpolated values are // not corrupting the stylesheet. #[cfg(all(debug_assertions, feature = "parser"))] - style_str.parse::()?; + style_str + .parse::() + .expect("debug: emitted style should parse"); let new_style = Self { inner: StyleContent { diff --git a/packages/stylist/tests/css_tests.rs b/packages/stylist/tests/macro_css_tests.rs similarity index 94% rename from packages/stylist/tests/css_tests.rs rename to packages/stylist/tests/macro_css_tests.rs index 83116f8..3fdcec7 100644 --- a/packages/stylist/tests/css_tests.rs +++ b/packages/stylist/tests/macro_css_tests.rs @@ -1,8 +1,7 @@ -use stylist::ast::ToStyleStr; -use stylist::*; - #[test] fn test_sheet_interpolation() { + use stylist::ast::ToStyleStr; + use stylist::*; let parsed = css!( r#" color: ${color}; diff --git a/packages/stylist/tests/macro_integration_tests.rs b/packages/stylist/tests/macro_integration_tests.rs new file mode 100644 index 0000000..a7c146f --- /dev/null +++ b/packages/stylist/tests/macro_integration_tests.rs @@ -0,0 +1,7 @@ +#[test] +fn test_macro_integrations() { + let t = trybuild::TestCases::new(); + + t.pass("tests/macro_integrations/*-pass.rs"); + t.compile_fail("tests/macro_integrations/*-fail.rs"); +} diff --git a/packages/stylist/tests/macro_integrations/at-supports-pass.rs b/packages/stylist/tests/macro_integrations/at-supports-pass.rs new file mode 100644 index 0000000..9f66436 --- /dev/null +++ b/packages/stylist/tests/macro_integrations/at-supports-pass.rs @@ -0,0 +1,19 @@ +fn main() { + let _ = env_logger::builder().is_test(true).try_init(); + let style = stylist::style! { + @supports (display: grid) { + background-color: grey; + } + } + .unwrap(); + let expected_result = format!( + r#"@supports (display:grid) {{ +.{cls} {{ +background-color: grey; +}} +}} +"#, + cls = style.get_class_name() + ); + assert_eq!(expected_result, style.get_style_str()); +} diff --git a/packages/stylist/tests/macro_integrations/block_in_attribute-fail.rs b/packages/stylist/tests/macro_integrations/block_in_attribute-fail.rs new file mode 100644 index 0000000..afdcbfc --- /dev/null +++ b/packages/stylist/tests/macro_integrations/block_in_attribute-fail.rs @@ -0,0 +1,10 @@ +fn main() { + let is_an_expression = "black"; + let _ = stylist::css! { + .outer { + border: ${is_an_expression}; + background-color: {should_be_expression}; + color: {another_expression}; + } + }; +} diff --git a/packages/stylist/tests/macro_integrations/block_in_attribute-fail.stderr b/packages/stylist/tests/macro_integrations/block_in_attribute-fail.stderr new file mode 100644 index 0000000..686c6f5 --- /dev/null +++ b/packages/stylist/tests/macro_integrations/block_in_attribute-fail.stderr @@ -0,0 +1,11 @@ +error: expected a valid part of an attribute, got a block. Did you mean to write `${..}` to interpolate an expression? + --> $DIR/block_in_attribute-fail.rs:6:31 + | +6 | background-color: {should_be_expression}; + | ^^^^^^^^^^^^^^^^^^^^^^ + +error: expected a valid part of an attribute, got a block. Did you mean to write `${..}` to interpolate an expression? + --> $DIR/block_in_attribute-fail.rs:7:20 + | +7 | color: {another_expression}; + | ^^^^^^^^^^^^^^^^^^^^ diff --git a/packages/stylist/tests/macro_integrations/complicated-attributes-pass.rs b/packages/stylist/tests/macro_integrations/complicated-attributes-pass.rs new file mode 100644 index 0000000..d26faa2 --- /dev/null +++ b/packages/stylist/tests/macro_integrations/complicated-attributes-pass.rs @@ -0,0 +1,69 @@ +fn main() { + let _ = env_logger::builder().is_test(true).try_init(); + let sheet = stylist::ast::sheet! { + border: medium dashed green; + // pseudo class, sibling + &:checked + label { + // color spec with #-bang spec + color: #9799a7; + } + // nth child, general sibling + &:nth-child(-n+4) ~ nav { + // suffixed value + max-height: 500px; + } + // pseudo-element selector + ::first-letter { + // attribute with different kinds of literals + box-shadow: 3px 3px red, -1rem 0 0.4rem olive; + } + // descendent selector + article span { + box-shadow: inset 0 1px 2px rgba(0.32, 0, 0, 15%); + } + // contains selector, begins with, ends with, spaced hyphenated + a[href*="login"], + a[href^="https://"], + // FIXME: should work, but incorrectly reparsed after emitting + // parsing in macro works fine. + //a[href$=".pdf" ], + a[rel~="tag"], + a[lang|="en"] + { + // string literals + background-image: url("images/pdf.png"); + } + // another pseudo selector + #content::after { + content: " (" attr(x) ")"; + } + }; + log::debug!("{:?}", sheet); + let style = stylist::Style::new(sheet).unwrap(); + let expected_result = format!( + r#".{cls} {{ +border: medium dashed green; +}} +.{cls}:checked+label {{ +color: #9799a7; +}} +.{cls}:nth-child(-n+4)~nav {{ +max-height: 500px; +}} +.{cls}::first-letter {{ +box-shadow: 3px 3px red,-1rem 0 0.4rem olive; +}} +.{cls} article span {{ +box-shadow: inset 0 1px 2px rgba(0.32,0,0,15%); +}} +.{cls} a[href*="login"], .{cls} a[href^="https://"], .{cls} a[rel~="tag"], .{cls} a[lang|="en"] {{ +background-image: url("images/pdf.png"); +}} +.{cls} #content::after {{ +content: " (" attr(x)")"; +}} +"#, + cls = style.get_class_name() + ); + assert_eq!(expected_result, style.get_style_str()); +} diff --git a/packages/stylist/tests/macro_integrations/illegal_qualifiers-fail.rs b/packages/stylist/tests/macro_integrations/illegal_qualifiers-fail.rs new file mode 100644 index 0000000..9644a75 --- /dev/null +++ b/packages/stylist/tests/macro_integrations/illegal_qualifiers-fail.rs @@ -0,0 +1,7 @@ +fn main() { + let _ = stylist::css! { + (not a function) 42 117.9f32 / ^ @ .okay ::also(okay) { + border: black; + } + }; +} diff --git a/packages/stylist/tests/macro_integrations/illegal_qualifiers-fail.stderr b/packages/stylist/tests/macro_integrations/illegal_qualifiers-fail.stderr new file mode 100644 index 0000000..4ace68f --- /dev/null +++ b/packages/stylist/tests/macro_integrations/illegal_qualifiers-fail.stderr @@ -0,0 +1,29 @@ +error: expected a valid part of a scope qualifier, not a block + --> $DIR/illegal_qualifiers-fail.rs:3:9 + | +3 | (not a function) 42 117.9f32 / ^ @ .okay ::also(okay) { + | ^^^^^^^^^^^^^^^^ + +error: only string literals are allowed in selectors + --> $DIR/illegal_qualifiers-fail.rs:3:26 + | +3 | (not a function) 42 117.9f32 / ^ @ .okay ::also(okay) { + | ^^ + +error: only string literals are allowed in selectors + --> $DIR/illegal_qualifiers-fail.rs:3:29 + | +3 | (not a function) 42 117.9f32 / ^ @ .okay ::also(okay) { + | ^^^^^^^^ + +error: unexpected punctuation in selector + --> $DIR/illegal_qualifiers-fail.rs:3:38 + | +3 | (not a function) 42 117.9f32 / ^ @ .okay ::also(okay) { + | ^ + +error: unexpected punctuation in selector + --> $DIR/illegal_qualifiers-fail.rs:3:42 + | +3 | (not a function) 42 117.9f32 / ^ @ .okay ::also(okay) { + | ^ diff --git a/packages/stylist/tests/macro_integrations/nested_at_rule-fail.rs b/packages/stylist/tests/macro_integrations/nested_at_rule-fail.rs new file mode 100644 index 0000000..b907ea6 --- /dev/null +++ b/packages/stylist/tests/macro_integrations/nested_at_rule-fail.rs @@ -0,0 +1,29 @@ +fn main() { + let _ = env_logger::builder().is_test(true).try_init(); + let style = stylist::style! { + .outer { + @media print { + background-color: grey; + } + @supports (display: grid) { + margin: 2cm; + } + } + } + .unwrap(); + let expected_reusult = format!( + r#"@media print {{ +.{cls} .outer {{ +background-color: grey; +}} +}} +@supports (display:grid) {{ +.{cls} .outer {{ +margin: 2cm; +}} +}} +"#, + cls = style.get_class_name() + ); + assert_eq!(expected_reusult, style.get_style_str()); +} diff --git a/packages/stylist/tests/macro_integrations/nested_at_rule-fail.stderr b/packages/stylist/tests/macro_integrations/nested_at_rule-fail.stderr new file mode 100644 index 0000000..050e0fd --- /dev/null +++ b/packages/stylist/tests/macro_integrations/nested_at_rule-fail.stderr @@ -0,0 +1,11 @@ +error: Can not nest @-rules (yet) + --> $DIR/nested_at_rule-fail.rs:5:13 + | +5 | @media print { + | ^^^^^^^^^^^^ + +error: Can not nest @-rules (yet) + --> $DIR/nested_at_rule-fail.rs:8:13 + | +8 | @supports (display: grid) { + | ^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/packages/stylist/tests/macro_integrations/nested_blocks-fail.rs b/packages/stylist/tests/macro_integrations/nested_blocks-fail.rs new file mode 100644 index 0000000..c612544 --- /dev/null +++ b/packages/stylist/tests/macro_integrations/nested_blocks-fail.rs @@ -0,0 +1,9 @@ +fn main() { + let _ = stylist::css! { + .outer { + .inner { + background-color: red; + } + } + }; +} diff --git a/packages/stylist/tests/macro_integrations/nested_blocks-fail.stderr b/packages/stylist/tests/macro_integrations/nested_blocks-fail.stderr new file mode 100644 index 0000000..2f45366 --- /dev/null +++ b/packages/stylist/tests/macro_integrations/nested_blocks-fail.stderr @@ -0,0 +1,5 @@ +error: Can not nest qualified blocks (yet) + --> $DIR/nested_blocks-fail.rs:4:13 + | +4 | .inner { + | ^^^^^^ diff --git a/packages/stylist/tests/macro_integrations/no_display_impl-fail.rs b/packages/stylist/tests/macro_integrations/no_display_impl-fail.rs new file mode 100644 index 0000000..40cb7a9 --- /dev/null +++ b/packages/stylist/tests/macro_integrations/no_display_impl-fail.rs @@ -0,0 +1,9 @@ +enum NoDisplay { + ND, +} +fn main() { + let expr = NoDisplay::ND; + let _ = stylist::css! { + background: ${expr}; + }; +} diff --git a/packages/stylist/tests/macro_integrations/no_display_impl-fail.stderr b/packages/stylist/tests/macro_integrations/no_display_impl-fail.stderr new file mode 100644 index 0000000..11c71ab --- /dev/null +++ b/packages/stylist/tests/macro_integrations/no_display_impl-fail.stderr @@ -0,0 +1,9 @@ +error[E0277]: `NoDisplay` doesn't implement `std::fmt::Display` + --> $DIR/no_display_impl-fail.rs:7:23 + | +7 | background: ${expr}; + | ^^^^ `NoDisplay` cannot be formatted with the default formatter + | + = help: the trait `std::fmt::Display` is not implemented for `NoDisplay` + = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead + = note: required for the cast to the object type `dyn std::fmt::Display` diff --git a/packages/stylist/tests/macro_integrations/selector_as_attribute-fail.rs b/packages/stylist/tests/macro_integrations/selector_as_attribute-fail.rs new file mode 100644 index 0000000..451f7ca --- /dev/null +++ b/packages/stylist/tests/macro_integrations/selector_as_attribute-fail.rs @@ -0,0 +1,5 @@ +fn main() { + let _ = stylist::css! { + looks_like_selector(): blue; + }; +} diff --git a/packages/stylist/tests/macro_integrations/selector_as_attribute-fail.stderr b/packages/stylist/tests/macro_integrations/selector_as_attribute-fail.stderr new file mode 100644 index 0000000..4ce4fae --- /dev/null +++ b/packages/stylist/tests/macro_integrations/selector_as_attribute-fail.stderr @@ -0,0 +1,5 @@ +error: unexpected ';' in selector, did you mean to write an attribute? + --> $DIR/selector_as_attribute-fail.rs:3:36 + | +3 | looks_like_selector(): blue; + | ^ diff --git a/packages/stylist/tests/macro_integrations/unsupported_rule-fail.rs b/packages/stylist/tests/macro_integrations/unsupported_rule-fail.rs new file mode 100644 index 0000000..46f1b3b --- /dev/null +++ b/packages/stylist/tests/macro_integrations/unsupported_rule-fail.rs @@ -0,0 +1,16 @@ +fn main() { + let is_an_expression = "black"; + let _ = stylist::css! { + @page { + margin: 1cm; + } + @property property-name { + syntax: ""; + inherits: false; + initial-value: #c0ffee; + } + @completely-unknown { + some-attribute: foo-value; + } + }; +} diff --git a/packages/stylist/tests/macro_integrations/unsupported_rule-fail.stderr b/packages/stylist/tests/macro_integrations/unsupported_rule-fail.stderr new file mode 100644 index 0000000..fa7e8c2 --- /dev/null +++ b/packages/stylist/tests/macro_integrations/unsupported_rule-fail.stderr @@ -0,0 +1,17 @@ +error: @-rule 'page' not supported + --> $DIR/unsupported_rule-fail.rs:4:10 + | +4 | @page { + | ^^^^ + +error: @-rule 'property' not supported + --> $DIR/unsupported_rule-fail.rs:7:10 + | +7 | @property property-name { + | ^^^^^^^^ + +error: @-rule 'completely-unknown' not supported + --> $DIR/unsupported_rule-fail.rs:12:10 + | +12 | @completely-unknown { + | ^^^^^^^^^^^^^^^^^^ diff --git a/packages/stylist/tests/macro_integrations/uses-display-impl-pass.rs b/packages/stylist/tests/macro_integrations/uses-display-impl-pass.rs new file mode 100644 index 0000000..c18469e --- /dev/null +++ b/packages/stylist/tests/macro_integrations/uses-display-impl-pass.rs @@ -0,0 +1,28 @@ +use std::fmt::{Display, Formatter, Result}; +enum Foo { + Bar, +} +impl Display for Foo { + fn fmt(&self, f: &mut Formatter) -> Result { + f.write_str("none") + } +} +impl Foo { + fn to_string(&self) -> String { + "confused user impl".into() + } +} +fn main() { + let style = stylist::style! { + display: ${Foo::Bar}; + } + .unwrap(); + let expected_result = format!( + r#".{cls} {{ +display: none; +}} +"#, + cls = style.get_class_name() + ); + assert_eq!(expected_result, style.get_style_str()); +} diff --git a/packages/stylist/tests/sheet_tests.rs b/packages/stylist/tests/macro_sheet_tests.rs similarity index 91% rename from packages/stylist/tests/sheet_tests.rs rename to packages/stylist/tests/macro_sheet_tests.rs index 2816f27..04594ae 100644 --- a/packages/stylist/tests/sheet_tests.rs +++ b/packages/stylist/tests/macro_sheet_tests.rs @@ -2,8 +2,14 @@ use std::borrow::Cow; use stylist::ast::*; +fn init() { + let _ = env_logger::builder().is_test(true).try_init(); +} + #[test] fn test_sheet_interpolation() { + init(); + let parsed = sheet!( r#" background-color: red; @@ -41,7 +47,11 @@ fn test_sheet_interpolation() { .into(), }), ScopeContent::Block(Block { - condition: vec![".nested".into(), ".some-selector".into()].into(), + condition: vec![ + vec![".nested".into()].into(), + vec![".some-selector".into()].into(), + ] + .into(), style_attributes: vec![ StyleAttribute { key: "background-color".into(), @@ -101,7 +111,7 @@ fn test_sheet_escaped() { let expected = Sheet::from(vec![ScopeContent::Block(Block { condition: vec![ - ".nested".into(), + vec![".nested".into()].into(), Selector { fragments: vec!["\"".into(), "${".into(), "var_a}\"".into()].into(), },