css_parse/
cursor_pretty_write_sink.rs

1use crate::{
2	AssociatedWhitespaceRules, Cursor, CursorSink, Kind, KindSet, QuoteStyle, SourceCursor, SourceCursorSink, Token,
3	Whitespace,
4};
5
6/// This is a [CursorSink] that wraps a sink (`impl SourceCursorSink`) and on each [CursorSink::append()] call, will write
7/// the contents of the cursor [Cursor] given into the given Writer - using the given `&'a str` as the original source.
8/// This also attempts to write additional newlines and indentation into the Writer to create a more aesthetically
9/// pleasing output. It can be used as a light-weight formatter for ToCursors structs.
10pub 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		// CSS demands it
32		first.needs_separator_for(second)
33		// It's a kind which might like some space around it.
34		|| (second != Kind::Whitespace && (first == SPACE_AFTER_KINDSET || first == '>' || first == '<' || first == '+' || first == '-'))
35	}
36
37	fn space_after(first: Token, second: Token) -> bool {
38		// It's a kind which might like some space around it.
39		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			// Don't create a newline for kinds that don't need one!
47			first != NEWLINE_AFTER_KINDSET ||
48			// Don't create a newline between `{}` with no inner content.
49			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		// Normalize quotes
81		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}