Skip to main content

csskit_transform/
reduce_urls.rs

1use crate::prelude::*;
2use css_ast::{CssTypes, Url, UrlOrString, VisitNode, Visitable};
3use css_parse::format_in;
4
5pub struct ReduceUrls<'a, 'ctx, N: Visitable + NodeWithMetadata<CssMetadata>> {
6	pub transformer: &'ctx Transformer<'a, CssMetadata, N, CssMinifierFeature>,
7}
8
9impl<'a, 'ctx, N> Transform<'a, 'ctx, CssMetadata, N, CssMinifierFeature> for ReduceUrls<'a, 'ctx, N>
10where
11	N: Visitable + NodeWithMetadata<CssMetadata>,
12{
13	fn skips_subtree(metadata: &CssMetadata) -> bool {
14		!metadata.has_value_kinds(CssTypes::Url)
15	}
16
17	fn new(transformer: &'ctx Transformer<'a, CssMetadata, N, CssMinifierFeature>) -> Self {
18		Self { transformer }
19	}
20}
21
22#[visitor]
23impl<'a, 'ctx, N> Visit for ReduceUrls<'a, 'ctx, N>
24where
25	N: Visitable + NodeWithMetadata<CssMetadata>,
26{
27	fn consider_node(&self, node: VisitNode) -> VisitFlow {
28		if node.node_id.is_some() && Self::skips_subtree(&node.subtree_metadata()) {
29			return VisitFlow::SKIP_CHILDREN;
30		}
31		VisitFlow::DESCEND
32	}
33
34	fn visit_url_or_string(&mut self, url_or_string: &UrlOrString) {
35		let UrlOrString::Url(url) = url_or_string else {
36			return;
37		};
38		let arena = self.transformer.alloc();
39		match url {
40			Url::UrlFunction(_, string, _) | Url::SrcFunction(_, string, _) => {
41				let sc = self.transformer.to_source_cursor((*string).into());
42				let token = sc.token();
43				let start = token.leading_len() as usize;
44				let end = sc.source().len() - token.trailing_len() as usize;
45				self.transformer.replace_parsed::<UrlOrString>(
46					url_or_string.to_span(),
47					format_in!(in arena, "\"{}\"", &sc.source()[start..end]).into_str(),
48				);
49			}
50			Url::Url(url_token) => {
51				let sc = self.transformer.to_source_cursor((*url_token).into());
52				let token = sc.token();
53				let leading_len = token.leading_len() as usize;
54				let trailing_len = token.trailing_len() as usize;
55				let url_content = &sc.source()[leading_len..(sc.source().len() - trailing_len)];
56				let url_content = url_content.trim();
57				self.transformer.replace_parsed::<UrlOrString>(
58					url_or_string.to_span(),
59					format_in!(in arena, "\"{url_content}\"").into_str(),
60				);
61			}
62		}
63	}
64}
65
66#[cfg(test)]
67mod tests {
68	use crate::test_helpers::{assert_no_transform, assert_transform};
69	use css_ast::{CssAtomSet, StyleSheet};
70
71	#[test]
72	fn reduces_url_function_to_string_in_import() {
73		assert_transform!(
74			CssMinifierFeature::ReduceUrls,
75			CssAtomSet,
76			StyleSheet,
77			"@import url(\"foo.css\");",
78			"@import \"foo.css\";"
79		);
80	}
81
82	#[test]
83	fn reduces_url_function_single_quotes_to_string_in_import() {
84		assert_transform!(
85			CssMinifierFeature::ReduceUrls,
86			CssAtomSet,
87			StyleSheet,
88			"@import url('foo.css');",
89			"@import \"foo.css\";"
90		);
91	}
92
93	#[test]
94	fn reduces_bare_url_to_string_in_import() {
95		assert_transform!(
96			CssMinifierFeature::ReduceUrls,
97			CssAtomSet,
98			StyleSheet,
99			"@import url(foo.css);",
100			"@import \"foo.css\";"
101		);
102	}
103
104	#[test]
105	fn reduces_url_with_media_query() {
106		assert_transform!(
107			CssMinifierFeature::ReduceUrls,
108			CssAtomSet,
109			StyleSheet,
110			"@import url(\"foo.css\") screen;",
111			"@import \"foo.css\" screen;"
112		);
113	}
114
115	#[test]
116	fn reduces_url_with_layer() {
117		assert_transform!(
118			CssMinifierFeature::ReduceUrls,
119			CssAtomSet,
120			StyleSheet,
121			"@import url(\"foo.css\") layer;",
122			"@import \"foo.css\" layer;"
123		);
124	}
125
126	#[test]
127	fn reduces_url_with_supports() {
128		assert_transform!(
129			CssMinifierFeature::ReduceUrls,
130			CssAtomSet,
131			StyleSheet,
132			"@import url(\"foo.css\") supports(not (display: flex));",
133			"@import \"foo.css\" supports(not (display: flex));"
134		);
135	}
136
137	#[test]
138	fn reduces_url_with_layer_and_supports() {
139		assert_transform!(
140			CssMinifierFeature::ReduceUrls,
141			CssAtomSet,
142			StyleSheet,
143			"@import url(\"foo.css\") layer(base) supports(not (display: flex));",
144			"@import \"foo.css\" layer(base) supports(not (display: flex));"
145		);
146	}
147
148	#[test]
149	fn does_not_transform_bare_string() {
150		assert_no_transform!(CssMinifierFeature::ReduceUrls, CssAtomSet, StyleSheet, "@import \"foo.css\";");
151	}
152
153	#[test]
154	fn reduces_namespace_url_to_string() {
155		assert_transform!(
156			CssMinifierFeature::ReduceUrls,
157			CssAtomSet,
158			StyleSheet,
159			"@namespace url(\"http://www.w3.org/1999/xhtml\");",
160			"@namespace \"http://www.w3.org/1999/xhtml\";"
161		);
162	}
163
164	#[test]
165	fn reduces_namespace_url_with_prefix() {
166		assert_transform!(
167			CssMinifierFeature::ReduceUrls,
168			CssAtomSet,
169			StyleSheet,
170			"@namespace html url(\"http://www.w3.org/1999/xhtml\");",
171			"@namespace html \"http://www.w3.org/1999/xhtml\";"
172		);
173	}
174}