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    StyleDialect, TransformCstArtifactV0, TransformPassKind,
9    build_transform_cst_artifact_with_dialect,
10};
11use omena_transform_passes::{
12    TransformExecutionSummaryV0, TransformPassPlanV0,
13    execute_transform_passes_on_source_with_dialect, plan_transform_passes,
14};
15use serde::Serialize;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
18#[serde(rename_all = "camelCase")]
19pub enum TransformPrintMode {
20    Identity,
21    Pretty,
22    Minified,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
26#[serde(rename_all = "camelCase")]
27pub struct TransformPrintOptionsV0 {
28    pub mode: TransformPrintMode,
29    pub include_source_map: bool,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
33#[serde(rename_all = "camelCase")]
34pub struct TransformSourceMapPointV0 {
35    pub byte_offset: usize,
36    pub line: usize,
37    pub utf8_column: usize,
38    pub utf16_column: usize,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
42#[serde(rename_all = "camelCase")]
43pub struct TransformSourceMapSegmentV0 {
44    pub source_path: String,
45    pub original_start: usize,
46    pub original_end: usize,
47    pub generated_start: usize,
48    pub generated_end: usize,
49    pub original_start_point: TransformSourceMapPointV0,
50    pub original_end_point: TransformSourceMapPointV0,
51    pub generated_start_point: TransformSourceMapPointV0,
52    pub generated_end_point: TransformSourceMapPointV0,
53    pub pass_id: &'static str,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
57#[serde(rename_all = "camelCase")]
58pub struct TransformPrintBoundarySummaryV0 {
59    pub schema_version: &'static str,
60    pub product: &'static str,
61    pub emission_pass_id: &'static str,
62    pub supported_modes: Vec<TransformPrintMode>,
63    pub source_map_contract: &'static str,
64    pub planner_surface: &'static str,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
68#[serde(rename_all = "camelCase")]
69pub struct TransformPrintArtifactV0 {
70    pub schema_version: &'static str,
71    pub product: &'static str,
72    pub source_path: String,
73    pub css: String,
74    pub source_map_segments: Vec<TransformSourceMapSegmentV0>,
75    pub cst_artifact: TransformCstArtifactV0,
76    pub pass_plan: TransformPassPlanV0,
77    pub provenance_preserved: bool,
78}
79
80pub fn summarize_omena_transform_print_boundary() -> TransformPrintBoundarySummaryV0 {
81    TransformPrintBoundarySummaryV0 {
82        schema_version: "0",
83        product: "omena-transform-print.boundary",
84        emission_pass_id: TransformPassKind::PrintCss.id(),
85        supported_modes: vec![TransformPrintMode::Identity, TransformPrintMode::Minified],
86        source_map_contract: "stable-IR provenance-anchor emission segments with byte offsets, UTF-8/UTF-16 line-column points, lexical identity fallback, minified deletion projection, and mutation-span segments",
87        planner_surface: "omena-transform-passes.plan",
88    }
89}
90
91pub fn print_transform_cst_source(
92    source_path: impl Into<String>,
93    source: &str,
94    semantic_signature: impl Into<String>,
95    upstream_passes: &[TransformPassKind],
96    options: TransformPrintOptionsV0,
97) -> TransformPrintArtifactV0 {
98    print_transform_cst_source_with_dialect(
99        source_path,
100        source,
101        StyleDialect::Css,
102        semantic_signature,
103        upstream_passes,
104        options,
105    )
106}
107
108pub fn print_transform_cst_source_with_dialect(
109    source_path: impl Into<String>,
110    source: &str,
111    dialect: StyleDialect,
112    semantic_signature: impl Into<String>,
113    upstream_passes: &[TransformPassKind],
114    options: TransformPrintOptionsV0,
115) -> TransformPrintArtifactV0 {
116    let source_path = source_path.into();
117    let mut passes = upstream_passes.to_vec();
118    if !passes.contains(&TransformPassKind::PrintCss) {
119        passes.push(TransformPassKind::PrintCss);
120    }
121    let pass_plan = plan_transform_passes(&passes);
122    let cst_artifact =
123        build_transform_cst_artifact_with_dialect(source, dialect, semantic_signature, &passes);
124    let rendered = render_css_for_print_mode(source, dialect, options.mode);
125    let css = rendered.css;
126    let source_map_segments = if options.include_source_map {
127        compose_identity_source_map_segments(
128            &source_path,
129            source,
130            &css,
131            &cst_artifact,
132            TransformPassKind::PrintCss.id(),
133            rendered.generated_offset_lookup.as_deref(),
134        )
135    } else {
136        Vec::new()
137    };
138
139    TransformPrintArtifactV0 {
140        schema_version: "0",
141        product: "omena-transform-print.artifact",
142        source_path,
143        css,
144        source_map_segments,
145        cst_artifact,
146        pass_plan,
147        provenance_preserved: true,
148    }
149}
150
151pub fn print_transform_execution_artifact(
152    source_path: impl Into<String>,
153    semantic_signature: impl Into<String>,
154    upstream_passes: &[TransformPassKind],
155    options: TransformPrintOptionsV0,
156    execution: &TransformExecutionSummaryV0,
157) -> TransformPrintArtifactV0 {
158    print_transform_execution_artifact_with_dialect(
159        source_path,
160        StyleDialect::Css,
161        semantic_signature,
162        upstream_passes,
163        options,
164        execution,
165    )
166}
167
168pub fn print_transform_execution_artifact_with_source(
169    source_path: impl Into<String>,
170    original_source: &str,
171    semantic_signature: impl Into<String>,
172    upstream_passes: &[TransformPassKind],
173    options: TransformPrintOptionsV0,
174    execution: &TransformExecutionSummaryV0,
175) -> TransformPrintArtifactV0 {
176    print_transform_execution_artifact_with_dialect_and_source(
177        source_path,
178        original_source,
179        StyleDialect::Css,
180        semantic_signature,
181        upstream_passes,
182        options,
183        execution,
184    )
185}
186
187pub fn print_transform_execution_artifact_with_dialect(
188    source_path: impl Into<String>,
189    dialect: StyleDialect,
190    semantic_signature: impl Into<String>,
191    upstream_passes: &[TransformPassKind],
192    options: TransformPrintOptionsV0,
193    execution: &TransformExecutionSummaryV0,
194) -> TransformPrintArtifactV0 {
195    let source_path = source_path.into();
196    let mut artifact = print_transform_cst_source_with_dialect(
197        source_path.clone(),
198        &execution.output_css,
199        dialect,
200        semantic_signature,
201        upstream_passes,
202        options,
203    );
204
205    if options.include_source_map {
206        artifact.source_map_segments = compose_source_map_segments_from_execution(
207            &source_path,
208            &execution.output_css,
209            &artifact.css,
210            execution,
211            generated_offset_lookup_for_print_mode(&execution.output_css, &artifact.css),
212        );
213    }
214
215    artifact.provenance_preserved = artifact.provenance_preserved && execution.provenance_preserved;
216    artifact
217}
218
219pub fn print_transform_execution_artifact_with_dialect_and_source(
220    source_path: impl Into<String>,
221    original_source: &str,
222    dialect: StyleDialect,
223    semantic_signature: impl Into<String>,
224    upstream_passes: &[TransformPassKind],
225    options: TransformPrintOptionsV0,
226    execution: &TransformExecutionSummaryV0,
227) -> TransformPrintArtifactV0 {
228    let source_path = source_path.into();
229    let mut artifact = print_transform_cst_source_with_dialect(
230        source_path.clone(),
231        &execution.output_css,
232        dialect,
233        semantic_signature,
234        upstream_passes,
235        options,
236    );
237
238    if options.include_source_map {
239        artifact.source_map_segments = compose_source_map_segments_from_execution(
240            &source_path,
241            original_source,
242            &artifact.css,
243            execution,
244            generated_offset_lookup_for_print_mode(&execution.output_css, &artifact.css),
245        );
246    }
247
248    artifact.provenance_preserved = artifact.provenance_preserved && execution.provenance_preserved;
249    artifact
250}
251
252pub const fn default_print_options() -> TransformPrintOptionsV0 {
253    TransformPrintOptionsV0 {
254        mode: TransformPrintMode::Identity,
255        include_source_map: true,
256    }
257}
258
259fn compose_source_map_segments_from_execution(
260    source_path: &str,
261    original_source: &str,
262    generated_source: &str,
263    execution: &TransformExecutionSummaryV0,
264    generated_offset_lookup: Option<Vec<usize>>,
265) -> Vec<TransformSourceMapSegmentV0> {
266    let generated_offset_lookup = generated_offset_lookup.as_deref();
267    execution
268        .provenance_derivation_forest
269        .nodes
270        .iter()
271        .flat_map(|node| {
272            let spans = if node.mutation_spans.is_empty() {
273                vec![(
274                    node.source_span_start,
275                    node.source_span_end,
276                    node.generated_span_start,
277                    node.generated_span_end,
278                )]
279            } else {
280                node.mutation_spans
281                    .iter()
282                    .map(|span| {
283                        (
284                            span.source_span_start,
285                            span.source_span_end,
286                            span.generated_span_start,
287                            span.generated_span_end,
288                        )
289                    })
290                    .collect::<Vec<_>>()
291            };
292
293            spans.into_iter().map(
294                |(original_start, original_end, generated_start, generated_end)| {
295                    let generated_start =
296                        generated_offset_lookup.map_or(generated_start, |lookup| {
297                            project_generated_offset(
298                                generated_start,
299                                generated_source.len(),
300                                Some(lookup),
301                            )
302                        });
303                    let generated_end = generated_offset_lookup.map_or(generated_end, |lookup| {
304                        project_generated_offset(
305                            generated_end,
306                            generated_source.len(),
307                            Some(lookup),
308                        )
309                    });
310                    source_map_segment(
311                        source_path,
312                        SourceMapSources {
313                            original: original_source,
314                            generated: generated_source,
315                        },
316                        SourceMapSpanOffsets {
317                            original_start,
318                            original_end,
319                            generated_start,
320                            generated_end,
321                        },
322                        node.pass_id,
323                    )
324                },
325            )
326        })
327        .collect()
328}
329
330fn compose_identity_source_map_segments(
331    source_path: &str,
332    source: &str,
333    generated: &str,
334    cst_artifact: &TransformCstArtifactV0,
335    pass_id: &'static str,
336    generated_offset_lookup: Option<&[usize]>,
337) -> Vec<TransformSourceMapSegmentV0> {
338    let anchor_segments = cst_artifact
339        .stable_ir
340        .provenance_anchors
341        .iter()
342        .filter(|anchor| anchor.source_span_start <= anchor.source_span_end)
343        .map(|anchor| {
344            source_map_segment(
345                source_path,
346                SourceMapSources {
347                    original: source,
348                    generated,
349                },
350                SourceMapSpanOffsets {
351                    original_start: anchor.source_span_start,
352                    original_end: anchor.source_span_end,
353                    generated_start: project_generated_offset(
354                        anchor.source_span_start,
355                        generated.len(),
356                        generated_offset_lookup,
357                    ),
358                    generated_end: project_generated_offset(
359                        anchor.source_span_end,
360                        generated.len(),
361                        generated_offset_lookup,
362                    ),
363                },
364                pass_id,
365            )
366        })
367        .collect::<Vec<_>>();
368    if !anchor_segments.is_empty() {
369        return anchor_segments;
370    }
371
372    if source.is_empty() {
373        return vec![source_map_segment(
374            source_path,
375            SourceMapSources {
376                original: source,
377                generated,
378            },
379            SourceMapSpanOffsets {
380                original_start: 0,
381                original_end: 0,
382                generated_start: project_generated_offset(
383                    0,
384                    generated.len(),
385                    generated_offset_lookup,
386                ),
387                generated_end: project_generated_offset(
388                    0,
389                    generated.len(),
390                    generated_offset_lookup,
391                ),
392            },
393            pass_id,
394        )];
395    }
396
397    let mut segments = Vec::new();
398    let mut segment_start = None;
399    for (index, byte) in source.bytes().enumerate() {
400        if byte.is_ascii_whitespace() {
401            if let Some(start) = segment_start.take() {
402                segments.push(source_map_segment(
403                    source_path,
404                    SourceMapSources {
405                        original: source,
406                        generated,
407                    },
408                    SourceMapSpanOffsets {
409                        original_start: start,
410                        original_end: index,
411                        generated_start: project_generated_offset(
412                            start,
413                            generated.len(),
414                            generated_offset_lookup,
415                        ),
416                        generated_end: project_generated_offset(
417                            index,
418                            generated.len(),
419                            generated_offset_lookup,
420                        ),
421                    },
422                    pass_id,
423                ));
424            }
425        } else if segment_start.is_none() {
426            segment_start = Some(index);
427        }
428    }
429
430    if let Some(start) = segment_start {
431        segments.push(source_map_segment(
432            source_path,
433            SourceMapSources {
434                original: source,
435                generated,
436            },
437            SourceMapSpanOffsets {
438                original_start: start,
439                original_end: source.len(),
440                generated_start: project_generated_offset(
441                    start,
442                    generated.len(),
443                    generated_offset_lookup,
444                ),
445                generated_end: project_generated_offset(
446                    source.len(),
447                    generated.len(),
448                    generated_offset_lookup,
449                ),
450            },
451            pass_id,
452        ));
453    }
454
455    segments
456}
457
458#[derive(Debug, Clone, Copy)]
459struct SourceMapSources<'a> {
460    original: &'a str,
461    generated: &'a str,
462}
463
464#[derive(Debug, Clone, Copy)]
465struct SourceMapSpanOffsets {
466    original_start: usize,
467    original_end: usize,
468    generated_start: usize,
469    generated_end: usize,
470}
471
472fn source_map_segment(
473    source_path: &str,
474    sources: SourceMapSources<'_>,
475    offsets: SourceMapSpanOffsets,
476    pass_id: &'static str,
477) -> TransformSourceMapSegmentV0 {
478    TransformSourceMapSegmentV0 {
479        source_path: source_path.to_string(),
480        original_start: offsets.original_start,
481        original_end: offsets.original_end,
482        generated_start: offsets.generated_start,
483        generated_end: offsets.generated_end,
484        original_start_point: source_map_point(sources.original, offsets.original_start),
485        original_end_point: source_map_point(sources.original, offsets.original_end),
486        generated_start_point: source_map_point(sources.generated, offsets.generated_start),
487        generated_end_point: source_map_point(sources.generated, offsets.generated_end),
488        pass_id,
489    }
490}
491
492fn source_map_point(source: &str, byte_offset: usize) -> TransformSourceMapPointV0 {
493    let byte_offset = byte_offset.min(source.len());
494    let mut line = 0;
495    let mut line_start_byte = 0;
496    let mut utf16_column = 0;
497
498    for (index, character) in source.char_indices() {
499        if index >= byte_offset {
500            break;
501        }
502        if character == '\n' {
503            line += 1;
504            line_start_byte = index + character.len_utf8();
505            utf16_column = 0;
506        } else {
507            utf16_column += character.len_utf16();
508        }
509    }
510
511    TransformSourceMapPointV0 {
512        byte_offset,
513        line,
514        utf8_column: byte_offset.saturating_sub(line_start_byte),
515        utf16_column,
516    }
517}
518
519#[derive(Debug, Clone, PartialEq, Eq)]
520struct RenderedPrintCss {
521    css: String,
522    generated_offset_lookup: Option<Vec<usize>>,
523}
524
525fn render_css_for_print_mode(
526    source: &str,
527    dialect: StyleDialect,
528    mode: TransformPrintMode,
529) -> RenderedPrintCss {
530    match mode {
531        TransformPrintMode::Minified => {
532            let execution = execute_transform_passes_on_source_with_dialect(
533                source,
534                dialect,
535                &[
536                    TransformPassKind::CommentStrip,
537                    TransformPassKind::WhitespaceStrip,
538                    TransformPassKind::PrintCss,
539                ],
540            );
541            let generated_offset_lookup =
542                generated_offset_lookup_for_deleted_subsequence(source, &execution.output_css);
543            RenderedPrintCss {
544                css: execution.output_css,
545                generated_offset_lookup: Some(generated_offset_lookup),
546            }
547        }
548        TransformPrintMode::Identity | TransformPrintMode::Pretty => RenderedPrintCss {
549            css: source.to_string(),
550            generated_offset_lookup: None,
551        },
552    }
553}
554
555fn generated_offset_lookup_for_print_mode(source: &str, generated: &str) -> Option<Vec<usize>> {
556    (source != generated)
557        .then(|| generated_offset_lookup_for_deleted_subsequence(source, generated))
558}
559
560fn generated_offset_lookup_for_deleted_subsequence(source: &str, generated: &str) -> Vec<usize> {
561    let source_bytes = source.as_bytes();
562    let generated_bytes = generated.as_bytes();
563    let mut lookup = vec![0; source_bytes.len() + 1];
564    let mut generated_index = 0usize;
565
566    for index in 0..source_bytes.len() {
567        lookup[index] = generated_index;
568        if generated_index < generated_bytes.len()
569            && source_bytes[index] == generated_bytes[generated_index]
570        {
571            generated_index += 1;
572        }
573    }
574    lookup[source_bytes.len()] = generated_bytes.len();
575    lookup
576}
577
578fn project_generated_offset(
579    source_offset: usize,
580    generated_len: usize,
581    generated_offset_lookup: Option<&[usize]>,
582) -> usize {
583    generated_offset_lookup
584        .and_then(|lookup| lookup.get(source_offset).copied())
585        .unwrap_or(source_offset)
586        .min(generated_len)
587}
588
589#[cfg(test)]
590mod tests {
591    use super::{
592        TransformPrintMode, TransformPrintOptionsV0, default_print_options,
593        print_transform_cst_source, print_transform_execution_artifact,
594        print_transform_execution_artifact_with_source, source_map_point,
595        summarize_omena_transform_print_boundary,
596    };
597    use omena_transform_cst::TransformPassKind;
598    use omena_transform_passes::execute_transform_passes_on_source;
599
600    #[test]
601    fn exposes_print_boundary() {
602        let boundary = summarize_omena_transform_print_boundary();
603
604        assert_eq!(boundary.product, "omena-transform-print.boundary");
605        assert_eq!(boundary.emission_pass_id, "print-css");
606        assert_eq!(
607            boundary.supported_modes,
608            vec![TransformPrintMode::Identity, TransformPrintMode::Minified]
609        );
610    }
611
612    #[test]
613    fn prints_identity_css_with_stable_ir_source_map_segments() {
614        let source = ".button { color: var(--brand); background: red; }";
615        let artifact = print_transform_cst_source(
616            "Button.module.css",
617            source,
618            "semantic:button:brand",
619            &[TransformPassKind::CalcReduction],
620            default_print_options(),
621        );
622
623        assert_eq!(artifact.product, "omena-transform-print.artifact");
624        assert_eq!(artifact.css, source);
625        assert!(artifact.provenance_preserved);
626        assert!(artifact.source_map_segments.len() > 1);
627        if artifact
628            .cst_artifact
629            .stable_ir
630            .provenance_anchors
631            .is_empty()
632        {
633            assert!(artifact.source_map_segments.len() > 1);
634        } else {
635            assert_eq!(
636                artifact.source_map_segments.len(),
637                artifact.cst_artifact.stable_ir.provenance_anchors.len()
638            );
639        }
640        assert!(
641            artifact
642                .source_map_segments
643                .iter()
644                .any(|segment| &source[segment.original_start..segment.original_end] == "button")
645        );
646        let expected_last_original_end = artifact
647            .cst_artifact
648            .stable_ir
649            .provenance_anchors
650            .last()
651            .map(|anchor| anchor.source_span_end)
652            .unwrap_or(source.len());
653        assert_eq!(
654            artifact
655                .source_map_segments
656                .last()
657                .map(|segment| segment.original_end),
658            Some(expected_last_original_end)
659        );
660        assert_eq!(
661            artifact.pass_plan.ordered_pass_ids,
662            vec!["calc-reduction", "print-css"]
663        );
664    }
665
666    #[test]
667    fn prints_minified_css_with_projected_source_map_offsets() {
668        let source = "/* remove */ .button { color: red; margin: 0px; }";
669        let artifact = print_transform_cst_source(
670            "Button.module.css",
671            source,
672            "semantic:button",
673            &[TransformPassKind::PrintCss],
674            TransformPrintOptionsV0 {
675                mode: TransformPrintMode::Minified,
676                include_source_map: true,
677            },
678        );
679
680        assert_eq!(artifact.css, ".button{color:red;margin:0px}");
681        assert!(artifact.provenance_preserved);
682        assert!(!artifact.source_map_segments.is_empty());
683        assert!(
684            artifact
685                .source_map_segments
686                .iter()
687                .all(|segment| segment.generated_end <= artifact.css.len())
688        );
689        assert!(
690            artifact
691                .source_map_segments
692                .iter()
693                .any(|segment| segment.generated_start < segment.original_start)
694        );
695    }
696
697    #[test]
698    fn source_map_points_include_column_precision_for_unicode_and_newlines() {
699        let source = ".초기 { color: red; }\n.button { color: blue; }";
700        let artifact = print_transform_cst_source(
701            "Button.module.css",
702            source,
703            "semantic:unicode:button",
704            &[TransformPassKind::PrintCss],
705            default_print_options(),
706        );
707        let unicode_start = source.find("초기").unwrap_or(source.len());
708        assert!(unicode_start < source.len());
709        let unicode_end = unicode_start + "초기".len();
710        let unicode_start_point = source_map_point(source, unicode_start);
711        let unicode_end_point = source_map_point(source, unicode_end);
712        let button_segment = artifact
713            .source_map_segments
714            .iter()
715            .find(|segment| &source[segment.original_start..segment.original_end] == "button");
716        assert!(button_segment.is_some());
717
718        assert_eq!(unicode_start_point.line, 0);
719        assert_eq!(unicode_start_point.utf8_column, 1);
720        assert_eq!(unicode_start_point.utf16_column, 1);
721        assert_eq!(unicode_end_point.utf8_column, 7);
722        assert_eq!(unicode_end_point.utf16_column, 3);
723        if let Some(button_segment) = button_segment {
724            assert_eq!(button_segment.original_start_point.line, 1);
725            assert_eq!(button_segment.original_start_point.utf8_column, 1);
726            assert_eq!(button_segment.original_start_point.utf16_column, 1);
727            assert_eq!(
728                button_segment.generated_start_point,
729                button_segment.original_start_point
730            );
731        }
732    }
733
734    #[test]
735    fn composes_source_map_segments_from_execution_provenance() {
736        let source = ".button { color: red; /* remove */ }";
737        let execution = execute_transform_passes_on_source(
738            source,
739            &[
740                TransformPassKind::CommentStrip,
741                TransformPassKind::WhitespaceStrip,
742                TransformPassKind::PrintCss,
743            ],
744        );
745        let artifact = print_transform_execution_artifact(
746            "Button.module.css",
747            "semantic:button",
748            &[
749                TransformPassKind::CommentStrip,
750                TransformPassKind::WhitespaceStrip,
751                TransformPassKind::PrintCss,
752            ],
753            default_print_options(),
754            &execution,
755        );
756
757        assert_eq!(artifact.css, execution.output_css);
758        assert!(artifact.provenance_preserved);
759        assert_eq!(
760            artifact.source_map_segments.len(),
761            execution.provenance_derivation_forest.node_count
762        );
763        assert_eq!(
764            artifact.source_map_segments[0].source_path,
765            "Button.module.css"
766        );
767        assert_eq!(
768            artifact.source_map_segments[0].original_start,
769            execution.provenance_derivation_forest.nodes[0].source_span_start
770        );
771        assert_eq!(
772            artifact.source_map_segments[0].original_end,
773            execution.provenance_derivation_forest.nodes[0].source_span_end
774        );
775        assert_eq!(
776            artifact.source_map_segments[0].generated_start,
777            execution.provenance_derivation_forest.nodes[0].generated_span_start
778        );
779        assert_eq!(
780            artifact.source_map_segments[0].generated_end,
781            execution.provenance_derivation_forest.nodes[0].generated_span_end
782        );
783        assert!(
784            artifact
785                .source_map_segments
786                .iter()
787                .any(|segment| segment.pass_id == "comment-strip")
788        );
789        assert_eq!(
790            artifact
791                .source_map_segments
792                .last()
793                .map(|segment| segment.generated_end),
794            Some(execution.output_byte_len)
795        );
796    }
797
798    #[test]
799    fn minified_execution_artifact_projects_upstream_segments_to_final_css() {
800        let source = ".button { color: red; }\n.card { color: blue; }";
801        let execution = execute_transform_passes_on_source(source, &[TransformPassKind::PrintCss]);
802        let artifact = print_transform_execution_artifact_with_source(
803            "Button.module.css",
804            source,
805            "semantic:button-card",
806            &[TransformPassKind::PrintCss],
807            TransformPrintOptionsV0 {
808                mode: TransformPrintMode::Minified,
809                include_source_map: true,
810            },
811            &execution,
812        );
813
814        assert_eq!(artifact.css, ".button{color:red}.card{color:blue}");
815        assert!(artifact.provenance_preserved);
816        assert_eq!(artifact.source_map_segments[0].generated_start, 0);
817        assert_eq!(
818            artifact
819                .source_map_segments
820                .last()
821                .map(|segment| segment.generated_end),
822            Some(artifact.css.len())
823        );
824    }
825
826    #[test]
827    fn emits_one_source_map_segment_per_mutation_span() {
828        let source = ".a { /* one */ color: red; }\n.b { /* two */ color: blue; }";
829        let execution = execute_transform_passes_on_source(
830            source,
831            &[TransformPassKind::CommentStrip, TransformPassKind::PrintCss],
832        );
833        let comment_node = execution
834            .provenance_derivation_forest
835            .nodes
836            .iter()
837            .find(|node| node.pass_id == "comment-strip");
838        assert!(comment_node.is_some());
839        if let Some(comment_node) = comment_node {
840            assert_eq!(comment_node.mutation_spans.len(), 2);
841        }
842
843        let artifact = print_transform_execution_artifact_with_source(
844            "Multi.module.css",
845            source,
846            "semantic:multi",
847            &[TransformPassKind::CommentStrip, TransformPassKind::PrintCss],
848            default_print_options(),
849            &execution,
850        );
851        let comment_segments = artifact
852            .source_map_segments
853            .iter()
854            .filter(|segment| segment.pass_id == "comment-strip")
855            .collect::<Vec<_>>();
856
857        assert_eq!(comment_segments.len(), 2);
858        assert!(comment_segments[0].original_start < comment_segments[1].original_start);
859        assert_eq!(comment_segments[1].original_start_point.line, 1);
860    }
861}