1use omena_transform_cst::{
4 IrEditRegionV0, IrNodeKindV0, IrTransactionV0, StyleDialect, TransformIrV0,
5 lower_transform_ir_from_source, materialize_transform_ir_printed_source,
6};
7use serde::Serialize;
8use std::collections::{BTreeMap, BTreeSet};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
11#[serde(rename_all = "camelCase")]
12pub struct PrettyFormatOptionsV0 {
13 pub line_width: usize,
14 pub indent_width: usize,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
18#[serde(rename_all = "camelCase")]
19pub enum FormatIrCoverageStrategyV0 {
20 Structured,
21 VerbatimLeaf,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
25#[serde(rename_all = "camelCase")]
26pub struct FormatIrCoverageEntryV0 {
27 pub node_kind: &'static str,
28 pub strategy: FormatIrCoverageStrategyV0,
29}
30
31pub const FORMAT_IR_COVERAGE_MANIFEST_V0: &[FormatIrCoverageEntryV0] = &[
32 FormatIrCoverageEntryV0 {
33 node_kind: "style-rule",
34 strategy: FormatIrCoverageStrategyV0::Structured,
35 },
36 FormatIrCoverageEntryV0 {
37 node_kind: "at-rule",
38 strategy: FormatIrCoverageStrategyV0::Structured,
39 },
40 FormatIrCoverageEntryV0 {
41 node_kind: "declaration",
42 strategy: FormatIrCoverageStrategyV0::Structured,
43 },
44 FormatIrCoverageEntryV0 {
45 node_kind: "selector",
46 strategy: FormatIrCoverageStrategyV0::Structured,
47 },
48 FormatIrCoverageEntryV0 {
49 node_kind: "value",
50 strategy: FormatIrCoverageStrategyV0::VerbatimLeaf,
51 },
52 FormatIrCoverageEntryV0 {
53 node_kind: "url-value",
54 strategy: FormatIrCoverageStrategyV0::VerbatimLeaf,
55 },
56];
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
59#[serde(rename_all = "camelCase")]
60pub struct PrettyFormatReportV0 {
61 pub schema_version: &'static str,
62 pub product: &'static str,
63 pub line_width: usize,
64 pub indent_width: usize,
65 pub covered_node_count: usize,
66 pub structured_node_count: usize,
67 pub verbatim_leaf_count: usize,
68 pub fallback_node_count: usize,
69 pub edit_count: usize,
70 pub fallback_reasons: Vec<&'static str>,
71 pub coverage_manifest: Vec<FormatIrCoverageEntryV0>,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub(crate) struct PrettyRenderResultV0 {
76 pub css: String,
77 pub generated_offset_lookup: Vec<usize>,
78 pub report: PrettyFormatReportV0,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
82enum FormatDocV0 {
83 Text {
84 value: String,
85 source_span: Option<(usize, usize)>,
86 },
87 Sequence(Vec<FormatDocV0>),
88 Group(Vec<FormatDocV0>),
89 Indent(Vec<FormatDocV0>),
90 SoftLine,
91 HardLine,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95enum TokenKindV0 {
96 Text,
97 Whitespace,
98 Comment,
99 OpenBrace,
100 CloseBrace,
101 Semicolon,
102 Comma,
103 Colon,
104 OpenParen,
105 CloseParen,
106 OpenBracket,
107 CloseBracket,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
111struct FormatTokenV0 {
112 kind: TokenKindV0,
113 text: String,
114 source_start: usize,
115 source_end: usize,
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119struct RenderedAnchorV0 {
120 original_start: usize,
121 original_end: usize,
122 generated_start: usize,
123 generated_end: usize,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq)]
127struct FormatEditV0 {
128 root_node_index: usize,
129 source_start: usize,
130 source_end: usize,
131 replacement: String,
132 anchors: Vec<RenderedAnchorV0>,
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136enum RenderModeV0 {
137 Flat,
138 Break,
139}
140
141struct RenderStateV0 {
142 output: String,
143 column: usize,
144 pending_indent: usize,
145 anchors: Vec<RenderedAnchorV0>,
146 options: PrettyFormatOptionsV0,
147}
148
149pub const fn default_pretty_format_options() -> PrettyFormatOptionsV0 {
150 PrettyFormatOptionsV0 {
151 line_width: 100,
152 indent_width: 2,
153 }
154}
155
156pub(crate) fn render_pretty_css_through_transform_ir(
157 source: &str,
158 dialect: StyleDialect,
159 source_id: &str,
160 options: PrettyFormatOptionsV0,
161) -> PrettyRenderResultV0 {
162 let mut ir = lower_transform_ir_from_source(source, dialect, source_id);
163 let (structured_node_count, verbatim_leaf_count) = coverage_counts(&ir);
164 let covered_node_count = structured_node_count + verbatim_leaf_count;
165 let mut report = PrettyFormatReportV0 {
166 schema_version: "0",
167 product: "omena-transform-print.pretty-format-report",
168 line_width: options.line_width,
169 indent_width: options.indent_width,
170 covered_node_count,
171 structured_node_count,
172 verbatim_leaf_count,
173 fallback_node_count: 0,
174 edit_count: 0,
175 fallback_reasons: Vec::new(),
176 coverage_manifest: FORMAT_IR_COVERAGE_MANIFEST_V0.to_vec(),
177 };
178
179 if dialect == StyleDialect::Sass {
180 report.fallback_node_count = ir.nodes.len();
181 report
182 .fallback_reasons
183 .push("indented-sass-stable-fallback");
184 return stable_result(source, report);
185 }
186 if ir.parser_error_count > 0 {
187 report.fallback_node_count = ir.nodes.len();
188 report.fallback_reasons.push("parse-error-stable-fallback");
189 return stable_result(source, report);
190 }
191
192 let edits = build_format_edits(source, &ir, dialect, options);
193 if edits.is_empty() {
194 report.fallback_node_count = ir.nodes.len();
195 report
196 .fallback_reasons
197 .push("no-structured-root-stable-fallback");
198 return stable_result(source, report);
199 }
200 report.edit_count = edits.len();
201
202 let node_count = ir.nodes.len();
203 let mut transaction =
204 IrTransactionV0::new(&mut ir, "pretty-format", IrEditRegionV0::full(source.len()));
205 for edit in &edits {
206 let node_id = omena_transform_cst::IrNodeIdV0(edit.root_node_index);
207 if transaction
208 .replace_node_covering_span(
209 node_id,
210 edit.replacement.clone(),
211 edit.source_start,
212 edit.source_end,
213 )
214 .is_err()
215 {
216 report.fallback_node_count = node_count;
217 report.fallback_reasons.push("edit-plan-stable-fallback");
218 return stable_result(source, report);
219 }
220 }
221 if transaction.commit().is_err() {
222 report.fallback_node_count = node_count;
223 report.fallback_reasons.push("transaction-stable-fallback");
224 return stable_result(source, report);
225 }
226 let Ok(css) = materialize_transform_ir_printed_source(&mut ir) else {
227 report.fallback_node_count = ir.nodes.len();
228 report
229 .fallback_reasons
230 .push("materialization-stable-fallback");
231 return stable_result(source, report);
232 };
233
234 let anchors = edits
235 .iter()
236 .scan(0usize, |generated_base, edit| {
237 let base = *generated_base;
238 *generated_base += edit.replacement.len();
239 Some(edit.anchors.iter().map(move |anchor| RenderedAnchorV0 {
240 original_start: anchor.original_start,
241 original_end: anchor.original_end,
242 generated_start: base + anchor.generated_start,
243 generated_end: base + anchor.generated_end,
244 }))
245 })
246 .flatten()
247 .collect::<Vec<_>>();
248 let generated_offset_lookup = generated_lookup_from_anchors(source.len(), css.len(), &anchors);
249 PrettyRenderResultV0 {
250 css,
251 generated_offset_lookup,
252 report,
253 }
254}
255
256fn stable_result(source: &str, report: PrettyFormatReportV0) -> PrettyRenderResultV0 {
257 PrettyRenderResultV0 {
258 css: source.to_string(),
259 generated_offset_lookup: (0..=source.len()).collect(),
260 report,
261 }
262}
263
264fn coverage_counts(ir: &TransformIrV0) -> (usize, usize) {
265 ir.nodes.iter().fold(
266 (0, 0),
267 |(structured, verbatim), node| match coverage_strategy(node.kind) {
268 FormatIrCoverageStrategyV0::Structured => (structured + 1, verbatim),
269 FormatIrCoverageStrategyV0::VerbatimLeaf => (structured, verbatim + 1),
270 },
271 )
272}
273
274const fn coverage_strategy(kind: IrNodeKindV0) -> FormatIrCoverageStrategyV0 {
275 match kind {
276 IrNodeKindV0::StyleRule
277 | IrNodeKindV0::AtRule
278 | IrNodeKindV0::Declaration
279 | IrNodeKindV0::Selector => FormatIrCoverageStrategyV0::Structured,
280 IrNodeKindV0::Value | IrNodeKindV0::UrlValue => FormatIrCoverageStrategyV0::VerbatimLeaf,
281 }
282}
283
284fn build_format_edits(
285 source: &str,
286 ir: &TransformIrV0,
287 dialect: StyleDialect,
288 options: PrettyFormatOptionsV0,
289) -> Vec<FormatEditV0> {
290 let mut root_nodes = ir
291 .root_nodes
292 .iter()
293 .filter_map(|node_id| ir.nodes.get(node_id.index()))
294 .filter(|node| matches!(node.kind, IrNodeKindV0::StyleRule | IrNodeKindV0::AtRule))
295 .collect::<Vec<_>>();
296 root_nodes.sort_by_key(|node| (node.source_span_start, node.source_span_end));
297 let declaration_colons = declaration_colon_offsets(source, ir);
298 let verbatim_leaf_spans = verbatim_leaf_spans(source, ir);
299 let root_count = root_nodes.len();
300
301 root_nodes
302 .iter()
303 .enumerate()
304 .filter_map(|(index, root)| {
305 let source_start = if index == 0 {
306 0
307 } else {
308 root.source_span_start
309 };
310 let source_end = root_nodes
311 .get(index + 1)
312 .map_or(source.len(), |next| next.source_span_start);
313 let chunk = source.get(source_start..source_end)?;
314 let tokens = tokenize(chunk, source_start, dialect, &verbatim_leaf_spans);
315 let document = document_from_tokens(&tokens, &declaration_colons);
316 let mut rendered = render_document(&document, options);
317 while rendered.output.ends_with([' ', '\t', '\n', '\r']) {
318 rendered.output.pop();
319 }
320 rendered.output.push('\n');
321 if index + 1 < root_count {
322 rendered.output.push('\n');
323 }
324 Some(FormatEditV0 {
325 root_node_index: root.node_id.index(),
326 source_start,
327 source_end,
328 replacement: rendered.output,
329 anchors: rendered.anchors,
330 })
331 })
332 .collect()
333}
334
335fn declaration_colon_offsets(source: &str, ir: &TransformIrV0) -> BTreeSet<usize> {
336 ir.nodes
337 .iter()
338 .filter(|node| node.kind == IrNodeKindV0::Declaration)
339 .filter_map(|node| {
340 let declaration = source.get(node.source_span_start..node.source_span_end)?;
341 first_unquoted_colon(declaration).map(|offset| node.source_span_start + offset)
342 })
343 .collect()
344}
345
346fn verbatim_leaf_spans(source: &str, ir: &TransformIrV0) -> BTreeMap<usize, usize> {
347 let mut spans = BTreeMap::new();
348 for node in &ir.nodes {
349 if !matches!(node.kind, IrNodeKindV0::Value | IrNodeKindV0::UrlValue) {
350 continue;
351 }
352 let mut start = node.source_span_start;
353 let mut end = node.source_span_end;
354 while source
355 .as_bytes()
356 .get(start)
357 .is_some_and(u8::is_ascii_whitespace)
358 {
359 start += 1;
360 }
361 while end > start
362 && source
363 .as_bytes()
364 .get(end - 1)
365 .is_some_and(u8::is_ascii_whitespace)
366 {
367 end -= 1;
368 }
369 spans
370 .entry(start)
371 .and_modify(|existing_end: &mut usize| *existing_end = (*existing_end).max(end))
372 .or_insert(end);
373 }
374 spans
375}
376
377fn first_unquoted_colon(source: &str) -> Option<usize> {
378 let bytes = source.as_bytes();
379 let mut index = 0;
380 let mut quote = None;
381 while index < bytes.len() {
382 match (quote, bytes[index]) {
383 (Some(_), b'\\') => index = (index + 2).min(bytes.len()),
384 (Some(active), byte) if byte == active => {
385 quote = None;
386 index += 1;
387 }
388 (Some(_), _) => index += 1,
389 (None, b'\'' | b'"') => {
390 quote = Some(bytes[index]);
391 index += 1;
392 }
393 (None, b':') => return Some(index),
394 _ => index += 1,
395 }
396 }
397 None
398}
399
400fn tokenize(
401 source: &str,
402 source_base: usize,
403 dialect: StyleDialect,
404 verbatim_leaf_spans: &BTreeMap<usize, usize>,
405) -> Vec<FormatTokenV0> {
406 let bytes = source.as_bytes();
407 let mut tokens = Vec::new();
408 let mut index = 0;
409 while index < bytes.len() {
410 let start = index;
411 let absolute_start = source_base + start;
412 if let Some(absolute_end) = verbatim_leaf_spans.get(&absolute_start).copied()
413 && absolute_end > absolute_start
414 && absolute_end <= source_base + source.len()
415 {
416 let end = absolute_end - source_base;
417 tokens.push(FormatTokenV0 {
418 kind: TokenKindV0::Text,
419 text: source[start..end].to_string(),
420 source_start: absolute_start,
421 source_end: absolute_end,
422 });
423 index = end;
424 continue;
425 }
426 let (kind, end) = match bytes[index] {
427 byte if byte.is_ascii_whitespace() => {
428 index += 1;
429 while index < bytes.len() && bytes[index].is_ascii_whitespace() {
430 index += 1;
431 }
432 (TokenKindV0::Whitespace, index)
433 }
434 b'/' if bytes.get(index + 1) == Some(&b'*') => {
435 index += 2;
436 while index + 1 < bytes.len() && !(bytes[index] == b'*' && bytes[index + 1] == b'/')
437 {
438 index += 1;
439 }
440 index = (index + 2).min(bytes.len());
441 (TokenKindV0::Comment, index)
442 }
443 b'/' if starts_line_comment(bytes, index, dialect) => {
444 index += 2;
445 while index < bytes.len() && bytes[index] != b'\n' {
446 index += 1;
447 }
448 (TokenKindV0::Comment, index)
449 }
450 b'\'' | b'"' => {
451 let quote = bytes[index];
452 index += 1;
453 while index < bytes.len() {
454 if bytes[index] == b'\\' {
455 index = (index + 2).min(bytes.len());
456 } else if bytes[index] == quote {
457 index += 1;
458 break;
459 } else {
460 index += 1;
461 }
462 }
463 (TokenKindV0::Text, index)
464 }
465 b'#' | b'@' if bytes.get(index + 1) == Some(&b'{') => {
466 index = scan_interpolation(bytes, index);
467 (TokenKindV0::Text, index)
468 }
469 b'{' => (TokenKindV0::OpenBrace, index + 1),
470 b'}' => (TokenKindV0::CloseBrace, index + 1),
471 b';' => (TokenKindV0::Semicolon, index + 1),
472 b',' => (TokenKindV0::Comma, index + 1),
473 b':' => (TokenKindV0::Colon, index + 1),
474 b'(' => (TokenKindV0::OpenParen, index + 1),
475 b')' => (TokenKindV0::CloseParen, index + 1),
476 b'[' => (TokenKindV0::OpenBracket, index + 1),
477 b']' => (TokenKindV0::CloseBracket, index + 1),
478 _ => {
479 index += 1;
480 while index < bytes.len() {
481 let byte = bytes[index];
482 if verbatim_leaf_spans.contains_key(&(source_base + index)) {
483 break;
484 }
485 let starts_comment = byte == b'/'
486 && (bytes.get(index + 1) == Some(&b'*')
487 || starts_line_comment(bytes, index, dialect));
488 if byte.is_ascii_whitespace()
489 || matches!(
490 byte,
491 b'{' | b'}'
492 | b';'
493 | b','
494 | b':'
495 | b'('
496 | b')'
497 | b'['
498 | b']'
499 | b'\''
500 | b'"'
501 )
502 || starts_comment
503 {
504 break;
505 }
506 index += 1;
507 }
508 (TokenKindV0::Text, index)
509 }
510 };
511 index = end;
512 tokens.push(FormatTokenV0 {
513 kind,
514 text: source[start..end].to_string(),
515 source_start: source_base + start,
516 source_end: source_base + end,
517 });
518 }
519 tokens
520}
521
522fn starts_line_comment(bytes: &[u8], index: usize, dialect: StyleDialect) -> bool {
523 dialect != StyleDialect::Css
524 && bytes.get(index + 1) == Some(&b'/')
525 && index
526 .checked_sub(1)
527 .and_then(|previous| bytes.get(previous))
528 != Some(&b':')
529}
530
531fn scan_interpolation(bytes: &[u8], start: usize) -> usize {
532 let mut depth = 1usize;
533 let mut index = start + 2;
534 while index < bytes.len() && depth > 0 {
535 match bytes[index] {
536 b'\\' => index = (index + 2).min(bytes.len()),
537 b'{' => {
538 depth += 1;
539 index += 1;
540 }
541 b'}' => {
542 depth -= 1;
543 index += 1;
544 }
545 _ => index += 1,
546 }
547 }
548 index
549}
550
551fn document_from_tokens(
552 tokens: &[FormatTokenV0],
553 declaration_colons: &BTreeSet<usize>,
554) -> FormatDocV0 {
555 let mut index = 0;
556 FormatDocV0::Sequence(parse_document_sequence(
557 tokens,
558 &mut index,
559 declaration_colons,
560 false,
561 ))
562}
563
564fn parse_document_sequence(
565 tokens: &[FormatTokenV0],
566 index: &mut usize,
567 declaration_colons: &BTreeSet<usize>,
568 stop_at_close: bool,
569) -> Vec<FormatDocV0> {
570 let mut output = Vec::new();
571 let mut inline = Vec::new();
572 let mut paren_depth = 0usize;
573 let mut bracket_depth = 0usize;
574
575 while *index < tokens.len() {
576 let token = &tokens[*index];
577 let structural = paren_depth == 0 && bracket_depth == 0;
578 match token.kind {
579 TokenKindV0::CloseBrace if structural && stop_at_close => {
580 flush_inline(&mut output, &mut inline, declaration_colons);
581 *index += 1;
582 break;
583 }
584 TokenKindV0::OpenBrace if structural => {
585 trim_inline_whitespace(&mut inline);
586 flush_inline(&mut output, &mut inline, declaration_colons);
587 push_text(&mut output, " {", Some(token));
588 *index += 1;
589 let body = parse_document_sequence(tokens, index, declaration_colons, true);
590 if body.is_empty() {
591 if let Some(FormatDocV0::Text { value, .. }) = output.last_mut() {
592 value.push('}');
593 } else {
594 push_text(&mut output, "}", None);
595 }
596 } else {
597 output.push(FormatDocV0::Indent({
598 let mut indented = vec![FormatDocV0::HardLine];
599 indented.extend(body);
600 indented
601 }));
602 push_hard_line(&mut output);
603 push_text(&mut output, "}", None);
604 }
605 if *index < tokens.len()
606 && !(stop_at_close && tokens[*index].kind == TokenKindV0::CloseBrace)
607 {
608 push_hard_line(&mut output);
609 }
610 }
611 TokenKindV0::Semicolon if structural => {
612 trim_inline_whitespace(&mut inline);
613 flush_inline(&mut output, &mut inline, declaration_colons);
614 push_text(&mut output, ";", Some(token));
615 push_hard_line(&mut output);
616 *index += 1;
617 }
618 TokenKindV0::OpenParen => {
619 paren_depth += 1;
620 inline.push(token.clone());
621 *index += 1;
622 }
623 TokenKindV0::CloseParen => {
624 paren_depth = paren_depth.saturating_sub(1);
625 inline.push(token.clone());
626 *index += 1;
627 }
628 TokenKindV0::OpenBracket => {
629 bracket_depth += 1;
630 inline.push(token.clone());
631 *index += 1;
632 }
633 TokenKindV0::CloseBracket => {
634 bracket_depth = bracket_depth.saturating_sub(1);
635 inline.push(token.clone());
636 *index += 1;
637 }
638 TokenKindV0::Comment
639 if inline
640 .iter()
641 .all(|item| item.kind == TokenKindV0::Whitespace) =>
642 {
643 inline.clear();
644 push_text(&mut output, token.text.as_str(), Some(token));
645 push_hard_line(&mut output);
646 *index += 1;
647 }
648 TokenKindV0::CloseBrace if structural => {
649 *index += 1;
650 }
651 _ => {
652 inline.push(token.clone());
653 *index += 1;
654 }
655 }
656 }
657 flush_inline(&mut output, &mut inline, declaration_colons);
658 trim_trailing_lines(&mut output);
659 output
660}
661
662fn flush_inline(
663 output: &mut Vec<FormatDocV0>,
664 inline: &mut Vec<FormatTokenV0>,
665 declaration_colons: &BTreeSet<usize>,
666) {
667 trim_inline_whitespace(inline);
668 if inline.is_empty() {
669 return;
670 }
671 output.push(FormatDocV0::Group(inline_document(
672 inline,
673 declaration_colons,
674 )));
675 inline.clear();
676}
677
678fn inline_document(
679 tokens: &[FormatTokenV0],
680 declaration_colons: &BTreeSet<usize>,
681) -> Vec<FormatDocV0> {
682 let mut output = Vec::new();
683 let mut suppress_whitespace = false;
684 for token in tokens {
685 match token.kind {
686 TokenKindV0::Whitespace => {
687 if !suppress_whitespace {
688 push_soft_line(&mut output);
689 }
690 }
691 TokenKindV0::Comma => {
692 trim_trailing_soft_lines(&mut output);
693 push_text(&mut output, token.text.as_str(), Some(token));
694 push_soft_line(&mut output);
695 suppress_whitespace = true;
696 continue;
697 }
698 TokenKindV0::Colon if declaration_colons.contains(&token.source_start) => {
699 trim_trailing_soft_lines(&mut output);
700 push_text(&mut output, token.text.as_str(), Some(token));
701 push_soft_line(&mut output);
702 suppress_whitespace = true;
703 continue;
704 }
705 TokenKindV0::Colon => {
706 trim_trailing_soft_lines(&mut output);
707 push_text(&mut output, token.text.as_str(), Some(token));
708 suppress_whitespace = true;
709 continue;
710 }
711 TokenKindV0::CloseParen | TokenKindV0::CloseBracket => {
712 trim_trailing_soft_lines(&mut output);
713 push_text(&mut output, token.text.as_str(), Some(token));
714 }
715 TokenKindV0::OpenParen | TokenKindV0::OpenBracket => {
716 trim_trailing_soft_lines(&mut output);
717 push_text(&mut output, token.text.as_str(), Some(token));
718 suppress_whitespace = true;
719 continue;
720 }
721 _ => push_text(&mut output, token.text.as_str(), Some(token)),
722 }
723 suppress_whitespace = false;
724 }
725 trim_trailing_soft_lines(&mut output);
726 output
727}
728
729fn trim_inline_whitespace(tokens: &mut Vec<FormatTokenV0>) {
730 while tokens
731 .last()
732 .is_some_and(|token| token.kind == TokenKindV0::Whitespace)
733 {
734 tokens.pop();
735 }
736 let first_non_whitespace = tokens
737 .iter()
738 .position(|token| token.kind != TokenKindV0::Whitespace)
739 .unwrap_or(tokens.len());
740 tokens.drain(..first_non_whitespace);
741}
742
743fn push_text(output: &mut Vec<FormatDocV0>, value: &str, token: Option<&FormatTokenV0>) {
744 output.push(FormatDocV0::Text {
745 value: value.to_string(),
746 source_span: token.map(|token| (token.source_start, token.source_end)),
747 });
748}
749
750fn push_soft_line(output: &mut Vec<FormatDocV0>) {
751 if !output
752 .last()
753 .is_some_and(|item| matches!(item, FormatDocV0::SoftLine | FormatDocV0::HardLine))
754 {
755 output.push(FormatDocV0::SoftLine);
756 }
757}
758
759fn push_hard_line(output: &mut Vec<FormatDocV0>) {
760 trim_trailing_soft_lines(output);
761 if !output
762 .last()
763 .is_some_and(|item| matches!(item, FormatDocV0::HardLine))
764 {
765 output.push(FormatDocV0::HardLine);
766 }
767}
768
769fn trim_trailing_soft_lines(output: &mut Vec<FormatDocV0>) {
770 while output
771 .last()
772 .is_some_and(|item| matches!(item, FormatDocV0::SoftLine))
773 {
774 output.pop();
775 }
776}
777
778fn trim_trailing_lines(output: &mut Vec<FormatDocV0>) {
779 while output
780 .last()
781 .is_some_and(|item| matches!(item, FormatDocV0::SoftLine | FormatDocV0::HardLine))
782 {
783 output.pop();
784 }
785}
786
787fn render_document(document: &FormatDocV0, options: PrettyFormatOptionsV0) -> RenderStateV0 {
788 let mut state = RenderStateV0 {
789 output: String::new(),
790 column: 0,
791 pending_indent: 0,
792 anchors: Vec::new(),
793 options,
794 };
795 render_doc(document, RenderModeV0::Break, 0, &mut state);
796 state
797}
798
799fn render_doc(doc: &FormatDocV0, mode: RenderModeV0, indent: usize, state: &mut RenderStateV0) {
800 match doc {
801 FormatDocV0::Text { value, source_span } => {
802 write_indent_if_needed(state);
803 let generated_start = state.output.len();
804 state.output.push_str(value);
805 state.column += value.chars().count();
806 if let Some((original_start, original_end)) = source_span {
807 state.anchors.push(RenderedAnchorV0 {
808 original_start: *original_start,
809 original_end: *original_end,
810 generated_start,
811 generated_end: state.output.len(),
812 });
813 }
814 }
815 FormatDocV0::Sequence(items) => render_sequence(items, mode, indent, state),
816 FormatDocV0::Group(items) => {
817 let remaining = state.options.line_width.saturating_sub(state.column);
818 let group_mode = if flat_width(items).is_some_and(|width| width <= remaining) {
819 RenderModeV0::Flat
820 } else {
821 RenderModeV0::Break
822 };
823 render_sequence(items, group_mode, indent, state);
824 }
825 FormatDocV0::Indent(items) => {
826 render_sequence(items, mode, indent + state.options.indent_width, state);
827 }
828 FormatDocV0::SoftLine => render_soft_line(mode, indent, 0, state),
829 FormatDocV0::HardLine => write_newline(indent, state),
830 }
831}
832
833fn render_sequence(
834 items: &[FormatDocV0],
835 mode: RenderModeV0,
836 indent: usize,
837 state: &mut RenderStateV0,
838) {
839 for (index, item) in items.iter().enumerate() {
840 if matches!(item, FormatDocV0::SoftLine) {
841 let next_width = flat_width_until_break(&items[index + 1..]);
842 render_soft_line(mode, indent, next_width, state);
843 } else {
844 render_doc(item, mode, indent, state);
845 }
846 }
847}
848
849fn render_soft_line(
850 mode: RenderModeV0,
851 indent: usize,
852 next_width: usize,
853 state: &mut RenderStateV0,
854) {
855 if mode == RenderModeV0::Flat || state.column + 1 + next_width <= state.options.line_width {
856 if !state.output.ends_with([' ', '\n']) {
857 state.output.push(' ');
858 state.column += 1;
859 }
860 } else {
861 write_newline(indent, state);
862 }
863}
864
865fn write_newline(indent: usize, state: &mut RenderStateV0) {
866 while state.output.ends_with([' ', '\t']) {
867 state.output.pop();
868 }
869 if !state.output.ends_with('\n') {
870 state.output.push('\n');
871 }
872 state.column = 0;
873 state.pending_indent = indent;
874}
875
876fn write_indent_if_needed(state: &mut RenderStateV0) {
877 if state.column == 0 && state.pending_indent > 0 {
878 state
879 .output
880 .extend(std::iter::repeat_n(' ', state.pending_indent));
881 state.column = state.pending_indent;
882 state.pending_indent = 0;
883 }
884}
885
886fn flat_width(items: &[FormatDocV0]) -> Option<usize> {
887 items.iter().try_fold(0usize, |width, item| {
888 flat_doc_width(item).map(|item_width| width + item_width)
889 })
890}
891
892fn flat_doc_width(doc: &FormatDocV0) -> Option<usize> {
893 match doc {
894 FormatDocV0::Text { value, .. } => Some(value.chars().count()),
895 FormatDocV0::Sequence(items) | FormatDocV0::Group(items) | FormatDocV0::Indent(items) => {
896 flat_width(items)
897 }
898 FormatDocV0::SoftLine => Some(1),
899 FormatDocV0::HardLine => None,
900 }
901}
902
903fn flat_width_until_break(items: &[FormatDocV0]) -> usize {
904 let mut width = 0;
905 for item in items {
906 match item {
907 FormatDocV0::SoftLine | FormatDocV0::HardLine => break,
908 _ => match flat_doc_width(item) {
909 Some(item_width) => width += item_width,
910 None => break,
911 },
912 }
913 }
914 width
915}
916
917fn generated_lookup_from_anchors(
918 source_len: usize,
919 generated_len: usize,
920 anchors: &[RenderedAnchorV0],
921) -> Vec<usize> {
922 let mut anchors = anchors.to_vec();
923 anchors.sort_by_key(|anchor| (anchor.original_start, anchor.original_end));
924 let mut lookup = vec![0usize; source_len + 1];
925 let mut original_cursor = 0usize;
926 let mut generated_cursor = 0usize;
927
928 for anchor in anchors {
929 if anchor.original_start < original_cursor || anchor.original_end > source_len {
930 continue;
931 }
932 fill_lookup_range(
933 &mut lookup,
934 original_cursor,
935 anchor.original_start,
936 generated_cursor,
937 anchor.generated_start,
938 );
939 fill_lookup_range(
940 &mut lookup,
941 anchor.original_start,
942 anchor.original_end,
943 anchor.generated_start,
944 anchor.generated_end,
945 );
946 original_cursor = anchor.original_end;
947 generated_cursor = anchor.generated_end;
948 }
949 fill_lookup_range(
950 &mut lookup,
951 original_cursor,
952 source_len,
953 generated_cursor,
954 generated_len,
955 );
956 for index in 1..lookup.len() {
957 lookup[index] = lookup[index].max(lookup[index - 1]).min(generated_len);
958 }
959 lookup
960}
961
962fn fill_lookup_range(
963 lookup: &mut [usize],
964 source_start: usize,
965 source_end: usize,
966 generated_start: usize,
967 generated_end: usize,
968) {
969 if source_start >= lookup.len() || source_end >= lookup.len() || source_start > source_end {
970 return;
971 }
972 let source_len = source_end.saturating_sub(source_start);
973 let generated_len = generated_end.saturating_sub(generated_start);
974 for (relative, mapped_offset) in lookup[source_start..=source_end].iter_mut().enumerate() {
975 let generated_relative = generated_len
976 .saturating_mul(relative)
977 .checked_div(source_len)
978 .unwrap_or(0);
979 *mapped_offset = generated_start + generated_relative;
980 }
981}
982
983#[cfg(test)]
984mod tests;