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}