csskit_transform 0.0.32-canary.13e01cac8c

AST transformation and minification utilities for CSS.
Documentation
use crate::prelude::*;
use css_ast::{CssTypes, Time, VisitNode, Visitable};
use css_parse::format_in;

pub struct ReduceTimeUnits<'a, 'ctx, N: Visitable + NodeWithMetadata<CssMetadata>> {
	pub transformer: &'ctx Transformer<'a, CssMetadata, N, CssMinifierFeature>,
}

impl<'a, 'ctx, N> Transform<'a, 'ctx, CssMetadata, N, CssMinifierFeature> for ReduceTimeUnits<'a, 'ctx, N>
where
	N: Visitable + NodeWithMetadata<CssMetadata>,
{
	fn skips_subtree(metadata: &CssMetadata) -> bool {
		!metadata.has_value_kinds(CssTypes::Time)
	}

	fn new(transformer: &'ctx Transformer<'a, CssMetadata, N, CssMinifierFeature>) -> Self {
		Self { transformer }
	}
}

#[visitor]
impl<'a, 'ctx, N> Visit for ReduceTimeUnits<'a, 'ctx, N>
where
	N: Visitable + NodeWithMetadata<CssMetadata>,
{
	fn consider_node(&self, node: VisitNode) -> VisitFlow {
		if node.node_id.is_some() && Self::skips_subtree(&node.subtree_metadata()) {
			return VisitFlow::SKIP_CHILDREN;
		}
		VisitFlow::DESCEND
	}

	fn visit_time(&mut self, time: &Time) {
		if let Time::Ms(dim) = time {
			let arena = self.transformer.alloc();
			let seconds = time.as_seconds();
			let sc = self.transformer.to_source_cursor((*dim).into());
			let value = if seconds.fract() == 0.0 {
				format_in!(in arena, "{}", seconds as i64)
			} else {
				format_in!(in arena, "{seconds}")
			};
			let seconds_len = value.len() - value.starts_with("0.") as usize - value.starts_with("-0.") as usize + 1;
			let ms_len = if sc.may_compact() {
				format_in!(in arena, "{}", self.transformer.to_source_cursor((*dim).into()).compact()).len()
			} else {
				sc.token().len() as usize
			};
			if seconds_len < ms_len {
				self.transformer.replace_parsed::<Time>(time.to_span(), format_in!(in arena, "{value}s").into_str());
			}
		}
	}
}

#[cfg(test)]
mod tests {
	use crate::test_helpers::{assert_no_transform, assert_transform};
	use css_ast::{CssAtomSet, StyleSheet};

	#[test]
	fn converts_milliseconds_to_seconds() {
		assert_transform!(
			CssMinifierFeature::ReduceTimeUnits,
			CssAtomSet,
			StyleSheet,
			"div { transition-duration: 500ms; }",
			"div { transition-duration: 0.5s; }"
		);
	}

	#[test]
	fn keeps_shorter_millisecond_values() {
		assert_no_transform!(
			CssMinifierFeature::ReduceTimeUnits,
			CssAtomSet,
			StyleSheet,
			"div { transition-duration: 50ms; }"
		);
	}

	#[test]
	fn keeps_second_values_when_not_shorter() {
		assert_no_transform!(
			CssMinifierFeature::ReduceTimeUnits,
			CssAtomSet,
			StyleSheet,
			"div { transition-duration: 2s; }"
		);
	}

	#[test]
	fn converts_whole_seconds() {
		assert_transform!(
			CssMinifierFeature::ReduceTimeUnits,
			CssAtomSet,
			StyleSheet,
			"div { animation-duration: 1000ms; }",
			"div { animation-duration: 1s; }"
		);
	}

	#[test]
	fn converts_only_when_compact_is_larger() {
		assert_no_transform!(
			CssMinifierFeature::ReduceTimeUnits,
			CssAtomSet,
			StyleSheet,
			"div { animation-duration: 00050ms; }"
		);
	}
}