css_ast 0.0.25

CSS Abstract Syntax Trees with visitable nodes and style value types.
Documentation
use crate::{
	CssAtomSet, CssDiagnostic, CssMetadata, NodeKinds, SelectorList, StyleValue, UnknownAtRule, UnknownQualifiedRule,
	rules,
};
use css_parse::{
	BumpBox, Cursor, DeclarationGroup, Diagnostic, NodeMetadata, NodeWithMetadata, Parse, Parser, QualifiedRule,
	Result as ParserResult, RuleVariants,
};
use csskit_derives::*;

/// Represents a "Style Rule", such as `body { width: 100% }`. See also the CSS-OM [CSSStyleRule][1] interface.
///
/// The Style Rule is comprised of two child nodes: the [SelectorList] represents the selectors of the rule.
/// Each [Declaration][css_parse::Declaration] will have a [StyleValue], and each rule will be a [NestedGroupRule].
///
/// [1]: https://drafts.csswg.org/cssom-1/#the-cssstylerule-interface
#[derive(Parse, Peek, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
pub struct StyleRule<'a> {
	pub rule: QualifiedRule<'a, SelectorList<'a>, StyleValue<'a>, NestedGroupRule<'a>, CssMetadata>,
}

impl<'a> NodeWithMetadata<CssMetadata> for StyleRule<'a> {
	fn self_metadata(&self) -> CssMetadata {
		let child_meta = self.rule.metadata();
		let is_empty = child_meta.declaration_kinds.is_none() && !child_meta.has_rules();
		let mut node_kinds = NodeKinds::StyleRule;
		if is_empty {
			node_kinds |= NodeKinds::EmptyBlock;
		}
		CssMetadata { node_kinds, ..Default::default() }
	}

	fn metadata(&self) -> CssMetadata {
		self.rule.metadata().merge(self.self_metadata())
	}
}

