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