csskit_transform/
transformer.rs

1use bumpalo::{Bump, collections::Vec};
2use css_lexer::{AtomSet, Cursor, DynAtomSet, Lexer, ToSpan};
3use css_parse::{
4	CursorOverlaySet, CursorToSourceCursorSink, NodeMetadata, NodeWithMetadata, OverlayKind, OverlaySegment, Parse,
5	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	bump: &'a Bump,
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(bump: &'a Bump, features: F, atoms: &'static dyn DynAtomSet, source_text: &'a str) -> Self {
50		Self {
51			bump,
52			features,
53			atoms,
54			changed: RefCell::new(false),
55			overlays: RefCell::new(CursorOverlaySet::new(bump)),
56			edits: RefCell::new(Vec::new_in(bump)),
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 bump(&self) -> &'a Bump {
75		self.bump
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.bump());
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.atom_bits();
91		if bits == 0 {
92			let source_cursor = self.to_source_cursor(c);
93			return A::from_str(&source_cursor.parse(self.bump));
94		}
95		A::from_bits(bits)
96	}
97
98	pub fn overlays(&self) -> std::cell::Ref<'_, CursorOverlaySet<'a>> {
99		self.overlays.borrow()
100	}
101
102	pub fn parse_value<T>(&self, source: &'a str) -> Vec<'a, SourceCursor<'a>>
103	where
104		T: Parse<'a> + ToCursors,
105	{
106		let lexer = Lexer::new(self.atoms, source);
107		let mut parser = Parser::new(self.bump, source, lexer);
108		let parsed = parser.parse_entirely::<T>();
109		debug_assert!(
110			parsed.output.is_some(),
111			"Transformer::parse_value failed to parse {:?}: {:?}",
112			source,
113			parsed.errors
114		);
115		let mut cursors = Vec::new_in(self.bump());
116		let mut sink = CursorToSourceCursorSink::new(source, &mut cursors);
117		parsed.to_cursors(&mut sink);
118		cursors
119	}
120
121	pub fn reset(&self) {
122		*self.changed.borrow_mut() = false;
123		self.overlays.borrow_mut().clear();
124		self.edits.borrow_mut().clear();
125	}
126
127	pub fn has_replacement(&self, span: impl ToSpan) -> bool {
128		self.overlays.borrow().has_overlay(span.to_span())
129	}
130
131	pub fn clear_pending_edits(&self, span: Span) -> bool {
132		let mut edits = self.edits.borrow_mut();
133		let len_before = edits.len();
134		edits.retain(|edit| match edit {
135			TransformEdit::Replace { target, .. } | TransformEdit::Delete { target } => target != &span,
136			_ => true,
137		});
138		len_before != edits.len()
139	}
140
141	pub fn replace(&self, span: impl ToSpan, cursors: Vec<'a, SourceCursor<'a>>) {
142		let span = span.to_span();
143		debug_assert!(span.start() <= span.end(), "Transformer::replace received invalid span: {:?}", span);
144		*self.changed.borrow_mut() = true;
145		self.edits.borrow_mut().push(TransformEdit::Replace { target: span, cursors });
146	}
147
148	pub fn delete(&self, span: impl ToSpan) {
149		let span = span.to_span();
150		debug_assert!(span.start() <= span.end(), "Transformer::delete received invalid span: {:?}", span);
151		*self.changed.borrow_mut() = true;
152		self.edits.borrow_mut().push(TransformEdit::Delete { target: span });
153	}
154
155	pub fn insert_before(&self, anchor: SourceOffset, cursors: Vec<'a, SourceCursor<'a>>) {
156		*self.changed.borrow_mut() = true;
157		self.edits.borrow_mut().push(TransformEdit::InsertBefore { anchor, cursors });
158	}
159
160	pub fn insert_after(&self, anchor: SourceOffset, cursors: Vec<'a, SourceCursor<'a>>) {
161		*self.changed.borrow_mut() = true;
162		self.edits.borrow_mut().push(TransformEdit::InsertAfter { anchor, cursors });
163	}
164
165	pub fn replace_parsed<T>(&self, span: impl ToSpan, css: &str)
166	where
167		T: Parse<'a> + ToCursors,
168	{
169		let owned = self.bump.alloc_str(css);
170		self.replace(span, self.parse_value::<T>(owned));
171	}
172
173	pub fn commit_overlays(&self) -> Result<(), CommitError> {
174		let mut edits = self.edits.borrow_mut();
175		if edits.is_empty() {
176			return Ok(());
177		}
178
179		let mut pending_segments: Vec<'a, PendingSegment<'a>> = Vec::with_capacity_in(edits.len(), self.bump);
180
181		for (order, edit) in edits.drain(..).enumerate() {
182			match edit {
183				TransformEdit::Replace { target, cursors } => {
184					if target.start() > target.end() {
185						return Err(CommitError::InvalidEdit { span: target });
186					}
187					pending_segments.push(PendingSegment {
188						span: target,
189						intent: OverlayKind::Replace,
190						order,
191						cursors,
192					});
193				}
194				TransformEdit::InsertBefore { anchor, cursors } => {
195					let span = Span::new(anchor, anchor);
196					pending_segments.push(PendingSegment { span, intent: OverlayKind::InsertBefore, order, cursors });
197				}
198				TransformEdit::InsertAfter { anchor, cursors } => {
199					let span = Span::new(anchor, anchor);
200					pending_segments.push(PendingSegment { span, intent: OverlayKind::InsertAfter, order, cursors });
201				}
202				TransformEdit::Delete { target } => {
203					pending_segments.push(PendingSegment {
204						span: target,
205						intent: OverlayKind::Replace,
206						order,
207						cursors: Vec::with_capacity_in(0, self.bump()),
208					});
209				}
210			}
211		}
212
213		pending_segments.sort_by(|a, b| {
214			a.span
215				.start()
216				.cmp(&b.span.start())
217				.then_with(|| a.span.end().cmp(&b.span.end()))
218				.then_with(|| a.intent.cmp(&b.intent))
219				.then_with(|| a.order.cmp(&b.order))
220		});
221
222		let mut last_non_zero: Option<Span> = None;
223		for segment in &pending_segments {
224			if segment.span.start() > segment.span.end() {
225				return Err(CommitError::InvalidEdit { span: segment.span });
226			}
227			if segment.span.start() == segment.span.end() {
228				continue;
229			}
230			if let Some(prev) = last_non_zero
231				&& segment.span.start() < prev.end()
232			{
233				return Err(CommitError::OverlappingEdit { previous: prev, new: segment.span });
234			}
235			last_non_zero = Some(segment.span);
236		}
237
238		let mut overlays = self.overlays.borrow_mut();
239		overlays.clear();
240		for segment in pending_segments {
241			let overlay_segment = OverlaySegment::new(segment.span, segment.cursors, segment.intent);
242			overlays.push_segment(overlay_segment);
243		}
244
245		Ok(())
246	}
247}
248
249#[macro_export]
250macro_rules! transformer {
251	($(#[$meta:meta])* $vis:vis enum $feature: ident [ $metadata: ident, $($node:tt)+ ] { $( $(#[$varmeta:meta])* $variant: ident$(,)?)+ } ) => {
252			use $crate::Transform;
253
254			$(#[$meta])*
255			#[bitmask(u16)]
256			pub enum $feature {
257				$(
258					$(#[$varmeta])*
259					$variant,
260				)+
261			}
262
263			impl<N> $crate::TransformerFeatures<$metadata, N> for $feature
264			where
265				N: $($node)+ + ::css_parse::NodeWithMetadata<$metadata>
266			{
267				fn transforms<'a, 'ctx>(self, transformer: &'ctx $crate::Transformer<'a, $metadata, N, Self>, node: &N) {
268					$(
269						if $variant::may_change(transformer.features, node) {
270							let mut transform = $variant::new(transformer);
271							node.accept(&mut transform);
272						}
273					)+
274				}
275			}
276    };
277	}
278
279#[cfg(test)]
280mod tests {
281	use crate::CssMinifierFeature;
282
283	use super::*;
284	use bumpalo::Bump;
285	use css_ast::{CssAtomSet, CssMetadata};
286	use css_parse::{ComponentValues, SourceOffset, Span};
287
288	#[test]
289	fn commit_overlays_rejects_overlapping_edits() {
290		let bump = Bump::default();
291		let context: Transformer<CssMetadata, ComponentValues, CssMinifierFeature> =
292			Transformer::new_in(&bump, CssMinifierFeature::all_bits(), &CssAtomSet::ATOMS, "");
293		let first = context.parse_value::<ComponentValues>("a");
294		let second = context.parse_value::<ComponentValues>("b");
295
296		context.replace(Span::new(SourceOffset(0), SourceOffset(2)), first);
297		context.replace(Span::new(SourceOffset(1), SourceOffset(3)), second);
298
299		let err = context.commit_overlays().expect_err("expected overlapping edits to fail");
300		match err {
301			CommitError::OverlappingEdit { previous, new } => {
302				assert_eq!(previous, Span::new(SourceOffset(0), SourceOffset(2)));
303				assert_eq!(new, Span::new(SourceOffset(1), SourceOffset(3)));
304			}
305			other => panic!("unexpected commit error: {other:?}"),
306		}
307	}
308
309	#[test]
310	fn commit_overlays_preserves_insert_order() {
311		let bump = Bump::default();
312		let context: Transformer<CssMetadata, ComponentValues, CssMinifierFeature> =
313			Transformer::new_in(&bump, CssMinifierFeature::all_bits(), &CssAtomSet::ATOMS, "");
314		let anchor = SourceOffset(5);
315
316		context.insert_before(anchor, context.parse_value::<ComponentValues>("A"));
317		context.insert_before(anchor, context.parse_value::<ComponentValues>("B"));
318		context.insert_after(anchor, context.parse_value::<ComponentValues>("C"));
319		context.insert_after(anchor, context.parse_value::<ComponentValues>("D"));
320
321		context.commit_overlays().expect("commit should succeed");
322		let overlays = context.overlays();
323		let segments = overlays.segments();
324
325		assert_eq!(segments.len(), 4);
326		assert_eq!(segments[0].cursors()[0].source(), "A");
327		assert_eq!(segments[1].cursors()[0].source(), "B");
328		assert_eq!(segments[2].cursors()[0].source(), "C");
329		assert_eq!(segments[3].cursors()[0].source(), "D");
330	}
331}