// https://drafts.csswg.org/css-nesting/#conditionals
macro_rules! apply_rules {
	($macro: ident) => {
		$macro! {
			Container(ContainerRule<'a>): "container",
			Layer(LayerRule<'a>): "layer",
			Media(MediaRule<'a>): "media",
			Scope(ScopeRule): "scope",
		}
	};
}

macro_rules! nested_group_rule {
    ( $(
        $name: ident($ty: ident$(<$a: lifetime>)?): $str: pat,
    )+ ) => {
		/// <https://drafts.csswg.org/cssom-1/#the-cssrule-interface>
		#[derive(ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
		#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable))]
		#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(untagged))]
		#[derive(csskit_derives::NodeWithMetadata)]
		#[metadata(delegate)]
		pub enum NestedGroupRule<'a> {
			$(
				$name(rules::$ty$(<$a>)?),
			)+
			Supports(BumpBox<'a, rules::SupportsRule<'a>>),
			UnknownAt(UnknownAtRule<'a>),
			Style(StyleRule<'a>),
			Unknown(UnknownQualifiedRule<'a>),
			Declarations(DeclarationGroup<'a, StyleValue<'a>, CssMetadata>),
		}
	}
}
apply_rules!(nested_group_rule);

impl<'a> RuleVariants<'a> for NestedGroupRule<'a> {
	type DeclarationValue = StyleValue<'a>;
	type Metadata = CssMetadata;

	fn parse_at_rule<I>(p: &mut Parser<'a, I>, name: Cursor) -> ParserResult<Self>
	where
		I: Iterator<Item = Cursor> + Clone,
	{
		macro_rules! parse_rule {
			( $(
				$name: ident($ty: ident$(<$a: lifetime>)?): $str: pat,
			)+ ) => {
				match p.to_atom::<CssAtomSet>(name) {
					$(CssAtomSet::$name => p.parse::<rules::$ty>().map(Self::$name),)+
					CssAtomSet::Supports => p.parse::<rules::SupportsRule>().map(|r| Self::Supports(BumpBox::new_in(p.bump(), r))),
					_ => Err(Diagnostic::new(name.into(), Diagnostic::unexpected_at_rule))?,
				}
			}
		}
		apply_rules!(parse_rule)
	}

	fn parse_unknown_at_rule<I>(p: &mut Parser<'a, I>, _name: Cursor) -> ParserResult<Self>
	where
		I: Iterator<Item = Cursor> + Clone,
	{
		p.parse::<UnknownAtRule>().map(Self::UnknownAt)
	}

	fn parse_qualified_rule<I>(p: &mut Parser<'a, I>, _name: Cursor) -> ParserResult<Self>
	where
		I: Iterator<Item = Cursor> + Clone,
	{
		p.parse::<StyleRule>().map(Self::Style)
	}

	fn parse_unknown_qualified_rule<I>(p: &mut Parser<'a, I>, _name: Cursor) -> ParserResult<Self>
	where
		I: Iterator<Item = Cursor> + Clone,
	{
		p.parse::<UnknownQualifiedRule>().map(Self::Unknown)
	}

	fn is_unknown(&self) -> bool {
		matches!(self, Self::UnknownAt(_) | Self::Unknown(_))
	}

	fn from_declaration_group(
		group: css_parse::DeclarationGroup<'a, Self::DeclarationValue, Self::Metadata>,
	) -> Option<Self> {
		Some(Self::Declarations(group))
	}
}

impl<'a> Parse<'a> for NestedGroupRule<'a> {
	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
	where
		I: Iterator<Item = Cursor> + Clone,
	{
		Self::parse_rule_variants(p)
	}
}

#[cfg(test)]
mod tests {
	use super::*;
	use crate::CssAtomSet;
	use css_parse::assert_parse;

	#[cfg(feature = "visitable")]
	use crate::assert_visits;

	#[test]
	fn size_test() {
		assert_eq!(std::mem::size_of::<StyleRule>(), 192);
	}

	#[test]
	fn test_writes() {
		assert_parse!(CssAtomSet::ATOMS, StyleRule, "body{}");
		assert_parse!(CssAtomSet::ATOMS, StyleRule, "body,body{}");
		assert_parse!(CssAtomSet::ATOMS, StyleRule, "body{width:1px;}");
		assert_parse!(CssAtomSet::ATOMS, StyleRule, "body{opacity:0;}");
		assert_parse!(CssAtomSet::ATOMS, StyleRule, ".foo *{}");
		assert_parse!(CssAtomSet::ATOMS, StyleRule, ":nth-child(1){opacity:0;}");
		assert_parse!(CssAtomSet::ATOMS, StyleRule, ".foo{--bar:(baz);}");
		assert_parse!(CssAtomSet::ATOMS, StyleRule, ".foo{width: calc(1px + (var(--foo)) + 1px);}");
		assert_parse!(CssAtomSet::ATOMS, StyleRule, ".foo{--bar:1}");
		assert_parse!(CssAtomSet::ATOMS, StyleRule, ":root{--custom:{width:0;height:0;};}");
		// Semicolons are "allowed" in geneirc preludes
		assert_parse!(CssAtomSet::ATOMS, StyleRule, ":root{a;b{}}");
		// Bad Declarations should be parsable.
		assert_parse!(CssAtomSet::ATOMS, StyleRule, ":root{$(var)-size: 100%;}");
	}

	#[test]
	#[cfg(feature = "visitable")]
	fn test_visits() {
		assert_visits!(
			":root{html:has(&[open]){overflow:hidden}}",
			StyleRule,
			SelectorList,
			CompoundSelector,
			PseudoClass,
			StyleRule,
			SelectorList,
			CompoundSelector,
			Tag,
			HtmlTag,
			HasPseudoFunction,
			SelectorList,
			CompoundSelector,
			Combinator,
			Attribute,
			StyleValue,
			OverflowStyleValue,
			OverflowBlockStyleValue
		);
	}
}