Skip to main content

csskit_transform/
reduce_charset_rule.rs

1use crate::prelude::*;
2use css_ast::{AtRuleId, AtomSet, CharsetRule, EncodingLabel, VisitNode, Visitable};
3use css_parse::{SourceOffset, format_in};
4
5pub struct ReduceCharsetRule<'a, 'ctx, N: Visitable + NodeWithMetadata<CssMetadata>> {
6	pub transformer: &'ctx Transformer<'a, CssMetadata, N, CssMinifierFeature>,
7	seen: bool,
8}
9
10impl<'a, 'ctx, N> Transform<'a, 'ctx, CssMetadata, N, CssMinifierFeature> for ReduceCharsetRule<'a, 'ctx, N>
11where
12	N: Visitable + NodeWithMetadata<CssMetadata>,
13{
14	fn skips_subtree(metadata: &CssMetadata) -> bool {
15		!metadata.used_at_rules.contains(AtRuleId::Charset)
16	}
17
18	fn new(transformer: &'ctx Transformer<'a, CssMetadata, N, CssMinifierFeature>) -> Self {
19		Self { transformer, seen: false }
20	}
21}
22
23#[visitor]
24impl<'a, 'ctx, N> Visit for ReduceCharsetRule<'a, 'ctx, N>
25where
26	N: Visitable + NodeWithMetadata<CssMetadata>,
27{
28	fn consider_node(&self, node: VisitNode) -> VisitFlow {
29		if node.node_id.is_some() && Self::skips_subtree(&node.subtree_metadata()) {
30			return VisitFlow::SKIP_CHILDREN;
31		}
32		VisitFlow::DESCEND
33	}
34
35	fn visit_charset_rule(&mut self, rule: &CharsetRule) {
36		let span = rule.to_span();
37		let source_text = self.transformer.source_text;
38		let encoding = rule.encoding(source_text);
39		if encoding == EncodingLabel::Unknown || self.seen {
40			self.transformer.delete(span);
41			return;
42		}
43		self.seen = true;
44		let label = rule.label(source_text);
45		let shortest = encoding.to_str();
46		let at_top = span.start() == SourceOffset(0);
47		if shortest == label {
48			if !at_top {
49				self.transformer.insert_before(SourceOffset(0), self.transformer.to_source_cursors(rule));
50				self.transformer.delete(span);
51			}
52			return;
53		}
54		let arena = self.transformer.alloc();
55		let css = format_in!(in arena, "@charset \"{shortest}\";").into_str();
56		if at_top {
57			self.transformer.replace_parsed::<CharsetRule>(span, css);
58		} else {
59			self.transformer.insert_before(SourceOffset(0), self.transformer.parse_value::<CharsetRule>(css));
60			self.transformer.delete(span);
61		}
62	}
63}
64
65#[cfg(test)]
66mod tests {
67	use crate::test_helpers::{assert_no_transform, assert_transform};
68	use css_ast::{CssAtomSet, StyleSheet};
69
70	#[test]
71	fn removes_utf8_charset() {
72		assert_transform!(
73			CssMinifierFeature::ReduceCharsetRule,
74			CssAtomSet,
75			StyleSheet,
76			"@charset \"UTF-8\";a{color:red}",
77			"a{color:red}"
78		);
79	}
80
81	#[test]
82	fn removes_lowercase_utf8_charset() {
83		assert_transform!(
84			CssMinifierFeature::ReduceCharsetRule,
85			CssAtomSet,
86			StyleSheet,
87			"@charset \"utf-8\";a{color:red}",
88			"a{color:red}"
89		);
90	}
91
92	#[test]
93	fn removes_duplicate_charset() {
94		assert_transform!(
95			CssMinifierFeature::ReduceCharsetRule,
96			CssAtomSet,
97			StyleSheet,
98			"@charset \"gbk\";@charset \"big5\";a{color:red}",
99			"@charset \"gbk\";a{color:red}"
100		);
101	}
102
103	#[test]
104	fn keeps_shortest_leading_label() {
105		assert_no_transform!(
106			CssMinifierFeature::ReduceCharsetRule,
107			CssAtomSet,
108			StyleSheet,
109			"@charset \"gbk\";a{color:red}"
110		);
111	}
112
113	#[test]
114	fn removes_uppercase_at_keyword() {
115		assert_transform!(
116			CssMinifierFeature::ReduceCharsetRule,
117			CssAtomSet,
118			StyleSheet,
119			"@CHARSET \"gbk\";a{color:red}",
120			"a{color:red}"
121		);
122	}
123
124	#[test]
125	fn removes_single_quoted_charset() {
126		assert_transform!(
127			CssMinifierFeature::ReduceCharsetRule,
128			CssAtomSet,
129			StyleSheet,
130			"@charset 'gbk';a{color:red}",
131			"a{color:red}"
132		);
133	}
134
135	#[test]
136	fn removes_escaped_charset() {
137		assert_transform!(
138			CssMinifierFeature::ReduceCharsetRule,
139			CssAtomSet,
140			StyleSheet,
141			"@charset \"\\67 bk\";a{color:red}",
142			"a{color:red}"
143		);
144	}
145
146	#[test]
147	fn shortens_label_to_shortest_alias() {
148		assert_transform!(
149			CssMinifierFeature::ReduceCharsetRule,
150			CssAtomSet,
151			StyleSheet,
152			"@charset \"ISO-8859-1\";a{color:red}",
153			"@charset \"l1\";a{color:red}"
154		);
155	}
156
157	#[test]
158	fn removes_utf16_charset() {
159		assert_transform!(
160			CssMinifierFeature::ReduceCharsetRule,
161			CssAtomSet,
162			StyleSheet,
163			"@charset \"utf-16be\";a{color:red}",
164			"a{color:red}"
165		);
166	}
167
168	#[test]
169	fn removes_unknown_label_charset() {
170		assert_transform!(
171			CssMinifierFeature::ReduceCharsetRule,
172			CssAtomSet,
173			StyleSheet,
174			"@charset \"not-a-charset\";a{color:red}",
175			"a{color:red}"
176		);
177	}
178
179	#[test]
180	fn moves_trailing_charset_to_top() {
181		assert_transform!(
182			CssMinifierFeature::ReduceCharsetRule,
183			CssAtomSet,
184			StyleSheet,
185			"a{color:red}@charset \"gbk\";",
186			"@charset \"gbk\";a{color:red}"
187		);
188	}
189
190	#[test]
191	fn moves_and_shortens_trailing_charset() {
192		assert_transform!(
193			CssMinifierFeature::ReduceCharsetRule,
194			CssAtomSet,
195			StyleSheet,
196			"a{color:red}@charset \"ISO-8859-1\";",
197			"@charset \"l1\";a{color:red}"
198		);
199	}
200
201	#[test]
202	fn moves_first_charset_to_top_and_drops_rest() {
203		assert_transform!(
204			CssMinifierFeature::ReduceCharsetRule,
205			CssAtomSet,
206			StyleSheet,
207			".foo{color:red}@charset \"gbk\";.bar{color:tan}@charset \"big5\";.baz{background:red}",
208			"@charset \"gbk\";.foo{color:red}.bar{color:tan}.baz{background:red}"
209		);
210	}
211
212	#[test]
213	fn removes_trailing_utf8_charset() {
214		assert_transform!(
215			CssMinifierFeature::ReduceCharsetRule,
216			CssAtomSet,
217			StyleSheet,
218			"a{color:red}@charset \"UTF-8\";",
219			"a{color:red}"
220		);
221	}
222}