css_parse/
cursor_pretty_write_sink.rs1use crate::{
2 AssociatedWhitespaceRules, Cursor, CursorSink, Kind, KindSet, QuoteStyle, SourceCursor, SourceCursorSink, Token,
3 Whitespace,
4};
5
6pub struct CursorPrettyWriteSink<'a, T: SourceCursorSink<'a>> {
11 source_text: &'a str,
12 sink: T,
13 last_token: Option<Token>,
14 indent_level: u8,
15 expand_tab: Option<u8>,
16 quotes: QuoteStyle,
17}
18
19const SPACE_AFTER_KINDSET: KindSet = KindSet::new(&[Kind::Comma]);
20const SPACE_BEFORE_KINDSET: KindSet = KindSet::new(&[Kind::LeftCurly]);
21const NEWLINE_AFTER_KINDSET: KindSet = KindSet::new(&[Kind::LeftCurly, Kind::RightCurly, Kind::Semicolon]);
22const INCREASE_INDENT_LEVEL_KINDSET: KindSet = KindSet::new(&[Kind::LeftCurly]);
23const DECREASE_INDENT_LEVEL_KINDSET: KindSet = KindSet::new(&[Kind::RightCurly]);
24
25impl<'a, T: SourceCursorSink<'a>> CursorPrettyWriteSink<'a, T> {
26 pub fn new(source_text: &'a str, sink: T, expand_tab: Option<u8>, quotes: QuoteStyle) -> Self {
27 Self { source_text, sink, last_token: None, indent_level: 0, expand_tab, quotes }
28 }
29
30 fn space_before(first: Token, second: Token) -> bool {
31 first.needs_separator_for(second)
33 || (second != Kind::Whitespace && (first == SPACE_AFTER_KINDSET || first == '>' || first == '<' || first == '+' || first == '-'))
35 }
36
37 fn space_after(first: Token, second: Token) -> bool {
38 first != Kind::Whitespace
40 && first != AssociatedWhitespaceRules::BanAfter
41 && (second == SPACE_BEFORE_KINDSET || second == '>' || second == '<')
42 }
43
44 fn newline_after(first: Token, second: Token) -> bool {
45 !(
46 first != NEWLINE_AFTER_KINDSET ||
48 first == '{' && second == '}'
50 )
51 }
52
53 fn write(&mut self, c: SourceCursor<'a>) {
54 let token = c.token();
55 if token == INCREASE_INDENT_LEVEL_KINDSET {
56 self.indent_level += 1;
57 } else if token == DECREASE_INDENT_LEVEL_KINDSET && self.indent_level > 0 {
58 self.indent_level -= 1;
59 }
60 if let Some(last) = self.last_token {
61 if Self::newline_after(last, token) {
62 self.sink.append(SourceCursor::NEWLINE);
63 }
64 if Self::newline_after(last, token)
65 || last == Kind::Whitespace && last.whitespace_style() == Whitespace::Newline
66 {
67 let (c, count) = if let Some(len) = self.expand_tab {
68 (SourceCursor::SPACE, self.indent_level * len)
69 } else {
70 (SourceCursor::TAB, self.indent_level)
71 };
72 for _ in 0..count {
73 self.sink.append(c);
74 }
75 } else if Self::space_before(last, token) || Self::space_after(last, token) {
76 self.sink.append(SourceCursor::SPACE);
77 }
78 }
79 self.last_token = Some(token);
80 if c.token() == Kind::String {
82 self.sink.append(c.with_quotes(self.quotes))
83 } else {
84 self.sink.append(c);
85 }
86 }
87}
88
89impl<'a, T: SourceCursorSink<'a>> CursorSink for CursorPrettyWriteSink<'a, T> {
90 fn append(&mut self, c: Cursor) {
91 self.write(SourceCursor::from(c, c.str_slice(self.source_text)))
92 }
93}
94
95impl<'a, T: SourceCursorSink<'a>> SourceCursorSink<'a> for CursorPrettyWriteSink<'a, T> {
96 fn append(&mut self, c: SourceCursor<'a>) {
97 self.write(c)
98 }
99}
100
101#[cfg(test)]
102mod test {
103 use super::*;
104 use crate::ToCursors;
105 use crate::{ComponentValues, EmptyAtomSet, Parser};
106 use bumpalo::Bump;
107 use css_lexer::Lexer;
108
109 macro_rules! assert_format {
110 ($struct: ident, $before: literal, $after: literal) => {
111 let source_text = $before;
112 let bump = Bump::default();
113 let mut sink = String::new();
114 let mut stream = CursorPrettyWriteSink::new(source_text, &mut sink, None, QuoteStyle::Double);
115 let lexer = Lexer::new(&EmptyAtomSet::ATOMS, source_text);
116 let mut parser = Parser::new(&bump, source_text, lexer);
117 parser.parse_entirely::<$struct>().output.unwrap().to_cursors(&mut stream);
118 assert_eq!(sink, $after.trim());
119 };
120 ($before: literal, $after: literal) => {
121 let source_text = $before;
122 let bump = Bump::default();
123 let mut sink = String::new();
124 let mut stream = CursorPrettyWriteSink::new(source_text, &mut sink, None, QuoteStyle::Double);
125 let lexer = Lexer::new(&EmptyAtomSet::ATOMS, source_text);
126 let mut parser = Parser::new(&bump, source_text, lexer);
127 parser.parse_entirely::<ComponentValues>().output.unwrap().to_cursors(&mut stream);
128 assert_eq!(sink, $after.trim());
129 };
130 }
131
132 #[test]
133 fn test_basic() {
134 assert_format!(
135 "foo{bar: baz();}",
136 r#"
137foo {
138 bar: baz();
139}
140"#
141 );
142 }
143
144 #[test]
145 fn test_does_not_repeat_whitespace() {
146 assert_format!(
147 "foo {bar: baz();}",
148 r#"
149foo {
150 bar: baz();
151}
152"#
153 );
154 }
155
156 #[test]
157 fn test_can_handle_nested_curlies() {
158 assert_format!(
159 "foo {bar{baz{bing{}}}}",
160 r#"
161foo {
162 bar {
163 baz {
164 bing {}
165 }
166 }
167}
168"#
169 );
170 }
171
172 #[test]
173 fn test_does_not_ignore_whitespace_in_selectors() {
174 assert_format!("div dialog:modal>td p a", "div dialog:modal > td p a");
175 }
176
177 #[test]
178 fn test_does_normalizes_quotes() {
179 assert_format!(
180 "foo[attr='bar']{baz:'bing';}",
181 r#"
182foo[attr="bar"] {
183 baz:"bing";
184}
185"#
186 );
187 }
188}