Skip to main content

omena_transform_print/
doc_ir.rs

1//! Width-sensitive document IR for pretty CSS formatting.
2
3use 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;