css_parse/
cursor_compact_write_sink.rs

1use crate::{Cursor, CursorSink, Kind, KindSet, QuoteStyle, SourceCursor, SourceCursorSink, Token};
2
3/// This is a [CursorSink] that wraps a sink (`impl SourceCursorSink`) and on each [CursorSink::append()] call, will write
4/// the contents of the cursor [Cursor] given into the given sink - using the given `&'a str` as the original source.
5/// Some tokens will not be output, and Whitespace tokens will always write out as a single `' '`. It can be used as a
6/// light-weight minifier for ToCursors structs.
7pub struct CursorCompactWriteSink<'a, T: SourceCursorSink<'a>> {
8	source_text: &'a str,
9	sink: T,
10	last_token: Option<Token>,
11	pending: Option<SourceCursor<'a>>,
12}
13
14const PENDING_KINDSET: KindSet = KindSet::new(&[Kind::Semicolon, Kind::Whitespace]);
15const REDUNDANT_SEMI_KINDSET: KindSet = KindSet::new(&[Kind::Semicolon, Kind::Colon, Kind::RightCurly]);
16const REDUNDANT_WHITESPACE_KINDSET: KindSet =
17	KindSet::new(&[Kind::Whitespace, Kind::Colon, Kind::Delim, Kind::LeftCurly, Kind::RightCurly]);
18
19impl<'a, T: SourceCursorSink<'a>> CursorCompactWriteSink<'a, T> {
20	pub fn new(source_text: &'a str, sink: T) -> Self {
21		Self { source_text, sink, last_token: None, pending: None }
22	}
23
24	fn write(&mut self, c: SourceCursor<'a>) {
25		if let Some(prev) = self.pending {
26			self.pending = None;
27			let is_redundant_semi = prev == Kind::Semicolon
28				&& (c == REDUNDANT_SEMI_KINDSET || self.last_token.is_some_and(|c| c == REDUNDANT_SEMI_KINDSET));
29			let is_redundant_whitespace = self.last_token.is_none()
30				|| prev == Kind::Whitespace
31					&& (c == REDUNDANT_WHITESPACE_KINDSET
32						|| self.last_token.is_some_and(|c| c == REDUNDANT_WHITESPACE_KINDSET));
33			if !is_redundant_semi && !is_redundant_whitespace {
34				self.last_token = Some(prev.token());
35				if prev == Kind::Whitespace {
36					// Whitespace can be minimised to a single space
37					self.sink.append(SourceCursor::SPACE);
38				} else {
39					self.sink.append(prev);
40				}
41			}
42		}
43		if c == PENDING_KINDSET {
44			self.pending = Some(c);
45			return;
46		}
47		if let Some(last) = self.last_token {
48			if last.needs_separator_for(c.token()) {
49				self.sink.append(SourceCursor::SPACE);
50			}
51		}
52		self.last_token = Some(c.token());
53		// Normalize quotes
54		if c == Kind::String {
55			self.sink.append(c.with_quotes(QuoteStyle::Double))
56		} else {
57			self.sink.append(c);
58		}
59	}
60}
61
62impl<'a, T: SourceCursorSink<'a>> CursorSink for CursorCompactWriteSink<'a, T> {
63	fn append(&mut self, c: Cursor) {
64		self.write(SourceCursor::from(c, c.str_slice(self.source_text)))
65	}
66}
67
68impl<'a, T: SourceCursorSink<'a>> SourceCursorSink<'a> for CursorCompactWriteSink<'a, T> {
69	fn append(&mut self, c: SourceCursor<'a>) {
70		self.write(c)
71	}
72}
73
74#[cfg(test)]
75mod test {
76	use super::*;
77	use crate::{ComponentValues, ToCursors, parse};
78	use bumpalo::Bump;
79
80	macro_rules! assert_format {
81		($before: literal, $after: literal) => {
82			assert_format!(ComponentValues, $before, $after);
83		};
84		($struct: ident, $before: literal, $after: literal) => {
85			let source_text = $before;
86			let bump = Bump::default();
87			let mut sink = String::new();
88			let mut stream = CursorCompactWriteSink::new(source_text, &mut sink);
89			parse!(in bump &source_text as $struct).output.unwrap().to_cursors(&mut stream);
90			assert_eq!(sink, $after.trim());
91		};
92	}
93
94	#[test]
95	fn test_basic() {
96		assert_format!("foo{bar: baz();}", r#"foo{bar:baz()}"#);
97	}
98
99	#[test]
100	fn test_removes_redundant_semis() {
101		assert_format!("foo{bar: 1;;;;bing: 2;;;}", r#"foo{bar:1;bing:2}"#);
102	}
103
104	#[test]
105	fn normalizes_quotes() {
106		assert_format!("bar:'baz';bing:'quux';x:url('foo')", r#"bar:"baz";bing:"quux";x:url("foo")"#);
107	}
108
109	#[test]
110	fn test_does_not_ignore_whitespace_component_values() {
111		assert_format!("div dialog:modal > td p a", "div dialog:modal > td p a");
112	}
113
114	#[test]
115	fn test_compacts_whitespace() {
116		assert_format!(
117			r#"
118		body   >   div {
119			bar:  baz
120		}
121		"#,
122			"body > div{bar:baz}"
123		);
124	}
125
126	#[test]
127	fn test_does_not_compact_whitespace_resulting_in_new_ident() {
128		assert_format!("12px - 1px", "12px - 1px");
129	}
130}