css_parse/
cursor_compact_write_sink.rs1use crate::{Cursor, CursorSink, Kind, KindSet, QuoteStyle, SourceCursor, SourceCursorSink, Token};
2
3pub 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 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 && last.needs_separator_for(c.token())
49 {
50 self.sink.append(SourceCursor::SPACE);
51 }
52 self.last_token = Some(c.token());
53 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, EmptyAtomSet, Parser, ToCursors};
78 use bumpalo::Bump;
79 use css_lexer::Lexer;
80
81 macro_rules! assert_format {
82 ($before: literal, $after: literal) => {
83 assert_format!(ComponentValues, $before, $after);
84 };
85 ($struct: ident, $before: literal, $after: literal) => {
86 let source_text = $before;
87 let bump = Bump::default();
88 let mut sink = String::new();
89 let mut stream = CursorCompactWriteSink::new(source_text, &mut sink);
90 let lexer = Lexer::new(&EmptyAtomSet::ATOMS, source_text);
91 let mut parser = Parser::new(&bump, source_text, lexer);
92 parser.parse_entirely::<$struct>().output.unwrap().to_cursors(&mut stream);
93 assert_eq!(sink, $after.trim());
94 };
95 }
96
97 #[test]
98 fn test_basic() {
99 assert_format!("foo{bar: baz();}", r#"foo{bar:baz()}"#);
100 }
101
102 #[test]
103 fn test_removes_redundant_semis() {
104 assert_format!("foo{bar: 1;;;;bing: 2;;;}", r#"foo{bar:1;bing:2}"#);
105 }
106
107 #[test]
108 fn normalizes_quotes() {
109 assert_format!("bar:'baz';bing:'quux';x:url('foo')", r#"bar:"baz";bing:"quux";x:url("foo")"#);
110 }
111
112 #[test]
113 fn test_does_not_ignore_whitespace_component_values() {
114 assert_format!("div dialog:modal > td p a", "div dialog:modal > td p a");
115 }
116
117 #[test]
118 fn test_compacts_whitespace() {
119 assert_format!(
120 r#"
121 body > div {
122 bar: baz
123 }
124 "#,
125 "body > div{bar:baz}"
126 );
127 }
128
129 #[test]
130 fn test_does_not_compact_whitespace_resulting_in_new_ident() {
131 assert_format!("12px - 1px", "12px - 1px");
132 }
133}