1use css_lexer::{AtomSet, Cursor, DynAtomSet, KindSet, Lexer, ToSpan};
2use css_parse::Vec;
3use css_parse::{
4 Arena, CursorOverlaySet, CursorToSourceCursorSink, NodeMetadata, NodeWithMetadata, OverlayKind, OverlaySegment,
5 Parse, Parser, SourceCursor, SourceOffset, Span, ToCursors,
6};
7use std::{cell::RefCell, marker::PhantomData};
8
9#[derive(Debug)]
10pub enum TransformEdit<'a> {
11 Replace { target: Span, cursors: Vec<'a, SourceCursor<'a>> },
12 InsertBefore { anchor: SourceOffset, cursors: Vec<'a, SourceCursor<'a>> },
13 InsertAfter { anchor: SourceOffset, cursors: Vec<'a, SourceCursor<'a>> },
14 Delete { target: Span },
15}
16
17#[derive(Debug)]
18pub enum CommitError {
19 OverlappingEdit { previous: Span, new: Span },
20 InvalidEdit { span: Span },
21}
22
23struct PendingSegment<'a> {
24 span: Span,
25 intent: OverlayKind,
26 order: usize,
27 cursors: Vec<'a, SourceCursor<'a>>,
28}
29
30pub trait TransformerFeatures<M, N>: Sized + Default + Copy {
31 fn transforms<'a, 'ctx>(self, transformer: &'ctx Transformer<'a, M, N, Self>, node: &N)
32 where
33 M: NodeMetadata,
34 N: NodeWithMetadata<M>;
35}
36
37pub struct Transformer<'a, M: NodeMetadata, N: NodeWithMetadata<M>, F: TransformerFeatures<M, N>> {
38 alloc: &'a Arena,
39 atoms: &'static dyn DynAtomSet,
40 pub(crate) features: F,
41 changed: RefCell<bool>,
42 overlays: RefCell<CursorOverlaySet<'a>>,
43 edits: RefCell<Vec<'a, TransformEdit<'a>>>,
44 pub(crate) source_text: &'a str,
45 _phantom: PhantomData<(M, N)>,
46}
47
48impl<'a, M: NodeMetadata, N: NodeWithMetadata<M>, F: TransformerFeatures<M, N>> Transformer<'a, M, N, F> {
49 pub fn new_in(alloc: &'a Arena, features: F, atoms: &'static dyn DynAtomSet, source_text: &'a str) -> Self {
50 Self {
51 alloc,
52 features,
53 atoms,
54 changed: RefCell::new(false),
55 overlays: RefCell::new(CursorOverlaySet::new(alloc)),
56 edits: RefCell::new(Vec::new_in(alloc)),
57 source_text,
58 _phantom: PhantomData,
59 }
60 }
61
62 pub fn transform(&mut self, node: &mut N) {
63 self.reset();
64 self.features.transforms(self, node);
65 if let Err(err) = self.commit_overlays() {
66 panic!("Transform overlays commit failed: {:?}", err);
67 }
68 }
69
70 pub fn has_changed(&self) -> bool {
71 *self.changed.borrow()
72 }
73
74 pub fn alloc(&self) -> &'a Arena {
75 self.alloc
76 }
77
78 pub fn to_source_cursor(&self, cursor: Cursor) -> SourceCursor<'a> {
79 SourceCursor::from(cursor, cursor.str_slice(self.source_text))
80 }
81
82 pub fn to_source_cursors(&self, parsed: &impl ToCursors) -> Vec<'a, SourceCursor<'a>> {
83 let mut cursors = Vec::new_in(self.alloc());
84 let mut sink = CursorToSourceCursorSink::new(self.source_text, &mut cursors);
85 parsed.to_cursors(&mut sink);
86 cursors
87 }
88
89 pub fn to_atom<A: AtomSet + PartialEq>(&self, c: Cursor) -> A {
90 let bits = c.token().atom_bits();
91 if bits == 0 {
92 if c != KindSet::ATOM_LIKE {
93 return A::from_bits(0);
94 }
95 let source_cursor = self.to_source_cursor(c);
96 return A::from_str(&source_cursor.parse(self.alloc));
97 }
98 A::from_bits(bits)
99 }
100
101 pub fn overlays(&self) -> std::cell::Ref<'_, CursorOverlaySet<'a>> {
102 self.overlays.borrow()
103 }
104
105 pub fn parse_value<T>(&self, source: &'a str) -> Vec<'a, SourceCursor<'a>>
106 where
107 T: Parse<'a> + ToCursors,
108 {
109 let lexer = Lexer::new(self.atoms, source);
110 let mut parser = Parser::new(self.alloc, source, lexer);
111 let parsed = parser.parse_entirely::<T>();
112 debug_assert!(
113 parsed.output.is_some(),
114 "Transformer::parse_value failed to parse {:?}: {:?}",
115 source,
116 parsed.errors
117 );
118 let mut cursors = Vec::new_in(self.alloc());
119 let mut sink = CursorToSourceCursorSink::new(source, &mut cursors);
120 parsed.to_cursors(&mut sink);
121 cursors
122 }
123
124 pub fn reset(&self) {
125 *self.changed.borrow_mut() = false;
126 self.overlays.borrow_mut().clear();
127 self.edits.borrow_mut().clear();
128 }
129
130 pub fn has_replacement(&self, span: impl ToSpan) -> bool {
131 self.overlays.borrow().has_overlay(span.to_span())
132 }
133
134 pub fn clear_pending_edits(&self, span: Span) -> bool {
135 let mut edits = self.edits.borrow_mut();
136 let len_before = edits.len();
137 edits.retain(|edit| {
138 let inner = match edit {
139 TransformEdit::Replace { target, .. } | TransformEdit::Delete { target } => *target,
140 TransformEdit::InsertBefore { anchor, .. } | TransformEdit::InsertAfter { anchor, .. } => {
141 Span::new(*anchor, *anchor)
142 }
143 };
144 !(span.start() <= inner.start() && inner.end() <= span.end())
145 });
146 len_before != edits.len()
147 }
148
149 pub fn replace(&self, span: impl ToSpan, cursors: Vec<'a, SourceCursor<'a>>) {
150 let span = span.to_span();
151 debug_assert!(span.start() <= span.end(), "Transformer::replace received invalid span: {:?}", span);
152 *self.changed.borrow_mut() = true;
153 self.edits.borrow_mut().push(TransformEdit::Replace { target: span, cursors });
154 }
155
156 pub fn delete(&self, span: impl ToSpan) {
157 let span = span.to_span();
158 debug_assert!(span.start() <= span.end(), "Transformer::delete received invalid span: {:?}", span);
159 *self.changed.borrow_mut() = true;
160 self.edits.borrow_mut().push(TransformEdit::Delete { target: span });
161 }
162
163 pub fn insert_before(&self, anchor: SourceOffset, cursors: Vec<'a, SourceCursor<'a>>) {
164 *self.changed.borrow_mut() = true;
165 self.edits.borrow_mut().push(TransformEdit::InsertBefore { anchor, cursors });
166 }
167
168 pub fn insert_after(&self, anchor: SourceOffset, cursors: Vec<'a, SourceCursor<'a>>) {
169 *self.changed.borrow_mut() = true;
170 self.edits.borrow_mut().push(TransformEdit::InsertAfter { anchor, cursors });
171 }
172
173 pub fn replace_parsed<T>(&self, span: impl ToSpan, css: &'a str)
174 where
175 T: Parse<'a> + ToCursors,
176 {
177 self.replace(span, self.parse_value::<T>(css));
178 }
179
180 pub fn commit_overlays(&self) -> Result<(), CommitError> {
181 let mut edits = self.edits.borrow_mut();
182 if edits.is_empty() {
183 return Ok(());
184 }
185
186 let mut pending_segments: Vec<'a, PendingSegment<'a>> = Vec::with_capacity_in(edits.len(), self.alloc);
187
188 for (order, edit) in edits.drain(..).enumerate() {
189 match edit {
190 TransformEdit::Replace { target, cursors } => {
191 if target.start() > target.end() {
192 return Err(CommitError::InvalidEdit { span: target });
193 }
194 pending_segments.push(PendingSegment {
195 span: target,
196 intent: OverlayKind::Replace,
197 order,
198 cursors,
199 });
200 }
201 TransformEdit::InsertBefore { anchor, cursors } => {
202 let span = Span::new(anchor, anchor);
203 pending_segments.push(PendingSegment { span, intent: OverlayKind::InsertBefore, order, cursors });
204 }
205 TransformEdit::InsertAfter { anchor, cursors } => {
206 let span = Span::new(anchor, anchor);
207 pending_segments.push(PendingSegment { span, intent: OverlayKind::InsertAfter, order, cursors });
208 }
209 TransformEdit::Delete { target } => {
210 pending_segments.push(PendingSegment {
211 span: target,
212 intent: OverlayKind::Replace,
213 order,
214 cursors: Vec::with_capacity_in(0, self.alloc()),
215 });
216 }
217 }
218 }
219
220 pending_segments.sort_by(|a, b| {
221 a.span
222 .start()
223 .cmp(&b.span.start())
224 .then_with(|| a.span.end().cmp(&b.span.end()))
225 .then_with(|| a.intent.cmp(&b.intent))
226 .then_with(|| a.order.cmp(&b.order))
227 });
228
229 let mut last_non_zero: Option<Span> = None;
230 for segment in &pending_segments {
231 if segment.span.start() > segment.span.end() {
232 return Err(CommitError::InvalidEdit { span: segment.span });
233 }
234 if segment.span.start() == segment.span.end() {
235 continue;
236 }
237 if let Some(prev) = last_non_zero
238 && segment.span.start() < prev.end()
239 {
240 return Err(CommitError::OverlappingEdit { previous: prev, new: segment.span });
241 }
242 last_non_zero = Some(segment.span);
243 }
244
245 let mut overlays = self.overlays.borrow_mut();
246 overlays.clear();
247 for segment in pending_segments {
248 let overlay_segment = OverlaySegment::new(segment.span, segment.cursors, segment.intent);
249 overlays.push_segment(overlay_segment);
250 }
251
252 Ok(())
253 }
254}
255
256#[macro_export]
257macro_rules! transformer {
258 ($(#[$meta:meta])* $vis:vis enum $feature: ident [ $metadata: ident, $($node:tt)+ ] { $( $(#[$varmeta:meta])* $variant: ident$(,)?)+ } ) => {
259 use $crate::Transform;
260
261 $(#[$meta])*
262 #[bitmask(u16)]
263 pub enum $feature {
264 $(
265 $(#[$varmeta])*
266 $variant,
267 )+
268 }
269
270 impl<N> $crate::TransformerFeatures<$metadata, N> for $feature
271 where
272 N: $($node)+ + ::css_parse::NodeWithMetadata<$metadata>
273 {
274 fn transforms<'a, 'ctx>(self, transformer: &'ctx $crate::Transformer<'a, $metadata, N, Self>, node: &N) {
275 let metadata = ::css_parse::NodeWithMetadata::metadata(node);
276 $(
277 if self.contains(Self::$variant)
278 && !<$variant<'a, 'ctx, N> as $crate::Transform<'a, 'ctx, $metadata, N, Self>>::skips_subtree(
279 &metadata,
280 ) {
281 let mut transform = $variant::new(transformer);
282 let _ = node.accept(&mut transform);
283 }
284 )+
285 }
286 }
287 };
288 }
289
290#[cfg(test)]
291mod tests {
292 use crate::CssMinifierFeature;
293
294 use super::*;
295 use css_ast::{CssAtomSet, CssMetadata};
296 use css_parse::{Arena, ComponentValues, SourceOffset, Span};
297
298 #[test]
299 fn commit_overlays_rejects_overlapping_edits() {
300 let alloc = Arena::default();
301 let context: Transformer<CssMetadata, ComponentValues, CssMinifierFeature> =
302 Transformer::new_in(&alloc, CssMinifierFeature::all_bits(), &CssAtomSet::ATOMS, "");
303 let first = context.parse_value::<ComponentValues>("a");
304 let second = context.parse_value::<ComponentValues>("b");
305
306 context.replace(Span::new(SourceOffset(0), SourceOffset(2)), first);
307 context.replace(Span::new(SourceOffset(1), SourceOffset(3)), second);
308
309 let err = context.commit_overlays().expect_err("expected overlapping edits to fail");
310 match err {
311 CommitError::OverlappingEdit { previous, new } => {
312 assert_eq!(previous, Span::new(SourceOffset(0), SourceOffset(2)));
313 assert_eq!(new, Span::new(SourceOffset(1), SourceOffset(3)));
314 }
315 other => panic!("unexpected commit error: {other:?}"),
316 }
317 }
318
319 #[test]
320 fn commit_overlays_preserves_insert_order() {
321 let alloc = Arena::default();
322 let context: Transformer<CssMetadata, ComponentValues, CssMinifierFeature> =
323 Transformer::new_in(&alloc, CssMinifierFeature::all_bits(), &CssAtomSet::ATOMS, "");
324 let anchor = SourceOffset(5);
325
326 context.insert_before(anchor, context.parse_value::<ComponentValues>("A"));
327 context.insert_before(anchor, context.parse_value::<ComponentValues>("B"));
328 context.insert_after(anchor, context.parse_value::<ComponentValues>("C"));
329 context.insert_after(anchor, context.parse_value::<ComponentValues>("D"));
330
331 context.commit_overlays().expect("commit should succeed");
332 let overlays = context.overlays();
333 let segments = overlays.segments();
334
335 assert_eq!(segments.len(), 4);
336 assert_eq!(segments[0].cursors()[0].source(), "A");
337 assert_eq!(segments[1].cursors()[0].source(), "B");
338 assert_eq!(segments[2].cursors()[0].source(), "C");
339 assert_eq!(segments[3].cursors()[0].source(), "D");
340 }
341}