csskit_transform 0.0.32-canary.13e01cac8c

AST transformation and minification utilities for CSS.
Documentation
use crate::prelude::*;
use css_ast::{AtRuleId, AtomSet, CharsetRule, EncodingLabel, VisitNode, Visitable};
use css_parse::{SourceOffset, format_in};

pub struct ReduceCharsetRule<'a, 'ctx, N: Visitable + NodeWithMetadata<CssMetadata>> {
	pub transformer: &'ctx Transformer<'a, CssMetadata, N, CssMinifierFeature>,
	seen: bool,
}

impl<'a, 'ctx, N> Transform<'a, 'ctx, CssMetadata, N, CssMinifierFeature> for ReduceCharsetRule<'a, 'ctx, N>
where
	N: Visitable + NodeWithMetadata<CssMetadata>,
{
	fn skips_subtree(metadata: &CssMetadata) -> bool {
		!metadata.used_at_rules.contains(AtRuleId::Charset)
	}

	fn new(transformer: &'ctx Transformer<'a, CssMetadata, N, CssMinifierFeature>) -> Self {
		Self { transformer, seen: false }
	}
}

#[visitor]
impl<'a, 'ctx, N> Visit for ReduceCharsetRule<'a, 'ctx, N>
where
	N: Visitable + NodeWithMetadata<CssMetadata>,
{
	fn consider_node(&self, node: VisitNode) -> VisitFlow {
		if node.node_id.is_some() && Self::skips_subtree(&node.subtree_metadata()) {
			return VisitFlow::SKIP_CHILDREN;
		}
		VisitFlow::DESCEND
	}

	fn visit_charset_rule(&mut self, rule: &CharsetRule) {
		let span = rule.to_span();
		let source_text = self.transformer.source_text;
		let encoding = rule.encoding(source_text);
		if encoding == EncodingLabel::Unknown || self.seen {
			self.transformer.delete(span);
			return;
		}
		self.seen = true;
		let label = rule.label(source_text);
		let shortest = encoding.to_str();
		let at_top = span.start() == SourceOffset(0);
		if shortest == label {
			if !at_top {
				self.transformer.insert_before(SourceOffset(0), self.transformer.to_source_cursors(rule));
				self.transformer.delete(span);
			}
			return;
		}
		let arena = self.transformer.alloc();
		let css = format_in!(in arena, "@charset \"{shortest}\";").into_str();
		if at_top {
			self.transformer.replace_parsed::<CharsetRule>(span, css);
		} else {
			self.transformer.insert_before(SourceOffset(0), self.transformer.parse_value::<CharsetRule>(css));
			self.transformer.delete(span);
		}
	}
}

#[cfg(test)]
mod tests {
	use crate::test_helpers::{assert_no_transform, assert_transform};
	use css_ast::{CssAtomSet, StyleSheet};

	#[test]
	fn removes_utf8_charset() {
		assert_transform!(
			CssMinifierFeature::ReduceCharsetRule,
			CssAtomSet,
			StyleSheet,
			"@charset \"UTF-8\";a{color:red}",
			"a{color:red}"
		);
	}

	#[test]
	fn removes_lowercase_utf8_charset() {
		assert_transform!(
			CssMinifierFeature::ReduceCharsetRule,
			CssAtomSet,
			StyleSheet,
			"@charset \"utf-8\";a{color:red}",
			"a{color:red}"
		);
	}

	#[test]
	fn removes_duplicate_charset() {
		assert_transform!(
			CssMinifierFeature::ReduceCharsetRule,
			CssAtomSet,
			StyleSheet,
			"@charset \"gbk\";@charset \"big5\";a{color:red}",
			"@charset \"gbk\";a{color:red}"
		);
	}

	#[test]
	fn keeps_shortest_leading_label() {
		assert_no_transform!(
			CssMinifierFeature::ReduceCharsetRule,
			CssAtomSet,
			StyleSheet,
			"@charset \"gbk\";a{color:red}"
		);
	}

	#[test]
	fn removes_uppercase_at_keyword() {
		assert_transform!(
			CssMinifierFeature::ReduceCharsetRule,
			CssAtomSet,
			StyleSheet,
			"@CHARSET \"gbk\";a{color:red}",
			"a{color:red}"
		);
	}

	#[test]
	fn removes_single_quoted_charset() {
		assert_transform!(
			CssMinifierFeature::ReduceCharsetRule,
			CssAtomSet,
			StyleSheet,
			"@charset 'gbk';a{color:red}",
			"a{color:red}"
		);
	}

	#[test]
	fn removes_escaped_charset() {
		assert_transform!(
			CssMinifierFeature::ReduceCharsetRule,
			CssAtomSet,
			StyleSheet,
			"@charset \"\\67 bk\";a{color:red}",
			"a{color:red}"
		);
	}

	#[test]
	fn shortens_label_to_shortest_alias() {
		assert_transform!(
			CssMinifierFeature::ReduceCharsetRule,
			CssAtomSet,
			StyleSheet,
			"@charset \"ISO-8859-1\";a{color:red}",
			"@charset \"l1\";a{color:red}"
		);
	}

	#[test]
	fn removes_utf16_charset() {
		assert_transform!(
			CssMinifierFeature::ReduceCharsetRule,
			CssAtomSet,
			StyleSheet,
			"@charset \"utf-16be\";a{color:red}",
			"a{color:red}"
		);
	}

	#[test]
	fn removes_unknown_label_charset() {
		assert_transform!(
			CssMinifierFeature::ReduceCharsetRule,
			CssAtomSet,
			StyleSheet,
			"@charset \"not-a-charset\";a{color:red}",
			"a{color:red}"
		);
	}

	#[test]
	fn moves_trailing_charset_to_top() {
		assert_transform!(
			CssMinifierFeature::ReduceCharsetRule,
			CssAtomSet,
			StyleSheet,
			"a{color:red}@charset \"gbk\";",
			"@charset \"gbk\";a{color:red}"
		);
	}

	#[test]
	fn moves_and_shortens_trailing_charset() {
		assert_transform!(
			CssMinifierFeature::ReduceCharsetRule,
			CssAtomSet,
			StyleSheet,
			"a{color:red}@charset \"ISO-8859-1\";",
			"@charset \"l1\";a{color:red}"
		);
	}

	#[test]
	fn moves_first_charset_to_top_and_drops_rest() {
		assert_transform!(
			CssMinifierFeature::ReduceCharsetRule,
			CssAtomSet,
			StyleSheet,
			".foo{color:red}@charset \"gbk\";.bar{color:tan}@charset \"big5\";.baz{background:red}",
			"@charset \"gbk\";.foo{color:red}.bar{color:tan}.baz{background:red}"
		);
	}

	#[test]
	fn removes_trailing_utf8_charset() {
		assert_transform!(
			CssMinifierFeature::ReduceCharsetRule,
			CssAtomSet,
			StyleSheet,
			"a{color:red}@charset \"UTF-8\";",
			"a{color:red}"
		);
	}
}