1use 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}