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