| 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}, |
| }, |
| }; |
| use serde::{Deserialize, Deserializer, Serialize}; |
| |
| #[derive(Deserialize, Debug, Clone)] |
| pub struct CustomAtRuleConfig { |
| #[serde(default, deserialize_with = "deserialize_prelude")] |
| prelude: Option<SyntaxString>, |
| body: Option<CustomAtRuleBodyType>, |
| } |
| |
| fn deserialize_prelude<'de, D>(deserializer: D) -> Result<Option<SyntaxString>, D::Error> |
| where |
| D: Deserializer<'de>, |
| { |
| let s = Option::<CowArcStr<'de>>::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<ParsedComponent<'i>>, |
| } |
| |
| #[derive(Serialize, Deserialize, Clone)] |
| pub struct AtRule<'i> { |
| #[serde(borrow)] |
| pub name: CowArcStr<'i>, |
| pub prelude: Option<ParsedComponent<'i>>, |
| pub body: Option<AtRuleBody<'i>>, |
| pub loc: Location, |
| } |
| |
| #[derive(Serialize, Deserialize, Clone)] |
| #[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<String, CustomAtRuleConfig>, |
| } |
| |
| 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<Self::Prelude, ParseError<'i, Self::Error>> { |
| 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>, |
| is_nested: bool, |
| ) -> Result<Self::AtRule, ParseError<'i, Self::Error>> { |
| 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, is_nested, |
| )?)), |
| } |
| } 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>, |
| _is_nested: bool, |
| ) -> Result<Self::AtRule, ()> { |
| 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<W>( |
| &self, |
| dest: &mut lightningcss::printer::Printer<W>, |
| ) -> 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(()) |
| } |
| } |
| |
| #[cfg(feature = "visitor")] |
| use lightningcss::visitor::{Visit, VisitTypes, Visitor}; |
| |
| #[cfg(feature = "visitor")] |
| 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(()), |
| } |
| } |
| } |