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;
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}