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, parse};
105 use bumpalo::Bump;
106
107 macro_rules! assert_format {
108 ($($struct: ident,)? $before: literal, $after: literal) => {
109 let source_text = $before;
110 let bump = Bump::default();
111 let mut sink = String::new();
112 let mut stream = CursorPrettyWriteSink::new(source_text, &mut sink, None, QuoteStyle::Double);
113 parse!(in bump &source_text $(as $struct)?).output.unwrap().to_cursors(&mut stream);
114 assert_eq!(sink, $after.trim());
115 };
116 }
117
118 #[test]
119 fn test_basic() {
120 assert_format!(
121 "foo{bar: baz();}",
122 r#"
123foo {
124 bar: baz();
125}
126"#
127 );
128 }
129
130 #[test]
131 fn test_does_not_repeat_whitespace() {
132 assert_format!(
133 "foo {bar: baz();}",
134 r#"
135foo {
136 bar: baz();
137}
138"#
139 );
140 }
141
142 #[test]
143 fn test_can_handle_nested_curlies() {
144 assert_format!(
145 "foo {bar{baz{bing{}}}}",
146 r#"
147foo {
148 bar {
149 baz {
150 bing {}
151 }
152 }
153}
154"#
155 );
156 }
157
158 #[test]
159 fn test_does_not_ignore_whitespace_in_selectors() {
160 assert_format!("div dialog:modal>td p a", "div dialog:modal > td p a");
161 }
162
163 #[test]
164 fn test_does_normalizes_quotes() {
165 assert_format!(
166 "foo[attr='bar']{baz:'bing';}",
167 r#"
168foo[attr="bar"] {
169 baz:"bing";
170}
171"#
172 );
173 }
174}