css_parse/
cursor_expanded_write_sink.rs1use crate::{Cursor, CursorSink, Kind, SourceCursor, SourceCursorSink, Token};
2
3pub struct CursorExpandedWriteSink<'a, T: SourceCursorSink<'a>> {
7 source_text: &'a str,
8 sink: T,
9 last_token: Option<Token>,
10 extra_semicolons: u8,
11 escape_idents: bool,
12}
13
14impl<'a, T: SourceCursorSink<'a>> CursorExpandedWriteSink<'a, T> {
15 pub fn new(source_text: &'a str, sink: T) -> Self {
16 Self { source_text, sink, last_token: None, extra_semicolons: 0, escape_idents: false }
17 }
18
19 pub fn with_extra_semicolons(mut self, count: u8) -> Self {
21 self.extra_semicolons = count;
22 self
23 }
24
25 pub fn with_escape_idents(mut self, escape: bool) -> Self {
27 self.escape_idents = escape;
28 self
29 }
30
31 fn write(&mut self, c: SourceCursor<'a>) {
32 if let Some(last) = self.last_token
33 && last.needs_separator_for(c.token())
34 {
35 self.sink.append(SourceCursor::SPACE);
36 }
37 self.last_token = Some(c.token());
38 let is_ident_like =
39 matches!(c.token().kind(), Kind::Ident | Kind::Function | Kind::AtKeyword | Kind::Hash | Kind::String);
40 let should_expand = matches!(c.token().kind(), Kind::Whitespace | Kind::Number | Kind::Dimension | Kind::Url)
41 || (is_ident_like && self.escape_idents);
42 if should_expand {
43 self.sink.append(c.expand());
44 } else {
45 self.sink.append(c);
46 }
47 if c == Kind::Semicolon {
49 for _ in 0..self.extra_semicolons {
50 self.sink.append(SourceCursor::SEMICOLON);
51 }
52 }
53 }
54}
55
56impl<'a, T: SourceCursorSink<'a>> CursorSink for CursorExpandedWriteSink<'a, T> {
57 fn append(&mut self, c: Cursor) {
58 self.write(SourceCursor::from(c, c.str_slice(self.source_text)))
59 }
60}
61
62impl<'a, T: SourceCursorSink<'a>> SourceCursorSink<'a> for CursorExpandedWriteSink<'a, T> {
63 fn append(&mut self, c: SourceCursor<'a>) {
64 self.write(c)
65 }
66}
67
68#[cfg(test)]
69mod test {
70 use super::*;
71 use crate::{ComponentValues, EmptyAtomSet, Parser, ToCursors};
72 use bumpalo::Bump;
73 use css_lexer::Lexer;
74
75 macro_rules! assert_expand {
76 ($before: literal, $after: literal) => {
77 assert_expand!(ComponentValues, $before, $after);
78 };
79 ($struct: ident, $before: literal, $after: literal) => {
80 let source_text = $before;
81 let bump = Bump::default();
82 let mut sink = String::new();
83 let mut stream = CursorExpandedWriteSink::new(source_text, &mut sink).with_escape_idents(true);
84 let lexer = Lexer::new(&EmptyAtomSet::ATOMS, source_text);
85 let mut parser = Parser::new(&bump, source_text, lexer);
86 parser.parse_entirely::<$struct>().output.unwrap().to_cursors(&mut stream);
87 assert_eq!(sink, $after);
88 };
89 }
90
91 #[test]
92 fn test_basic() {
93 assert_expand!(
97 "foo{bar: baz();}",
98 r#"\000066 \00006f \00006f {\000062 \000061 \000072 : \000062 \000061 \00007a ();}"#
99 );
100 }
101
102 #[test]
103 fn test_expands_numbers() {
104 assert_expand!(
106 "opacity: 0.8",
107 r#"\00006f \000070 \000061 \000063 \000069 \000074 \000079 : +8.00000000000000e-0000000001"#
108 );
109 assert_expand!(
110 "opacity: .5",
111 r#"\00006f \000070 \000061 \000063 \000069 \000074 \000079 : +5.00000000000000e-0000000001"#
112 );
113 assert_expand!(
115 "width: 1.0px",
116 r#"\000077 \000069 \000064 \000074 \000068 : +1.00000000000000e+0000000000\000070 \000078 "#
117 );
118 }
119
120 #[test]
121 fn test_expands_whitespace() {
122 assert_expand!("a b", r#"\000061 \000062 "#);
124 }
125
126 #[test]
127 fn test_expands_url() {
128 assert_expand!("url(foo.png)", r#"url( foo.png )"#);
129 }
130
131 #[test]
132 fn test_expands_strings() {
133 assert_expand!(
135 r#"content: "hello""#,
136 r#"\000063 \00006f \00006e \000074 \000065 \00006e \000074 : '\000068 \000065 \00006c \00006c \00006f '"#
137 );
138 }
139}