Skip to main content

omena_transform_print/
lib.rs

1//! CSS emission and source-map boundary for Omena CSS transforms.
2//!
3//! The initial printer is deliberately identity-preserving. It establishes the
4//! output and source-map contract before later pretty/minified emitters start
5//! changing bytes.
6
7use omena_transform_cst::{
8    TransformCstArtifactV0, TransformPassKind, build_transform_cst_artifact,
9};
10use omena_transform_passes::{
11    TransformExecutionSummaryV0, TransformPassPlanV0, plan_transform_passes,
12};
13use serde::Serialize;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
16#[serde(rename_all = "camelCase")]
17pub enum TransformPrintMode {
18    Identity,
19    Pretty,
20    Minified,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
24#[serde(rename_all = "camelCase")]
25pub struct TransformPrintOptionsV0 {
26    pub mode: TransformPrintMode,
27    pub include_source_map: bool,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
31#[serde(rename_all = "camelCase")]
32pub struct TransformSourceMapSegmentV0 {
33    pub source_path: String,
34    pub original_start: usize,
35    pub original_end: usize,
36    pub generated_start: usize,
37    pub generated_end: usize,
38    pub pass_id: &'static str,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
42#[serde(rename_all = "camelCase")]
43pub struct TransformPrintBoundarySummaryV0 {
44    pub schema_version: &'static str,
45    pub product: &'static str,
46    pub emission_pass_id: &'static str,
47    pub supported_modes: Vec<TransformPrintMode>,
48    pub source_map_contract: &'static str,
49    pub planner_surface: &'static str,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
53#[serde(rename_all = "camelCase")]
54pub struct TransformPrintArtifactV0 {
55    pub schema_version: &'static str,
56    pub product: &'static str,
57    pub source_path: String,
58    pub css: String,
59    pub source_map_segments: Vec<TransformSourceMapSegmentV0>,
60    pub cst_artifact: TransformCstArtifactV0,
61    pub pass_plan: TransformPassPlanV0,
62    pub provenance_preserved: bool,
63}
64
65pub fn summarize_omena_transform_print_boundary() -> TransformPrintBoundarySummaryV0 {
66    TransformPrintBoundarySummaryV0 {
67        schema_version: "0",
68        product: "omena-transform-print.boundary",
69        emission_pass_id: TransformPassKind::PrintCss.id(),
70        supported_modes: vec![
71            TransformPrintMode::Identity,
72            TransformPrintMode::Pretty,
73            TransformPrintMode::Minified,
74        ],
75        source_map_contract: "line-level identity segments plus provenance mutation-span segments",
76        planner_surface: "omena-transform-passes.plan",
77    }
78}
79
80pub fn print_transform_cst_source(
81    source_path: impl Into<String>,
82    source: &str,
83    semantic_signature: impl Into<String>,
84    upstream_passes: &[TransformPassKind],
85    options: TransformPrintOptionsV0,
86) -> TransformPrintArtifactV0 {
87    let source_path = source_path.into();
88    let mut passes = upstream_passes.to_vec();
89    if !passes.contains(&TransformPassKind::PrintCss) {
90        passes.push(TransformPassKind::PrintCss);
91    }
92    let pass_plan = plan_transform_passes(&passes);
93    let cst_artifact = build_transform_cst_artifact(source, semantic_signature, &passes);
94    let css = render_identity_preserving_css(source, options.mode);
95    let source_map_segments = if options.include_source_map {
96        compose_identity_source_map_segments(
97            &source_path,
98            source,
99            &css,
100            TransformPassKind::PrintCss.id(),
101        )
102    } else {
103        Vec::new()
104    };
105
106    TransformPrintArtifactV0 {
107        schema_version: "0",
108        product: "omena-transform-print.artifact",
109        source_path,
110        css,
111        source_map_segments,
112        cst_artifact,
113        pass_plan,
114        provenance_preserved: true,
115    }
116}
117
118pub fn print_transform_execution_artifact(
119    source_path: impl Into<String>,
120    semantic_signature: impl Into<String>,
121    upstream_passes: &[TransformPassKind],
122    options: TransformPrintOptionsV0,
123    execution: &TransformExecutionSummaryV0,
124) -> TransformPrintArtifactV0 {
125    let source_path = source_path.into();
126    let mut artifact = print_transform_cst_source(
127        source_path.clone(),
128        &execution.output_css,
129        semantic_signature,
130        upstream_passes,
131        options,
132    );
133
134    if options.include_source_map {
135        artifact.source_map_segments =
136            compose_source_map_segments_from_execution(&source_path, execution);
137    }
138
139    artifact.provenance_preserved = artifact.provenance_preserved && execution.provenance_preserved;
140    artifact
141}
142
143pub const fn default_print_options() -> TransformPrintOptionsV0 {
144    TransformPrintOptionsV0 {
145        mode: TransformPrintMode::Identity,
146        include_source_map: true,
147    }
148}
149
150fn compose_source_map_segments_from_execution(
151    source_path: &str,
152    execution: &TransformExecutionSummaryV0,
153) -> Vec<TransformSourceMapSegmentV0> {
154    execution
155        .provenance_derivation_forest
156        .nodes
157        .iter()
158        .flat_map(|node| {
159            let spans = if node.mutation_spans.is_empty() {
160                vec![(
161                    node.source_span_start,
162                    node.source_span_end,
163                    node.generated_span_start,
164                    node.generated_span_end,
165                )]
166            } else {
167                node.mutation_spans
168                    .iter()
169                    .map(|span| {
170                        (
171                            span.source_span_start,
172                            span.source_span_end,
173                            span.generated_span_start,
174                            span.generated_span_end,
175                        )
176                    })
177                    .collect::<Vec<_>>()
178            };
179
180            spans.into_iter().map(
181                |(original_start, original_end, generated_start, generated_end)| {
182                    TransformSourceMapSegmentV0 {
183                        source_path: source_path.to_string(),
184                        original_start,
185                        original_end,
186                        generated_start,
187                        generated_end,
188                        pass_id: node.pass_id,
189                    }
190                },
191            )
192        })
193        .collect()
194}
195
196fn compose_identity_source_map_segments(
197    source_path: &str,
198    source: &str,
199    generated: &str,
200    pass_id: &'static str,
201) -> Vec<TransformSourceMapSegmentV0> {
202    if source.is_empty() {
203        return vec![TransformSourceMapSegmentV0 {
204            source_path: source_path.to_string(),
205            original_start: 0,
206            original_end: 0,
207            generated_start: 0,
208            generated_end: 0,
209            pass_id,
210        }];
211    }
212
213    let mut segments = Vec::new();
214    let mut start = 0usize;
215    for (index, byte) in source.bytes().enumerate() {
216        if byte == b'\n' {
217            let end = index + 1;
218            segments.push(TransformSourceMapSegmentV0 {
219                source_path: source_path.to_string(),
220                original_start: start,
221                original_end: end,
222                generated_start: start,
223                generated_end: end.min(generated.len()),
224                pass_id,
225            });
226            start = end;
227        }
228    }
229
230    if start < source.len() {
231        segments.push(TransformSourceMapSegmentV0 {
232            source_path: source_path.to_string(),
233            original_start: start,
234            original_end: source.len(),
235            generated_start: start,
236            generated_end: generated.len(),
237            pass_id,
238        });
239    }
240
241    segments
242}
243
244fn render_identity_preserving_css(source: &str, _mode: TransformPrintMode) -> String {
245    source.to_string()
246}
247
248#[cfg(test)]
249mod tests {
250    use super::{
251        default_print_options, print_transform_cst_source, print_transform_execution_artifact,
252        summarize_omena_transform_print_boundary,
253    };
254    use omena_transform_cst::TransformPassKind;
255    use omena_transform_passes::execute_transform_passes_on_source;
256
257    #[test]
258    fn exposes_print_boundary() {
259        let boundary = summarize_omena_transform_print_boundary();
260
261        assert_eq!(boundary.product, "omena-transform-print.boundary");
262        assert_eq!(boundary.emission_pass_id, "print-css");
263        assert_eq!(boundary.supported_modes.len(), 3);
264    }
265
266    #[test]
267    fn prints_identity_css_with_line_level_source_map_segments() {
268        let source = ".button {\n  color: var(--brand);\n}";
269        let artifact = print_transform_cst_source(
270            "Button.module.css",
271            source,
272            "semantic:button:brand",
273            &[TransformPassKind::CalcReduction],
274            default_print_options(),
275        );
276
277        assert_eq!(artifact.product, "omena-transform-print.artifact");
278        assert_eq!(artifact.css, source);
279        assert!(artifact.provenance_preserved);
280        assert_eq!(artifact.source_map_segments.len(), 3);
281        assert_eq!(artifact.source_map_segments[0].original_start, 0);
282        assert_eq!(artifact.source_map_segments[0].original_end, 10);
283        assert_eq!(
284            artifact
285                .source_map_segments
286                .last()
287                .map(|segment| segment.original_end),
288            Some(source.len())
289        );
290        assert_eq!(
291            artifact.pass_plan.ordered_pass_ids,
292            vec!["calc-reduction", "print-css"]
293        );
294    }
295
296    #[test]
297    fn composes_source_map_segments_from_execution_provenance() {
298        let source = ".button { color: red; /* remove */ }";
299        let execution = execute_transform_passes_on_source(
300            source,
301            &[
302                TransformPassKind::CommentStrip,
303                TransformPassKind::WhitespaceStrip,
304                TransformPassKind::PrintCss,
305            ],
306        );
307        let artifact = print_transform_execution_artifact(
308            "Button.module.css",
309            "semantic:button",
310            &[
311                TransformPassKind::CommentStrip,
312                TransformPassKind::WhitespaceStrip,
313                TransformPassKind::PrintCss,
314            ],
315            default_print_options(),
316            &execution,
317        );
318
319        assert_eq!(artifact.css, execution.output_css);
320        assert!(artifact.provenance_preserved);
321        assert_eq!(
322            artifact.source_map_segments.len(),
323            execution.provenance_derivation_forest.node_count
324        );
325        assert_eq!(
326            artifact.source_map_segments[0].source_path,
327            "Button.module.css"
328        );
329        assert_eq!(
330            artifact.source_map_segments[0].original_start,
331            execution.provenance_derivation_forest.nodes[0].source_span_start
332        );
333        assert_eq!(
334            artifact.source_map_segments[0].original_end,
335            execution.provenance_derivation_forest.nodes[0].source_span_end
336        );
337        assert_eq!(
338            artifact.source_map_segments[0].generated_start,
339            execution.provenance_derivation_forest.nodes[0].generated_span_start
340        );
341        assert_eq!(
342            artifact.source_map_segments[0].generated_end,
343            execution.provenance_derivation_forest.nodes[0].generated_span_end
344        );
345        assert!(
346            artifact
347                .source_map_segments
348                .iter()
349                .any(|segment| segment.pass_id == "comment-strip")
350        );
351        assert_eq!(
352            artifact
353                .source_map_segments
354                .last()
355                .map(|segment| segment.generated_end),
356            Some(execution.output_byte_len)
357        );
358    }
359
360    #[test]
361    fn emits_one_source_map_segment_per_mutation_span() {
362        let source = ".a { /* one */ color: red; }\n.b { /* two */ color: blue; }";
363        let execution = execute_transform_passes_on_source(
364            source,
365            &[TransformPassKind::CommentStrip, TransformPassKind::PrintCss],
366        );
367        let comment_node = execution
368            .provenance_derivation_forest
369            .nodes
370            .iter()
371            .find(|node| node.pass_id == "comment-strip");
372        assert!(comment_node.is_some());
373        if let Some(comment_node) = comment_node {
374            assert_eq!(comment_node.mutation_spans.len(), 2);
375        }
376
377        let artifact = print_transform_execution_artifact(
378            "Multi.module.css",
379            "semantic:multi",
380            &[TransformPassKind::CommentStrip, TransformPassKind::PrintCss],
381            default_print_options(),
382            &execution,
383        );
384        let comment_segments = artifact
385            .source_map_segments
386            .iter()
387            .filter(|segment| segment.pass_id == "comment-strip")
388            .collect::<Vec<_>>();
389
390        assert_eq!(comment_segments.len(), 2);
391        assert!(comment_segments[0].original_start < comment_segments[1].original_start);
392    }
393}