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
7mod doc_ir;
8
9pub use doc_ir::{
10    FORMAT_IR_COVERAGE_MANIFEST_V0, FormatIrCoverageEntryV0, FormatIrCoverageStrategyV0,
11    PrettyFormatOptionsV0, PrettyFormatReportV0, default_pretty_format_options,
12};
13pub use omena_transform_cst::StyleDialect;
14
15use omena_transform_cst::{
16    TransformCstArtifactV0, TransformPassKind, build_transform_cst_artifact_with_dialect,
17};
18use omena_transform_passes::{
19    TransformExecutionSummaryV0, TransformPassPlanV0,
20    execute_transform_passes_on_source_with_dialect, plan_transform_passes,
21};
22use serde::{Deserialize, Serialize};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
25#[serde(rename_all = "camelCase")]
26pub enum TransformPrintMode {
27    Identity,
28    Pretty,
29    Minified,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
33#[serde(rename_all = "camelCase")]
34pub struct TransformPrintOptionsV0 {
35    pub mode: TransformPrintMode,
36    pub include_source_map: bool,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
40#[serde(rename_all = "camelCase")]
41pub struct TransformSourceMapPointV0 {
42    pub byte_offset: usize,
43    pub line: usize,
44    pub utf8_column: usize,
45    pub utf16_column: usize,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
49#[serde(rename_all = "camelCase")]
50pub struct TransformSourceMapSegmentV0 {
51    pub source_path: String,
52    pub original_start: usize,
53    pub original_end: usize,
54    pub generated_start: usize,
55    pub generated_end: usize,
56    pub original_start_point: TransformSourceMapPointV0,
57    pub original_end_point: TransformSourceMapPointV0,
58    pub generated_start_point: TransformSourceMapPointV0,
59    pub generated_end_point: TransformSourceMapPointV0,
60    pub pass_id: &'static str,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
64#[serde(rename_all = "camelCase")]
65pub struct TransformSourceMapV3V0 {
66    pub version: u8,
67    pub file: String,
68    pub sources: Vec<String>,
69    #[serde(rename = "sourcesContent")]
70    pub sources_content: Vec<String>,
71    pub names: Vec<String>,
72    pub mappings: String,
73    #[serde(rename = "x_omenaSchemaVersion")]
74    pub x_omena_schema_version: &'static str,
75    #[serde(rename = "x_omenaProduct")]
76    pub x_omena_product: &'static str,
77    #[serde(rename = "x_omenaSegmentCount")]
78    pub x_omena_segment_count: usize,
79    #[serde(rename = "x_omenaPassIds")]
80    pub x_omena_pass_ids: Vec<&'static str>,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct ParsedTransformSourceMapV3V0 {
85    pub version: u8,
86    pub file: Option<String>,
87    pub sources: Vec<String>,
88    pub sources_content: Vec<Option<String>>,
89    pub names: Vec<String>,
90    pub mappings: String,
91    pub decoded_segments: Vec<DecodedSourceMapSegmentV0>,
92    pub generated_line_count: usize,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct DecodedSourceMapSegmentV0 {
97    pub generated_line: usize,
98    pub generated_utf16_column: usize,
99    pub source_index: usize,
100    pub original_line: usize,
101    pub original_utf16_column: usize,
102    pub name_index: Option<usize>,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct TransformSourceMapCompositionV0 {
107    pub source_map: TransformSourceMapV3V0,
108    pub upstream_map_applied: bool,
109    pub fallback_reason: Option<String>,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq)]
113pub enum TransformSourceMapV3ParseErrorV0 {
114    Json(String),
115    UnsupportedVersion(u8),
116    InvalidMapping(String),
117    SourceIndexOutOfRange { index: usize, source_count: usize },
118    NameIndexOutOfRange { index: usize, name_count: usize },
119}
120
121#[derive(Debug, Deserialize)]
122#[serde(rename_all = "camelCase")]
123struct RawSourceMapV3V0 {
124    version: u8,
125    #[serde(default)]
126    file: Option<String>,
127    #[serde(default)]
128    sources: Vec<String>,
129    #[serde(rename = "sourcesContent", default)]
130    sources_content: Vec<Option<String>>,
131    #[serde(default)]
132    names: Vec<String>,
133    mappings: String,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
137#[serde(rename_all = "camelCase")]
138pub struct TransformPrintBoundarySummaryV0 {
139    pub schema_version: &'static str,
140    pub product: &'static str,
141    pub emission_pass_id: &'static str,
142    pub supported_modes: Vec<TransformPrintMode>,
143    pub source_map_contract: &'static str,
144    pub planner_surface: &'static str,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
148#[serde(rename_all = "camelCase")]
149pub struct TransformPrintArtifactV0 {
150    pub schema_version: &'static str,
151    pub product: &'static str,
152    pub source_path: String,
153    pub css: String,
154    pub source_map_segments: Vec<TransformSourceMapSegmentV0>,
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub source_map_v3: Option<TransformSourceMapV3V0>,
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub pretty_format_report: Option<PrettyFormatReportV0>,
159    pub cst_artifact: TransformCstArtifactV0,
160    pub pass_plan: TransformPassPlanV0,
161    pub provenance_preserved: bool,
162}
163
164#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
165#[serde(rename_all = "camelCase")]
166pub struct TransformSourceMapIntegrityFixtureReportV0 {
167    pub fixture: &'static str,
168    pub pass_ids: Vec<&'static str>,
169    pub mutation_count: usize,
170    pub source_byte_len: usize,
171    pub generated_byte_len: usize,
172    pub segment_count: usize,
173    pub surviving_node_count: usize,
174    pub mapped_surviving_node_count: usize,
175    pub map_parsed: bool,
176    pub decoded_segment_count: usize,
177    pub upstream_decoded_segment_count: usize,
178    pub upstream_map_applied: bool,
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub composition_fallback_reason: Option<String>,
181    pub composed_map_parsed: bool,
182    pub no_dangling_segments: bool,
183    pub complete: bool,
184}
185
186#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
187#[serde(rename_all = "camelCase")]
188pub struct TransformSourceMapIntegrityReportV0 {
189    pub schema_version: &'static str,
190    pub product: &'static str,
191    pub fixture_count: usize,
192    pub destructive_fixture_count: usize,
193    pub surviving_node_count: usize,
194    pub mapped_surviving_node_count: usize,
195    pub reports: Vec<TransformSourceMapIntegrityFixtureReportV0>,
196    pub complete: bool,
197}
198
199type TransformSourceMapIntegrityFixtureV0 =
200    (&'static str, &'static str, &'static [TransformPassKind]);
201
202pub fn summarize_omena_transform_print_boundary() -> TransformPrintBoundarySummaryV0 {
203    TransformPrintBoundarySummaryV0 {
204        schema_version: "0",
205        product: "omena-transform-print.boundary",
206        emission_pass_id: TransformPassKind::PrintCss.id(),
207        supported_modes: vec![
208            TransformPrintMode::Identity,
209            TransformPrintMode::Pretty,
210            TransformPrintMode::Minified,
211        ],
212        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, mutation-span segments, and Source Map V3 mappings serialization",
213        planner_surface: "omena-transform-passes.plan",
214    }
215}
216
217pub fn summarize_transform_source_map_integrity_v0() -> TransformSourceMapIntegrityReportV0 {
218    const COMMENT_REMOVAL_PASSES: &[TransformPassKind] =
219        &[TransformPassKind::CommentStrip, TransformPassKind::PrintCss];
220    const DUPLICATE_RULE_REMOVAL_PASSES: &[TransformPassKind] = &[
221        TransformPassKind::RuleDeduplication,
222        TransformPassKind::PrintCss,
223    ];
224    const EMPTY_RULE_REMOVAL_PASSES: &[TransformPassKind] = &[
225        TransformPassKind::EmptyRuleRemoval,
226        TransformPassKind::PrintCss,
227    ];
228    let fixtures: [TransformSourceMapIntegrityFixtureV0; 3] = [
229        (
230            "comment-removal",
231            "/* remove */ .kept { color: red; }",
232            COMMENT_REMOVAL_PASSES,
233        ),
234        (
235            "duplicate-rule-removal",
236            ".kept { color: red; } .kept { color: red; }",
237            DUPLICATE_RULE_REMOVAL_PASSES,
238        ),
239        (
240            "empty-rule-removal",
241            ".empty {} .kept { color: red; }",
242            EMPTY_RULE_REMOVAL_PASSES,
243        ),
244    ];
245    summarize_transform_source_map_integrity_for_fixtures(fixtures.as_slice())
246}
247
248fn summarize_transform_source_map_integrity_for_fixtures(
249    fixtures: &[TransformSourceMapIntegrityFixtureV0],
250) -> TransformSourceMapIntegrityReportV0 {
251    let reports = fixtures
252        .iter()
253        .map(|(fixture, source, passes)| {
254            source_map_integrity_fixture_report(fixture, source, passes)
255        })
256        .collect::<Vec<_>>();
257    let destructive_fixture_count = reports
258        .iter()
259        .filter(|report| {
260            report.mutation_count > 0 && report.generated_byte_len < report.source_byte_len
261        })
262        .count();
263    let surviving_node_count = reports
264        .iter()
265        .map(|report| report.surviving_node_count)
266        .sum::<usize>();
267    let mapped_surviving_node_count = reports
268        .iter()
269        .map(|report| report.mapped_surviving_node_count)
270        .sum::<usize>();
271    let complete = !reports.is_empty()
272        && destructive_fixture_count == reports.len()
273        && surviving_node_count > 0
274        && mapped_surviving_node_count == surviving_node_count
275        && reports.iter().all(|report| report.complete);
276
277    TransformSourceMapIntegrityReportV0 {
278        schema_version: "0",
279        product: "omena-transform-print.source-map-integrity",
280        fixture_count: reports.len(),
281        destructive_fixture_count,
282        surviving_node_count,
283        mapped_surviving_node_count,
284        reports,
285        complete,
286    }
287}
288
289fn source_map_integrity_fixture_report(
290    fixture: &'static str,
291    source: &str,
292    passes: &[TransformPassKind],
293) -> TransformSourceMapIntegrityFixtureReportV0 {
294    let source_path = format!("{fixture}.module.css");
295    let execution =
296        execute_transform_passes_on_source_with_dialect(source, StyleDialect::Css, passes);
297    let artifact = print_transform_execution_artifact_with_dialect_and_source(
298        source_path.as_str(),
299        source,
300        StyleDialect::Css,
301        format!("source-map-integrity:{fixture}"),
302        passes,
303        default_print_options(),
304        &execution,
305    );
306    let surviving_nodes = artifact
307        .cst_artifact
308        .stable_ir
309        .nodes
310        .iter()
311        .filter(|node| node.source_span_start < node.source_span_end)
312        .collect::<Vec<_>>();
313    let mapped_surviving_node_count = surviving_nodes
314        .iter()
315        .filter(|node| {
316            artifact.source_map_segments.iter().any(|segment| {
317                segment.generated_start <= node.source_span_start
318                    && segment.generated_end >= node.source_span_end
319            })
320        })
321        .count();
322    let no_dangling_segments = artifact.source_map_segments.iter().all(|segment| {
323        segment.original_start <= segment.original_end
324            && segment.original_end <= source.len()
325            && segment.generated_start <= segment.generated_end
326            && segment.generated_end <= artifact.css.len()
327            && source.is_char_boundary(segment.original_start)
328            && source.is_char_boundary(segment.original_end)
329            && artifact.css.is_char_boundary(segment.generated_start)
330            && artifact.css.is_char_boundary(segment.generated_end)
331    });
332    let parsed_map = artifact.source_map_v3.as_ref().and_then(|source_map| {
333        serde_json::to_string(source_map)
334            .ok()
335            .and_then(|json| parse_transform_source_map_v3_json(json.as_str()).ok())
336    });
337    let map_parsed = parsed_map.is_some();
338    let decoded_segment_count = parsed_map
339        .as_ref()
340        .map_or(0, |parsed| parsed.decoded_segments.len());
341    let upstream_segments = full_source_identity_segments(source_path.as_str(), source);
342    let upstream_map = serialize_transform_source_map_v3(
343        source_path.as_str(),
344        source,
345        source_path.as_str(),
346        Some(source),
347        upstream_segments.as_slice(),
348    );
349    let composition = artifact.source_map_v3.as_ref().and_then(|downstream| {
350        serde_json::to_string(&upstream_map)
351            .ok()
352            .map(|upstream_json| {
353                compose_transform_source_map_v3_with_upstream_map(
354                    downstream,
355                    source_path.as_str(),
356                    upstream_json.as_str(),
357                )
358            })
359    });
360    let upstream_decoded_segment_count = serde_json::to_string(&upstream_map)
361        .ok()
362        .and_then(|json| parse_transform_source_map_v3_json(json.as_str()).ok())
363        .map_or(0, |parsed| parsed.decoded_segments.len());
364    let upstream_map_applied = composition
365        .as_ref()
366        .is_some_and(|composition| composition.upstream_map_applied);
367    let composition_fallback_reason = composition
368        .as_ref()
369        .and_then(|composition| composition.fallback_reason.clone());
370    let composed_map_parsed = composition.as_ref().is_some_and(|composition| {
371        serde_json::to_string(&composition.source_map)
372            .ok()
373            .and_then(|json| parse_transform_source_map_v3_json(json.as_str()).ok())
374            .is_some()
375    });
376    let segment_count = artifact.source_map_segments.len();
377    let surviving_node_count = surviving_nodes.len();
378    let complete = execution.mutation_count > 0
379        && artifact.css.len() < source.len()
380        && segment_count > 0
381        && surviving_node_count > 0
382        && mapped_surviving_node_count == surviving_node_count
383        && map_parsed
384        && upstream_map_applied
385        && composed_map_parsed
386        && no_dangling_segments;
387
388    TransformSourceMapIntegrityFixtureReportV0 {
389        fixture,
390        pass_ids: passes.iter().map(|pass| pass.id()).collect(),
391        mutation_count: execution.mutation_count,
392        source_byte_len: source.len(),
393        generated_byte_len: artifact.css.len(),
394        segment_count,
395        surviving_node_count,
396        mapped_surviving_node_count,
397        map_parsed,
398        decoded_segment_count,
399        upstream_decoded_segment_count,
400        upstream_map_applied,
401        composition_fallback_reason,
402        composed_map_parsed,
403        no_dangling_segments,
404        complete,
405    }
406}
407
408fn full_source_identity_segments(
409    source_path: &str,
410    source: &str,
411) -> Vec<TransformSourceMapSegmentV0> {
412    let mut line_starts = vec![0];
413    line_starts.extend(
414        source
415            .char_indices()
416            .filter_map(|(index, character)| (character == '\n').then_some(index + 1)),
417    );
418    line_starts
419        .iter()
420        .enumerate()
421        .map(|(index, start)| {
422            let end = line_starts.get(index + 1).copied().unwrap_or(source.len());
423            source_map_segment(
424                source_path,
425                SourceMapSources {
426                    original: source,
427                    generated: source,
428                },
429                SourceMapSpanOffsets {
430                    original_start: *start,
431                    original_end: end,
432                    generated_start: *start,
433                    generated_end: end,
434                },
435                TransformPassKind::PrintCss.id(),
436            )
437        })
438        .collect()
439}
440
441pub fn print_transform_cst_source(
442    source_path: impl Into<String>,
443    source: &str,
444    semantic_signature: impl Into<String>,
445    upstream_passes: &[TransformPassKind],
446    options: TransformPrintOptionsV0,
447) -> TransformPrintArtifactV0 {
448    print_transform_cst_source_with_dialect(
449        source_path,
450        source,
451        StyleDialect::Css,
452        semantic_signature,
453        upstream_passes,
454        options,
455    )
456}
457
458pub fn print_transform_cst_source_with_dialect(
459    source_path: impl Into<String>,
460    source: &str,
461    dialect: StyleDialect,
462    semantic_signature: impl Into<String>,
463    upstream_passes: &[TransformPassKind],
464    options: TransformPrintOptionsV0,
465) -> TransformPrintArtifactV0 {
466    print_transform_cst_source_with_dialect_and_pretty_options(
467        source_path,
468        source,
469        dialect,
470        semantic_signature,
471        upstream_passes,
472        options,
473        default_pretty_format_options(),
474    )
475}
476
477pub fn print_transform_cst_source_with_dialect_and_pretty_options(
478    source_path: impl Into<String>,
479    source: &str,
480    dialect: StyleDialect,
481    semantic_signature: impl Into<String>,
482    upstream_passes: &[TransformPassKind],
483    options: TransformPrintOptionsV0,
484    pretty_options: PrettyFormatOptionsV0,
485) -> TransformPrintArtifactV0 {
486    let source_path = source_path.into();
487    let mut passes = upstream_passes.to_vec();
488    if !passes.contains(&TransformPassKind::PrintCss) {
489        passes.push(TransformPassKind::PrintCss);
490    }
491    let pass_plan = plan_transform_passes(&passes);
492    let cst_artifact =
493        build_transform_cst_artifact_with_dialect(source, dialect, semantic_signature, &passes);
494    let rendered = render_css_for_print_mode(
495        source,
496        dialect,
497        source_path.as_str(),
498        options.mode,
499        pretty_options,
500    );
501    let css = rendered.css;
502    let source_map_segments = if options.include_source_map {
503        compose_identity_source_map_segments(
504            &source_path,
505            source,
506            &css,
507            &cst_artifact,
508            TransformPassKind::PrintCss.id(),
509            rendered.generated_offset_lookup.as_deref(),
510        )
511    } else {
512        Vec::new()
513    };
514    let source_map_v3 = options.include_source_map.then(|| {
515        serialize_transform_source_map_v3(
516            &source_path,
517            &css,
518            &source_path,
519            Some(source),
520            source_map_segments.as_slice(),
521        )
522    });
523
524    TransformPrintArtifactV0 {
525        schema_version: "0",
526        product: "omena-transform-print.artifact",
527        source_path,
528        css,
529        source_map_segments,
530        source_map_v3,
531        pretty_format_report: rendered.pretty_format_report,
532        cst_artifact,
533        pass_plan,
534        provenance_preserved: true,
535    }
536}
537
538pub fn print_transform_execution_artifact(
539    source_path: impl Into<String>,
540    semantic_signature: impl Into<String>,
541    upstream_passes: &[TransformPassKind],
542    options: TransformPrintOptionsV0,
543    execution: &TransformExecutionSummaryV0,
544) -> TransformPrintArtifactV0 {
545    print_transform_execution_artifact_with_dialect(
546        source_path,
547        StyleDialect::Css,
548        semantic_signature,
549        upstream_passes,
550        options,
551        execution,
552    )
553}
554
555pub fn print_transform_execution_artifact_with_source(
556    source_path: impl Into<String>,
557    original_source: &str,
558    semantic_signature: impl Into<String>,
559    upstream_passes: &[TransformPassKind],
560    options: TransformPrintOptionsV0,
561    execution: &TransformExecutionSummaryV0,
562) -> TransformPrintArtifactV0 {
563    print_transform_execution_artifact_with_dialect_and_source(
564        source_path,
565        original_source,
566        StyleDialect::Css,
567        semantic_signature,
568        upstream_passes,
569        options,
570        execution,
571    )
572}
573
574pub fn print_transform_execution_artifact_with_dialect(
575    source_path: impl Into<String>,
576    dialect: StyleDialect,
577    semantic_signature: impl Into<String>,
578    upstream_passes: &[TransformPassKind],
579    options: TransformPrintOptionsV0,
580    execution: &TransformExecutionSummaryV0,
581) -> TransformPrintArtifactV0 {
582    let source_path = source_path.into();
583    let mut artifact = print_transform_cst_source_with_dialect(
584        source_path.clone(),
585        &execution.output_css,
586        dialect,
587        semantic_signature,
588        upstream_passes,
589        options,
590    );
591
592    if options.include_source_map {
593        artifact.source_map_segments = compose_source_map_segments_from_execution(
594            &source_path,
595            &execution.output_css,
596            &artifact.css,
597            execution,
598            generated_offset_lookup_for_print_mode(&execution.output_css, &artifact.css),
599        );
600        artifact.source_map_v3 = Some(serialize_transform_source_map_v3(
601            &source_path,
602            &artifact.css,
603            &source_path,
604            Some(&execution.output_css),
605            artifact.source_map_segments.as_slice(),
606        ));
607    }
608
609    artifact.provenance_preserved = artifact.provenance_preserved && execution.provenance_preserved;
610    artifact
611}
612
613pub fn print_transform_execution_artifact_with_dialect_and_source(
614    source_path: impl Into<String>,
615    original_source: &str,
616    dialect: StyleDialect,
617    semantic_signature: impl Into<String>,
618    upstream_passes: &[TransformPassKind],
619    options: TransformPrintOptionsV0,
620    execution: &TransformExecutionSummaryV0,
621) -> TransformPrintArtifactV0 {
622    let source_path = source_path.into();
623    let mut artifact = print_transform_cst_source_with_dialect(
624        source_path.clone(),
625        &execution.output_css,
626        dialect,
627        semantic_signature,
628        upstream_passes,
629        options,
630    );
631
632    if options.include_source_map {
633        artifact.source_map_segments = compose_source_map_segments_from_execution(
634            &source_path,
635            original_source,
636            &artifact.css,
637            execution,
638            generated_offset_lookup_for_print_mode(&execution.output_css, &artifact.css),
639        );
640        artifact.source_map_v3 = Some(serialize_transform_source_map_v3(
641            &source_path,
642            &artifact.css,
643            &source_path,
644            Some(original_source),
645            artifact.source_map_segments.as_slice(),
646        ));
647    }
648
649    artifact.provenance_preserved = artifact.provenance_preserved && execution.provenance_preserved;
650    artifact
651}
652
653pub const fn default_print_options() -> TransformPrintOptionsV0 {
654    TransformPrintOptionsV0 {
655        mode: TransformPrintMode::Identity,
656        include_source_map: true,
657    }
658}
659
660fn compose_source_map_segments_from_execution(
661    source_path: &str,
662    original_source: &str,
663    generated_source: &str,
664    execution: &TransformExecutionSummaryV0,
665    generated_offset_lookup: Option<Vec<usize>>,
666) -> Vec<TransformSourceMapSegmentV0> {
667    let generated_offset_lookup = generated_offset_lookup.as_deref();
668    execution
669        .provenance_derivation_forest
670        .nodes
671        .iter()
672        .flat_map(|node| {
673            let spans = if node.mutation_spans.is_empty() {
674                vec![(
675                    node.source_span_start,
676                    node.source_span_end,
677                    node.generated_span_start,
678                    node.generated_span_end,
679                )]
680            } else {
681                node.mutation_spans
682                    .iter()
683                    .map(|span| {
684                        (
685                            span.source_span_start,
686                            span.source_span_end,
687                            span.generated_span_start,
688                            span.generated_span_end,
689                        )
690                    })
691                    .collect::<Vec<_>>()
692            };
693
694            spans.into_iter().map(
695                |(original_start, original_end, generated_start, generated_end)| {
696                    let generated_start =
697                        generated_offset_lookup.map_or(generated_start, |lookup| {
698                            project_generated_offset(
699                                generated_start,
700                                generated_source.len(),
701                                Some(lookup),
702                            )
703                        });
704                    let generated_end = generated_offset_lookup.map_or(generated_end, |lookup| {
705                        project_generated_offset(
706                            generated_end,
707                            generated_source.len(),
708                            Some(lookup),
709                        )
710                    });
711                    source_map_segment(
712                        source_path,
713                        SourceMapSources {
714                            original: original_source,
715                            generated: generated_source,
716                        },
717                        SourceMapSpanOffsets {
718                            original_start,
719                            original_end,
720                            generated_start,
721                            generated_end,
722                        },
723                        node.pass_id,
724                    )
725                },
726            )
727        })
728        .collect()
729}
730
731fn compose_identity_source_map_segments(
732    source_path: &str,
733    source: &str,
734    generated: &str,
735    cst_artifact: &TransformCstArtifactV0,
736    pass_id: &'static str,
737    generated_offset_lookup: Option<&[usize]>,
738) -> Vec<TransformSourceMapSegmentV0> {
739    let anchor_segments = cst_artifact
740        .stable_ir
741        .provenance_anchors
742        .iter()
743        .filter(|anchor| anchor.source_span_start <= anchor.source_span_end)
744        .map(|anchor| {
745            source_map_segment(
746                source_path,
747                SourceMapSources {
748                    original: source,
749                    generated,
750                },
751                SourceMapSpanOffsets {
752                    original_start: anchor.source_span_start,
753                    original_end: anchor.source_span_end,
754                    generated_start: project_generated_offset(
755                        anchor.source_span_start,
756                        generated.len(),
757                        generated_offset_lookup,
758                    ),
759                    generated_end: project_generated_offset(
760                        anchor.source_span_end,
761                        generated.len(),
762                        generated_offset_lookup,
763                    ),
764                },
765                pass_id,
766            )
767        })
768        .collect::<Vec<_>>();
769    if !anchor_segments.is_empty() {
770        return anchor_segments;
771    }
772
773    if source.is_empty() {
774        return vec![source_map_segment(
775            source_path,
776            SourceMapSources {
777                original: source,
778                generated,
779            },
780            SourceMapSpanOffsets {
781                original_start: 0,
782                original_end: 0,
783                generated_start: project_generated_offset(
784                    0,
785                    generated.len(),
786                    generated_offset_lookup,
787                ),
788                generated_end: project_generated_offset(
789                    0,
790                    generated.len(),
791                    generated_offset_lookup,
792                ),
793            },
794            pass_id,
795        )];
796    }
797
798    let mut segments = Vec::new();
799    let mut segment_start = None;
800    for (index, byte) in source.bytes().enumerate() {
801        if byte.is_ascii_whitespace() {
802            if let Some(start) = segment_start.take() {
803                segments.push(source_map_segment(
804                    source_path,
805                    SourceMapSources {
806                        original: source,
807                        generated,
808                    },
809                    SourceMapSpanOffsets {
810                        original_start: start,
811                        original_end: index,
812                        generated_start: project_generated_offset(
813                            start,
814                            generated.len(),
815                            generated_offset_lookup,
816                        ),
817                        generated_end: project_generated_offset(
818                            index,
819                            generated.len(),
820                            generated_offset_lookup,
821                        ),
822                    },
823                    pass_id,
824                ));
825            }
826        } else if segment_start.is_none() {
827            segment_start = Some(index);
828        }
829    }
830
831    if let Some(start) = segment_start {
832        segments.push(source_map_segment(
833            source_path,
834            SourceMapSources {
835                original: source,
836                generated,
837            },
838            SourceMapSpanOffsets {
839                original_start: start,
840                original_end: source.len(),
841                generated_start: project_generated_offset(
842                    start,
843                    generated.len(),
844                    generated_offset_lookup,
845                ),
846                generated_end: project_generated_offset(
847                    source.len(),
848                    generated.len(),
849                    generated_offset_lookup,
850                ),
851            },
852            pass_id,
853        ));
854    }
855
856    segments
857}
858
859pub fn serialize_transform_source_map_v3(
860    file: impl Into<String>,
861    generated_css: &str,
862    source_path: impl Into<String>,
863    source_content: Option<&str>,
864    segments: &[TransformSourceMapSegmentV0],
865) -> TransformSourceMapV3V0 {
866    let source_path = source_path.into();
867    let source_contents = source_content
868        .map(|source_content| vec![(source_path.as_str(), source_content)])
869        .unwrap_or_default();
870    serialize_transform_source_map_v3_with_source_contents(
871        file,
872        generated_css,
873        source_path.as_str(),
874        source_contents.as_slice(),
875        segments,
876    )
877}
878
879pub fn serialize_transform_source_map_v3_with_source_contents(
880    file: impl Into<String>,
881    generated_css: &str,
882    source_path: impl Into<String>,
883    source_contents: &[(&str, &str)],
884    segments: &[TransformSourceMapSegmentV0],
885) -> TransformSourceMapV3V0 {
886    let source_path = source_path.into();
887    let sources = collect_source_map_sources(&source_path, segments);
888    let mappings = encode_source_map_v3_mappings(generated_css, &sources, segments);
889    let pass_ids = collect_source_map_pass_ids(segments);
890    let sources_content = sources
891        .iter()
892        .map(|source| source_content_for_source_path(source, source_contents))
893        .collect::<Vec<_>>();
894
895    TransformSourceMapV3V0 {
896        version: 3,
897        file: file.into(),
898        sources,
899        sources_content,
900        names: Vec::new(),
901        mappings,
902        x_omena_schema_version: "0",
903        x_omena_product: "omena-transform-print.source-map-v3",
904        x_omena_segment_count: segments.len(),
905        x_omena_pass_ids: pass_ids,
906    }
907}
908
909pub fn transform_source_map_point(source: &str, byte_offset: usize) -> TransformSourceMapPointV0 {
910    source_map_point(source, byte_offset)
911}
912
913pub fn parse_transform_source_map_v3_json(
914    source_map_json: &str,
915) -> Result<ParsedTransformSourceMapV3V0, TransformSourceMapV3ParseErrorV0> {
916    let raw = serde_json::from_str::<RawSourceMapV3V0>(source_map_json)
917        .map_err(|error| TransformSourceMapV3ParseErrorV0::Json(error.to_string()))?;
918    if raw.version != 3 {
919        return Err(TransformSourceMapV3ParseErrorV0::UnsupportedVersion(
920            raw.version,
921        ));
922    }
923    let generated_line_count = raw.mappings.split(';').count().max(1);
924    let decoded_segments =
925        decode_source_map_v3_mappings(&raw.mappings, raw.sources.len(), raw.names.len())?;
926
927    Ok(ParsedTransformSourceMapV3V0 {
928        version: raw.version,
929        file: raw.file,
930        sources: raw.sources,
931        sources_content: raw.sources_content,
932        names: raw.names,
933        mappings: raw.mappings,
934        decoded_segments,
935        generated_line_count,
936    })
937}
938
939fn parse_transform_source_map_v3(
940    source_map: &TransformSourceMapV3V0,
941) -> Result<ParsedTransformSourceMapV3V0, TransformSourceMapV3ParseErrorV0> {
942    let generated_line_count = source_map.mappings.split(';').count().max(1);
943    let decoded_segments = decode_source_map_v3_mappings(
944        &source_map.mappings,
945        source_map.sources.len(),
946        source_map.names.len(),
947    )?;
948
949    Ok(ParsedTransformSourceMapV3V0 {
950        version: source_map.version,
951        file: Some(source_map.file.clone()),
952        sources: source_map.sources.clone(),
953        sources_content: source_map
954            .sources_content
955            .iter()
956            .map(|source_content| Some(source_content.clone()))
957            .collect(),
958        names: source_map.names.clone(),
959        mappings: source_map.mappings.clone(),
960        decoded_segments,
961        generated_line_count,
962    })
963}
964
965pub fn compose_transform_source_map_v3_with_upstream_map(
966    downstream_map: &TransformSourceMapV3V0,
967    upstream_source_path: &str,
968    upstream_map_json: &str,
969) -> TransformSourceMapCompositionV0 {
970    let downstream_parsed = match parse_transform_source_map_v3(downstream_map) {
971        Ok(parsed) => parsed,
972        Err(error) => {
973            return TransformSourceMapCompositionV0 {
974                source_map: downstream_map.clone(),
975                upstream_map_applied: false,
976                fallback_reason: Some(format!("downstream source map parse failed: {error:?}")),
977            };
978        }
979    };
980    let upstream_parsed = match parse_transform_source_map_v3_json(upstream_map_json) {
981        Ok(parsed) => parsed,
982        Err(error) => {
983            return TransformSourceMapCompositionV0 {
984                source_map: downstream_map.clone(),
985                upstream_map_applied: false,
986                fallback_reason: Some(format!("upstream source map parse failed: {error:?}")),
987            };
988        }
989    };
990    let Some(upstream_source_index) = downstream_map
991        .sources
992        .iter()
993        .position(|source| source == upstream_source_path)
994    else {
995        return TransformSourceMapCompositionV0 {
996            source_map: downstream_map.clone(),
997            upstream_map_applied: false,
998            fallback_reason: Some(format!(
999                "downstream source map does not include upstream source {upstream_source_path}"
1000            )),
1001        };
1002    };
1003
1004    let mut composed_segments = Vec::<ComposedSourceMapSegmentV0>::new();
1005    let mut upstream_map_applied = false;
1006    for segment in downstream_parsed.decoded_segments {
1007        if segment.source_index == upstream_source_index
1008            && let Some(upstream_segment) = find_decoded_source_map_segment_for_generated_position(
1009                upstream_parsed.decoded_segments.as_slice(),
1010                segment.original_line,
1011                segment.original_utf16_column,
1012            )
1013        {
1014            let source_path = upstream_parsed
1015                .sources
1016                .get(upstream_segment.source_index)
1017                .cloned()
1018                .unwrap_or_else(|| upstream_source_path.to_string());
1019            let source_content = source_content_at_index(
1020                upstream_parsed.sources_content.as_slice(),
1021                upstream_segment.source_index,
1022            );
1023            composed_segments.push(ComposedSourceMapSegmentV0 {
1024                generated_line: segment.generated_line,
1025                generated_utf16_column: segment.generated_utf16_column,
1026                source_path,
1027                source_content,
1028                original_line: upstream_segment.original_line,
1029                original_utf16_column: upstream_segment.original_utf16_column,
1030                name_index: None,
1031            });
1032            upstream_map_applied = true;
1033            continue;
1034        }
1035
1036        let source_path = downstream_map
1037            .sources
1038            .get(segment.source_index)
1039            .cloned()
1040            .unwrap_or_else(|| upstream_source_path.to_string());
1041        let source_content = source_content_at_index_as_string(
1042            downstream_map.sources_content.as_slice(),
1043            segment.source_index,
1044        );
1045        composed_segments.push(ComposedSourceMapSegmentV0 {
1046            generated_line: segment.generated_line,
1047            generated_utf16_column: segment.generated_utf16_column,
1048            source_path,
1049            source_content,
1050            original_line: segment.original_line,
1051            original_utf16_column: segment.original_utf16_column,
1052            name_index: segment.name_index,
1053        });
1054    }
1055
1056    if !upstream_map_applied {
1057        return TransformSourceMapCompositionV0 {
1058            source_map: downstream_map.clone(),
1059            upstream_map_applied: false,
1060            fallback_reason: Some(format!(
1061                "upstream source map had no segment for {upstream_source_path}"
1062            )),
1063        };
1064    }
1065
1066    let (sources, sources_content) = collect_composed_source_map_sources(&composed_segments);
1067    let mappings = encode_composed_source_map_v3_mappings(
1068        downstream_parsed.generated_line_count,
1069        sources.as_slice(),
1070        composed_segments.as_slice(),
1071    );
1072    TransformSourceMapCompositionV0 {
1073        source_map: TransformSourceMapV3V0 {
1074            version: downstream_map.version,
1075            file: downstream_map.file.clone(),
1076            sources,
1077            sources_content,
1078            names: downstream_map.names.clone(),
1079            mappings,
1080            x_omena_schema_version: downstream_map.x_omena_schema_version,
1081            x_omena_product: downstream_map.x_omena_product,
1082            x_omena_segment_count: composed_segments.len(),
1083            x_omena_pass_ids: downstream_map.x_omena_pass_ids.clone(),
1084        },
1085        upstream_map_applied: true,
1086        fallback_reason: None,
1087    }
1088}
1089
1090fn source_content_for_source_path(source_path: &str, source_contents: &[(&str, &str)]) -> String {
1091    source_contents
1092        .iter()
1093        .find_map(|(candidate_path, source_content)| {
1094            (*candidate_path == source_path).then(|| (*source_content).to_string())
1095        })
1096        .unwrap_or_default()
1097}
1098
1099fn collect_source_map_sources(
1100    fallback_source_path: &str,
1101    segments: &[TransformSourceMapSegmentV0],
1102) -> Vec<String> {
1103    let mut sources = segments
1104        .iter()
1105        .map(|segment| segment.source_path.clone())
1106        .collect::<Vec<_>>();
1107    if sources.is_empty() {
1108        sources.push(fallback_source_path.to_string());
1109    }
1110    sources.sort();
1111    sources.dedup();
1112    sources
1113}
1114
1115fn collect_source_map_pass_ids(segments: &[TransformSourceMapSegmentV0]) -> Vec<&'static str> {
1116    let mut pass_ids = segments
1117        .iter()
1118        .map(|segment| segment.pass_id)
1119        .collect::<Vec<_>>();
1120    pass_ids.sort();
1121    pass_ids.dedup();
1122    pass_ids
1123}
1124
1125fn encode_source_map_v3_mappings(
1126    generated_css: &str,
1127    sources: &[String],
1128    segments: &[TransformSourceMapSegmentV0],
1129) -> String {
1130    let mut sortable_segments = segments
1131        .iter()
1132        .filter(|segment| {
1133            segment.generated_start_point.byte_offset <= segment.generated_end_point.byte_offset
1134        })
1135        .collect::<Vec<_>>();
1136    sortable_segments.sort_by(|left, right| {
1137        (
1138            left.generated_start_point.line,
1139            left.generated_start_point.utf16_column,
1140            left.source_path.as_str(),
1141            left.original_start_point.line,
1142            left.original_start_point.utf16_column,
1143        )
1144            .cmp(&(
1145                right.generated_start_point.line,
1146                right.generated_start_point.utf16_column,
1147                right.source_path.as_str(),
1148                right.original_start_point.line,
1149                right.original_start_point.utf16_column,
1150            ))
1151    });
1152
1153    let max_generated_line = generated_css
1154        .chars()
1155        .filter(|character| *character == '\n')
1156        .count()
1157        .max(
1158            sortable_segments
1159                .iter()
1160                .map(|segment| segment.generated_start_point.line)
1161                .max()
1162                .unwrap_or(0),
1163        );
1164    let mut lines = vec![Vec::<String>::new(); max_generated_line + 1];
1165    let mut previous_source_index = 0isize;
1166    let mut previous_original_line = 0isize;
1167    let mut previous_original_column = 0isize;
1168
1169    let mut index = 0;
1170    while index < sortable_segments.len() {
1171        let line_index = sortable_segments[index].generated_start_point.line;
1172        let mut line_end = index + 1;
1173        while line_end < sortable_segments.len()
1174            && sortable_segments[line_end].generated_start_point.line == line_index
1175        {
1176            line_end += 1;
1177        }
1178
1179        let mut previous_generated_column = 0isize;
1180        let mut seen = Vec::<(usize, usize, usize, usize)>::new();
1181        for segment in &sortable_segments[index..line_end] {
1182            let source_index = sources
1183                .iter()
1184                .position(|source| source == &segment.source_path)
1185                .unwrap_or(0);
1186            let generated_column = segment.generated_start_point.utf16_column;
1187            let original_line = segment.original_start_point.line;
1188            let original_column = segment.original_start_point.utf16_column;
1189            let key = (
1190                generated_column,
1191                source_index,
1192                original_line,
1193                original_column,
1194            );
1195            if seen.contains(&key) {
1196                continue;
1197            }
1198            seen.push(key);
1199
1200            let encoded_segment = [
1201                generated_column as isize - previous_generated_column,
1202                source_index as isize - previous_source_index,
1203                original_line as isize - previous_original_line,
1204                original_column as isize - previous_original_column,
1205            ]
1206            .into_iter()
1207            .map(encode_vlq_value)
1208            .collect::<String>();
1209            lines[line_index].push(encoded_segment);
1210
1211            previous_generated_column = generated_column as isize;
1212            previous_source_index = source_index as isize;
1213            previous_original_line = original_line as isize;
1214            previous_original_column = original_column as isize;
1215        }
1216
1217        index = line_end;
1218    }
1219
1220    lines
1221        .into_iter()
1222        .map(|line_segments| line_segments.join(","))
1223        .collect::<Vec<_>>()
1224        .join(";")
1225}
1226
1227fn encode_vlq_value(value: isize) -> String {
1228    let mut value = if value < 0 {
1229        ((-value as usize) << 1) + 1
1230    } else {
1231        (value as usize) << 1
1232    };
1233    let mut output = String::new();
1234    loop {
1235        let mut digit = value & 0b11111;
1236        value >>= 5;
1237        if value > 0 {
1238            digit |= 0b100000;
1239        }
1240        output.push(BASE64_VLQ_DIGITS[digit] as char);
1241        if value == 0 {
1242            break;
1243        }
1244    }
1245    output
1246}
1247
1248pub fn decode_vlq_values(
1249    encoded_segment: &str,
1250) -> Result<Vec<isize>, TransformSourceMapV3ParseErrorV0> {
1251    let mut values = Vec::new();
1252    let mut value = 0usize;
1253    let mut shift = 0usize;
1254    let mut has_digit = false;
1255
1256    for character in encoded_segment.bytes() {
1257        let digit = decode_base64_vlq_digit(character).ok_or_else(|| {
1258            TransformSourceMapV3ParseErrorV0::InvalidMapping(format!(
1259                "invalid base64 VLQ digit '{}'",
1260                character as char
1261            ))
1262        })?;
1263        has_digit = true;
1264        value += (digit & 0b11111) << shift;
1265        if (digit & 0b100000) == 0 {
1266            values.push(decode_signed_vlq_value(value));
1267            value = 0;
1268            shift = 0;
1269            has_digit = false;
1270        } else {
1271            shift += 5;
1272            if shift > 60 {
1273                return Err(TransformSourceMapV3ParseErrorV0::InvalidMapping(
1274                    "base64 VLQ value exceeds supported width".to_string(),
1275                ));
1276            }
1277        }
1278    }
1279
1280    if has_digit {
1281        return Err(TransformSourceMapV3ParseErrorV0::InvalidMapping(
1282            "unterminated base64 VLQ value".to_string(),
1283        ));
1284    }
1285
1286    Ok(values)
1287}
1288
1289fn decode_source_map_v3_mappings(
1290    mappings: &str,
1291    source_count: usize,
1292    name_count: usize,
1293) -> Result<Vec<DecodedSourceMapSegmentV0>, TransformSourceMapV3ParseErrorV0> {
1294    let mut decoded_segments = Vec::new();
1295    let mut previous_source_index = 0isize;
1296    let mut previous_original_line = 0isize;
1297    let mut previous_original_column = 0isize;
1298    let mut previous_name_index = 0isize;
1299
1300    for (generated_line, line) in mappings.split(';').enumerate() {
1301        let mut previous_generated_column = 0isize;
1302        for encoded_segment in line.split(',').filter(|segment| !segment.is_empty()) {
1303            let values = decode_vlq_values(encoded_segment)?;
1304            if values.len() == 1 {
1305                previous_generated_column += values[0];
1306                continue;
1307            }
1308            if values.len() != 4 && values.len() != 5 {
1309                return Err(TransformSourceMapV3ParseErrorV0::InvalidMapping(
1310                    "source map segments must have 1, 4, or 5 fields".to_string(),
1311                ));
1312            }
1313
1314            previous_generated_column += values[0];
1315            previous_source_index += values[1];
1316            previous_original_line += values[2];
1317            previous_original_column += values[3];
1318            if previous_generated_column < 0
1319                || previous_source_index < 0
1320                || previous_original_line < 0
1321                || previous_original_column < 0
1322            {
1323                return Err(TransformSourceMapV3ParseErrorV0::InvalidMapping(
1324                    "source map segment produced a negative position".to_string(),
1325                ));
1326            }
1327            let source_index = previous_source_index as usize;
1328            if source_index >= source_count {
1329                return Err(TransformSourceMapV3ParseErrorV0::SourceIndexOutOfRange {
1330                    index: source_index,
1331                    source_count,
1332                });
1333            }
1334            let name_index = if values.len() == 5 {
1335                previous_name_index += values[4];
1336                if previous_name_index < 0 {
1337                    return Err(TransformSourceMapV3ParseErrorV0::InvalidMapping(
1338                        "source map segment produced a negative name index".to_string(),
1339                    ));
1340                }
1341                let name_index = previous_name_index as usize;
1342                if name_index >= name_count {
1343                    return Err(TransformSourceMapV3ParseErrorV0::NameIndexOutOfRange {
1344                        index: name_index,
1345                        name_count,
1346                    });
1347                }
1348                Some(name_index)
1349            } else {
1350                None
1351            };
1352
1353            decoded_segments.push(DecodedSourceMapSegmentV0 {
1354                generated_line,
1355                generated_utf16_column: previous_generated_column as usize,
1356                source_index,
1357                original_line: previous_original_line as usize,
1358                original_utf16_column: previous_original_column as usize,
1359                name_index,
1360            });
1361        }
1362    }
1363
1364    Ok(decoded_segments)
1365}
1366
1367fn decode_base64_vlq_digit(character: u8) -> Option<usize> {
1368    BASE64_VLQ_DIGITS
1369        .iter()
1370        .position(|candidate| *candidate == character)
1371}
1372
1373fn decode_signed_vlq_value(value: usize) -> isize {
1374    let shifted = (value >> 1) as isize;
1375    if (value & 1) == 1 { -shifted } else { shifted }
1376}
1377
1378#[derive(Debug, Clone, PartialEq, Eq)]
1379struct ComposedSourceMapSegmentV0 {
1380    generated_line: usize,
1381    generated_utf16_column: usize,
1382    source_path: String,
1383    source_content: String,
1384    original_line: usize,
1385    original_utf16_column: usize,
1386    name_index: Option<usize>,
1387}
1388
1389fn find_decoded_source_map_segment_for_generated_position(
1390    segments: &[DecodedSourceMapSegmentV0],
1391    generated_line: usize,
1392    generated_utf16_column: usize,
1393) -> Option<&DecodedSourceMapSegmentV0> {
1394    segments
1395        .iter()
1396        .filter(|segment| {
1397            segment.generated_line == generated_line
1398                && segment.generated_utf16_column <= generated_utf16_column
1399        })
1400        .max_by_key(|segment| segment.generated_utf16_column)
1401}
1402
1403fn source_content_at_index(source_contents: &[Option<String>], source_index: usize) -> String {
1404    source_contents
1405        .get(source_index)
1406        .and_then(|source_content| source_content.clone())
1407        .unwrap_or_default()
1408}
1409
1410fn source_content_at_index_as_string(source_contents: &[String], source_index: usize) -> String {
1411    source_contents
1412        .get(source_index)
1413        .cloned()
1414        .unwrap_or_default()
1415}
1416
1417fn collect_composed_source_map_sources(
1418    segments: &[ComposedSourceMapSegmentV0],
1419) -> (Vec<String>, Vec<String>) {
1420    let mut sources = Vec::<String>::new();
1421    let mut sources_content = Vec::<String>::new();
1422    for segment in segments {
1423        if sources.contains(&segment.source_path) {
1424            continue;
1425        }
1426        sources.push(segment.source_path.clone());
1427        sources_content.push(segment.source_content.clone());
1428    }
1429    if sources.is_empty() {
1430        sources.push(String::new());
1431        sources_content.push(String::new());
1432    }
1433    (sources, sources_content)
1434}
1435
1436fn encode_composed_source_map_v3_mappings(
1437    generated_line_count: usize,
1438    sources: &[String],
1439    segments: &[ComposedSourceMapSegmentV0],
1440) -> String {
1441    let mut sortable_segments = segments.iter().collect::<Vec<_>>();
1442    sortable_segments.sort_by(|left, right| {
1443        (
1444            left.generated_line,
1445            left.generated_utf16_column,
1446            left.source_path.as_str(),
1447            left.original_line,
1448            left.original_utf16_column,
1449        )
1450            .cmp(&(
1451                right.generated_line,
1452                right.generated_utf16_column,
1453                right.source_path.as_str(),
1454                right.original_line,
1455                right.original_utf16_column,
1456            ))
1457    });
1458    let max_generated_line = generated_line_count.saturating_sub(1).max(
1459        sortable_segments
1460            .iter()
1461            .map(|segment| segment.generated_line)
1462            .max()
1463            .unwrap_or(0),
1464    );
1465    let mut lines = vec![Vec::<String>::new(); max_generated_line + 1];
1466    let mut previous_source_index = 0isize;
1467    let mut previous_original_line = 0isize;
1468    let mut previous_original_column = 0isize;
1469    let mut previous_name_index = 0isize;
1470
1471    let mut index = 0;
1472    while index < sortable_segments.len() {
1473        let line_index = sortable_segments[index].generated_line;
1474        let mut line_end = index + 1;
1475        while line_end < sortable_segments.len()
1476            && sortable_segments[line_end].generated_line == line_index
1477        {
1478            line_end += 1;
1479        }
1480
1481        let mut previous_generated_column = 0isize;
1482        for segment in &sortable_segments[index..line_end] {
1483            let source_index = sources
1484                .iter()
1485                .position(|source| source == &segment.source_path)
1486                .unwrap_or(0);
1487            let mut values = vec![
1488                segment.generated_utf16_column as isize - previous_generated_column,
1489                source_index as isize - previous_source_index,
1490                segment.original_line as isize - previous_original_line,
1491                segment.original_utf16_column as isize - previous_original_column,
1492            ];
1493            if let Some(name_index) = segment.name_index {
1494                values.push(name_index as isize - previous_name_index);
1495                previous_name_index = name_index as isize;
1496            }
1497            let encoded_segment = values.into_iter().map(encode_vlq_value).collect::<String>();
1498            lines[line_index].push(encoded_segment);
1499
1500            previous_generated_column = segment.generated_utf16_column as isize;
1501            previous_source_index = source_index as isize;
1502            previous_original_line = segment.original_line as isize;
1503            previous_original_column = segment.original_utf16_column as isize;
1504        }
1505
1506        index = line_end;
1507    }
1508
1509    lines
1510        .into_iter()
1511        .map(|line_segments| line_segments.join(","))
1512        .collect::<Vec<_>>()
1513        .join(";")
1514}
1515
1516const BASE64_VLQ_DIGITS: &[u8; 64] =
1517    b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1518
1519#[derive(Debug, Clone, Copy)]
1520struct SourceMapSources<'a> {
1521    original: &'a str,
1522    generated: &'a str,
1523}
1524
1525#[derive(Debug, Clone, Copy)]
1526struct SourceMapSpanOffsets {
1527    original_start: usize,
1528    original_end: usize,
1529    generated_start: usize,
1530    generated_end: usize,
1531}
1532
1533fn source_map_segment(
1534    source_path: &str,
1535    sources: SourceMapSources<'_>,
1536    offsets: SourceMapSpanOffsets,
1537    pass_id: &'static str,
1538) -> TransformSourceMapSegmentV0 {
1539    TransformSourceMapSegmentV0 {
1540        source_path: source_path.to_string(),
1541        original_start: offsets.original_start,
1542        original_end: offsets.original_end,
1543        generated_start: offsets.generated_start,
1544        generated_end: offsets.generated_end,
1545        original_start_point: source_map_point(sources.original, offsets.original_start),
1546        original_end_point: source_map_point(sources.original, offsets.original_end),
1547        generated_start_point: source_map_point(sources.generated, offsets.generated_start),
1548        generated_end_point: source_map_point(sources.generated, offsets.generated_end),
1549        pass_id,
1550    }
1551}
1552
1553fn source_map_point(source: &str, byte_offset: usize) -> TransformSourceMapPointV0 {
1554    let byte_offset = byte_offset.min(source.len());
1555    let mut line = 0;
1556    let mut line_start_byte = 0;
1557    let mut utf16_column = 0;
1558
1559    for (index, character) in source.char_indices() {
1560        if index >= byte_offset {
1561            break;
1562        }
1563        if character == '\n' {
1564            line += 1;
1565            line_start_byte = index + character.len_utf8();
1566            utf16_column = 0;
1567        } else {
1568            utf16_column += character.len_utf16();
1569        }
1570    }
1571
1572    TransformSourceMapPointV0 {
1573        byte_offset,
1574        line,
1575        utf8_column: byte_offset.saturating_sub(line_start_byte),
1576        utf16_column,
1577    }
1578}
1579
1580#[derive(Debug, Clone, PartialEq, Eq)]
1581struct RenderedPrintCss {
1582    css: String,
1583    generated_offset_lookup: Option<Vec<usize>>,
1584    pretty_format_report: Option<PrettyFormatReportV0>,
1585}
1586
1587fn render_css_for_print_mode(
1588    source: &str,
1589    dialect: StyleDialect,
1590    source_id: &str,
1591    mode: TransformPrintMode,
1592    pretty_options: PrettyFormatOptionsV0,
1593) -> RenderedPrintCss {
1594    match mode {
1595        TransformPrintMode::Minified => {
1596            let execution = execute_transform_passes_on_source_with_dialect(
1597                source,
1598                dialect,
1599                &[
1600                    TransformPassKind::CommentStrip,
1601                    TransformPassKind::WhitespaceStrip,
1602                    TransformPassKind::PrintCss,
1603                ],
1604            );
1605            let generated_offset_lookup =
1606                generated_offset_lookup_for_deleted_subsequence(source, &execution.output_css);
1607            RenderedPrintCss {
1608                css: execution.output_css,
1609                generated_offset_lookup: Some(generated_offset_lookup),
1610                pretty_format_report: None,
1611            }
1612        }
1613        TransformPrintMode::Pretty => {
1614            let rendered = doc_ir::render_pretty_css_through_transform_ir(
1615                source,
1616                dialect,
1617                source_id,
1618                pretty_options,
1619            );
1620            RenderedPrintCss {
1621                css: rendered.css,
1622                generated_offset_lookup: Some(rendered.generated_offset_lookup),
1623                pretty_format_report: Some(rendered.report),
1624            }
1625        }
1626        TransformPrintMode::Identity => RenderedPrintCss {
1627            css: source.to_string(),
1628            generated_offset_lookup: None,
1629            pretty_format_report: None,
1630        },
1631    }
1632}
1633
1634fn generated_offset_lookup_for_print_mode(source: &str, generated: &str) -> Option<Vec<usize>> {
1635    (source != generated)
1636        .then(|| generated_offset_lookup_for_deleted_subsequence(source, generated))
1637}
1638
1639fn generated_offset_lookup_for_deleted_subsequence(source: &str, generated: &str) -> Vec<usize> {
1640    let source_bytes = source.as_bytes();
1641    let generated_bytes = generated.as_bytes();
1642    let mut lookup = vec![0; source_bytes.len() + 1];
1643    let mut generated_index = 0usize;
1644
1645    for index in 0..source_bytes.len() {
1646        lookup[index] = generated_index;
1647        if generated_index < generated_bytes.len()
1648            && source_bytes[index] == generated_bytes[generated_index]
1649        {
1650            generated_index += 1;
1651        }
1652    }
1653    lookup[source_bytes.len()] = generated_bytes.len();
1654    lookup
1655}
1656
1657fn project_generated_offset(
1658    source_offset: usize,
1659    generated_len: usize,
1660    generated_offset_lookup: Option<&[usize]>,
1661) -> usize {
1662    generated_offset_lookup
1663        .and_then(|lookup| lookup.get(source_offset).copied())
1664        .unwrap_or(source_offset)
1665        .min(generated_len)
1666}
1667
1668#[cfg(test)]
1669mod tests {
1670    use super::{
1671        PrettyFormatOptionsV0, TransformPrintMode, TransformPrintOptionsV0, TransformSourceMapV3V0,
1672        compose_transform_source_map_v3_with_upstream_map, decode_vlq_values,
1673        default_print_options, encode_vlq_value, parse_transform_source_map_v3_json,
1674        print_transform_cst_source, print_transform_cst_source_with_dialect_and_pretty_options,
1675        print_transform_execution_artifact, print_transform_execution_artifact_with_source,
1676        serialize_transform_source_map_v3, source_map_point,
1677        summarize_omena_transform_print_boundary,
1678    };
1679    use omena_transform_cst::{StyleDialect, TransformPassKind};
1680    use omena_transform_passes::execute_transform_passes_on_source;
1681
1682    #[test]
1683    fn exposes_print_boundary() {
1684        let boundary = summarize_omena_transform_print_boundary();
1685
1686        assert_eq!(boundary.product, "omena-transform-print.boundary");
1687        assert_eq!(boundary.emission_pass_id, "print-css");
1688        assert_eq!(
1689            boundary.supported_modes,
1690            vec![
1691                TransformPrintMode::Identity,
1692                TransformPrintMode::Pretty,
1693                TransformPrintMode::Minified,
1694            ]
1695        );
1696    }
1697
1698    #[test]
1699    fn pretty_mode_uses_transform_ir_and_preserves_source_map_contract() {
1700        let source = ".button,.button--primary{color:var(--brand);padding:calc(1rem + 2px);}";
1701        let artifact = print_transform_cst_source_with_dialect_and_pretty_options(
1702            "Button.module.css",
1703            source,
1704            StyleDialect::Css,
1705            "semantic:button:pretty",
1706            &[],
1707            TransformPrintOptionsV0 {
1708                mode: TransformPrintMode::Pretty,
1709                include_source_map: true,
1710            },
1711            PrettyFormatOptionsV0 {
1712                line_width: 80,
1713                indent_width: 2,
1714            },
1715        );
1716
1717        assert_ne!(artifact.css, source);
1718        assert!(artifact.css.contains("\n  color: var(--brand);"));
1719        assert!(
1720            artifact
1721                .pretty_format_report
1722                .as_ref()
1723                .is_some_and(|report| { report.edit_count > 0 && report.fallback_node_count == 0 })
1724        );
1725        assert!(!artifact.source_map_segments.is_empty());
1726        assert!(artifact.source_map_segments.iter().all(|segment| {
1727            segment.original_start <= segment.original_end
1728                && segment.original_end <= source.len()
1729                && segment.generated_start <= segment.generated_end
1730                && segment.generated_end <= artifact.css.len()
1731        }));
1732        assert!(artifact.source_map_segments.iter().any(|segment| {
1733            segment.generated_start != segment.original_start
1734                && source.get(segment.original_start..segment.original_end)
1735                    == artifact
1736                        .css
1737                        .get(segment.generated_start..segment.generated_end)
1738        }));
1739        assert!(artifact.source_map_v3.as_ref().is_some_and(|source_map| {
1740            source_map.version == 3
1741                && !source_map.mappings.is_empty()
1742                && source_map.x_omena_segment_count == artifact.source_map_segments.len()
1743        }));
1744
1745        let stable = print_transform_cst_source(
1746            "Button.module.css",
1747            source,
1748            "semantic:button:stable",
1749            &[],
1750            TransformPrintOptionsV0 {
1751                mode: TransformPrintMode::Identity,
1752                include_source_map: true,
1753            },
1754        );
1755        assert_eq!(stable.css, source);
1756        assert!(stable.pretty_format_report.is_none());
1757    }
1758
1759    #[test]
1760    fn destructive_rewrites_keep_surviving_source_map_nodes_mapped() {
1761        let report = super::summarize_transform_source_map_integrity_v0();
1762
1763        assert!(report.complete, "{report:#?}");
1764        assert!(report.fixture_count > 0);
1765        assert_eq!(report.destructive_fixture_count, report.fixture_count);
1766        assert!(report.surviving_node_count > 0);
1767        assert_eq!(
1768            report.mapped_surviving_node_count,
1769            report.surviving_node_count
1770        );
1771        assert!(report.reports.iter().all(|fixture| {
1772            fixture.map_parsed
1773                && fixture.upstream_map_applied
1774                && fixture.composed_map_parsed
1775                && fixture.no_dangling_segments
1776        }));
1777    }
1778
1779    #[test]
1780    fn source_map_integrity_requires_a_destructive_fixture() {
1781        let report = super::summarize_transform_source_map_integrity_for_fixtures(&[]);
1782
1783        assert_eq!(report.fixture_count, 0);
1784        assert_eq!(report.destructive_fixture_count, 0);
1785        assert!(!report.complete);
1786    }
1787
1788    #[test]
1789    fn prints_identity_css_with_stable_ir_source_map_segments() {
1790        let source = ".button { color: var(--brand); background: red; }";
1791        let artifact = print_transform_cst_source(
1792            "Button.module.css",
1793            source,
1794            "semantic:button:brand",
1795            &[TransformPassKind::CalcReduction],
1796            default_print_options(),
1797        );
1798
1799        assert_eq!(artifact.product, "omena-transform-print.artifact");
1800        assert_eq!(artifact.css, source);
1801        assert!(artifact.provenance_preserved);
1802        assert!(artifact.source_map_segments.len() > 1);
1803        if artifact
1804            .cst_artifact
1805            .stable_ir
1806            .provenance_anchors
1807            .is_empty()
1808        {
1809            assert!(artifact.source_map_segments.len() > 1);
1810        } else {
1811            assert_eq!(
1812                artifact.source_map_segments.len(),
1813                artifact.cst_artifact.stable_ir.provenance_anchors.len()
1814            );
1815        }
1816        assert!(
1817            artifact
1818                .source_map_segments
1819                .iter()
1820                .any(|segment| &source[segment.original_start..segment.original_end] == "button")
1821        );
1822        let expected_last_original_end = artifact
1823            .cst_artifact
1824            .stable_ir
1825            .provenance_anchors
1826            .last()
1827            .map(|anchor| anchor.source_span_end)
1828            .unwrap_or(source.len());
1829        assert_eq!(
1830            artifact
1831                .source_map_segments
1832                .last()
1833                .map(|segment| segment.original_end),
1834            Some(expected_last_original_end)
1835        );
1836        assert_eq!(
1837            artifact.pass_plan.ordered_pass_ids,
1838            vec!["calc-reduction", "print-css"]
1839        );
1840        assert!(artifact.source_map_v3.as_ref().is_some_and(|source_map| {
1841            source_map.version == 3
1842                && source_map.file == "Button.module.css"
1843                && source_map.sources.len() == 1
1844                && source_map
1845                    .sources
1846                    .first()
1847                    .is_some_and(|source_path| source_path == "Button.module.css")
1848                && source_map.sources_content.len() == 1
1849                && source_map
1850                    .sources_content
1851                    .first()
1852                    .is_some_and(|source_content| source_content == source)
1853                && !source_map.mappings.is_empty()
1854                && source_map.x_omena_segment_count == artifact.source_map_segments.len()
1855        }));
1856    }
1857
1858    #[test]
1859    fn prints_minified_css_with_projected_source_map_offsets() {
1860        let source = "/* remove */ .button { color: red; margin: 0px; }";
1861        let artifact = print_transform_cst_source(
1862            "Button.module.css",
1863            source,
1864            "semantic:button",
1865            &[TransformPassKind::PrintCss],
1866            TransformPrintOptionsV0 {
1867                mode: TransformPrintMode::Minified,
1868                include_source_map: true,
1869            },
1870        );
1871
1872        assert_eq!(artifact.css, ".button{color:red;margin:0px}");
1873        assert!(artifact.provenance_preserved);
1874        assert!(!artifact.source_map_segments.is_empty());
1875        assert!(
1876            artifact
1877                .source_map_segments
1878                .iter()
1879                .all(|segment| segment.generated_end <= artifact.css.len())
1880        );
1881        assert!(
1882            artifact
1883                .source_map_segments
1884                .iter()
1885                .any(|segment| segment.generated_start < segment.original_start)
1886        );
1887        assert!(
1888            artifact
1889                .source_map_v3
1890                .as_ref()
1891                .is_some_and(|source_map| !source_map.mappings.is_empty())
1892        );
1893    }
1894
1895    #[test]
1896    fn serializes_source_map_v3_mappings_from_existing_segments() {
1897        let source = ".a { color: red; }\n.b { color: blue; }";
1898        let artifact = print_transform_cst_source(
1899            "Mapped.module.css",
1900            source,
1901            "semantic:mapped",
1902            &[TransformPassKind::PrintCss],
1903            default_print_options(),
1904        );
1905        let source_map = serialize_transform_source_map_v3(
1906            "Mapped.module.css",
1907            &artifact.css,
1908            "Mapped.module.css",
1909            Some(source),
1910            artifact.source_map_segments.as_slice(),
1911        );
1912
1913        assert_eq!(source_map.version, 3);
1914        assert_eq!(source_map.names, Vec::<String>::new());
1915        assert_eq!(source_map.sources, vec!["Mapped.module.css"]);
1916        assert_eq!(source_map.sources_content, vec![source]);
1917        assert_eq!(
1918            source_map.x_omena_pass_ids,
1919            vec![TransformPassKind::PrintCss.id()]
1920        );
1921        assert_eq!(
1922            source_map.x_omena_segment_count,
1923            artifact.source_map_segments.len()
1924        );
1925        assert!(
1926            source_map.mappings.contains(';'),
1927            "multi-line generated CSS should produce semicolon-separated mapping lines"
1928        );
1929    }
1930
1931    #[test]
1932    fn vlq_encoder_matches_source_map_signed_base64_values() {
1933        assert_eq!(encode_vlq_value(0), "A");
1934        assert_eq!(encode_vlq_value(1), "C");
1935        assert_eq!(encode_vlq_value(-1), "D");
1936        assert_eq!(encode_vlq_value(16), "gB");
1937    }
1938
1939    #[test]
1940    fn vlq_decoder_matches_source_map_signed_base64_values() {
1941        assert_eq!(decode_vlq_values("A"), Ok(vec![0]));
1942        assert_eq!(decode_vlq_values("C"), Ok(vec![1]));
1943        assert_eq!(decode_vlq_values("D"), Ok(vec![-1]));
1944        assert_eq!(decode_vlq_values("gB"), Ok(vec![16]));
1945        assert!(decode_vlq_values("!").is_err());
1946    }
1947
1948    #[test]
1949    fn parses_source_map_v3_fixture_mappings() {
1950        let upstream_map = include_str!("../fixtures/source-map/button.css.map");
1951        let parsed = parse_transform_source_map_v3_json(upstream_map);
1952
1953        assert!(
1954            parsed.is_ok(),
1955            "fixture source map should parse: {parsed:?}"
1956        );
1957        let Ok(parsed) = parsed else {
1958            return;
1959        };
1960        assert_eq!(parsed.version, 3);
1961        assert_eq!(parsed.sources, vec!["button.scss"]);
1962        let remapped_color = super::find_decoded_source_map_segment_for_generated_position(
1963            parsed.decoded_segments.as_slice(),
1964            1,
1965            2,
1966        );
1967        assert!(
1968            remapped_color.is_some(),
1969            "fixture should map compiled CSS color declaration"
1970        );
1971        if let Some(remapped_color) = remapped_color {
1972            assert_eq!(remapped_color.source_index, 0);
1973            assert_eq!(remapped_color.original_line, 3);
1974            assert_eq!(remapped_color.original_utf16_column, 9);
1975        }
1976    }
1977
1978    #[test]
1979    fn composes_upstream_sass_source_map_with_omena_origin_chain() {
1980        let compiled_css = include_str!("../fixtures/source-map/button.css");
1981        let original_scss = include_str!("../fixtures/source-map/button.scss");
1982        let upstream_map = include_str!("../fixtures/source-map/button.css.map");
1983        let downstream_map = TransformSourceMapV3V0 {
1984            version: 3,
1985            file: "bundle.css".to_string(),
1986            sources: vec!["button.css".to_string()],
1987            sources_content: vec![compiled_css.to_string()],
1988            names: Vec::new(),
1989            mappings: "AAAA;EACE;AACF".to_string(),
1990            x_omena_schema_version: "0",
1991            x_omena_product: "omena-transform-print.source-map-v3",
1992            x_omena_segment_count: 3,
1993            x_omena_pass_ids: vec![TransformPassKind::PrintCss.id()],
1994        };
1995
1996        let composition = compose_transform_source_map_v3_with_upstream_map(
1997            &downstream_map,
1998            "button.css",
1999            upstream_map,
2000        );
2001
2002        assert!(
2003            composition.upstream_map_applied,
2004            "upstream map should compose: {:?}",
2005            composition.fallback_reason
2006        );
2007        assert_eq!(composition.fallback_reason, None);
2008        assert_eq!(composition.source_map.sources, vec!["button.scss"]);
2009        assert_eq!(composition.source_map.sources_content, vec![original_scss]);
2010        let composed_json = serde_json::to_string(&composition.source_map);
2011        assert!(
2012            composed_json.is_ok(),
2013            "composed source map should serialize: {composed_json:?}"
2014        );
2015        let Ok(composed_json) = composed_json else {
2016            return;
2017        };
2018        let parsed_composed = parse_transform_source_map_v3_json(&composed_json);
2019        assert!(
2020            parsed_composed.is_ok(),
2021            "composed source map should parse: {parsed_composed:?}"
2022        );
2023        let Ok(parsed_composed) = parsed_composed else {
2024            return;
2025        };
2026        let composed_color = super::find_decoded_source_map_segment_for_generated_position(
2027            parsed_composed.decoded_segments.as_slice(),
2028            1,
2029            2,
2030        );
2031        assert!(
2032            composed_color.is_some(),
2033            "composed map should retain the bundle output color position"
2034        );
2035        if let Some(composed_color) = composed_color {
2036            assert_eq!(
2037                parsed_composed.sources.get(composed_color.source_index),
2038                Some(&"button.scss".to_string())
2039            );
2040            assert_eq!(composed_color.original_line, 3);
2041            assert_eq!(composed_color.original_utf16_column, 9);
2042        }
2043    }
2044
2045    #[test]
2046    fn upstream_source_map_composition_falls_back_on_malformed_maps() {
2047        let compiled_css = include_str!("../fixtures/source-map/button.css");
2048        let malformed_map = include_str!("../fixtures/source-map/malformed-vlq.css.map");
2049        let downstream_map = TransformSourceMapV3V0 {
2050            version: 3,
2051            file: "bundle.css".to_string(),
2052            sources: vec!["button.css".to_string()],
2053            sources_content: vec![compiled_css.to_string()],
2054            names: Vec::new(),
2055            mappings: "AAAA;EACE;AACF".to_string(),
2056            x_omena_schema_version: "0",
2057            x_omena_product: "omena-transform-print.source-map-v3",
2058            x_omena_segment_count: 3,
2059            x_omena_pass_ids: vec![TransformPassKind::PrintCss.id()],
2060        };
2061
2062        let composition = compose_transform_source_map_v3_with_upstream_map(
2063            &downstream_map,
2064            "button.css",
2065            malformed_map,
2066        );
2067
2068        assert!(!composition.upstream_map_applied);
2069        assert!(
2070            composition
2071                .fallback_reason
2072                .as_deref()
2073                .is_some_and(|reason| { reason.contains("upstream source map parse failed") })
2074        );
2075        assert_eq!(composition.source_map, downstream_map);
2076    }
2077
2078    #[test]
2079    fn source_map_points_include_column_precision_for_unicode_and_newlines() {
2080        let source = ".초기 { color: red; }\n.button { color: blue; }";
2081        let artifact = print_transform_cst_source(
2082            "Button.module.css",
2083            source,
2084            "semantic:unicode:button",
2085            &[TransformPassKind::PrintCss],
2086            default_print_options(),
2087        );
2088        let unicode_start = source.find("초기").unwrap_or(source.len());
2089        assert!(unicode_start < source.len());
2090        let unicode_end = unicode_start + "초기".len();
2091        let unicode_start_point = source_map_point(source, unicode_start);
2092        let unicode_end_point = source_map_point(source, unicode_end);
2093        let button_segment = artifact
2094            .source_map_segments
2095            .iter()
2096            .find(|segment| &source[segment.original_start..segment.original_end] == "button");
2097        assert!(button_segment.is_some());
2098
2099        assert_eq!(unicode_start_point.line, 0);
2100        assert_eq!(unicode_start_point.utf8_column, 1);
2101        assert_eq!(unicode_start_point.utf16_column, 1);
2102        assert_eq!(unicode_end_point.utf8_column, 7);
2103        assert_eq!(unicode_end_point.utf16_column, 3);
2104        if let Some(button_segment) = button_segment {
2105            assert_eq!(button_segment.original_start_point.line, 1);
2106            assert_eq!(button_segment.original_start_point.utf8_column, 1);
2107            assert_eq!(button_segment.original_start_point.utf16_column, 1);
2108            assert_eq!(
2109                button_segment.generated_start_point,
2110                button_segment.original_start_point
2111            );
2112        }
2113    }
2114
2115    #[test]
2116    fn composes_source_map_segments_from_execution_provenance() {
2117        let source = ".button { color: red; /* remove */ }";
2118        let execution = execute_transform_passes_on_source(
2119            source,
2120            &[
2121                TransformPassKind::CommentStrip,
2122                TransformPassKind::WhitespaceStrip,
2123                TransformPassKind::PrintCss,
2124            ],
2125        );
2126        let artifact = print_transform_execution_artifact(
2127            "Button.module.css",
2128            "semantic:button",
2129            &[
2130                TransformPassKind::CommentStrip,
2131                TransformPassKind::WhitespaceStrip,
2132                TransformPassKind::PrintCss,
2133            ],
2134            default_print_options(),
2135            &execution,
2136        );
2137
2138        assert_eq!(artifact.css, execution.output_css);
2139        assert!(artifact.provenance_preserved);
2140        assert_eq!(
2141            artifact.source_map_segments.len(),
2142            execution.provenance_derivation_forest.node_count
2143        );
2144        assert_eq!(
2145            artifact.source_map_segments[0].source_path,
2146            "Button.module.css"
2147        );
2148        assert_eq!(
2149            artifact.source_map_segments[0].original_start,
2150            execution.provenance_derivation_forest.nodes[0].source_span_start
2151        );
2152        assert_eq!(
2153            artifact.source_map_segments[0].original_end,
2154            execution.provenance_derivation_forest.nodes[0].source_span_end
2155        );
2156        assert_eq!(
2157            artifact.source_map_segments[0].generated_start,
2158            execution.provenance_derivation_forest.nodes[0].generated_span_start
2159        );
2160        assert_eq!(
2161            artifact.source_map_segments[0].generated_end,
2162            execution.provenance_derivation_forest.nodes[0].generated_span_end
2163        );
2164        assert!(
2165            artifact
2166                .source_map_segments
2167                .iter()
2168                .any(|segment| segment.pass_id == "comment-strip")
2169        );
2170        assert_eq!(
2171            artifact
2172                .source_map_segments
2173                .last()
2174                .map(|segment| segment.generated_end),
2175            Some(execution.output_byte_len)
2176        );
2177    }
2178
2179    #[test]
2180    fn minified_execution_artifact_projects_upstream_segments_to_final_css() {
2181        let source = ".button { color: red; }\n.card { color: blue; }";
2182        let execution = execute_transform_passes_on_source(source, &[TransformPassKind::PrintCss]);
2183        let artifact = print_transform_execution_artifact_with_source(
2184            "Button.module.css",
2185            source,
2186            "semantic:button-card",
2187            &[TransformPassKind::PrintCss],
2188            TransformPrintOptionsV0 {
2189                mode: TransformPrintMode::Minified,
2190                include_source_map: true,
2191            },
2192            &execution,
2193        );
2194
2195        assert_eq!(artifact.css, ".button{color:red}.card{color:blue}");
2196        assert!(artifact.provenance_preserved);
2197        assert_eq!(artifact.source_map_segments[0].generated_start, 0);
2198        assert_eq!(
2199            artifact
2200                .source_map_segments
2201                .last()
2202                .map(|segment| segment.generated_end),
2203            Some(artifact.css.len())
2204        );
2205    }
2206
2207    #[test]
2208    fn emits_one_source_map_segment_per_mutation_span() {
2209        let source = ".a { /* one */ color: red; }\n.b { /* two */ color: blue; }";
2210        let execution = execute_transform_passes_on_source(
2211            source,
2212            &[TransformPassKind::CommentStrip, TransformPassKind::PrintCss],
2213        );
2214        let comment_node = execution
2215            .provenance_derivation_forest
2216            .nodes
2217            .iter()
2218            .find(|node| node.pass_id == "comment-strip");
2219        assert!(comment_node.is_some());
2220        if let Some(comment_node) = comment_node {
2221            assert_eq!(comment_node.mutation_spans.len(), 2);
2222        }
2223
2224        let artifact = print_transform_execution_artifact_with_source(
2225            "Multi.module.css",
2226            source,
2227            "semantic:multi",
2228            &[TransformPassKind::CommentStrip, TransformPassKind::PrintCss],
2229            default_print_options(),
2230            &execution,
2231        );
2232        let comment_segments = artifact
2233            .source_map_segments
2234            .iter()
2235            .filter(|segment| segment.pass_id == "comment-strip")
2236            .collect::<Vec<_>>();
2237
2238        assert_eq!(comment_segments.len(), 2);
2239        assert!(comment_segments[0].original_start < comment_segments[1].original_start);
2240        assert_eq!(comment_segments[1].original_start_point.line, 1);
2241    }
2242}