css_parse/
cursor_expanded_write_sink.rs

1use crate::{Cursor, CursorSink, Kind, 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/// Tokens will be expanded to their most verbose form. It can be used as an "anti-minifier" for ToCursors structs.
6pub struct CursorExpandedWriteSink<'a, T: SourceCursorSink<'a>> {
7	source_text: &'a str,
8	sink: T,
9	last_token: Option<Token>,
10	extra_semicolons: u8,
11	escape_idents: bool,
12}
13
14impl<'a, T: SourceCursorSink<'a>> CursorExpandedWriteSink<'a, T> {
15	pub fn new(source_text: &'a str, sink: T) -> Self {
16		Self { source_text, sink, last_token: None, extra_semicolons: 0, escape_idents: false }
17	}
18
19	/// Set the number of extra semicolons to add after each semicolon.
20	pub fn with_extra_semicolons(mut self, count: u8) -> Self {
21		self.extra_semicolons = count;
22		self
23	}
24
25	/// Disable ident escaping for a milder expansion effect.
26	pub fn with_escape_idents(mut self, escape: bool) -> Self {
27		self.escape_idents = escape;
28		self
29	}
30
31	fn write(&mut self, c: SourceCursor<'a>) {
32		if let Some(last) = self.last_token
33			&& last.needs_separator_for(c.token())
34		{
35			self.sink.append(SourceCursor::SPACE);
36		}
37		self.last_token = Some(c.token());
38		let is_ident_like =
39			matches!(c.token().kind(), Kind::Ident | Kind::Function | Kind::AtKeyword | Kind::Hash | Kind::String);
40		let should_expand = matches!(c.token().kind(), Kind::Whitespace | Kind::Number | Kind::Dimension | Kind::Url)
41			|| (is_ident_like && self.escape_idents);
42		if should_expand {
43			self.sink.append(c.expand());
44		} else {
45			self.sink.append(c);
46		}
47		// Add extra semicolons after each semicolon
48		if c == Kind::Semicolon {
49			for _ in 0..self.extra_semicolons {
50				self.sink.append(SourceCursor::SEMICOLON);
51			}
52		}
53	}
54}
55
56impl<'a, T: SourceCursorSink<'a>> CursorSink for CursorExpandedWriteSink<'a, T> {
57	fn append(&mut self, c: Cursor) {
58		self.write(SourceCursor::from(c, c.str_slice(self.source_text)))
59	}
60}
61
62impl<'a, T: SourceCursorSink<'a>> SourceCursorSink<'a> for CursorExpandedWriteSink<'a, T> {
63	fn append(&mut self, c: SourceCursor<'a>) {
64		self.write(c)
65	}
66}
67
68#[cfg(test)]
69mod test {
70	use super::*;
71	use crate::{ComponentValues, EmptyAtomSet, Parser, ToCursors};
72	use bumpalo::Bump;
73	use css_lexer::Lexer;
74
75	macro_rules! assert_expand {
76		($before: literal, $after: literal) => {
77			assert_expand!(ComponentValues, $before, $after);
78		};
79		($struct: ident, $before: literal, $after: literal) => {
80			let source_text = $before;
81			let bump = Bump::default();
82			let mut sink = String::new();
83			let mut stream = CursorExpandedWriteSink::new(source_text, &mut sink).with_escape_idents(true);
84			let lexer = Lexer::new(&EmptyAtomSet::ATOMS, source_text);
85			let mut parser = Parser::new(&bump, source_text, lexer);
86			parser.parse_entirely::<$struct>().output.unwrap().to_cursors(&mut stream);
87			assert_eq!(sink, $after);
88		};
89	}
90
91	#[test]
92	fn test_basic() {
93		// foo -> \000066 \00006f \00006f (f=0x66, o=0x6f)
94		// bar -> \000062 \000061 \000072 (b=0x62, a=0x61, r=0x72)
95		// baz -> \000062 \000061 \00007a (z=0x7a)
96		assert_expand!(
97			"foo{bar: baz();}",
98			r#"\000066 \00006f \00006f {\000062 \000061 \000072 :    \000062 \000061 \00007a ();}"#
99		);
100	}
101
102	#[test]
103	fn test_expands_numbers() {
104		// opacity -> o=0x6f p=0x70 a=0x61 c=0x63 i=0x69 t=0x74 y=0x79
105		assert_expand!(
106			"opacity: 0.8",
107			r#"\00006f \000070 \000061 \000063 \000069 \000074 \000079 :    +8.00000000000000e-0000000001"#
108		);
109		assert_expand!(
110			"opacity: .5",
111			r#"\00006f \000070 \000061 \000063 \000069 \000074 \000079 :    +5.00000000000000e-0000000001"#
112		);
113		// px -> p=0x70 x=0x78
114		assert_expand!(
115			"width: 1.0px",
116			r#"\000077 \000069 \000064 \000074 \000068 :    +1.00000000000000e+0000000000\000070 \000078 "#
117		);
118	}
119
120	#[test]
121	fn test_expands_whitespace() {
122		// a=0x61, b=0x62
123		assert_expand!("a b", r#"\000061     \000062 "#);
124	}
125
126	#[test]
127	fn test_expands_url() {
128		assert_expand!("url(foo.png)", r#"url(   foo.png   )"#);
129	}
130
131	#[test]
132	fn test_expands_strings() {
133		// h=0x68 e=0x65 l=0x6c o=0x6f
134		assert_expand!(
135			r#"content: "hello""#,
136			r#"\000063 \00006f \00006e \000074 \000065 \00006e \000074 :    '\000068 \000065 \00006c \00006c \00006f '"#
137		);
138	}
139}