diff --git a/c/src/lib.rs b/c/src/lib.rs index d57db8c8..5b21bfdc 100644 --- a/c/src/lib.rs +++ b/c/src/lib.rs @@ -285,7 +285,6 @@ pub extern "C" fn lightningcss_stylesheet_parse( error_recovery: options.error_recovery, source_index: 0, warnings: Some(warnings.clone()), - at_rule_parser: None, }; let stylesheet = unwrap!(StyleSheet::parse(code, opts), error, std::ptr::null_mut()); diff --git a/examples/custom_at_rule.rs b/examples/custom_at_rule.rs index 43fe79eb..ada19de6 100644 --- a/examples/custom_at_rule.rs +++ b/examples/custom_at_rule.rs @@ -10,7 +10,7 @@ use lightningcss::{ selector::{Component, Selector}, stylesheet::{ParserOptions, PrinterOptions, StyleSheet}, targets::Browsers, - traits::ToCss, + traits::{AtRuleParser, ToCss}, values::{color::CssColor, length::LengthValue}, vendor_prefix::VendorPrefix, visit_types, @@ -21,17 +21,11 @@ fn main() { let args: Vec = std::env::args().collect(); let source = std::fs::read_to_string(&args[1]).unwrap(); let opts = ParserOptions { - at_rule_parser: Some(TailwindAtRuleParser), filename: args[1].clone(), - nesting: true, - custom_media: false, - css_modules: None, - error_recovery: false, - warnings: None, - source_index: 0, + ..Default::default() }; - let mut stylesheet = StyleSheet::parse(&source, opts).unwrap(); + let mut stylesheet = StyleSheet::parse_with(&source, opts, &mut TailwindAtRuleParser).unwrap(); println!("{:?}", stylesheet); @@ -103,6 +97,7 @@ impl<'i> AtRuleParser<'i> for TailwindAtRuleParser { &mut self, name: CowRcStr<'i>, input: &mut Parser<'i, 't>, + _options: &ParserOptions<'_, 'i>, ) -> Result> { match_ignore_ascii_case! {&*name, "tailwind" => { @@ -135,7 +130,12 @@ impl<'i> AtRuleParser<'i> for TailwindAtRuleParser { } } - fn rule_without_block(&mut self, prelude: Self::Prelude, start: &ParserState) -> Result { + fn rule_without_block( + &mut self, + prelude: Self::Prelude, + start: &ParserState, + _options: &ParserOptions<'_, 'i>, + ) -> Result { let loc = start.source_location(); match prelude { Prelude::Tailwind(directive) => Ok(AtRule::Tailwind(TailwindRule { directive, loc })), diff --git a/node/ast.d.ts b/node/ast.d.ts index b6b4be69..cab655e6 100644 --- a/node/ast.d.ts +++ b/node/ast.d.ts @@ -6039,22 +6039,16 @@ export type SelectorComponent = | ( | { type: "namespace"; - value: "none"; + kind: "none"; } | { type: "namespace"; - value: "any"; - } - | { - type: "namespace"; - url: string; - value: "default"; + kind: "any"; } | { type: "namespace"; + kind: "named"; prefix: string; - url: string; - value: "some"; } ) | { @@ -7042,7 +7036,7 @@ export interface StyleRule { /** * The declarations within the style rule. */ - declarations: DeclarationBlock; + declarations?: DeclarationBlock; /** * The location of the rule in the source file. */ diff --git a/node/index.d.ts b/node/index.d.ts index 583966e9..02319582 100644 --- a/node/index.d.ts +++ b/node/index.d.ts @@ -1,9 +1,9 @@ -import type { Angle, CssColor, Rule, CustomProperty, EnvironmentVariable, Function, Image, LengthValue, MediaQuery, Declaration, Ratio, Resolution, Selector, SupportsCondition, Time, Token, TokenOrValue, UnknownAtRule, Url, Variable, StyleRule, DeclarationBlock } from './ast'; +import type { Angle, CssColor, Rule, CustomProperty, EnvironmentVariable, Function, Image, LengthValue, MediaQuery, Declaration, Ratio, Resolution, Selector, SupportsCondition, Time, Token, TokenOrValue, UnknownAtRule, Url, Variable, StyleRule, DeclarationBlock, ParsedComponent, Multiplier } from './ast'; import type { Targets } from './targets'; export * from './ast'; -export interface TransformOptions { +export interface TransformOptions { /** The filename being transformed. Used for error messages and source maps. */ filename: string, /** The source code to transform. */ @@ -55,7 +55,14 @@ export interface TransformOptions { * For optimal performance, visitors should be as specific as possible about what types of values * they care about so that JavaScript has to be called as little as possible. */ - visitor?: Visitor + visitor?: Visitor, + /** + * Defines how to parse custom CSS at-rules. Each at-rule can have a prelude, defined using a CSS + * [syntax string](https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings), and + * a block body. The body can be a declaration list, rule list, or style block as defined in the + * [css spec](https://drafts.csswg.org/css-syntax/#declaration-rule-list). + */ + customAtRules?: C } // This is a hack to make TS still provide autocomplete for `property` vs. just making it `string`. @@ -79,12 +86,54 @@ type MappedRuleVisitors = { [Name in Exclude]?: RuleVisitor>>; } -type UnknownVisitors = { - [name: string]: RuleVisitor +type UnknownVisitors = { + [name: string]: RuleVisitor } -type RuleVisitors = MappedRuleVisitors & { - unknown?: UnknownVisitors | RuleVisitor +type CustomVisitors = { + [Name in keyof T]?: RuleVisitor> +}; + +type AnyCustomAtRule = { + [Key in keyof C]: CustomAtRule +}[keyof C]; + +type RuleVisitors = MappedRuleVisitors & { + unknown?: UnknownVisitors | Omit, keyof CallableFunction>, + custom?: CustomVisitors | Omit>, keyof CallableFunction> +}; + +type PreludeTypes = Exclude; +type SyntaxString = `<${PreludeTypes}>` | `<${PreludeTypes}>+` | `<${PreludeTypes}>#` | (string & {}); +type ComponentTypes = { + [Key in PreludeTypes as `<${Key}>`]: FindByType +}; + +type Repetitions = { + [Key in PreludeTypes as `<${Key}>+` | `<${Key}>#`]: { + type: "repeated", + value: { + components: FindByType[], + multiplier: Multiplier + } + } +}; + +type MappedPrelude = ComponentTypes & Repetitions; +type MappedBody

= P extends 'style-block' ? 'rule-list' : P; +interface CustomAtRule { + name: N, + prelude: R['prelude'] extends keyof MappedPrelude ? MappedPrelude[R['prelude']] : ParsedComponent, + body: FindByType>, + loc: Location +} + +type CustomAtRuleBody = { + type: 'declaration-list', + value: Required +} | { + type: 'rule-list', + value: RequiredValue[] }; type FindProperty = Union extends { property: Name } ? Union : never; @@ -113,9 +162,9 @@ type EnvironmentVariableVisitors = { [name: string]: EnvironmentVariableVisitor }; -export interface Visitor { - Rule?: RuleVisitor | RuleVisitors; - RuleExit?: RuleVisitor | RuleVisitors; +export interface Visitor { + Rule?: RuleVisitor | RuleVisitors; + RuleExit?: RuleVisitor | RuleVisitors; Declaration?: DeclarationVisitor | DeclarationVisitors; DeclarationExit?: DeclarationVisitor | DeclarationVisitors; Url?(url: Url): Url | void; @@ -143,14 +192,38 @@ export interface Visitor { EnvironmentVariableExit?: EnvironmentVariableVisitor | EnvironmentVariableVisitors; } +export interface CustomAtRules { + [name: string]: CustomAtRuleDefinition +} + +export interface CustomAtRuleDefinition { + /** + * Defines the syntax for a custom at-rule prelude. The value should be a + * CSS [syntax string](https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings) + * representing the types of values that are accepted. This property may be omitted or + * set to null to indicate that no prelude is accepted. + */ + prelude?: SyntaxString | null, + /** + * Defines the type of body contained within the at-rule block. + * - declaration-list: A CSS declaration list, as in a style rule. + * - rule-list: A list of CSS rules, as supported within a non-nested + * at-rule such as `@media` or `@supports`. + * - style-block: Both a declaration list and rule list, as accepted within + * a nested at-rule within a style rule (e.g. `@media` inside a style rule + * with directly nested declarations). + */ + body?: 'declaration-list' | 'rule-list' | 'style-block' | null +} + export interface DependencyOptions { /** Whether to preserve `@import` rules rather than removing them. */ preserveImports?: boolean } -export type BundleOptions = Omit; +export type BundleOptions = Omit, 'code'>; -export interface BundleAsyncOptions extends BundleOptions { +export interface BundleAsyncOptions extends BundleOptions { resolver?: Resolver; } @@ -299,7 +372,7 @@ export interface ErrorLocation extends Location { * Compiles a CSS file, including optionally minifying and lowering syntax to the given * targets. A source map may also be generated, but this is not enabled by default. */ -export declare function transform(options: TransformOptions): TransformResult; +export declare function transform(options: TransformOptions): TransformResult; export interface TransformAttributeOptions { /** The filename in which the style attribute appeared. Used for error messages and dependencies. */ @@ -329,7 +402,7 @@ export interface TransformAttributeOptions { * For optimal performance, visitors should be as specific as possible about what types of values * they care about so that JavaScript has to be called as little as possible. */ - visitor?: Visitor + visitor?: Visitor } export interface TransformAttributeResult { @@ -355,14 +428,14 @@ export declare function browserslistToTargets(browserslist: string[]): Targets; /** * Bundles a CSS file and its dependencies, inlining @import rules. */ -export declare function bundle(options: BundleOptions): TransformResult; +export declare function bundle(options: BundleOptions): TransformResult; /** * Bundles a CSS file and its dependencies asynchronously, inlining @import rules. */ -export declare function bundleAsync(options: BundleAsyncOptions): Promise; +export declare function bundleAsync(options: BundleAsyncOptions): Promise; /** * Composes multiple visitor objects into a single one. */ -export declare function composeVisitors(visitors: Visitor[]): Visitor; +export declare function composeVisitors(visitors: Visitor[]): Visitor; diff --git a/node/src/at_rule_parser.rs b/node/src/at_rule_parser.rs new file mode 100644 index 00000000..c147ee2d --- /dev/null +++ b/node/src/at_rule_parser.rs @@ -0,0 +1,210 @@ +use std::collections::HashMap; + +use cssparser::*; +use lightningcss::{ + declaration::DeclarationBlock, + error::ParserError, + rules::{CssRuleList, Location}, + stylesheet::ParserOptions, + traits::{AtRuleParser, ToCss}, + values::{ + string::CowArcStr, + syntax::{ParsedComponent, SyntaxString}, + }, + visitor::{Visit, VisitTypes, Visitor}, +}; +use serde::{Deserialize, Deserializer, Serialize}; + +#[derive(Deserialize, Debug, Clone)] +pub struct CustomAtRuleConfig { + #[serde(default, deserialize_with = "deserialize_prelude")] + prelude: Option, + body: Option, +} + +fn deserialize_prelude<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s = Option::>::deserialize(deserializer)?; + if let Some(s) = s { + Ok(Some( + SyntaxString::parse_string(&s).map_err(|_| serde::de::Error::custom("invalid syntax string"))?, + )) + } else { + Ok(None) + } +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "kebab-case")] +enum CustomAtRuleBodyType { + DeclarationList, + RuleList, + StyleBlock, +} + +pub struct Prelude<'i> { + name: CowArcStr<'i>, + prelude: Option>, +} + +#[derive(Serialize, Deserialize)] +pub struct AtRule<'i> { + #[serde(borrow)] + pub name: CowArcStr<'i>, + pub prelude: Option>, + pub body: Option>, + pub loc: Location, +} + +#[derive(Serialize, Deserialize)] +#[serde(tag = "type", content = "value", rename_all = "kebab-case")] +pub enum AtRuleBody<'i> { + #[serde(borrow)] + DeclarationList(DeclarationBlock<'i>), + RuleList(CssRuleList<'i, AtRule<'i>>), +} + +#[derive(Clone)] +pub struct CustomAtRuleParser { + pub configs: HashMap, +} + +impl<'i> AtRuleParser<'i> for CustomAtRuleParser { + type Prelude = Prelude<'i>; + type Error = ParserError<'i>; + type AtRule = AtRule<'i>; + + fn parse_prelude<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + _options: &ParserOptions<'_, 'i>, + ) -> Result> { + if let Some(config) = self.configs.get(name.as_ref()) { + let prelude = if let Some(prelude) = &config.prelude { + Some(prelude.parse_value(input)?) + } else { + None + }; + Ok(Prelude { + name: name.into(), + prelude, + }) + } else { + Err(input.new_error(BasicParseErrorKind::AtRuleInvalid(name))) + } + } + + fn parse_block<'t>( + &mut self, + prelude: Self::Prelude, + start: &ParserState, + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result> { + let config = self.configs.get(prelude.name.as_ref()).unwrap(); + let body = if let Some(body) = &config.body { + match body { + CustomAtRuleBodyType::DeclarationList => { + Some(AtRuleBody::DeclarationList(DeclarationBlock::parse(input, options)?)) + } + CustomAtRuleBodyType::RuleList => { + Some(AtRuleBody::RuleList(CssRuleList::parse_with(input, options, self)?)) + } + CustomAtRuleBodyType::StyleBlock => Some(AtRuleBody::RuleList(CssRuleList::parse_style_block_with( + input, options, self, + )?)), + } + } else { + return Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)); + }; + + let loc = start.source_location(); + Ok(AtRule { + name: prelude.name, + prelude: prelude.prelude, + body, + loc: Location { + source_index: options.source_index, + line: loc.line, + column: loc.column, + }, + }) + } + + fn rule_without_block( + &mut self, + prelude: Self::Prelude, + start: &ParserState, + options: &ParserOptions<'_, 'i>, + ) -> Result { + let config = self.configs.get(prelude.name.as_ref()).unwrap(); + if config.body.is_some() { + return Err(()); + } + + let loc = start.source_location(); + Ok(AtRule { + name: prelude.name, + prelude: prelude.prelude, + body: None, + loc: Location { + source_index: options.source_index, + line: loc.line, + column: loc.column, + }, + }) + } +} + +impl<'i> ToCss for AtRule<'i> { + fn to_css( + &self, + dest: &mut lightningcss::printer::Printer, + ) -> Result<(), lightningcss::error::PrinterError> + where + W: std::fmt::Write, + { + dest.write_char('@')?; + serialize_identifier(&self.name, dest)?; + if let Some(prelude) = &self.prelude { + dest.write_char(' ')?; + prelude.to_css(dest)?; + } + + if let Some(body) = &self.body { + match body { + AtRuleBody::DeclarationList(decls) => { + decls.to_css_block(dest)?; + } + AtRuleBody::RuleList(rules) => { + dest.whitespace()?; + dest.write_char('{')?; + dest.indent(); + dest.newline()?; + rules.to_css(dest)?; + dest.dedent(); + dest.newline()?; + dest.write_char('}')?; + } + } + } + + Ok(()) + } +} + +impl<'i, V: Visitor<'i, AtRule<'i>>> Visit<'i, AtRule<'i>, V> for AtRule<'i> { + const CHILD_TYPES: VisitTypes = VisitTypes::empty(); + + fn visit_children(&mut self, visitor: &mut V) -> Result<(), V::Error> { + self.prelude.visit(visitor)?; + match &mut self.body { + Some(AtRuleBody::DeclarationList(decls)) => decls.visit(visitor), + Some(AtRuleBody::RuleList(rules)) => rules.visit(visitor), + None => Ok(()), + } + } +} diff --git a/node/src/lib.rs b/node/src/lib.rs index fc752c6e..ef290bca 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -2,6 +2,7 @@ #[global_allocator] static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc; +use at_rule_parser::{AtRule, CustomAtRuleConfig, CustomAtRuleParser}; use lightningcss::bundler::{BundleErrorKind, Bundler, FileProvider, SourceProvider}; use lightningcss::css_modules::{CssModuleExports, CssModuleReferences, PatternParseError}; use lightningcss::dependencies::{Dependency, DependencyOptions}; @@ -13,13 +14,14 @@ use lightningcss::targets::Browsers; use lightningcss::visitor::Visit; use parcel_sourcemap::SourceMap; use serde::{Deserialize, Serialize}; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::{Arc, Mutex, RwLock}; use transformer::JsVisitor; +mod at_rule_parser; #[cfg(not(target_arch = "wasm32"))] mod threadsafe_function; mod transformer; @@ -126,12 +128,20 @@ mod bundle { let config: BundleConfig = ctx.env.from_js_value(opts)?; let fs = FileProvider::new(); + + // This is pretty silly, but works around a rust limitation that you cannot + // explicitly annotate lifetime bounds on closures. + fn annotate<'i, 'o, F>(f: F) -> F + where + F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>, + { + f + } + let res = compile_bundle( &fs, &config, - visitor - .as_mut() - .map(|visitor| |stylesheet: &mut StyleSheet| stylesheet.visit(visitor)), + visitor.as_mut().map(|visitor| annotate(|stylesheet| stylesheet.visit(visitor))), ); match res { @@ -242,7 +252,7 @@ mod bundle { } struct VisitMessage { - stylesheet: &'static mut StyleSheet<'static, 'static>, + stylesheet: &'static mut StyleSheet<'static, 'static, AtRule<'static>>, tx: Sender>, } @@ -396,15 +406,16 @@ mod bundle { unsafe { std::mem::transmute::<&'_ P, &'static P>(&provider) }, &config, tsfn.map(move |tsfn| { - move |stylesheet: &mut StyleSheet| { + move |stylesheet: &mut StyleSheet| { CHANNEL.with(|channel| { let message = VisitMessage { // SAFETY: we immediately lock the thread until we get a response, // so stylesheet cannot be dropped in that time. stylesheet: unsafe { - std::mem::transmute::<&'_ mut StyleSheet<'_, '_>, &'static mut StyleSheet<'static, 'static>>( - stylesheet, - ) + std::mem::transmute::< + &'_ mut StyleSheet<'_, '_, AtRule>, + &'static mut StyleSheet<'static, 'static, AtRule>, + >(stylesheet) }, tx: channel.0.clone(), }; @@ -490,6 +501,7 @@ struct Config { pub pseudo_classes: Option, pub unused_symbols: Option>, pub error_recovery: Option, + pub custom_at_rules: Option>, } #[derive(Debug, Deserialize)] @@ -533,6 +545,7 @@ struct BundleConfig { pub pseudo_classes: Option, pub unused_symbols: Option>, pub error_recovery: Option, + pub custom_at_rules: Option>, } #[derive(Debug, Deserialize)] @@ -586,7 +599,7 @@ fn compile<'i>( }; let res = { - let mut stylesheet = StyleSheet::parse( + let mut stylesheet = StyleSheet::parse_with( &code, ParserOptions { filename: filename.clone(), @@ -614,7 +627,9 @@ fn compile<'i>( source_index: 0, error_recovery: config.error_recovery.unwrap_or_default(), warnings: warnings.clone(), - at_rule_parser: ParserOptions::default_at_rule_parser(), + }, + &mut CustomAtRuleParser { + configs: config.custom_at_rules.clone().unwrap_or_default(), }, )?; @@ -677,7 +692,12 @@ fn compile<'i>( }) } -fn compile_bundle<'i, 'o, P: SourceProvider, F: FnOnce(&mut StyleSheet<'i, 'o>) -> napi::Result<()>>( +fn compile_bundle< + 'i, + 'o, + P: SourceProvider, + F: FnOnce(&mut StyleSheet<'i, 'o, AtRule<'i>>) -> napi::Result<()>, +>( fs: &'i P, config: &'o BundleConfig, visit: Option, @@ -715,10 +735,16 @@ fn compile_bundle<'i, 'o, P: SourceProvider, F: FnOnce(&mut StyleSheet<'i, 'o>) }, error_recovery: config.error_recovery.unwrap_or_default(), warnings: warnings.clone(), - ..ParserOptions::default() + filename: String::new(), + source_index: 0, + }; + + let mut at_rule_parser = CustomAtRuleParser { + configs: config.custom_at_rules.clone().unwrap_or_default(), }; - let mut bundler = Bundler::new(fs, source_map.as_mut(), parser_options); + let mut bundler = + Bundler::new_with_at_rule_parser(fs, source_map.as_mut(), parser_options, &mut at_rule_parser); let mut stylesheet = bundler.bundle(Path::new(&config.filename))?; if let Some(visit) = visit { diff --git a/node/src/transformer.rs b/node/src/transformer.rs index 4aabf887..f65cd092 100644 --- a/node/src/transformer.rs +++ b/node/src/transformer.rs @@ -17,6 +17,8 @@ use napi::{Env, JsFunction, JsObject, JsUnknown, Ref, ValueType}; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; +use crate::at_rule_parser::AtRule; + pub struct JsVisitor { env: Env, visit_rule: VisitorsRef, @@ -237,7 +239,7 @@ impl JsVisitor { } } -impl<'i> Visitor<'i> for JsVisitor { +impl<'i> Visitor<'i, AtRule<'i>> for JsVisitor { type Error = napi::Error; const TYPES: lightningcss::visitor::VisitTypes = VisitTypes::all(); @@ -246,7 +248,10 @@ impl<'i> Visitor<'i> for JsVisitor { self.types } - fn visit_rule_list(&mut self, rules: &mut lightningcss::rules::CssRuleList<'i>) -> Result<(), Self::Error> { + fn visit_rule_list( + &mut self, + rules: &mut lightningcss::rules::CssRuleList<'i, AtRule<'i>>, + ) -> Result<(), Self::Error> { if self.types.contains(VisitTypes::RULES) { let env = self.env; let rule_map = self.rule_map.get::(&env); @@ -285,7 +290,17 @@ impl<'i> Visitor<'i> for JsVisitor { "unknown" } } - CssRule::Ignored | CssRule::Custom(..) => return Ok(None), + CssRule::Custom(c) => { + let name = c.name.as_ref(); + if let Some(visit) = rule_map.custom(stage, "custom", name) { + let js_value = env.to_js_value(c)?; + let res = visit.call(None, &[js_value])?; + return env.from_js_value(res).map(serde_detach::detach); + } else { + "custom" + } + } + CssRule::Ignored => return Ok(None), }; if let Some(visit) = rule_map.named(stage, name).as_ref().or(visit_rule.for_stage(stage)) { diff --git a/node/test/customAtRules.mjs b/node/test/customAtRules.mjs new file mode 100644 index 00000000..0c0585e7 --- /dev/null +++ b/node/test/customAtRules.mjs @@ -0,0 +1,276 @@ +// @ts-check + +import { test } from 'uvu'; +import * as assert from 'uvu/assert'; +import { bundle, transform } from '../index.mjs'; + +test('declaration list', () => { + let definitions = new Map(); + let res = transform({ + filename: 'test.css', + minify: true, + code: Buffer.from(` + @theme spacing { + foo: 16px; + bar: 32px; + } + + .foo { + width: theme('spacing.foo'); + } + `), + customAtRules: { + theme: { + prelude: '', + body: 'declaration-list' + } + }, + visitor: { + Rule: { + custom: { + theme(rule) { + for (let decl of rule.body.value.declarations) { + if (decl.property === 'custom') { + definitions.set(rule.prelude.value + '.' + decl.value.name, decl.value.value); + } + } + return []; + } + } + }, + Function: { + theme(f) { + if (f.arguments[0].type === 'token' && f.arguments[0].value.type === 'string') { + return definitions.get(f.arguments[0].value.value); + } + } + } + } + }); + + assert.equal(res.code.toString(), '.foo{width:16px}'); +}); + +test('mixin', () => { + let mixins = new Map(); + let res = transform({ + filename: 'test.css', + minify: true, + code: Buffer.from(` + @mixin color { + color: red; + + &.bar { + color: yellow; + } + } + + .foo { + @apply color; + } + `), + drafts: { nesting: true }, + targets: { chrome: 100 << 16 }, + customAtRules: { + mixin: { + prelude: '', + body: 'style-block' + }, + apply: { + prelude: '' + } + }, + visitor: { + Rule: { + custom: { + mixin(rule) { + mixins.set(rule.prelude.value, rule.body.value); + return []; + }, + apply(rule) { + return mixins.get(rule.prelude.value); + } + } + } + } + }); + + assert.equal(res.code.toString(), '.foo{color:red}.foo.bar{color:#ff0}'); +}); + +test('rule list', () => { + let res = transform({ + filename: 'test.css', + minify: true, + code: Buffer.from(` + @breakpoint 1024px { + .foo { color: yellow; } + } + `), + customAtRules: { + breakpoint: { + prelude: '', + body: 'rule-list' + } + }, + visitor: { + Rule: { + custom: { + breakpoint(rule) { + return { + type: 'media', + value: { + query: { + mediaQueries: [{ mediaType: 'all', condition: { type: 'feature', value: { type: 'range', name: 'width', operator: 'less-than-equal', value: rule.prelude } } }] + }, + rules: rule.body.value, + loc: rule.loc + } + } + } + } + } + } + }); + + assert.equal(res.code.toString(), '@media (width<=1024px){.foo{color:#ff0}}'); +}); + + +test('style block', () => { + let res = transform({ + filename: 'test.css', + minify: true, + code: Buffer.from(` + .foo { + @breakpoint 1024px { + color: yellow; + + &.bar { + color: red; + } + } + } + `), + drafts: { + nesting: true + }, + targets: { + chrome: 105 << 16 + }, + customAtRules: { + breakpoint: { + prelude: '', + body: 'style-block' + } + }, + visitor: { + Rule: { + custom: { + breakpoint(rule) { + return { + type: 'media', + value: { + query: { + mediaQueries: [{ mediaType: 'all', condition: { type: 'feature', value: { type: 'range', name: 'width', operator: 'less-than-equal', value: rule.prelude } } }] + }, + rules: rule.body.value, + loc: rule.loc + } + } + } + } + } + } + }); + + assert.equal(res.code.toString(), '@media (width<=1024px){.foo{color:#ff0}.foo.bar{color:red}}'); +}); + +test('multiple', () => { + let res = transform({ + filename: 'test.css', + minify: true, + code: Buffer.from(` + @breakpoint 1024px { + @theme spacing { + foo: 16px; + bar: 32px; + } + } + `), + customAtRules: { + breakpoint: { + prelude: '', + body: 'rule-list' + }, + theme: { + prelude: '', + body: 'declaration-list' + } + }, + visitor: { + Rule: { + custom(rule) { + if (rule.name === 'breakpoint') { + return { + type: 'media', + value: { + query: { + mediaQueries: [{ mediaType: 'all', condition: { type: 'feature', value: { type: 'range', name: 'width', operator: 'less-than-equal', value: rule.prelude } } }] + }, + rules: rule.body.value, + loc: rule.loc + } + } + } else { + return { + type: 'style', + value: { + selectors: [[{ type: 'pseudo-class', kind: 'root' }]], + declarations: rule.body.value, + loc: rule.loc + } + } + } + } + } + } + }); + + assert.equal(res.code.toString(), '@media (width<=1024px){:root{foo:16px;bar:32px}}'); +}); + +test('bundler', () => { + let mixins = new Map(); + let res = bundle({ + filename: 'tests/testdata/apply.css', + minify: true, + drafts: { nesting: true }, + targets: { chrome: 100 << 16 }, + customAtRules: { + mixin: { + prelude: '', + body: 'style-block' + }, + apply: { + prelude: '' + } + }, + visitor: { + Rule: { + custom: { + mixin(rule) { + mixins.set(rule.prelude.value, rule.body.value); + return []; + }, + apply(rule) { + return mixins.get(rule.prelude.value); + } + } + } + } + }); + + assert.equal(res.code.toString(), '.foo{color:red}.foo.bar{color:#ff0}'); +}); diff --git a/scripts/build-flow.js b/scripts/build-flow.js index 46ee8da0..6e1a1ed9 100644 --- a/scripts/build-flow.js +++ b/scripts/build-flow.js @@ -3,6 +3,8 @@ const { compiler, beautify } = require('flowgen'); let dir = `${__dirname}/../`; let contents = fs.readFileSync(dir + '/node/index.d.ts', 'utf8').replace('`${PropertyStart}${string}`', 'string'); +contents = contents.replace(/`.*`/g, 'string'); +contents = contents.replaceAll('(string & {})', 'string'); let index = beautify(compiler.compileDefinitionString(contents, { inexact: false, interfaceRecords: true })); index = index.replace('{ code: any }', '{| code: any |}'); index = index.replace(/from "(.*?)";/g, 'from "$1.js.flow";'); diff --git a/selectors/parser.rs b/selectors/parser.rs index 5796a070..9b66523b 100644 --- a/selectors/parser.rs +++ b/selectors/parser.rs @@ -2009,21 +2009,19 @@ where }), QNamePrefix::ExplicitNoNamespace => sink.push(Component::ExplicitNoNamespace), QNamePrefix::ExplicitAnyNamespace => { - match parser.default_namespace() { - // Element type selectors that have no namespace - // component (no namespace separator) represent elements - // without regard to the element's namespace (equivalent - // to "*|") unless a default namespace has been declared - // for namespaced selectors (e.g. in CSS, in the style - // sheet). If a default namespace has been declared, - // such selectors will represent only elements in the - // default namespace. - // -- Selectors § 6.1.1 - // So we'll have this act the same as the - // QNamePrefix::ImplicitAnyNamespace case. - None => {} - Some(_) => sink.push(Component::ExplicitAnyNamespace), - } + // Element type selectors that have no namespace + // component (no namespace separator) represent elements + // without regard to the element's namespace (equivalent + // to "*|") unless a default namespace has been declared + // for namespaced selectors (e.g. in CSS, in the style + // sheet). If a default namespace has been declared, + // such selectors will represent only elements in the + // default namespace. + // -- Selectors § 6.1.1 + // So we'll have this act the same as the + // QNamePrefix::ImplicitAnyNamespace case. + // For lightning css this logic was removed, should be handled when matching. + sink.push(Component::ExplicitAnyNamespace) } QNamePrefix::ImplicitNoNamespace => { unreachable!() // Not returned with in_attr_selector = false @@ -3117,38 +3115,38 @@ pub mod tests { ); // When the default namespace is not set, *| should be elided. // https://github.com/servo/servo/pull/17537 - assert_eq!( - parse_expected("*|e", Some("e")), - Ok(SelectorList::from_vec(vec![Selector::from_vec( - vec![Component::LocalName(LocalName { - name: DummyAtom::from("e"), - lower_name: DummyAtom::from("e"), - })], - specificity(0, 0, 1), - Default::default(), - )])) - ); + // assert_eq!( + // parse_expected("*|e", Some("e")), + // Ok(SelectorList::from_vec(vec![Selector::from_vec( + // vec![Component::LocalName(LocalName { + // name: DummyAtom::from("e"), + // lower_name: DummyAtom::from("e"), + // })], + // specificity(0, 0, 1), + // Default::default(), + // )])) + // ); // When the default namespace is set, *| should _not_ be elided (as foo // is no longer equivalent to *|foo--the former is only for foo in the // default namespace). // https://github.com/servo/servo/issues/16020 - assert_eq!( - parse_ns( - "*|e", - &DummyParser::default_with_namespace(DummyAtom::from("https://mozilla.org")) - ), - Ok(SelectorList::from_vec(vec![Selector::from_vec( - vec![ - Component::ExplicitAnyNamespace, - Component::LocalName(LocalName { - name: DummyAtom::from("e"), - lower_name: DummyAtom::from("e"), - }), - ], - specificity(0, 0, 1), - Default::default(), - )])) - ); + // assert_eq!( + // parse_ns( + // "*|e", + // &DummyParser::default_with_namespace(DummyAtom::from("https://mozilla.org")) + // ), + // Ok(SelectorList::from_vec(vec![Selector::from_vec( + // vec![ + // Component::ExplicitAnyNamespace, + // Component::LocalName(LocalName { + // name: DummyAtom::from("e"), + // lower_name: DummyAtom::from("e"), + // }), + // ], + // specificity(0, 0, 1), + // Default::default(), + // )])) + // ); assert_eq!( parse("*"), Ok(SelectorList::from_vec(vec![Selector::from_vec( @@ -3165,14 +3163,14 @@ pub mod tests { Default::default(), )])) ); - assert_eq!( - parse_expected("*|*", Some("*")), - Ok(SelectorList::from_vec(vec![Selector::from_vec( - vec![Component::ExplicitUniversalType], - specificity(0, 0, 0), - Default::default(), - )])) - ); + // assert_eq!( + // parse_expected("*|*", Some("*")), + // Ok(SelectorList::from_vec(vec![Selector::from_vec( + // vec![Component::ExplicitUniversalType], + // specificity(0, 0, 0), + // Default::default(), + // )])) + // ); assert_eq!( parse_ns( "*|*", @@ -3517,21 +3515,21 @@ pub mod tests { ); // *| should be elided if there is no default namespace. // https://github.com/servo/servo/pull/17537 - assert_eq!( - parse_ns_expected(":not(*|*)", &parser, Some(":not(*)")), - Ok(SelectorList::from_vec(vec![Selector::from_vec( - vec![Component::Negation( - vec![Selector::from_vec( - vec![Component::ExplicitUniversalType], - specificity(0, 0, 0), - Default::default() - )] - .into_boxed_slice() - )], - specificity(0, 0, 0), - Default::default(), - )])) - ); + // assert_eq!( + // parse_ns_expected(":not(*|*)", &parser, Some(":not(*)")), + // Ok(SelectorList::from_vec(vec![Selector::from_vec( + // vec![Component::Negation( + // vec![Selector::from_vec( + // vec![Component::ExplicitUniversalType], + // specificity(0, 0, 0), + // Default::default() + // )] + // .into_boxed_slice() + // )], + // specificity(0, 0, 0), + // Default::default(), + // )])) + // ); assert!(parse("::slotted()").is_err()); assert!(parse("::slotted(div)").is_ok()); diff --git a/selectors/serialization.rs b/selectors/serialization.rs index 0696d68b..dff9ceff 100644 --- a/selectors/serialization.rs +++ b/selectors/serialization.rs @@ -9,6 +9,7 @@ use crate::{ }; use std::borrow::Cow; +use cssparser::CowRcStr; #[cfg(feature = "jsonschema")] use schemars::JsonSchema; @@ -61,12 +62,11 @@ enum SerializedComponent<'i, 's, Impl: SelectorImpl<'s>, PseudoClass, PseudoElem #[derive(serde::Serialize, serde::Deserialize)] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] -#[serde(tag = "value", rename_all = "kebab-case")] +#[serde(tag = "kind", rename_all = "kebab-case")] enum Namespace<'i> { None, Any, - Default { url: Cow<'i, str> }, - Some { prefix: Cow<'i, str>, url: Cow<'i, str> }, + Named { prefix: Cow<'i, str> }, } #[derive(serde::Serialize, serde::Deserialize)] @@ -288,12 +288,10 @@ where Component::ExplicitUniversalType => SerializedComponent::Universal, Component::ExplicitAnyNamespace => SerializedComponent::Namespace(Namespace::Any), Component::ExplicitNoNamespace => SerializedComponent::Namespace(Namespace::None), - Component::DefaultNamespace(url) => SerializedComponent::Namespace(Namespace::Default { - url: url.as_ref().into(), - }), - Component::Namespace(prefix, url) => SerializedComponent::Namespace(Namespace::Some { + // can't actually happen anymore. + Component::DefaultNamespace(_url) => SerializedComponent::Namespace(Namespace::Any), + Component::Namespace(prefix, _url) => SerializedComponent::Namespace(Namespace::Named { prefix: prefix.as_ref().into(), - url: url.as_ref().into(), }), Component::LocalName(name) => SerializedComponent::Type { name: name.name.as_ref().into(), @@ -440,8 +438,7 @@ where SerializedComponent::Namespace(n) => match n { Namespace::Any => Component::ExplicitAnyNamespace, Namespace::None => Component::ExplicitNoNamespace, - Namespace::Default { url } => Component::DefaultNamespace(url.into()), - Namespace::Some { prefix, url } => Component::Namespace(prefix.into(), url.into()), + Namespace::Named { prefix } => Component::Namespace(prefix.into(), CowRcStr::from("").into()), }, SerializedComponent::Type { name } => { let name: Impl::LocalName = name.into(); diff --git a/src/bundler.rs b/src/bundler.rs index 80f68af6..bbe01314 100644 --- a/src/bundler.rs +++ b/src/bundler.rs @@ -25,6 +25,7 @@ use crate::{ error::ErrorLocation, + parser::DefaultAtRuleParser, properties::{ css_modules::Specifier, custom::{ @@ -36,7 +37,7 @@ use crate::{ layer::{LayerBlockRule, LayerName}, Location, }, - traits::ToCss, + traits::{AtRuleParser, ToCss}, values::ident::DashedIdentReference, }; use crate::{ @@ -50,7 +51,6 @@ use crate::{ }, stylesheet::{ParserOptions, StyleSheet}, }; -use cssparser::AtRuleParser; use dashmap::DashMap; use parcel_sourcemap::SourceMap; use rayon::prelude::*; @@ -67,11 +67,17 @@ pub struct Bundler<'a, 'o, 's, P, T: AtRuleParser<'a>> { source_map: Option>, fs: &'a P, source_indexes: DashMap, - stylesheets: Mutex>>, - options: ParserOptions<'o, 'a, T>, + stylesheets: Mutex>>, + options: ParserOptions<'o, 'a>, + at_rule_parser: Mutex>, } -struct BundleStyleSheet<'i, 'o, T: AtRuleParser<'i>> { +enum AtRuleParserValue<'a, T> { + Owned(T), + Borrowed(&'a mut T), +} + +struct BundleStyleSheet<'i, 'o, T> { stylesheet: Option>, dependencies: Vec, css_modules_deps: Vec, @@ -190,6 +196,26 @@ impl<'i, T: std::error::Error> BundleErrorKind<'i, T> { } } +impl<'a, 'o, 's, P: SourceProvider> Bundler<'a, 'o, 's, P, DefaultAtRuleParser> { + /// Creates a new Bundler using the given source provider. + /// If a source map is given, the content of each source file included in the bundle will + /// be added accordingly. + pub fn new( + fs: &'a P, + source_map: Option<&'s mut SourceMap>, + options: ParserOptions<'o, 'a>, + ) -> Bundler<'a, 'o, 's, P, DefaultAtRuleParser> { + Bundler { + source_map: source_map.map(Mutex::new), + fs, + source_indexes: DashMap::new(), + stylesheets: Mutex::new(Vec::new()), + options, + at_rule_parser: Mutex::new(AtRuleParserValue::Owned(DefaultAtRuleParser)), + } + } +} + impl<'a, 'o, 's, P: SourceProvider, T: AtRuleParser<'a> + Clone + Sync + Send> Bundler<'a, 'o, 's, P, T> where T::AtRule: Sync + Send + ToCss, @@ -197,13 +223,19 @@ where /// Creates a new Bundler using the given source provider. /// If a source map is given, the content of each source file included in the bundle will /// be added accordingly. - pub fn new(fs: &'a P, source_map: Option<&'s mut SourceMap>, options: ParserOptions<'o, 'a, T>) -> Self { + pub fn new_with_at_rule_parser( + fs: &'a P, + source_map: Option<&'s mut SourceMap>, + options: ParserOptions<'o, 'a>, + at_rule_parser: &'s mut T, + ) -> Self { Bundler { source_map: source_map.map(Mutex::new), fs, source_indexes: DashMap::new(), stylesheets: Mutex::new(Vec::new()), options, + at_rule_parser: Mutex::new(AtRuleParserValue::Borrowed(at_rule_parser)), } } @@ -211,7 +243,7 @@ where pub fn bundle<'e>( &mut self, entry: &'e Path, - ) -> Result, Error>> { + ) -> Result, Error>> { // Phase 1: load and parse all files. This is done in parallel. self.load_file( &entry, @@ -345,7 +377,15 @@ where opts.filename = filename.to_owned(); opts.source_index = source_index; - let mut stylesheet = StyleSheet::parse(code, opts)?; + let mut stylesheet = { + let mut at_rule_parser = self.at_rule_parser.lock().unwrap(); + let at_rule_parser = match &mut *at_rule_parser { + AtRuleParserValue::Owned(owned) => owned, + AtRuleParserValue::Borrowed(borrowed) => *borrowed, + }; + + StyleSheet::::parse_with(code, opts, at_rule_parser)? + }; if let Some(source_map) = &self.source_map { // Only add source if we don't have an input source map. @@ -550,7 +590,7 @@ where fn order(&mut self) { process(self.stylesheets.get_mut().unwrap(), 0, &mut HashSet::new()); - fn process<'i, T: AtRuleParser<'i>>( + fn process<'i, T>( stylesheets: &mut Vec>, source_index: u32, visited: &mut HashSet, @@ -593,10 +633,10 @@ where fn inline(&mut self, dest: &mut Vec>) { process(self.stylesheets.get_mut().unwrap(), 0, dest); - fn process<'a, T: AtRuleParser<'a>>( + fn process<'a, T>( stylesheets: &mut Vec>, source_index: u32, - dest: &mut Vec>, + dest: &mut Vec>, ) { let stylesheet = &mut stylesheets[source_index as usize]; let mut rules = std::mem::take(&mut stylesheet.stylesheet.as_mut().unwrap().rules.0); diff --git a/src/declaration.rs b/src/declaration.rs index c596c443..b6a3afd1 100644 --- a/src/declaration.rs +++ b/src/declaration.rs @@ -43,7 +43,7 @@ use cssparser::*; /// Properties are separated into a list of `!important` declararations, /// and a list of normal declarations. This reduces memory usage compared /// with storing a boolean along with each property. -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone, Default)] #[cfg_attr(feature = "visitor", derive(Visit), visit(visit_declaration_block, PROPERTIES))] #[cfg_attr(feature = "into_owned", derive(lightningcss_derive::IntoOwned))] #[cfg_attr( @@ -63,9 +63,9 @@ pub struct DeclarationBlock<'i> { impl<'i> DeclarationBlock<'i> { /// Parses a declaration block from CSS syntax. - pub fn parse<'a, 'o, 't, T>( + pub fn parse<'a, 'o, 't>( input: &mut Parser<'i, 't>, - options: &'a ParserOptions<'o, 'i, T>, + options: &'a ParserOptions<'o, 'i>, ) -> Result>> { let mut important_declarations = DeclarationList::new(); let mut declarations = DeclarationList::new(); @@ -94,9 +94,9 @@ impl<'i> DeclarationBlock<'i> { } /// Parses a declaration block from a string. - pub fn parse_string<'o, T>( + pub fn parse_string<'o>( input: &'i str, - options: ParserOptions<'o, 'i, T>, + options: ParserOptions<'o, 'i>, ) -> Result>> { let mut input = ParserInput::new(input); let mut parser = Parser::new(&mut input); @@ -403,14 +403,14 @@ impl<'i> DeclarationBlock<'i> { } } -struct PropertyDeclarationParser<'a, 'o, 'i, T> { +struct PropertyDeclarationParser<'a, 'o, 'i> { important_declarations: &'a mut Vec>, declarations: &'a mut Vec>, - options: &'a ParserOptions<'o, 'i, T>, + options: &'a ParserOptions<'o, 'i>, } /// Parse a declaration within {} block: `color: blue` -impl<'a, 'o, 'i, T> cssparser::DeclarationParser<'i> for PropertyDeclarationParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'i> cssparser::DeclarationParser<'i> for PropertyDeclarationParser<'a, 'o, 'i> { type Declaration = (); type Error = ParserError<'i>; @@ -430,18 +430,18 @@ impl<'a, 'o, 'i, T> cssparser::DeclarationParser<'i> for PropertyDeclarationPars } /// Default methods reject all at rules. -impl<'a, 'o, 'i, T> AtRuleParser<'i> for PropertyDeclarationParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'i> AtRuleParser<'i> for PropertyDeclarationParser<'a, 'o, 'i> { type Prelude = (); type AtRule = (); type Error = ParserError<'i>; } -pub(crate) fn parse_declaration<'i, 't, T>( +pub(crate) fn parse_declaration<'i, 't>( name: CowRcStr<'i>, input: &mut cssparser::Parser<'i, 't>, declarations: &mut DeclarationList<'i>, important_declarations: &mut DeclarationList<'i>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i>, ) -> Result<(), cssparser::ParseError<'i, ParserError<'i>>> { let property = input.parse_until_before(Delimiter::Bang, |input| { Property::parse(PropertyId::from(CowArcStr::from(name)), input, options) diff --git a/src/error.rs b/src/error.rs index a203dc56..f694ca16 100644 --- a/src/error.rs +++ b/src/error.rs @@ -71,6 +71,8 @@ impl fmt::Display for ErrorLocation { pub enum ParserError<'i> { /// An at rule body was invalid. AtRuleBodyInvalid, + /// An at rule prelude was invalid + AtRulePreludeInvalid, /// An unknown or unsupported at rule was encountered. AtRuleInvalid(CowArcStr<'i>), /// Unexpectedly encountered the end of input data. @@ -106,6 +108,7 @@ impl<'i> fmt::Display for ParserError<'i> { use ParserError::*; match self { AtRuleBodyInvalid => write!(f, "Invalid @ rule body"), + AtRulePreludeInvalid => write!(f, "Invalid @ rule prelude"), AtRuleInvalid(name) => write!(f, "Unknown at rule: @{}", name), EndOfInput => write!(f, "Unexpected end of input"), InvalidDeclaration => write!(f, "Invalid declaration"), diff --git a/src/lib.rs b/src/lib.rs index 64f2a99e..62134822 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5644,7 +5644,7 @@ mod tests { minify_test("a:is(.foo) { color: yellow }", "a.foo{color:#ff0}"); minify_test("a:is([data-test]) { color: yellow }", "a[data-test]{color:#ff0}"); minify_test(".foo:is(a) { color: yellow }", ".foo:is(a){color:#ff0}"); - minify_test(".foo:is(*|a) { color: yellow }", ".foo:is(a){color:#ff0}"); + minify_test(".foo:is(*|a) { color: yellow }", ".foo:is(*|a){color:#ff0}"); minify_test(".foo:is(*) { color: yellow }", ".foo:is(*){color:#ff0}"); minify_test( "@namespace svg url(http://www.w3.org/2000/svg); .foo:is(svg|a) { color: yellow }", diff --git a/src/parser.rs b/src/parser.rs index 88f9d4ca..159ad591 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -34,12 +34,11 @@ use crate::vendor_prefix::VendorPrefix; use crate::visitor::{Visit, VisitTypes, Visitor}; use cssparser::*; use parcel_selectors::parser::NestingRequirement; -use std::collections::HashMap; use std::sync::{Arc, RwLock}; /// CSS parsing options. #[derive(Clone, Debug, Default)] -pub struct ParserOptions<'o, 'i, T = DefaultAtRuleParser> { +pub struct ParserOptions<'o, 'i> { /// Filename to use in error messages. pub filename: String, /// Whether the enable the [CSS nesting](https://www.w3.org/TR/css-nesting-1/) draft syntax. @@ -55,11 +54,9 @@ pub struct ParserOptions<'o, 'i, T = DefaultAtRuleParser> { pub error_recovery: bool, /// A list that will be appended to when a warning occurs. pub warnings: Option>>>>>, - /// A custom at rule parser. - pub at_rule_parser: Option, } -impl<'o, 'i, T> ParserOptions<'o, 'i, T> { +impl<'o, 'i> ParserOptions<'o, 'i> { #[inline] pub(crate) fn warn(&self, warning: ParseError<'i, ParserError<'i>>) { if let Some(warnings) = &self.warnings { @@ -70,31 +67,10 @@ impl<'o, 'i, T> ParserOptions<'o, 'i, T> { } } -impl<'o, 'i> ParserOptions<'o, 'i, DefaultAtRuleParser> { - /// Returns the default parser options. - pub fn default() -> Self { - ParserOptions { - filename: String::default(), - nesting: false, - custom_media: false, - css_modules: None, - source_index: 0, - error_recovery: false, - warnings: None, - at_rule_parser: None, - } - } - - /// Returns the default at-rule parser. - pub fn default_at_rule_parser() -> Option { - None - } -} - #[derive(Clone, Default)] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] pub struct DefaultAtRuleParser; -impl<'i> AtRuleParser<'i> for DefaultAtRuleParser { +impl<'i> crate::traits::AtRuleParser<'i> for DefaultAtRuleParser { type AtRule = DefaultAtRule; type Error = (); type Prelude = (); @@ -133,27 +109,24 @@ enum State { /// The parser for the top-level rules in a stylesheet. pub struct TopLevelRuleParser<'a, 'o, 'i, T> { - default_namespace: Option>, - namespace_prefixes: HashMap, CowArcStr<'i>>, - pub options: &'a mut ParserOptions<'o, 'i, T>, + pub options: &'a ParserOptions<'o, 'i>, state: State, + at_rule_parser: &'a mut T, } impl<'a, 'o, 'b, 'i, T> TopLevelRuleParser<'a, 'o, 'i, T> { - pub fn new(options: &'a mut ParserOptions<'o, 'i, T>) -> Self { + pub fn new(options: &'a ParserOptions<'o, 'i>, at_rule_parser: &'a mut T) -> Self { TopLevelRuleParser { - default_namespace: None, - namespace_prefixes: HashMap::new(), options, state: State::Start, + at_rule_parser, } } fn nested<'x: 'b>(&'x mut self) -> NestedRuleParser<'_, 'o, 'i, T> { NestedRuleParser { - default_namespace: &mut self.default_namespace, - namespace_prefixes: &mut self.namespace_prefixes, - options: &mut self.options, + options: &self.options, + at_rule_parser: self.at_rule_parser, } } } @@ -211,7 +184,7 @@ pub enum AtRulePrelude<'i, T> { Custom(T), } -impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for TopLevelRuleParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for TopLevelRuleParser<'a, 'o, 'i, T> { type Prelude = AtRulePrelude<'i, T::Prelude>; type AtRule = (SourcePosition, CssRule<'i, T::AtRule>); type Error = ParserError<'i>; @@ -319,12 +292,6 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for TopLevelRuleParser<'a AtRulePrelude::Namespace(prefix, url) => { self.state = State::Namespaces; - if let Some(prefix) = &prefix { - self.namespace_prefixes.insert(prefix.into(), url.clone().into()); - } else { - self.default_namespace = Some(url.clone().into()); - } - CssRule::Namespace(NamespaceRule { prefix: prefix.map(|x| x.into()), url: url.into(), @@ -362,7 +329,9 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for TopLevelRuleParser<'a } } -impl<'a, 'o, 'i, T: AtRuleParser<'i>> QualifiedRuleParser<'i> for TopLevelRuleParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> QualifiedRuleParser<'i> + for TopLevelRuleParser<'a, 'o, 'i, T> +{ type Prelude = SelectorList<'i>; type QualifiedRule = (SourcePosition, CssRule<'i, T::AtRule>); type Error = ParserError<'i>; @@ -388,21 +357,19 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> QualifiedRuleParser<'i> for TopLevelRulePa } } -struct NestedRuleParser<'a, 'o, 'i, T> { - default_namespace: &'a Option>, - namespace_prefixes: &'a HashMap, CowArcStr<'i>>, - options: &'a mut ParserOptions<'o, 'i, T>, +pub struct NestedRuleParser<'a, 'o, 'i, T> { + pub options: &'a ParserOptions<'o, 'i>, + pub at_rule_parser: &'a mut T, } -impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> NestedRuleParser<'a, 'o, 'i, T> { - fn parse_nested_rules<'t>( +impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> NestedRuleParser<'a, 'o, 'i, T> { + pub fn parse_nested_rules<'t>( &mut self, input: &mut Parser<'i, 't>, ) -> Result, ParseError<'i, ParserError<'i>>> { let nested_parser = NestedRuleParser { - default_namespace: self.default_namespace, - namespace_prefixes: self.namespace_prefixes, options: self.options, + at_rule_parser: self.at_rule_parser, }; let mut iter = RuleListParser::new_for_nested_rule(input, nested_parser); @@ -434,7 +401,7 @@ impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> NestedRuleParser<'a, 'o, 'i, T> { } } -impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for NestedRuleParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for NestedRuleParser<'a, 'o, 'i, T> { type Prelude = AtRulePrelude<'i, T::Prelude>; type AtRule = CssRule<'i, T::AtRule>; type Error = ParserError<'i>; @@ -533,18 +500,7 @@ impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for NestedRuleParser< let condition = MediaCondition::parse(input, true)?; Ok(AtRulePrelude::Container(name, condition)) }, - _ => { - if let Some(at_rule_parser) = &mut self.options.at_rule_parser { - if let Ok(prelude) = at_rule_parser.parse_prelude(name.clone(), input) { - return Ok(AtRulePrelude::Custom(prelude)) - } - } - - self.options.warn(input.new_error(BasicParseErrorKind::AtRuleInvalid(name.clone()))); - input.skip_whitespace(); - let tokens = TokenList::parse(input, &self.options, 0)?; - Ok(AtRulePrelude::Unknown(name.into(), tokens)) - } + _ => parse_custom_at_rule_prelude(&name, input, self.options, self.at_rule_parser) } } @@ -665,14 +621,7 @@ impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for NestedRuleParser< loc, })), AtRulePrelude::Custom(prelude) => { - if let Some(at_rule_parser) = &mut self.options.at_rule_parser { - at_rule_parser - .parse_block(prelude, start, input) - .map(|prelude| CssRule::Custom(prelude)) - .map_err(|_| input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)) - } else { - Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)) - } + parse_custom_at_rule_body(prelude, input, start, self.options, self.at_rule_parser) } } } @@ -699,20 +648,16 @@ impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for NestedRuleParser< loc, })), AtRulePrelude::Custom(prelude) => { - if let Some(at_rule_parser) = &mut self.options.at_rule_parser { - at_rule_parser - .rule_without_block(prelude, start) - .map(|prelude| CssRule::Custom(prelude)) - } else { - Err(()) - } + parse_custom_at_rule_without_block(prelude, start, self.options, self.at_rule_parser) } _ => Err(()), } } } -impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> QualifiedRuleParser<'i> for NestedRuleParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> QualifiedRuleParser<'i> + for NestedRuleParser<'a, 'o, 'i, T> +{ type Prelude = SelectorList<'i>; type QualifiedRule = CssRule<'i, T::AtRule>; type Error = ParserError<'i>; @@ -722,8 +667,6 @@ impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> QualifiedRuleParser<'i> for NestedRule input: &mut Parser<'i, 't>, ) -> Result> { let selector_parser = SelectorParser { - default_namespace: self.default_namespace, - namespace_prefixes: self.namespace_prefixes, is_nesting_allowed: false, options: &self.options, }; @@ -738,7 +681,7 @@ impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> QualifiedRuleParser<'i> for NestedRule ) -> Result, ParseError<'i, Self::Error>> { let loc = self.loc(start); let (declarations, rules) = if self.options.nesting { - parse_declarations_and_nested_rules(input, self.default_namespace, self.namespace_prefixes, self.options)? + parse_declarations_and_nested_rules(input, self.options, self.at_rule_parser)? } else { (DeclarationBlock::parse(input, self.options)?, CssRuleList(vec![])) }; @@ -752,22 +695,79 @@ impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> QualifiedRuleParser<'i> for NestedRule } } -fn parse_declarations_and_nested_rules<'a, 'o, 'i, 't, T: AtRuleParser<'i>>( +fn parse_custom_at_rule_prelude<'i, 't, T: crate::traits::AtRuleParser<'i>>( + name: &CowRcStr<'i>, input: &mut Parser<'i, 't>, - default_namespace: &'a Option>, - namespace_prefixes: &'a HashMap, CowArcStr<'i>>, - options: &'a mut ParserOptions<'o, 'i, T>, + options: &ParserOptions<'_, 'i>, + at_rule_parser: &mut T, +) -> Result, ParseError<'i, ParserError<'i>>> { + match at_rule_parser.parse_prelude(name.clone(), input, options) { + Ok(prelude) => return Ok(AtRulePrelude::Custom(prelude)), + Err(ParseError { + kind: ParseErrorKind::Basic(BasicParseErrorKind::AtRuleInvalid(..)), + .. + }) => {} + Err(err) => { + return Err(match &err.kind { + ParseErrorKind::Basic(kind) => ParseError { + kind: ParseErrorKind::Basic(kind.clone()), + location: err.location, + }, + _ => input.new_custom_error(ParserError::AtRulePreludeInvalid), + }) + } + } + + options.warn(input.new_error(BasicParseErrorKind::AtRuleInvalid(name.clone()))); + input.skip_whitespace(); + let tokens = TokenList::parse(input, &options, 0)?; + Ok(AtRulePrelude::Unknown(name.into(), tokens)) +} + +fn parse_custom_at_rule_body<'i, 't, T: crate::traits::AtRuleParser<'i>>( + prelude: T::Prelude, + input: &mut Parser<'i, 't>, + start: &ParserState, + options: &ParserOptions<'_, 'i>, + at_rule_parser: &mut T, +) -> Result, ParseError<'i, ParserError<'i>>> { + at_rule_parser + .parse_block(prelude, start, input, options) + .map(|prelude| CssRule::Custom(prelude)) + .map_err(|err| match &err.kind { + ParseErrorKind::Basic(kind) => ParseError { + kind: ParseErrorKind::Basic(kind.clone()), + location: err.location, + }, + _ => input.new_error(BasicParseErrorKind::AtRuleBodyInvalid), + }) +} + +fn parse_custom_at_rule_without_block<'i, 't, T: crate::traits::AtRuleParser<'i>>( + prelude: T::Prelude, + start: &ParserState, + options: &ParserOptions<'_, 'i>, + at_rule_parser: &mut T, +) -> Result, ()> { + at_rule_parser + .rule_without_block(prelude, start, options) + .map(|prelude| CssRule::Custom(prelude)) +} + +fn parse_declarations_and_nested_rules<'a, 'o, 'i, 't, T: crate::traits::AtRuleParser<'i>>( + input: &mut Parser<'i, 't>, + options: &'a ParserOptions<'o, 'i>, + at_rule_parser: &mut T, ) -> Result<(DeclarationBlock<'i>, CssRuleList<'i, T::AtRule>), ParseError<'i, ParserError<'i>>> { let mut important_declarations = DeclarationList::new(); let mut declarations = DeclarationList::new(); let mut rules = CssRuleList(vec![]); let mut parser = StyleRuleParser { - default_namespace, - namespace_prefixes, options, declarations: &mut declarations, important_declarations: &mut important_declarations, rules: &mut rules, + at_rule_parser, }; // In the v2 nesting spec, declarations and nested rules may be mixed. @@ -812,17 +812,18 @@ fn parse_declarations_and_nested_rules<'a, 'o, 'i, 't, T: AtRuleParser<'i>>( )) } -pub struct StyleRuleParser<'a, 'o, 'i, T: AtRuleParser<'i>> { - default_namespace: &'a Option>, - namespace_prefixes: &'a HashMap, CowArcStr<'i>>, - options: &'a mut ParserOptions<'o, 'i, T>, +pub struct StyleRuleParser<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> { + options: &'a ParserOptions<'o, 'i>, declarations: &'a mut DeclarationList<'i>, important_declarations: &'a mut DeclarationList<'i>, rules: &'a mut CssRuleList<'i, T::AtRule>, + at_rule_parser: &'a mut T, } /// Parse a declaration within {} block: `color: blue` -impl<'a, 'o, 'i, T: AtRuleParser<'i>> cssparser::DeclarationParser<'i> for StyleRuleParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> cssparser::DeclarationParser<'i> + for StyleRuleParser<'a, 'o, 'i, T> +{ type Declaration = (); type Error = ParserError<'i>; @@ -841,7 +842,7 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> cssparser::DeclarationParser<'i> for Style } } -impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for StyleRuleParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'i, T: crate::traits::AtRuleParser<'i>> AtRuleParser<'i> for StyleRuleParser<'a, 'o, 'i, T> { type Prelude = AtRulePrelude<'i, T::Prelude>; type AtRule = (); type Error = ParserError<'i>; @@ -873,26 +874,13 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for StyleRuleParser<'a, ' "nest" => { self.options.warn(input.new_custom_error(ParserError::DeprecatedNestRule)); let selector_parser = SelectorParser { - default_namespace: self.default_namespace, - namespace_prefixes: self.namespace_prefixes, is_nesting_allowed: true, options: &self.options, }; let selectors = SelectorList::parse(&selector_parser, input, NestingRequirement::Contained)?; Ok(AtRulePrelude::Nest(selectors)) }, - _ => { - if let Some(at_rule_parser) = &mut self.options.at_rule_parser { - if let Ok(prelude) = at_rule_parser.parse_prelude(name.clone(), input) { - return Ok(AtRulePrelude::Custom(prelude)) - } - } - - self.options.warn(input.new_error(BasicParseErrorKind::AtRuleInvalid(name.clone()))); - input.skip_whitespace(); - let tokens = TokenList::parse(input, &self.options, 0)?; - Ok(AtRulePrelude::Unknown(name.into(), tokens)) - } + _ => parse_custom_at_rule_prelude(&name, input, self.options, self.at_rule_parser) } } @@ -912,13 +900,7 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for StyleRuleParser<'a, ' AtRulePrelude::Media(query) => { self.rules.0.push(CssRule::Media(MediaRule { query, - rules: parse_nested_at_rule( - input, - self.options.source_index, - self.default_namespace, - self.namespace_prefixes, - self.options, - )?, + rules: parse_nested_at_rule(input, self.options, self.at_rule_parser)?, loc, })); Ok(()) @@ -926,13 +908,7 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for StyleRuleParser<'a, ' AtRulePrelude::Supports(condition) => { self.rules.0.push(CssRule::Supports(SupportsRule { condition, - rules: parse_nested_at_rule( - input, - self.options.source_index, - self.default_namespace, - self.namespace_prefixes, - self.options, - )?, + rules: parse_nested_at_rule(input, self.options, self.at_rule_parser)?, loc, })); Ok(()) @@ -941,13 +917,7 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for StyleRuleParser<'a, ' self.rules.0.push(CssRule::Container(ContainerRule { name, condition, - rules: parse_nested_at_rule( - input, - self.options.source_index, - self.default_namespace, - self.namespace_prefixes, - self.options, - )?, + rules: parse_nested_at_rule(input, self.options, self.at_rule_parser)?, loc, })); Ok(()) @@ -955,24 +925,13 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for StyleRuleParser<'a, ' AtRulePrelude::LayerBlock(name) => { self.rules.0.push(CssRule::LayerBlock(LayerBlockRule { name, - rules: parse_nested_at_rule( - input, - self.options.source_index, - self.default_namespace, - self.namespace_prefixes, - self.options, - )?, + rules: parse_nested_at_rule(input, self.options, self.at_rule_parser)?, loc, })); Ok(()) } AtRulePrelude::Nest(selectors) => { - let (declarations, rules) = parse_declarations_and_nested_rules( - input, - self.default_namespace, - self.namespace_prefixes, - self.options, - )?; + let (declarations, rules) = parse_declarations_and_nested_rules(input, self.options, self.at_rule_parser)?; self.rules.0.push(CssRule::Nesting(NestingRule { style: StyleRule { selectors, @@ -995,15 +954,14 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for StyleRuleParser<'a, ' Ok(()) } AtRulePrelude::Custom(prelude) => { - if let Some(at_rule_parser) = &mut self.options.at_rule_parser { - let rule = at_rule_parser - .parse_block(prelude, start, input) - .map_err(|_| input.new_error(BasicParseErrorKind::AtRuleBodyInvalid))?; - self.rules.0.push(CssRule::Custom(rule)); - Ok(()) - } else { - Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)) - } + self.rules.0.push(parse_custom_at_rule_body( + prelude, + input, + start, + self.options, + self.at_rule_parser, + )?); + Ok(()) } _ => Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)), } @@ -1026,38 +984,34 @@ impl<'a, 'o, 'i, T: AtRuleParser<'i>> AtRuleParser<'i> for StyleRuleParser<'a, ' Ok(()) } AtRulePrelude::Custom(prelude) => { - if let Some(at_rule_parser) = &mut self.options.at_rule_parser { - let rule = at_rule_parser.rule_without_block(prelude, start)?; - self.rules.0.push(CssRule::Custom(rule)); - Ok(()) - } else { - Err(()) - } + self.rules.0.push(parse_custom_at_rule_without_block( + prelude, + start, + self.options, + self.at_rule_parser, + )?); + Ok(()) } _ => Err(()), } } } -#[inline] -fn parse_nested_at_rule<'a, 'o, 'i, 't, T: AtRuleParser<'i>>( +pub fn parse_nested_at_rule<'a, 'o, 'i, 't, T: crate::traits::AtRuleParser<'i>>( input: &mut Parser<'i, 't>, - source_index: u32, - default_namespace: &'a Option>, - namespace_prefixes: &'a HashMap, CowArcStr<'i>>, - options: &'a mut ParserOptions<'o, 'i, T>, + options: &'a ParserOptions<'o, 'i>, + at_rule_parser: &mut T, ) -> Result, ParseError<'i, ParserError<'i>>> { let loc = input.current_source_location(); let loc = Location { - source_index, + source_index: options.source_index, line: loc.line, column: loc.column, }; // Declarations can be immediately within @media and @supports blocks that are nested within a parent style rule. // These act the same way as if they were nested within a `& { ... }` block. - let (declarations, mut rules) = - parse_declarations_and_nested_rules(input, default_namespace, namespace_prefixes, options)?; + let (declarations, mut rules) = parse_declarations_and_nested_rules(input, options, at_rule_parser)?; if declarations.len() > 0 { rules.0.insert( @@ -1075,7 +1029,9 @@ fn parse_nested_at_rule<'a, 'o, 'i, 't, T: AtRuleParser<'i>>( Ok(rules) } -impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> QualifiedRuleParser<'i> for StyleRuleParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'b, 'i, T: crate::traits::AtRuleParser<'i>> QualifiedRuleParser<'i> + for StyleRuleParser<'a, 'o, 'i, T> +{ type Prelude = SelectorList<'i>; type QualifiedRule = (); type Error = ParserError<'i>; @@ -1085,8 +1041,6 @@ impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> QualifiedRuleParser<'i> for StyleRuleP input: &mut Parser<'i, 't>, ) -> Result> { let selector_parser = SelectorParser { - default_namespace: self.default_namespace, - namespace_prefixes: self.namespace_prefixes, is_nesting_allowed: true, options: &self.options, }; @@ -1100,8 +1054,7 @@ impl<'a, 'o, 'b, 'i, T: AtRuleParser<'i>> QualifiedRuleParser<'i> for StyleRuleP input: &mut Parser<'i, 't>, ) -> Result<(), ParseError<'i, Self::Error>> { let loc = start.source_location(); - let (declarations, rules) = - parse_declarations_and_nested_rules(input, self.default_namespace, self.namespace_prefixes, self.options)?; + let (declarations, rules) = parse_declarations_and_nested_rules(input, self.options, self.at_rule_parser)?; self.rules.0.push(CssRule::Style(StyleRule { selectors, vendor_prefix: VendorPrefix::empty(), diff --git a/src/printer.rs b/src/printer.rs index 424b585e..4c19b4de 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -3,7 +3,8 @@ use crate::css_modules::CssModule; use crate::dependencies::{Dependency, DependencyOptions}; use crate::error::{Error, ErrorLocation, PrinterError, PrinterErrorKind}; -use crate::rules::Location; +use crate::rules::{Location, StyleContext}; +use crate::selector::SelectorList; use crate::targets::Browsers; use crate::vendor_prefix::VendorPrefix; use cssparser::{serialize_identifier, serialize_name}; @@ -85,6 +86,7 @@ pub struct Printer<'a, 'b, 'c, W> { pub(crate) dependencies: Option>, pub(crate) remove_imports: bool, pub(crate) pseudo_classes: Option>, + context: Option<&'a StyleContext<'a, 'b>>, } impl<'a, 'b, 'c, W: std::fmt::Write + Sized> Printer<'a, 'b, 'c, W> { @@ -117,6 +119,7 @@ impl<'a, 'b, 'c, W: std::fmt::Write + Sized> Printer<'a, 'b, 'c, W> { }, remove_imports: matches!(&options.analyze_dependencies, Some(d) if d.remove_imports), pseudo_classes: options.pseudo_classes, + context: None, } } @@ -330,6 +333,29 @@ impl<'a, 'b, 'c, W: std::fmt::Write + Sized> Printer<'a, 'b, 'c, W> { }), } } + + pub(crate) fn with_context) -> Result>( + &mut self, + selectors: &SelectorList, + f: F, + ) -> Result { + let parent = std::mem::take(&mut self.context); + let ctx = StyleContext { + selectors: unsafe { std::mem::transmute(selectors) }, + parent, + }; + + // I can't figure out what lifetime to use here to convince the compiler that + // the reference doesn't live beyond the function call. + self.context = Some(unsafe { std::mem::transmute(&ctx) }); + let res = f(self); + self.context = parent; + res + } + + pub(crate) fn context(&self) -> Option<&'a StyleContext<'a, 'b>> { + self.context.clone() + } } impl<'a, 'b, 'c, W: std::fmt::Write + Sized> std::fmt::Write for Printer<'a, 'b, 'c, W> { diff --git a/src/properties/custom.rs b/src/properties/custom.rs index 3ce1a136..e394fa5b 100644 --- a/src/properties/custom.rs +++ b/src/properties/custom.rs @@ -46,10 +46,10 @@ pub struct CustomProperty<'i> { impl<'i> CustomProperty<'i> { /// Parses a custom property with the given name. - pub fn parse<'t, T>( + pub fn parse<'t>( name: CustomPropertyName<'i>, input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i>, ) -> Result>> { let value = input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| { TokenList::parse(input, options, 0) @@ -146,10 +146,10 @@ pub struct UnparsedProperty<'i> { impl<'i> UnparsedProperty<'i> { /// Parses a property with the given id as a token list. - pub fn parse<'t, T>( + pub fn parse<'t>( property_id: PropertyId<'i>, input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i>, ) -> Result>> { let value = input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| { TokenList::parse(input, options, 0) @@ -259,19 +259,19 @@ impl<'i> TokenOrValue<'i> { } } -impl<'i, T> ParseWithOptions<'i, T> for TokenList<'i> { +impl<'i> ParseWithOptions<'i> for TokenList<'i> { fn parse_with_options<'t>( input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i>, ) -> Result>> { TokenList::parse(input, options, 0) } } impl<'i> TokenList<'i> { - pub(crate) fn parse<'t, T>( + pub(crate) fn parse<'t>( input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i>, depth: usize, ) -> Result>> { let mut tokens = vec![]; @@ -293,10 +293,10 @@ impl<'i> TokenList<'i> { return Ok(TokenList(tokens)); } - fn parse_into<'t, T>( + fn parse_into<'t>( input: &mut Parser<'i, 't>, tokens: &mut Vec>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i>, depth: usize, ) -> Result<(), ParseError<'i, ParserError<'i>>> { if depth > 500 { @@ -1058,9 +1058,9 @@ pub struct Variable<'i> { } impl<'i> Variable<'i> { - fn parse<'t, T>( + fn parse<'t>( input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i>, depth: usize, ) -> Result>> { let name = DashedIdentReference::parse_with_options(input, options)?; @@ -1209,18 +1209,18 @@ impl<'i> ToCss for EnvironmentVariableName<'i> { } impl<'i> EnvironmentVariable<'i> { - pub(crate) fn parse<'t, T>( + pub(crate) fn parse<'t>( input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i>, depth: usize, ) -> Result>> { input.expect_function_matching("env")?; input.parse_nested_block(|input| Self::parse_nested(input, options, depth)) } - pub(crate) fn parse_nested<'t, T>( + pub(crate) fn parse_nested<'t>( input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i>, depth: usize, ) -> Result>> { let name = EnvironmentVariableName::parse(input)?; @@ -1345,10 +1345,10 @@ pub enum UnresolvedColor<'i> { } impl<'i> UnresolvedColor<'i> { - fn parse<'t, T>( + fn parse<'t>( f: &CowArcStr<'i>, input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i>, ) -> Result>> { let parser = ComponentParser::new(false); match_ignore_ascii_case! { &*f, diff --git a/src/properties/mod.rs b/src/properties/mod.rs index adc3182d..a50949c2 100644 --- a/src/properties/mod.rs +++ b/src/properties/mod.rs @@ -676,7 +676,7 @@ macro_rules! define_properties { impl<'i> Property<'i> { /// Parses a CSS property by name. - pub fn parse<'t, T>(property_id: PropertyId<'i>, input: &mut Parser<'i, 't>, options: &ParserOptions) -> Result, ParseError<'i, ParserError<'i>>> { + pub fn parse<'t>(property_id: PropertyId<'i>, input: &mut Parser<'i, 't>, options: &ParserOptions<'_, 'i>) -> Result, ParseError<'i, ParserError<'i>>> { let state = input.state(); match property_id { @@ -717,7 +717,7 @@ macro_rules! define_properties { } /// Parses a CSS property from a string. - pub fn parse_string(property_id: PropertyId<'i>, input: &'i str, options: ParserOptions) -> Result>> { + pub fn parse_string(property_id: PropertyId<'i>, input: &'i str, options: ParserOptions<'_, 'i>) -> Result>> { let mut input = ParserInput::new(input); let mut parser = Parser::new(&mut input); Self::parse(property_id, &mut parser, &options) diff --git a/src/rules/container.rs b/src/rules/container.rs index 2aaa7bef..1851dc47 100644 --- a/src/rules/container.rs +++ b/src/rules/container.rs @@ -8,7 +8,6 @@ use crate::error::{MinifyError, ParserError, PrinterError}; use crate::media_query::MediaCondition; use crate::parser::DefaultAtRule; use crate::printer::Printer; -use crate::rules::{StyleContext, ToCssWithContext}; use crate::traits::{Parse, ToCss}; use crate::values::ident::CustomIdent; #[cfg(feature = "visitor")] @@ -70,12 +69,8 @@ impl<'i, T> ContainerRule<'i, T> { } } -impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for ContainerRule<'i, T> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> +impl<'a, 'i, T: ToCss> ToCss for ContainerRule<'i, T> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { @@ -97,7 +92,7 @@ impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for ContainerRule<'i, T> { dest.write_char('{')?; dest.indent(); dest.newline()?; - self.rules.to_css_with_context(dest, context)?; + self.rules.to_css(dest)?; dest.dedent(); dest.newline()?; dest.write_char('}') diff --git a/src/rules/layer.rs b/src/rules/layer.rs index 261d4a65..7bebd154 100644 --- a/src/rules/layer.rs +++ b/src/rules/layer.rs @@ -1,6 +1,6 @@ //! The `@layer` rule. -use super::{CssRuleList, Location, MinifyContext, StyleContext, ToCssWithContext}; +use super::{CssRuleList, Location, MinifyContext}; use crate::error::{MinifyError, ParserError, PrinterError}; use crate::parser::DefaultAtRule; use crate::printer::Printer; @@ -140,12 +140,8 @@ impl<'i, T> LayerBlockRule<'i, T> { } } -impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for LayerBlockRule<'i, T> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> +impl<'a, 'i, T: ToCss> ToCss for LayerBlockRule<'i, T> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { @@ -161,7 +157,7 @@ impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for LayerBlockRule<'i, T> { dest.write_char('{')?; dest.indent(); dest.newline()?; - self.rules.to_css_with_context(dest, context)?; + self.rules.to_css(dest)?; dest.dedent(); dest.newline()?; dest.write_char('}') diff --git a/src/rules/media.rs b/src/rules/media.rs index 8857bade..f3898c9a 100644 --- a/src/rules/media.rs +++ b/src/rules/media.rs @@ -6,7 +6,6 @@ use crate::error::{MinifyError, PrinterError}; use crate::media_query::MediaList; use crate::parser::DefaultAtRule; use crate::printer::Printer; -use crate::rules::{StyleContext, ToCssWithContext}; use crate::traits::ToCss; #[cfg(feature = "visitor")] use crate::visitor::Visit; @@ -43,18 +42,14 @@ impl<'i, T> MediaRule<'i, T> { } } -impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for MediaRule<'i, T> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> +impl<'a, 'i, T: ToCss> ToCss for MediaRule<'i, T> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { // If the media query always matches, we can just output the nested rules. if dest.minify && self.query.always_matches() { - self.rules.to_css_with_context(dest, context)?; + self.rules.to_css(dest)?; return Ok(()); } @@ -66,7 +61,7 @@ impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for MediaRule<'i, T> { dest.write_char('{')?; dest.indent(); dest.newline()?; - self.rules.to_css_with_context(dest, context)?; + self.rules.to_css(dest)?; dest.dedent(); dest.newline()?; dest.write_char('}') diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 9388949b..ecea16f0 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -62,21 +62,23 @@ use crate::context::PropertyHandlerContext; use crate::declaration::DeclarationHandler; use crate::dependencies::{Dependency, ImportDependency}; use crate::error::{MinifyError, ParserError, PrinterError, PrinterErrorKind}; -use crate::parser::{DefaultAtRule, TopLevelRuleParser}; +use crate::parser::{ + parse_nested_at_rule, DefaultAtRule, DefaultAtRuleParser, NestedRuleParser, TopLevelRuleParser, +}; use crate::prefixes::Feature; use crate::printer::Printer; use crate::rules::keyframes::KeyframesName; -use crate::selector::{downlevel_selectors, get_prefix, is_equivalent}; +use crate::selector::{downlevel_selectors, get_prefix, is_equivalent, SelectorList}; use crate::stylesheet::ParserOptions; use crate::targets::Browsers; -use crate::traits::ToCss; +use crate::traits::{AtRuleParser, ToCss}; use crate::values::string::CowArcStr; use crate::vendor_prefix::VendorPrefix; #[cfg(feature = "visitor")] use crate::visitor::{Visit, VisitTypes, Visitor}; use container::ContainerRule; use counter_style::CounterStyleRule; -use cssparser::{parse_one_rule, AtRuleParser, ParseError, Parser, ParserInput}; +use cssparser::{parse_one_rule, ParseError, Parser, ParserInput}; use custom_media::CustomMediaRule; use document::MozDocumentRule; use font_face::FontFaceRule; @@ -92,19 +94,10 @@ use supports::SupportsRule; use unknown::UnknownAtRule; use viewport::ViewportRule; -pub(crate) trait ToCssWithContext<'a, 'i, T> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> - where - W: std::fmt::Write; -} - -pub(crate) struct StyleContext<'a, 'i, T> { - pub rule: &'a StyleRule<'i, T>, - pub parent: Option<&'a StyleContext<'a, 'i, T>>, +#[derive(Clone)] +pub(crate) struct StyleContext<'a, 'i> { + pub selectors: &'a SelectorList<'i>, + pub parent: Option<&'a StyleContext<'a, 'i>>, } /// A source location. @@ -320,34 +313,30 @@ impl<'i, 'de: 'i, R: serde::Deserialize<'de>> serde::Deserialize<'de> for CssRul } } -impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for CssRule<'i, T> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> +impl<'a, 'i, T: ToCss> ToCss for CssRule<'i, T> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { match self { - CssRule::Media(media) => media.to_css_with_context(dest, context), + CssRule::Media(media) => media.to_css(dest), CssRule::Import(import) => import.to_css(dest), - CssRule::Style(style) => style.to_css_with_context(dest, context), + CssRule::Style(style) => style.to_css(dest), CssRule::Keyframes(keyframes) => keyframes.to_css(dest), CssRule::FontFace(font_face) => font_face.to_css(dest), CssRule::FontPaletteValues(f) => f.to_css(dest), CssRule::Page(font_face) => font_face.to_css(dest), - CssRule::Supports(supports) => supports.to_css_with_context(dest, context), + CssRule::Supports(supports) => supports.to_css(dest), CssRule::CounterStyle(counter_style) => counter_style.to_css(dest), CssRule::Namespace(namespace) => namespace.to_css(dest), CssRule::MozDocument(document) => document.to_css(dest), - CssRule::Nesting(nesting) => nesting.to_css_with_context(dest, context), + CssRule::Nesting(nesting) => nesting.to_css(dest), CssRule::Viewport(viewport) => viewport.to_css(dest), CssRule::CustomMedia(custom_media) => custom_media.to_css(dest), CssRule::LayerStatement(layer) => layer.to_css(dest), - CssRule::LayerBlock(layer) => layer.to_css_with_context(dest, context), + CssRule::LayerBlock(layer) => layer.to_css(dest), CssRule::Property(property) => property.to_css(dest), - CssRule::Container(container) => container.to_css_with_context(dest, context), + CssRule::Container(container) => container.to_css(dest), CssRule::Unknown(unknown) => unknown.to_css(dest), CssRule::Custom(rule) => rule.to_css(dest).map_err(|_| PrinterError { kind: PrinterErrorKind::FmtError, @@ -358,33 +347,44 @@ impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for CssRule<'i, T> { } } +impl<'i> CssRule<'i, DefaultAtRule> { + /// Parse a single rule. + pub fn parse<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result>> { + Self::parse_with(input, options, &mut DefaultAtRuleParser) + } + + /// Parse a single rule from a string. + pub fn parse_string( + input: &'i str, + options: ParserOptions<'_, 'i>, + ) -> Result>> { + Self::parse_string_with(input, options, &mut DefaultAtRuleParser) + } +} + impl<'i, T> CssRule<'i, T> { /// Parse a single rule. - pub fn parse<'t, P: AtRuleParser<'i, AtRule = T>>( + pub fn parse_with<'t, P: AtRuleParser<'i, AtRule = T>>( input: &mut Parser<'i, 't>, - options: &mut ParserOptions<'_, 'i, P>, + options: &ParserOptions<'_, 'i>, + at_rule_parser: &mut P, ) -> Result>> { - let (_, rule) = parse_one_rule(input, &mut TopLevelRuleParser::new(options))?; + let (_, rule) = parse_one_rule(input, &mut TopLevelRuleParser::new(options, at_rule_parser))?; Ok(rule) } /// Parse a single rule from a string. - pub fn parse_string>( + pub fn parse_string_with>( input: &'i str, - mut options: ParserOptions<'_, 'i, P>, + options: ParserOptions<'_, 'i>, + at_rule_parser: &mut P, ) -> Result>> { let mut input = ParserInput::new(input); let mut parser = Parser::new(&mut input); - Self::parse(&mut parser, &mut options) - } -} - -impl<'i, T: ToCss> ToCss for CssRule<'i, T> { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - self.to_css_with_context(dest, None) + Self::parse_with(&mut parser, &options, at_rule_parser) } } @@ -396,6 +396,50 @@ pub struct CssRuleList<'i, R = DefaultAtRule>( #[cfg_attr(feature = "serde", serde(borrow))] pub Vec>, ); +impl<'i> CssRuleList<'i, DefaultAtRule> { + /// Parse a rule list. + pub fn parse<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result>> { + Self::parse_with(input, options, &mut DefaultAtRuleParser) + } + + /// Parse a style block, with both declarations and rules. + /// Resulting declarations are returned in a nested style rule. + pub fn parse_style_block<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result>> { + Self::parse_style_block_with(input, options, &mut DefaultAtRuleParser) + } +} + +impl<'i, T> CssRuleList<'i, T> { + /// Parse a rule list with a custom at rule parser. + pub fn parse_with<'t, P: AtRuleParser<'i, AtRule = T>>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + at_rule_parser: &mut P, + ) -> Result>> { + let mut nested_parser = NestedRuleParser { + options, + at_rule_parser, + }; + nested_parser.parse_nested_rules(input) + } + + /// Parse a style block, with both declarations and rules. + /// Resulting declarations are returned in a nested style rule. + pub fn parse_style_block_with<'t, P: AtRuleParser<'i, AtRule = T>>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + at_rule_parser: &mut P, + ) -> Result>> { + parse_nested_at_rule(input, options, at_rule_parser) + } +} + // Manually implemented to avoid circular child types. #[cfg(feature = "visitor")] #[cfg_attr(docsrs, doc(cfg(feature = "visitor")))] @@ -687,21 +731,8 @@ fn merge_style_rules<'i, T>( false } -impl<'i, T: ToCss> ToCss for CssRuleList<'i, T> { +impl<'a, 'i, T: ToCss> ToCss for CssRuleList<'i, T> { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: std::fmt::Write, - { - self.to_css_with_context(dest, None) - } -} - -impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for CssRuleList<'i, T> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> where W: std::fmt::Write, { @@ -743,7 +774,7 @@ impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for CssRuleList<'i, T> { } dest.newline()?; } - rule.to_css_with_context(dest, context)?; + rule.to_css(dest)?; last_without_block = matches!( rule, CssRule::Import(..) | CssRule::Namespace(..) | CssRule::LayerStatement(..) diff --git a/src/rules/nesting.rs b/src/rules/nesting.rs index b834a27f..d2696202 100644 --- a/src/rules/nesting.rs +++ b/src/rules/nesting.rs @@ -6,7 +6,6 @@ use super::MinifyContext; use crate::error::{MinifyError, PrinterError}; use crate::parser::DefaultAtRule; use crate::printer::Printer; -use crate::rules::{StyleContext, ToCssWithContext}; use crate::traits::ToCss; #[cfg(feature = "visitor")] use crate::visitor::Visit; @@ -34,20 +33,16 @@ impl<'i, T> NestingRule<'i, T> { } } -impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for NestingRule<'i, T> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> +impl<'a, 'i, T: ToCss> ToCss for NestingRule<'i, T> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { #[cfg(feature = "sourcemap")] dest.add_mapping(self.loc); - if context.is_none() { + if dest.context().is_none() { dest.write_str("@nest ")?; } - self.style.to_css_with_context(dest, context) + self.style.to_css(dest) } } diff --git a/src/rules/page.rs b/src/rules/page.rs index b418cf19..5f808649 100644 --- a/src/rules/page.rs +++ b/src/rules/page.rs @@ -174,11 +174,11 @@ pub struct PageRule<'i> { } impl<'i> PageRule<'i> { - pub(crate) fn parse<'t, 'o, T>( + pub(crate) fn parse<'t, 'o>( selectors: Vec>, input: &mut Parser<'i, 't>, loc: Location, - options: &ParserOptions<'o, 'i, T>, + options: &ParserOptions<'o, 'i>, ) -> Result>> { let mut declarations = DeclarationBlock::new(); let mut rules = Vec::new(); @@ -301,13 +301,13 @@ impl<'i> ToCss for PageSelector<'i> { } } -struct PageRuleParser<'a, 'o, 'i, T> { +struct PageRuleParser<'a, 'o, 'i> { declarations: &'a mut DeclarationBlock<'i>, rules: &'a mut Vec>, - options: &'a ParserOptions<'o, 'i, T>, + options: &'a ParserOptions<'o, 'i>, } -impl<'a, 'o, 'i, T> cssparser::DeclarationParser<'i> for PageRuleParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'i> cssparser::DeclarationParser<'i> for PageRuleParser<'a, 'o, 'i> { type Declaration = (); type Error = ParserError<'i>; @@ -326,7 +326,7 @@ impl<'a, 'o, 'i, T> cssparser::DeclarationParser<'i> for PageRuleParser<'a, 'o, } } -impl<'a, 'o, 'i, T> AtRuleParser<'i> for PageRuleParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'i> AtRuleParser<'i> for PageRuleParser<'a, 'o, 'i> { type Prelude = PageMarginBox; type AtRule = (); type Error = ParserError<'i>; diff --git a/src/rules/style.rs b/src/rules/style.rs index d9c1a114..1a75f0ec 100644 --- a/src/rules/style.rs +++ b/src/rules/style.rs @@ -11,7 +11,7 @@ use crate::error::ParserError; use crate::error::{MinifyError, PrinterError, PrinterErrorKind}; use crate::parser::DefaultAtRule; use crate::printer::Printer; -use crate::rules::{CssRuleList, StyleContext, ToCssWithContext}; +use crate::rules::CssRuleList; use crate::selector::{is_compatible, is_unused, SelectorList}; use crate::targets::Browsers; use crate::traits::ToCss; @@ -34,6 +34,7 @@ pub struct StyleRule<'i, R = DefaultAtRule> { #[cfg_attr(feature = "visitor", skip_visit)] pub vendor_prefix: VendorPrefix, /// The declarations within the style rule. + #[cfg_attr(feature = "serde", serde(default))] pub declarations: DeclarationBlock<'i>, /// Nested rules within the style rule. #[cfg_attr(feature = "serde", serde(default = "default_rule_list::"))] @@ -156,17 +157,13 @@ where } } -impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for StyleRule<'i, T> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> +impl<'a, 'i, T: ToCss> ToCss for StyleRule<'i, T> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { if self.vendor_prefix.is_empty() { - self.to_css_base(dest, context) + self.to_css_base(dest) } else { let mut first_rule = true; macro_rules! prefix { @@ -182,7 +179,7 @@ impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for StyleRule<'i, T> { dest.newline()?; } dest.vendor_prefix = VendorPrefix::$prefix; - self.to_css_base(dest, context)?; + self.to_css_base(dest)?; } }; } @@ -200,11 +197,7 @@ impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for StyleRule<'i, T> { } impl<'a, 'i, T: ToCss> StyleRule<'i, T> { - fn to_css_base( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> + fn to_css_base(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { @@ -218,7 +211,7 @@ impl<'a, 'i, T: ToCss> StyleRule<'i, T> { if has_declarations { #[cfg(feature = "sourcemap")] dest.add_mapping(self.loc); - self.selectors.to_css_with_context(dest, context)?; + self.selectors.to_css(dest)?; dest.whitespace()?; dest.write_char('{')?; dest.indent(); @@ -286,13 +279,7 @@ impl<'a, 'i, T: ToCss> StyleRule<'i, T> { } else { end!(); newline!(); - self.rules.to_css_with_context( - dest, - Some(&StyleContext { - rule: self, - parent: context, - }), - )?; + dest.with_context(&self.selectors, |dest| self.rules.to_css(dest))?; } Ok(()) diff --git a/src/rules/supports.rs b/src/rules/supports.rs index 2481f584..365ba11b 100644 --- a/src/rules/supports.rs +++ b/src/rules/supports.rs @@ -6,7 +6,6 @@ use crate::error::{MinifyError, ParserError, PrinterError}; use crate::parser::DefaultAtRule; use crate::printer::Printer; use crate::properties::PropertyId; -use crate::rules::{StyleContext, ToCssWithContext}; use crate::targets::Browsers; use crate::traits::{Parse, ToCss}; use crate::values::string::CowArcStr; @@ -48,12 +47,8 @@ impl<'i, T> SupportsRule<'i, T> { } } -impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for SupportsRule<'i, T> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> +impl<'a, 'i, T: ToCss> ToCss for SupportsRule<'i, T> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { @@ -65,7 +60,7 @@ impl<'a, 'i, T: ToCss> ToCssWithContext<'a, 'i, T> for SupportsRule<'i, T> { dest.write_char('{')?; dest.indent(); dest.newline()?; - self.rules.to_css_with_context(dest, context)?; + self.rules.to_css(dest)?; dest.dedent(); dest.newline()?; dest.write_char('}') diff --git a/src/selector.rs b/src/selector.rs index 703b5fba..efe6cf12 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -2,13 +2,12 @@ use crate::compat::Feature; use crate::error::{ParserError, PrinterError}; -use crate::parser::DefaultAtRule; use crate::printer::Printer; use crate::properties::custom::TokenList; -use crate::rules::{StyleContext, ToCssWithContext}; +use crate::rules::StyleContext; use crate::stylesheet::{ParserOptions, PrinterOptions}; use crate::targets::Browsers; -use crate::traits::{Parse, ToCss}; +use crate::traits::{Parse, ParseWithOptions, ToCss}; use crate::values::ident::Ident; use crate::values::string::CSSString; use crate::vendor_prefix::VendorPrefix; @@ -21,7 +20,6 @@ use parcel_selectors::{ attr::{AttrSelectorOperator, ParsedAttrSelectorOperation, ParsedCaseSensitivity}, parser::SelectorImpl, }; -use std::collections::HashMap; use std::collections::HashSet; use std::fmt; @@ -62,19 +60,16 @@ impl<'i> SelectorImpl<'i> for Selectors { fn to_css(selectors: &SelectorList<'i>, dest: &mut W) -> std::fmt::Result { let mut printer = Printer::new(dest, PrinterOptions::default()); - serialize_selector_list::<_, _, DefaultAtRule>(selectors.0.iter(), &mut printer, None, false) - .map_err(|_| std::fmt::Error) + serialize_selector_list(selectors.0.iter(), &mut printer, None, false).map_err(|_| std::fmt::Error) } } -pub(crate) struct SelectorParser<'a, 'o, 'i, T> { - pub default_namespace: &'a Option>, - pub namespace_prefixes: &'a HashMap, CowArcStr<'i>>, +pub(crate) struct SelectorParser<'a, 'o, 'i> { pub is_nesting_allowed: bool, - pub options: &'a ParserOptions<'o, 'i, T>, + pub options: &'a ParserOptions<'o, 'i>, } -impl<'a, 'o, 'i, T> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o, 'i, T> { +impl<'a, 'o, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o, 'i> { type Impl = Selectors; type Error = ParserError<'i>; @@ -303,11 +298,11 @@ impl<'a, 'o, 'i, T> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, } fn default_namespace(&self) -> Option> { - self.default_namespace.clone() + None } fn namespace_for_prefix(&self, prefix: &Ident<'i>) -> Option> { - self.namespace_prefixes.get(&prefix.0).cloned() + Some(prefix.0.clone()) } #[inline] @@ -560,178 +555,176 @@ impl<'i> cssparser::ToCss for PseudoClass<'i> { } } -impl<'a, 'i, T> ToCssWithContext<'a, 'i, T> for PseudoClass<'i> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> - where - W: fmt::Write, - { - use PseudoClass::*; - match &self { - Lang { languages: lang } => { - dest.write_str(":lang(")?; - let mut first = true; - for lang in lang { - if first { - first = false; - } else { - dest.delim(',', false)?; - } - serialize_identifier(lang, dest)?; +fn serialize_pseudo_class<'a, 'i, W>( + pseudo_class: &PseudoClass<'i>, + dest: &mut Printer, + context: Option<&StyleContext>, +) -> Result<(), PrinterError> +where + W: fmt::Write, +{ + use PseudoClass::*; + match pseudo_class { + Lang { languages: lang } => { + dest.write_str(":lang(")?; + let mut first = true; + for lang in lang { + if first { + first = false; + } else { + dest.delim(',', false)?; } - return dest.write_str(")"); + serialize_identifier(lang, dest)?; } - Dir { direction: dir } => { - dest.write_str(":dir(")?; - dir.to_css(dest)?; - return dest.write_str(")"); - } - _ => {} + return dest.write_str(")"); } - - macro_rules! write_prefixed { - ($prefix: ident, $val: expr) => {{ - dest.write_char(':')?; - // If the printer has a vendor prefix override, use that. - let vp = if !dest.vendor_prefix.is_empty() { - dest.vendor_prefix - } else { - *$prefix - }; - vp.to_css(dest)?; - dest.write_str($val) - }}; + Dir { direction: dir } => { + dest.write_str(":dir(")?; + dir.to_css(dest)?; + return dest.write_str(")"); } + _ => {} + } - macro_rules! pseudo { - ($key: ident, $s: literal) => {{ - let class = if let Some(pseudo_classes) = &dest.pseudo_classes { - pseudo_classes.$key - } else { - None - }; - - if let Some(class) = class { - dest.write_char('.')?; - dest.write_ident(class) - } else { - dest.write_str($s) - } - }}; - } - - match &self { - // https://drafts.csswg.org/selectors-4/#useraction-pseudos - Hover => pseudo!(hover, ":hover"), - Active => pseudo!(active, ":active"), - Focus => pseudo!(focus, ":focus"), - FocusVisible => pseudo!(focus_visible, ":focus-visible"), - FocusWithin => pseudo!(focus_within, ":focus-within"), - - // https://drafts.csswg.org/selectors-4/#time-pseudos - Current => dest.write_str(":current"), - Past => dest.write_str(":past"), - Future => dest.write_str(":future"), + macro_rules! write_prefixed { + ($prefix: ident, $val: expr) => {{ + dest.write_char(':')?; + // If the printer has a vendor prefix override, use that. + let vp = if !dest.vendor_prefix.is_empty() { + dest.vendor_prefix + } else { + *$prefix + }; + vp.to_css(dest)?; + dest.write_str($val) + }}; + } - // https://drafts.csswg.org/selectors-4/#resource-pseudos - Playing => dest.write_str(":playing"), - Paused => dest.write_str(":paused"), - Seeking => dest.write_str(":seeking"), - Buffering => dest.write_str(":buffering"), - Stalled => dest.write_str(":stalled"), - Muted => dest.write_str(":muted"), - VolumeLocked => dest.write_str(":volume-locked"), + macro_rules! pseudo { + ($key: ident, $s: literal) => {{ + let class = if let Some(pseudo_classes) = &dest.pseudo_classes { + pseudo_classes.$key + } else { + None + }; - // https://fullscreen.spec.whatwg.org/#:fullscreen-pseudo-class - Fullscreen(prefix) => { - dest.write_char(':')?; - let vp = if !dest.vendor_prefix.is_empty() { - dest.vendor_prefix - } else { - *prefix - }; - vp.to_css(dest)?; - if vp == VendorPrefix::WebKit || vp == VendorPrefix::Moz { - dest.write_str("full-screen") - } else { - dest.write_str("fullscreen") - } + if let Some(class) = class { + dest.write_char('.')?; + dest.write_ident(class) + } else { + dest.write_str($s) } + }}; + } - // https://drafts.csswg.org/selectors-4/#the-defined-pseudo - Defined => dest.write_str(":defined"), - - // https://drafts.csswg.org/selectors-4/#location - AnyLink(prefix) => write_prefixed!(prefix, "any-link"), - Link => dest.write_str(":link"), - LocalLink => dest.write_str(":local-link"), - Target => dest.write_str(":target"), - TargetWithin => dest.write_str(":target-within"), - Visited => dest.write_str(":visited"), - - // https://drafts.csswg.org/selectors-4/#input-pseudos - Enabled => dest.write_str(":enabled"), - Disabled => dest.write_str(":disabled"), - ReadOnly(prefix) => write_prefixed!(prefix, "read-only"), - ReadWrite(prefix) => write_prefixed!(prefix, "read-write"), - PlaceholderShown(prefix) => write_prefixed!(prefix, "placeholder-shown"), - Default => dest.write_str(":default"), - Checked => dest.write_str(":checked"), - Indeterminate => dest.write_str(":indeterminate"), - Blank => dest.write_str(":blank"), - Valid => dest.write_str(":valid"), - Invalid => dest.write_str(":invalid"), - InRange => dest.write_str(":in-range"), - OutOfRange => dest.write_str(":out-of-range"), - Required => dest.write_str(":required"), - Optional => dest.write_str(":optional"), - UserValid => dest.write_str(":user-valid"), - UserInvalid => dest.write_str(":user-invalid"), - - // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill - Autofill(prefix) => write_prefixed!(prefix, "autofill"), - - Local { selector } => selector.to_css_with_context(dest, context), - Global { selector } => { - let css_module = std::mem::take(&mut dest.css_module); - selector.to_css_with_context(dest, context)?; - dest.css_module = css_module; - Ok(()) + match pseudo_class { + // https://drafts.csswg.org/selectors-4/#useraction-pseudos + Hover => pseudo!(hover, ":hover"), + Active => pseudo!(active, ":active"), + Focus => pseudo!(focus, ":focus"), + FocusVisible => pseudo!(focus_visible, ":focus-visible"), + FocusWithin => pseudo!(focus_within, ":focus-within"), + + // https://drafts.csswg.org/selectors-4/#time-pseudos + Current => dest.write_str(":current"), + Past => dest.write_str(":past"), + Future => dest.write_str(":future"), + + // https://drafts.csswg.org/selectors-4/#resource-pseudos + Playing => dest.write_str(":playing"), + Paused => dest.write_str(":paused"), + Seeking => dest.write_str(":seeking"), + Buffering => dest.write_str(":buffering"), + Stalled => dest.write_str(":stalled"), + Muted => dest.write_str(":muted"), + VolumeLocked => dest.write_str(":volume-locked"), + + // https://fullscreen.spec.whatwg.org/#:fullscreen-pseudo-class + Fullscreen(prefix) => { + dest.write_char(':')?; + let vp = if !dest.vendor_prefix.is_empty() { + dest.vendor_prefix + } else { + *prefix + }; + vp.to_css(dest)?; + if vp == VendorPrefix::WebKit || vp == VendorPrefix::Moz { + dest.write_str("full-screen") + } else { + dest.write_str("fullscreen") } + } - // https://webkit.org/blog/363/styling-scrollbars/ - WebKitScrollbar(s) => { - use WebKitScrollbarPseudoClass::*; - dest.write_str(match s { - Horizontal => ":horizontal", - Vertical => ":vertical", - Decrement => ":decrement", - Increment => ":increment", - Start => ":start", - End => ":end", - DoubleButton => ":double-button", - SingleButton => ":single-button", - NoButton => ":no-button", - CornerPresent => ":corner-present", - WindowInactive => ":window-inactive", - }) - } + // https://drafts.csswg.org/selectors-4/#the-defined-pseudo + Defined => dest.write_str(":defined"), + + // https://drafts.csswg.org/selectors-4/#location + AnyLink(prefix) => write_prefixed!(prefix, "any-link"), + Link => dest.write_str(":link"), + LocalLink => dest.write_str(":local-link"), + Target => dest.write_str(":target"), + TargetWithin => dest.write_str(":target-within"), + Visited => dest.write_str(":visited"), + + // https://drafts.csswg.org/selectors-4/#input-pseudos + Enabled => dest.write_str(":enabled"), + Disabled => dest.write_str(":disabled"), + ReadOnly(prefix) => write_prefixed!(prefix, "read-only"), + ReadWrite(prefix) => write_prefixed!(prefix, "read-write"), + PlaceholderShown(prefix) => write_prefixed!(prefix, "placeholder-shown"), + Default => dest.write_str(":default"), + Checked => dest.write_str(":checked"), + Indeterminate => dest.write_str(":indeterminate"), + Blank => dest.write_str(":blank"), + Valid => dest.write_str(":valid"), + Invalid => dest.write_str(":invalid"), + InRange => dest.write_str(":in-range"), + OutOfRange => dest.write_str(":out-of-range"), + Required => dest.write_str(":required"), + Optional => dest.write_str(":optional"), + UserValid => dest.write_str(":user-valid"), + UserInvalid => dest.write_str(":user-invalid"), + + // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-autofill + Autofill(prefix) => write_prefixed!(prefix, "autofill"), + + Local { selector } => serialize_selector(selector, dest, context, false), + Global { selector } => { + let css_module = std::mem::take(&mut dest.css_module); + serialize_selector(selector, dest, context, false)?; + dest.css_module = css_module; + Ok(()) + } - Lang { languages: _ } | Dir { direction: _ } => unreachable!(), - Custom { name } => { - dest.write_char(':')?; - return dest.write_str(&name); - } - CustomFunction { name, arguments: args } => { - dest.write_char(':')?; - dest.write_str(name)?; - dest.write_char('(')?; - args.to_css(dest, false)?; - dest.write_char(')') - } + // https://webkit.org/blog/363/styling-scrollbars/ + WebKitScrollbar(s) => { + use WebKitScrollbarPseudoClass::*; + dest.write_str(match s { + Horizontal => ":horizontal", + Vertical => ":vertical", + Decrement => ":decrement", + Increment => ":increment", + Start => ":start", + End => ":end", + DoubleButton => ":double-button", + SingleButton => ":single-button", + NoButton => ":no-button", + CornerPresent => ":corner-present", + WindowInactive => ":window-inactive", + }) + } + + Lang { languages: _ } | Dir { direction: _ } => unreachable!(), + Custom { name } => { + dest.write_char(':')?; + return dest.write_str(&name); + } + CustomFunction { name, arguments: args } => { + dest.write_char(':')?; + dest.write_str(name)?; + dest.write_char('(')?; + args.to_css(dest, false)?; + dest.write_char(')') } } } @@ -872,99 +865,101 @@ impl<'i> cssparser::ToCss for PseudoElement<'i> { } } -impl<'i> ToCss for PseudoElement<'i> { - fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> - where - W: fmt::Write, - { - use PseudoElement::*; +fn serialize_pseudo_element<'a, 'i, W>( + pseudo_element: &PseudoElement, + dest: &mut Printer, + context: Option<&StyleContext>, +) -> Result<(), PrinterError> +where + W: fmt::Write, +{ + use PseudoElement::*; + + macro_rules! write_prefix { + ($prefix: ident) => {{ + dest.write_str("::")?; + // If the printer has a vendor prefix override, use that. + let vp = if !dest.vendor_prefix.is_empty() { + dest.vendor_prefix + } else { + *$prefix + }; + vp.to_css(dest)?; + vp + }}; + } - macro_rules! write_prefix { - ($prefix: ident) => {{ - dest.write_str("::")?; - // If the printer has a vendor prefix override, use that. - let vp = if !dest.vendor_prefix.is_empty() { - dest.vendor_prefix - } else { - *$prefix - }; - vp.to_css(dest)?; - vp - }}; - } + macro_rules! write_prefixed { + ($prefix: ident, $val: expr) => {{ + write_prefix!($prefix); + dest.write_str($val) + }}; + } - macro_rules! write_prefixed { - ($prefix: ident, $val: expr) => {{ - write_prefix!($prefix); - dest.write_str($val) - }}; + match pseudo_element { + // CSS2 pseudo elements support a single colon syntax in addition + // to the more correct double colon for other pseudo elements. + // We use that here because it's supported everywhere and is shorter. + After => dest.write_str(":after"), + Before => dest.write_str(":before"), + FirstLine => dest.write_str(":first-line"), + FirstLetter => dest.write_str(":first-letter"), + Marker => dest.write_str("::marker"), + Selection(prefix) => write_prefixed!(prefix, "selection"), + Cue => dest.write_str("::cue"), + CueRegion => dest.write_str("::cue-region"), + CueFunction { selector } => { + dest.write_str("::cue(")?; + serialize_selector(selector, dest, context, false)?; + dest.write_char(')') } - - match &self { - // CSS2 pseudo elements support a single colon syntax in addition - // to the more correct double colon for other pseudo elements. - // We use that here because it's supported everywhere and is shorter. - After => dest.write_str(":after"), - Before => dest.write_str(":before"), - FirstLine => dest.write_str(":first-line"), - FirstLetter => dest.write_str(":first-letter"), - Marker => dest.write_str("::marker"), - Selection(prefix) => write_prefixed!(prefix, "selection"), - Cue => dest.write_str("::cue"), - CueRegion => dest.write_str("::cue-region"), - CueFunction { selector } => { - dest.write_str("::cue(")?; - serialize_selector::<_, DefaultAtRule>(selector, dest, None, false)?; - dest.write_char(')') - } - CueRegionFunction(selector) => { - dest.write_str("::cue-region(")?; - serialize_selector::<_, DefaultAtRule>(selector, dest, None, false)?; - dest.write_char(')') - } - Placeholder(prefix) => { - let vp = write_prefix!(prefix); - if vp == VendorPrefix::WebKit || vp == VendorPrefix::Ms { - dest.write_str("input-placeholder") - } else { - dest.write_str("placeholder") - } - } - Backdrop(prefix) => write_prefixed!(prefix, "backdrop"), - FileSelectorButton(prefix) => { - let vp = write_prefix!(prefix); - if vp == VendorPrefix::WebKit { - dest.write_str("file-upload-button") - } else if vp == VendorPrefix::Ms { - dest.write_str("browse") - } else { - dest.write_str("file-selector-button") - } - } - WebKitScrollbar(s) => { - use WebKitScrollbarPseudoElement::*; - dest.write_str(match s { - Scrollbar => "::-webkit-scrollbar", - Button => "::-webkit-scrollbar-button", - Track => "::-webkit-scrollbar-track", - TrackPiece => "::-webkit-scrollbar-track-piece", - Thumb => "::-webkit-scrollbar-thumb", - Corner => "::-webkit-scrollbar-corner", - Resizer => "::-webkit-resizer", - }) - } - Custom { name: val } => { - dest.write_str("::")?; - return dest.write_str(val); + CueRegionFunction(selector) => { + dest.write_str("::cue-region(")?; + serialize_selector(selector, dest, context, false)?; + dest.write_char(')') + } + Placeholder(prefix) => { + let vp = write_prefix!(prefix); + if vp == VendorPrefix::WebKit || vp == VendorPrefix::Ms { + dest.write_str("input-placeholder") + } else { + dest.write_str("placeholder") } - CustomFunction { name, arguments: args } => { - dest.write_str("::")?; - dest.write_str(name)?; - dest.write_char('(')?; - args.to_css(dest, false)?; - dest.write_char(')') + } + Backdrop(prefix) => write_prefixed!(prefix, "backdrop"), + FileSelectorButton(prefix) => { + let vp = write_prefix!(prefix); + if vp == VendorPrefix::WebKit { + dest.write_str("file-upload-button") + } else if vp == VendorPrefix::Ms { + dest.write_str("browse") + } else { + dest.write_str("file-selector-button") } } + WebKitScrollbar(s) => { + use WebKitScrollbarPseudoElement::*; + dest.write_str(match s { + Scrollbar => "::-webkit-scrollbar", + Button => "::-webkit-scrollbar-button", + Track => "::-webkit-scrollbar-track", + TrackPiece => "::-webkit-scrollbar-track-piece", + Thumb => "::-webkit-scrollbar-thumb", + Corner => "::-webkit-scrollbar-corner", + Resizer => "::-webkit-resizer", + }) + } + Custom { name: val } => { + dest.write_str("::")?; + return dest.write_str(val); + } + CustomFunction { name, arguments: args } => { + dest.write_str("::")?; + dest.write_str(name)?; + dest.write_char('(')?; + args.to_css(dest, false)?; + dest.write_char(')') + } } } @@ -1030,25 +1025,12 @@ impl<'i> PseudoElement<'i> { } } -impl<'a, 'i, T> ToCssWithContext<'a, 'i, T> for SelectorList<'i> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> - where - W: fmt::Write, - { - serialize_selector_list(self.0.iter(), dest, context, false) - } -} - -impl<'i> ToCss for SelectorList<'i> { +impl<'a, 'i> ToCss for SelectorList<'i> { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where - W: std::fmt::Write, + W: fmt::Write, { - serialize_selector_list::<_, _, DefaultAtRule>(self.0.iter(), dest, None, false) + serialize_selector_list(self.0.iter(), dest, dest.context(), false) } } @@ -1068,23 +1050,19 @@ impl ToCss for Combinator { } // Copied from the selectors crate and modified to override to_css implementation. -impl<'a, 'i, T> ToCssWithContext<'a, 'i, T> for Selector<'i> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> +impl<'a, 'i> ToCss for Selector<'i> { + fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: fmt::Write, { - serialize_selector(self, dest, context, false) + serialize_selector(self, dest, dest.context(), false) } } -fn serialize_selector<'a, 'i, W, T>( +fn serialize_selector<'a, 'i, W>( selector: &Selector<'i>, dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, + context: Option<&StyleContext>, mut is_relative: bool, ) -> Result<(), PrinterError> where @@ -1169,7 +1147,7 @@ where } for simple in iter { - simple.to_css_with_context(dest, context)?; + serialize_component(simple, dest, context)?; } if swap_nesting { @@ -1199,15 +1177,15 @@ where // This ensures that the compiled selector is valid. e.g. (div.foo is valid, .foodiv is not). let nesting = iter.next().unwrap(); let local = iter.next().unwrap(); - local.to_css_with_context(dest, context)?; + serialize_component(local, dest, context)?; // Also check the next item in case of namespaces. if first_non_namespace > first_index { let local = iter.next().unwrap(); - local.to_css_with_context(dest, context)?; + serialize_component(local, dest, context)?; } - nesting.to_css_with_context(dest, context)?; + serialize_component(nesting, dest, context)?; } else if has_leading_nesting && context.is_some() { // Nesting selector may serialize differently if it is leading, due to type selectors. iter.next(); @@ -1224,7 +1202,7 @@ where continue; } } - simple.to_css_with_context(dest, context)?; + serialize_component(simple, dest, context)?; } } @@ -1248,115 +1226,113 @@ where Ok(()) } -impl<'a, 'i, T> ToCssWithContext<'a, 'i, T> for Component<'i> { - fn to_css_with_context( - &self, - dest: &mut Printer, - context: Option<&StyleContext<'a, 'i, T>>, - ) -> Result<(), PrinterError> - where - W: fmt::Write, - { - match &self { - Component::Combinator(ref c) => c.to_css(dest), - Component::AttributeInNoNamespace { - ref local_name, - operator, - ref value, - case_sensitivity, - .. - } => { - dest.write_char('[')?; - cssparser::ToCss::to_css(local_name, dest)?; - cssparser::ToCss::to_css(operator, dest)?; - - if dest.minify { - // Serialize as both an identifier and a string and choose the shorter one. - let mut id = String::new(); - serialize_identifier(&value, &mut id)?; - - let s = value.to_css_string(Default::default())?; - - if id.len() > 0 && id.len() < s.len() { - dest.write_str(&id)?; - } else { - dest.write_str(&s)?; - } +fn serialize_component<'a, 'i, W>( + component: &Component, + dest: &mut Printer, + context: Option<&StyleContext>, +) -> Result<(), PrinterError> +where + W: fmt::Write, +{ + match component { + Component::Combinator(ref c) => c.to_css(dest), + Component::AttributeInNoNamespace { + ref local_name, + operator, + ref value, + case_sensitivity, + .. + } => { + dest.write_char('[')?; + cssparser::ToCss::to_css(local_name, dest)?; + cssparser::ToCss::to_css(operator, dest)?; + + if dest.minify { + // Serialize as both an identifier and a string and choose the shorter one. + let mut id = String::new(); + serialize_identifier(&value, &mut id)?; + + let s = value.to_css_string(Default::default())?; + + if id.len() > 0 && id.len() < s.len() { + dest.write_str(&id)?; } else { - value.to_css(dest)?; - } - - match case_sensitivity { - parcel_selectors::attr::ParsedCaseSensitivity::CaseSensitive - | parcel_selectors::attr::ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => {} - parcel_selectors::attr::ParsedCaseSensitivity::AsciiCaseInsensitive => dest.write_str(" i")?, - parcel_selectors::attr::ParsedCaseSensitivity::ExplicitCaseSensitive => dest.write_str(" s")?, + dest.write_str(&s)?; } - dest.write_char(']') + } else { + value.to_css(dest)?; } - Component::Is(ref list) - | Component::Where(ref list) - | Component::Negation(ref list) - | Component::Any(_, ref list) => { - match *self { - Component::Where(..) => dest.write_str(":where(")?, - Component::Is(ref selectors) => { - // If there's only one simple selector, serialize it directly. - if selectors.len() == 1 { - let first = selectors.first().unwrap(); - if !has_type_selector(first) && is_simple(first) { - serialize_selector(first, dest, context, false)?; - return Ok(()); - } - } - let vp = dest.vendor_prefix; - if vp.intersects(VendorPrefix::WebKit | VendorPrefix::Moz) { - dest.write_char(':')?; - vp.to_css(dest)?; - dest.write_str("any(")?; - } else { - dest.write_str(":is(")?; + match case_sensitivity { + parcel_selectors::attr::ParsedCaseSensitivity::CaseSensitive + | parcel_selectors::attr::ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => {} + parcel_selectors::attr::ParsedCaseSensitivity::AsciiCaseInsensitive => dest.write_str(" i")?, + parcel_selectors::attr::ParsedCaseSensitivity::ExplicitCaseSensitive => dest.write_str(" s")?, + } + dest.write_char(']') + } + Component::Is(ref list) + | Component::Where(ref list) + | Component::Negation(ref list) + | Component::Any(_, ref list) => { + match *component { + Component::Where(..) => dest.write_str(":where(")?, + Component::Is(ref selectors) => { + // If there's only one simple selector, serialize it directly. + if selectors.len() == 1 { + let first = selectors.first().unwrap(); + if !has_type_selector(first) && is_simple(first) { + serialize_selector(first, dest, context, false)?; + return Ok(()); } } - Component::Negation(..) => return serialize_negation(list.iter(), dest, context), - Component::Any(ref prefix, ..) => { + + let vp = dest.vendor_prefix; + if vp.intersects(VendorPrefix::WebKit | VendorPrefix::Moz) { dest.write_char(':')?; - prefix.to_css(dest)?; + vp.to_css(dest)?; dest.write_str("any(")?; + } else { + dest.write_str(":is(")?; } - _ => unreachable!(), } - serialize_selector_list(list.iter(), dest, context, false)?; - dest.write_str(")") - } - Component::Has(ref list) => { - dest.write_str(":has(")?; - serialize_selector_list(list.iter(), dest, context, true)?; - dest.write_str(")") - } - Component::NonTSPseudoClass(pseudo) => pseudo.to_css_with_context(dest, context), - Component::PseudoElement(pseudo) => pseudo.to_css(dest), - Component::Nesting => serialize_nesting(dest, context, false), - Component::Class(ref class) => { - dest.write_char('.')?; - dest.write_ident(&class.0) - } - Component::ID(ref id) => { - dest.write_char('#')?; - dest.write_ident(&id.0) - } - _ => { - cssparser::ToCss::to_css(self, dest)?; - Ok(()) + Component::Negation(..) => return serialize_negation(list.iter(), dest, context), + Component::Any(ref prefix, ..) => { + dest.write_char(':')?; + prefix.to_css(dest)?; + dest.write_str("any(")?; + } + _ => unreachable!(), } + serialize_selector_list(list.iter(), dest, context, false)?; + dest.write_str(")") + } + Component::Has(ref list) => { + dest.write_str(":has(")?; + serialize_selector_list(list.iter(), dest, context, true)?; + dest.write_str(")") + } + Component::NonTSPseudoClass(pseudo) => serialize_pseudo_class(pseudo, dest, context), + Component::PseudoElement(pseudo) => serialize_pseudo_element(pseudo, dest, context), + Component::Nesting => serialize_nesting(dest, context, false), + Component::Class(ref class) => { + dest.write_char('.')?; + dest.write_ident(&class.0) + } + Component::ID(ref id) => { + dest.write_char('#')?; + dest.write_ident(&id.0) + } + _ => { + cssparser::ToCss::to_css(component, dest)?; + Ok(()) } } } -fn serialize_nesting( +fn serialize_nesting( dest: &mut Printer, - context: Option<&StyleContext>, + context: Option<&StyleContext>, first: bool, ) -> Result<(), PrinterError> where @@ -1367,13 +1343,13 @@ where // Otherwise, use an :is() pseudo class. // Type selectors are only allowed at the start of a compound selector, // so use :is() if that is not the case. - if ctx.rule.selectors.0.len() == 1 - && (first || (!has_type_selector(&ctx.rule.selectors.0[0]) && is_simple(&ctx.rule.selectors.0[0]))) + if ctx.selectors.0.len() == 1 + && (first || (!has_type_selector(&ctx.selectors.0[0]) && is_simple(&ctx.selectors.0[0]))) { - ctx.rule.selectors.0.first().unwrap().to_css_with_context(dest, ctx.parent) + serialize_selector(ctx.selectors.0.first().unwrap(), dest, ctx.parent, false) } else { dest.write_str(":is(")?; - serialize_selector_list(ctx.rule.selectors.0.iter(), dest, ctx.parent, false)?; + serialize_selector_list(ctx.selectors.0.iter(), dest, ctx.parent, false)?; dest.write_char(')') } } else { @@ -1416,10 +1392,10 @@ fn is_namespace(component: Option<&Component>) -> bool { ) } -fn serialize_selector_list<'a, 'i: 'a, I, W, T>( +fn serialize_selector_list<'a, 'i: 'a, I, W>( iter: I, dest: &mut Printer, - context: Option<&StyleContext<'_, 'i, T>>, + context: Option<&StyleContext>, is_relative: bool, ) -> Result<(), PrinterError> where @@ -1437,10 +1413,10 @@ where Ok(()) } -fn serialize_negation<'a, 'i: 'a, I, W, T>( +fn serialize_negation<'a, 'i: 'a, I, W>( iter: I, dest: &mut Printer, - context: Option<&StyleContext<'_, 'i, T>>, + context: Option<&StyleContext>, ) -> Result<(), PrinterError> where I: Iterator>, @@ -1460,7 +1436,7 @@ where } else { for selector in iter { dest.write_str(":not(")?; - selector.to_css_with_context(dest, context)?; + serialize_selector(selector, dest, context, false)?; dest.write_char(')')?; } } @@ -1854,3 +1830,34 @@ impl<'i, T: Visit<'i, T, V>, V: Visitor<'i, T>> Visit<'i, T, V> for Selector<'i> Ok(()) } } + +impl<'i> ParseWithOptions<'i> for Selector<'i> { + fn parse_with_options<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result>> { + Selector::parse( + &SelectorParser { + is_nesting_allowed: options.nesting, + options: &options, + }, + input, + ) + } +} + +impl<'i> ParseWithOptions<'i> for SelectorList<'i> { + fn parse_with_options<'t>( + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result>> { + SelectorList::parse( + &SelectorParser { + is_nesting_allowed: options.nesting, + options: &options, + }, + input, + parcel_selectors::parser::NestingRequirement::None, + ) + } +} diff --git a/src/stylesheet.rs b/src/stylesheet.rs index 70a32655..384e6268 100644 --- a/src/stylesheet.rs +++ b/src/stylesheet.rs @@ -9,14 +9,14 @@ use crate::css_modules::{CssModule, CssModuleExports, CssModuleReferences}; use crate::declaration::{DeclarationBlock, DeclarationHandler}; use crate::dependencies::Dependency; use crate::error::{Error, ErrorLocation, MinifyErrorKind, ParserError, PrinterError, PrinterErrorKind}; -use crate::parser::{DefaultAtRuleParser, TopLevelRuleParser}; +use crate::parser::{DefaultAtRule, DefaultAtRuleParser, TopLevelRuleParser}; use crate::printer::Printer; use crate::rules::{CssRule, CssRuleList, MinifyContext}; use crate::targets::Browsers; -use crate::traits::ToCss; +use crate::traits::{AtRuleParser, ToCss}; #[cfg(feature = "visitor")] use crate::visitor::{Visit, VisitTypes, Visitor}; -use cssparser::{AtRuleParser, Parser, ParserInput, RuleListParser}; +use cssparser::{Parser, ParserInput, RuleListParser}; #[cfg(feature = "sourcemap")] use parcel_sourcemap::SourceMap; use std::collections::{HashMap, HashSet}; @@ -68,24 +68,12 @@ pub use crate::printer::PseudoClasses; #[cfg_attr( feature = "jsonschema", derive(schemars::JsonSchema), - schemars( - rename = "StyleSheet", - bound = "T: schemars::JsonSchema, T::AtRule: schemars::JsonSchema" - ) + schemars(rename = "StyleSheet", bound = "T: schemars::JsonSchema") )] -pub struct StyleSheet<'i, 'o, T: AtRuleParser<'i> = DefaultAtRuleParser> { +pub struct StyleSheet<'i, 'o, T = DefaultAtRule> { /// A list of top-level rules within the style sheet. - #[cfg_attr( - feature = "serde", - serde( - borrow, - bound( - serialize = "T::AtRule: serde::Serialize", - deserialize = "T::AtRule: serde::Deserialize<'de>" - ) - ) - )] - pub rules: CssRuleList<'i, T::AtRule>, + #[cfg_attr(feature = "serde", serde(borrow))] + pub rules: CssRuleList<'i, T>, /// A list of file names for all source files included within the style sheet. /// Sources are referenced by index in the `loc` property of each rule. pub sources: Vec, @@ -93,7 +81,7 @@ pub struct StyleSheet<'i, 'o, T: AtRuleParser<'i> = DefaultAtRuleParser> { pub(crate) source_map_urls: Vec>, #[cfg_attr(feature = "serde", serde(skip))] /// The options the style sheet was originally parsed with. - options: ParserOptions<'o, 'i, T>, + options: ParserOptions<'o, 'i>, } /// Options for the `minify` function of a [StyleSheet](StyleSheet) @@ -124,15 +112,22 @@ pub struct ToCssResult { pub dependencies: Option>, } -impl<'i, 'o, T: AtRuleParser<'i>> StyleSheet<'i, 'o, T> +impl<'i, 'o> StyleSheet<'i, 'o, DefaultAtRule> { + /// Parse a style sheet from a string. + pub fn parse(code: &'i str, options: ParserOptions<'o, 'i>) -> Result>> { + Self::parse_with(code, options, &mut DefaultAtRuleParser) + } +} + +impl<'i, 'o, T> StyleSheet<'i, 'o, T> where - T::AtRule: ToCss, + T: ToCss, { /// Creates a new style sheet with the given source filenames and rules. pub fn new( sources: Vec, - rules: CssRuleList<'i, T::AtRule>, - options: ParserOptions<'o, 'i, T>, + rules: CssRuleList<'i, T>, + options: ParserOptions<'o, 'i>, ) -> StyleSheet<'i, 'o, T> { StyleSheet { sources, @@ -143,11 +138,15 @@ where } /// Parse a style sheet from a string. - pub fn parse(code: &'i str, mut options: ParserOptions<'o, 'i, T>) -> Result>> { + pub fn parse_with>( + code: &'i str, + mut options: ParserOptions<'o, 'i>, + at_rule_parser: &mut P, + ) -> Result>> { let mut input = ParserInput::new(&code); let mut parser = Parser::new(&mut input); let mut rule_list_parser = - RuleListParser::new_for_stylesheet(&mut parser, TopLevelRuleParser::new(&mut options)); + RuleListParser::new_for_stylesheet(&mut parser, TopLevelRuleParser::new(&mut options, at_rule_parser)); let mut rules = vec![]; while let Some(rule) = rule_list_parser.next() { @@ -280,11 +279,10 @@ where #[cfg(feature = "visitor")] #[cfg_attr(docsrs, doc(cfg(feature = "visitor")))] -impl<'i, 'o, T, V> Visit<'i, T::AtRule, V> for StyleSheet<'i, 'o, T> +impl<'i, 'o, T, V> Visit<'i, T, V> for StyleSheet<'i, 'o, T> where - T: AtRuleParser<'i>, - T::AtRule: Visit<'i, T::AtRule, V>, - V: Visitor<'i, T::AtRule>, + T: Visit<'i, T, V>, + V: Visitor<'i, T>, { const CHILD_TYPES: VisitTypes = VisitTypes::all(); @@ -329,9 +327,9 @@ pub struct StyleAttribute<'i> { impl<'i> StyleAttribute<'i> { /// Parses a style attribute from a string. - pub fn parse( + pub fn parse( code: &'i str, - options: ParserOptions<'_, 'i, T>, + options: ParserOptions<'_, 'i>, ) -> Result, Error>> { let mut input = ParserInput::new(&code); let mut parser = Parser::new(&mut input); diff --git a/src/traits.rs b/src/traits.rs index f96d83be..dad8397e 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -28,17 +28,17 @@ pub trait Parse<'i>: Sized { } /// Trait for things that can be parsed from CSS syntax and require ParserOptions. -pub trait ParseWithOptions<'i, T>: Sized { +pub trait ParseWithOptions<'i>: Sized { /// Parse a value of this type with the given options. fn parse_with_options<'t>( input: &mut Parser<'i, 't>, - options: &ParserOptions, + options: &ParserOptions<'_, 'i>, ) -> Result>>; /// Parse a value from a string with the given options. fn parse_string_with_options( input: &'i str, - options: ParserOptions, + options: ParserOptions<'_, 'i>, ) -> Result>> { let mut input = ParserInput::new(input); let mut parser = Parser::new(&mut input); @@ -46,11 +46,11 @@ pub trait ParseWithOptions<'i, T>: Sized { } } -impl<'i, T: Parse<'i>, U> ParseWithOptions<'i, U> for T { +impl<'i, T: Parse<'i>> ParseWithOptions<'i> for T { #[inline] fn parse_with_options<'t>( input: &mut Parser<'i, 't>, - _options: &ParserOptions, + _options: &ParserOptions, ) -> Result>> { T::parse(input) } @@ -234,3 +234,94 @@ pub trait Zero { /// Returns whether the value is zero. fn is_zero(&self) -> bool; } + +/// A trait to provide parsing of custom at-rules. +/// +/// For example, there could be different implementations for top-level at-rules +/// (`@media`, `@font-face`, …) +/// and for page-margin rules inside `@page`. +/// +/// Default implementations that reject all at-rules are provided, +/// so that `impl AtRuleParser<(), ()> for ... {}` can be used +/// for using `DeclarationListParser` to parse a declarations list with only qualified rules. +/// +/// Note: this trait is copied from cssparser and modified to provide parser options. +pub trait AtRuleParser<'i>: Sized { + /// The intermediate representation of prelude of an at-rule. + type Prelude; + + /// The finished representation of an at-rule. + type AtRule; + + /// The error type that is included in the ParseError value that can be returned. + type Error: 'i; + + /// Parse the prelude of an at-rule with the given `name`. + /// + /// Return the representation of the prelude and the type of at-rule, + /// or `Err(())` to ignore the entire at-rule as invalid. + /// + /// The prelude is the part after the at-keyword + /// and before the `;` semicolon or `{ /* ... */ }` block. + /// + /// At-rule name matching should be case-insensitive in the ASCII range. + /// This can be done with `std::ascii::Ascii::eq_ignore_ascii_case`, + /// or with the `match_ignore_ascii_case!` macro. + /// + /// The given `input` is a "delimited" parser + /// that ends wherever the prelude should end. + /// (Before the next semicolon, the next `{`, or the end of the current block.) + fn parse_prelude<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result> { + let _ = name; + let _ = input; + let _ = options; + Err(input.new_error(BasicParseErrorKind::AtRuleInvalid(name))) + } + + /// End an at-rule which doesn't have block. Return the finished + /// representation of the at-rule. + /// + /// The location passed in is source location of the start of the prelude. + /// + /// This is only called when either the `;` semicolon indeed follows the prelude, + /// or parser is at the end of the input. + fn rule_without_block( + &mut self, + prelude: Self::Prelude, + start: &ParserState, + options: &ParserOptions<'_, 'i>, + ) -> Result { + let _ = prelude; + let _ = start; + let _ = options; + Err(()) + } + + /// Parse the content of a `{ /* ... */ }` block for the body of the at-rule. + /// + /// The location passed in is source location of the start of the prelude. + /// + /// Return the finished representation of the at-rule + /// as returned by `RuleListParser::next` or `DeclarationListParser::next`, + /// or `Err(())` to ignore the entire at-rule as invalid. + /// + /// This is only called when a block was found following the prelude. + fn parse_block<'t>( + &mut self, + prelude: Self::Prelude, + start: &ParserState, + input: &mut Parser<'i, 't>, + options: &ParserOptions<'_, 'i>, + ) -> Result> { + let _ = prelude; + let _ = start; + let _ = input; + let _ = options; + Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)) + } +} diff --git a/src/values/ident.rs b/src/values/ident.rs index f7c93419..e8831722 100644 --- a/src/values/ident.rs +++ b/src/values/ident.rs @@ -110,10 +110,10 @@ pub struct DashedIdentReference<'i> { pub from: Option>, } -impl<'i, T> ParseWithOptions<'i, T> for DashedIdentReference<'i> { +impl<'i> ParseWithOptions<'i> for DashedIdentReference<'i> { fn parse_with_options<'t>( input: &mut Parser<'i, 't>, - options: &crate::stylesheet::ParserOptions, + options: &crate::stylesheet::ParserOptions, ) -> Result>> { let ident = DashedIdent::parse(input)?; diff --git a/src/values/syntax.rs b/src/values/syntax.rs index a4df102f..5dede895 100644 --- a/src/values/syntax.rs +++ b/src/values/syntax.rs @@ -6,6 +6,8 @@ use crate::error::{ParserError, PrinterError}; use crate::printer::Printer; use crate::traits::{Parse, ToCss}; use crate::values; +#[cfg(feature = "visitor")] +use crate::visitor::Visit; use cssparser::*; /// A CSS [syntax string](https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings) @@ -83,6 +85,7 @@ pub enum SyntaxComponentKind { /// A [multiplier](https://drafts.css-houdini.org/css-properties-values-api/#multipliers) for a /// [SyntaxComponent](SyntaxComponent). Indicates whether and how the component may be repeated. #[derive(Debug, PartialEq, Clone)] +#[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), @@ -100,6 +103,7 @@ pub enum Multiplier { /// A parsed value for a [SyntaxComponent](SyntaxComponent). #[derive(Debug, PartialEq, Clone)] +#[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr(feature = "into_owned", derive(lightningcss_derive::IntoOwned))] #[cfg_attr( feature = "serde", @@ -142,6 +146,7 @@ pub enum ParsedComponent<'i> { /// A repeated component value. Repeated { /// The components to repeat. + #[cfg_attr(feature = "visitor", skip_type)] components: Vec>, /// A multiplier describing how the components repeat. multiplier: Multiplier, diff --git a/test.js b/test.js index bcfbada3..cdb2d1c0 100644 --- a/test.js +++ b/test.js @@ -30,108 +30,49 @@ if (process.argv[process.argv.length - 1] !== __filename) { let res = css.transform({ filename: __filename, - // minify: true, - // targets: { - // safari: 4 << 16, - // firefox: 3 << 16 | 5 << 8, - // opera: 10 << 16 | 5 << 8 - // }, code: Buffer.from(` - @namespace "http://foo.com"; - @namespace svg "http://bar.com"; - - .selector > .nested[data-foo=bar]:not(.foo):hover::part(tab active) { - width: 32px; - --foo: var(--bar, 30px); - background: linear-gradient(red, green); - } - - svg|foo { - test: foo; + @breakpoints { + .foo { color: yellow; } } - @media (hover) and (width > 50px) { - .foo { - color: red; - background: inline('.gitignore'); + .foo { + color: red; + @bar { + width: 25px; } } `), - visitor: { - visitLength(length) { - if (length.unit === 'px') { - return { - unit: 'rem', - value: length.value / 16 - } - } - return length; - }, - visitColor(color) { - console.log(color); - return color; - }, - visitImage(image) { - // console.log(image.value.value); - image.value.value[1].push('moz'); - return image; - }, - visitProperty(property) { - // console.log(require('util').inspect(property, {depth: 50})) - if (property.property === 'background') { - property.value[0].repeat.x = 'no-repeat'; - property.value[0].repeat.y = 'no-repeat'; - } - - return property; - }, - // visitRule(rule) { - // console.log(require('util').inspect(rule, {depth: 10})); - // if (rule.type === 'style') { - // for (let selector of rule.value.selectors) { - // for (let component of selector) { - // if (component.type === 'class') { - // component.value = 'tw-' + component.value; - // } - // } - // } - // } - // return rule; - // }, - visitMediaQuery(query) { - // console.log(query); - query.media_type = 'print'; - return query; + drafts: { + nesting: true + }, + targets: { + safari: 16 << 16 + }, + customAtRules: { + breakpoints: { + // Syntax string defining the at rule prelude. + // https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings + prelude: null, + // Type of the at rule block. + // Can be declaration-list, rule-list, or style-block. + // https://www.w3.org/TR/css-syntax-3/#declaration-rule-list + body: 'rule-list' }, - visitFunction(fn) { - // console.log(require('util').inspect(fn, {depth: 50})); - if (fn.name === 'inline') { - return { - name: 'url', - arguments: [{ - type: 'token', - value: { - type: 'string', - value: fs.readFileSync(fn.arguments[0].value.value).toString('base64'), - } - }] - } + bar: { + body: 'style-block' + } + }, + visitor: { + Rule: { + custom(rule) { + console.log(rule.body); } - return fn; }, - visitSelector(selector) { - console.log(require('util').inspect(selector, { depth: 10 })); - for (let component of selector) { - if (component.type === 'class') { - component.name = 'tw-' + component.name; - } else if (component.type === 'attribute') { - component.name = 'tw-' + component.name; - component.operation.operator = 'includes'; - } - } - return selector; + Length(length) { + length.value *= 2; + return length; } - }, + } }); console.log(res.code.toString()); diff --git a/tests/test_custom_parser.rs b/tests/test_custom_parser.rs index 9e5e8868..b75fa9be 100644 --- a/tests/test_custom_parser.rs +++ b/tests/test_custom_parser.rs @@ -4,19 +4,12 @@ use lightningcss::{ error::{ParserError, PrinterError}, printer::Printer, stylesheet::{ParserOptions, PrinterOptions, StyleSheet}, - traits::{Parse, ToCss}, + traits::{AtRuleParser, Parse, ToCss}, values::ident::Ident, }; fn minify_test(source: &str, expected: &str) { - let mut stylesheet = StyleSheet::parse( - &source, - ParserOptions { - at_rule_parser: Some(TestAtRuleParser), - ..Default::default() - }, - ) - .unwrap(); + let mut stylesheet = StyleSheet::parse_with(&source, ParserOptions::default(), &mut TestAtRuleParser).unwrap(); stylesheet.minify(Default::default()).unwrap(); let res = stylesheet .to_css(PrinterOptions { @@ -85,6 +78,7 @@ impl<'i> AtRuleParser<'i> for TestAtRuleParser { &mut self, name: CowRcStr<'i>, input: &mut Parser<'i, 't>, + _options: &ParserOptions<'_, 'i>, ) -> Result> { let location = input.current_source_location(); match_ignore_ascii_case! {&*name, @@ -102,7 +96,12 @@ impl<'i> AtRuleParser<'i> for TestAtRuleParser { } } - fn rule_without_block(&mut self, prelude: Self::Prelude, _start: &ParserState) -> Result { + fn rule_without_block( + &mut self, + prelude: Self::Prelude, + _start: &ParserState, + _options: &ParserOptions<'_, 'i>, + ) -> Result { match prelude { Prelude::Inline(name) => Ok(AtRule::Inline(InlineRule { name })), _ => unreachable!(), @@ -114,6 +113,7 @@ impl<'i> AtRuleParser<'i> for TestAtRuleParser { prelude: Self::Prelude, _start: &ParserState, input: &mut Parser<'i, 't>, + _options: &ParserOptions<'_, 'i>, ) -> Result> { match prelude { Prelude::Block(name) => Ok(AtRule::Block(BlockRule { diff --git a/tests/testdata/apply.css b/tests/testdata/apply.css new file mode 100644 index 00000000..e2508b31 --- /dev/null +++ b/tests/testdata/apply.css @@ -0,0 +1,5 @@ +@import "./mixin.css"; + +.foo { + @apply color; +} diff --git a/tests/testdata/mixin.css b/tests/testdata/mixin.css new file mode 100644 index 00000000..a836131c --- /dev/null +++ b/tests/testdata/mixin.css @@ -0,0 +1,7 @@ +@mixin color { + color: red; + + &.bar { + color: yellow; + } +} diff --git a/website/pages/transforms.md b/website/pages/transforms.md index eb155554..6e4e93d2 100644 --- a/website/pages/transforms.md +++ b/website/pages/transforms.md @@ -235,6 +235,99 @@ assert.equal(res.code.toString(), '.foo{padding:40px}'); Each visitor object has the opportunity to visit every value once. If a visitor returns a new value, that value is visited by the other visitor objects but not again by the original visitor that created it. If other visitors subsequently modify the value, the previous visitors will not revisit the value. This is to avoid infinite loops. +## Unknown at-rules + +By default, unknown at-rules are stored in the AST as raw tokens. This allows you to interpret them however you like by writing a custom visitor. The following example allows declaring static variables using named at-rules, and inlines them when an `at-keyword` token is seen: + +```js +let declared = new Map(); +let res = transform({ + filename: 'test.css', + minify: true, + code: Buffer.from(` + @blue #056ef0; + + .menu_link { + background: @blue; + } + `), + visitor: { + Rule: { + unknown(rule) { + declared.set(rule.name, rule.prelude); + return []; + } + }, + Token: { + 'at-keyword'(token) { + return declared.get(token.value); + } + } + } +}); + +assert.equal(res.code.toString(), '.menu_link{background:#056ef0}'); +``` + +## Custom at-rules + +Raw tokens as stored in unknown at-rules are fine for simple cases, but in more complex cases, you may wish to interpret a custom at-rule body as a standard CSS declaration list or rule list. However, by default, Lightning CSS does not know how unknown rules should be parsed. You can define their syntax using the `customAtRules` option. + +The syntax of the at-rule prelude can be defined with a [CSS syntax string](https://drafts.css-houdini.org/css-properties-values-api/#syntax-strings), which Lightning CSS will interpret and use to validate the input CSS. This uses the same syntax as the [@property](https://developer.mozilla.org/en-US/docs/Web/CSS/@property) rule. The body syntax is defined using one of the following options: + +* `"declaration-list"` – A list of CSS declarations (property value pairs), as in a style rule or other at-rules like `@font-face`. +* `"rule-list"` – A list of nested CSS rules, including style rules and at rules. Directly nested declarations with CSS nesting are not allowed. This matches how rules like `@keyframes` are parsed. +* `"style-block"` – A list of CSS declarations and/or nested rules. This matches the behavior of rules like `@media` and `@supports` which support directly nested declarations when inside a style rule. Note that the [nesting](transpilation.html#nesting) and [targets](transpilation.html#browser-targets) options must be defined for nesting to be compiled. + +This example defines two custom at-rules. `@mixin` defines a reusable style block, supporting both directly nested declarations and nested rules. A visitor function registers the mixin in a map and removes the custom rule. `@apply` looks up the requested mixin in the map and returns the nested rules, which are inlined into the parent. + +```js +let mixins = new Map(); +let res = transform({ + filename: 'test.css', + minify: true, + drafts: { nesting: true }, + targets: { chrome: 100 << 16 }, + code: Buffer.from(` + @mixin color { + color: red; + + &.bar { + color: yellow; + } + } + + .foo { + @apply color; + } + `), + customAtRules: { + mixin: { + prelude: '', + body: 'style-block' + }, + apply: { + prelude: '' + } + }, + visitor: { + Rule: { + custom: { + mixin(rule) { + mixins.set(rule.prelude.value, rule.body.value); + return []; + }, + apply(rule) { + return mixins.get(rule.prelude.value); + } + } + } + } +}); + +assert.equal(res.code.toString(), '.foo{color:red}.foo.bar{color:#ff0}'); +``` + ## Examples For examples of visitors that perform a variety of real world tasks, see the Lightning CSS [visitor tests](https://github.com/parcel-bundler/lightningcss/blob/master/node/test/visitor.test.mjs).