blob: 919eda2dc7839102ef4b5a54c107b80317c1be73 [file] [log] [blame]
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(()),
}
}
}