Skip to main content

csskit_transform/
transformer.rs

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}