Skip to main content

eure_fmt/
source.rs

1//! Formatter for SourceDocument.
2//!
3//! This module provides formatting functionality for `SourceDocument`,
4//! which is used when converting from other formats (TOML, JSON, etc.) to Eure.
5//!
6//! The implementation builds a `Doc` IR that integrates with eure-fmt's
7//! pretty-printing infrastructure.
8
9use crate::config::FormatConfig;
10use crate::doc::Doc;
11use crate::printer::Printer;
12
13use eure_document::document::node::{NodeArray, NodeMap, NodeTuple, NodeValue};
14use eure_document::document::{EureDocument, NodeId};
15use eure_document::identifier::Identifier;
16use eure_document::path::ArrayIndexKind;
17use eure_document::source::{
18    ArrayElementSource, BindSource, BindingSource, Comment, EureSource, SectionBody, SectionSource,
19    SourceDocument, SourceId, SourceKey, SourcePathSegment, StringStyle, Trivia,
20};
21use eure_document::text::{Language, SyntaxHint};
22use eure_document::value::{ObjectKey, PartialObjectKey, PrimitiveValue};
23
24/// Build a Doc IR from a SourceDocument.
25///
26/// This produces a document IR that can be printed with the pretty-printer.
27pub fn build_source_doc(source: &SourceDocument) -> Doc {
28    SourceDocBuilder::new(source).build()
29}
30
31/// Format a SourceDocument to Eure source string.
32///
33/// This produces output that can be parsed back to an equivalent EureDocument.
34/// Comments and section ordering from the source structure are preserved.
35pub fn format_source_document(source: &SourceDocument) -> String {
36    let doc = build_source_doc(source);
37    Printer::new(FormatConfig::default()).print(&doc)
38}
39
40struct SourceDocBuilder<'a> {
41    source: &'a SourceDocument,
42}
43
44impl<'a> SourceDocBuilder<'a> {
45    fn new(source: &'a SourceDocument) -> Self {
46        Self { source }
47    }
48
49    fn doc(&self) -> &EureDocument {
50        &self.source.document
51    }
52
53    fn get_source(&self, id: SourceId) -> &EureSource {
54        self.source.source(id)
55    }
56
57    fn build(&self) -> Doc {
58        self.build_eure_source(self.source.root_source())
59    }
60
61    fn build_eure_source(&self, eure: &EureSource) -> Doc {
62        let mut parts = Vec::new();
63
64        // Leading trivia
65        if !eure.leading_trivia.is_empty() {
66            parts.push(self.build_trivia(&eure.leading_trivia));
67        }
68
69        // Value binding (if present)
70        if let Some(node_id) = eure.value {
71            parts.push(
72                Doc::text("= ")
73                    .concat(self.build_value(node_id))
74                    .concat(Doc::hardline()),
75            );
76        }
77
78        // Bindings
79        for binding in &eure.bindings {
80            parts.push(self.build_binding(binding));
81        }
82
83        // Sections
84        for section in &eure.sections {
85            parts.push(self.build_section(section));
86        }
87
88        // Trailing trivia
89        if !eure.trailing_trivia.is_empty() {
90            parts.push(self.build_trivia(&eure.trailing_trivia));
91        }
92
93        Doc::concat_all(parts)
94    }
95
96    fn build_trivia(&self, trivia: &[Trivia]) -> Doc {
97        let mut parts = Vec::new();
98        for item in trivia {
99            match item {
100                Trivia::Comment(comment) => {
101                    parts.push(self.build_comment(comment));
102                }
103                Trivia::BlankLine => {
104                    parts.push(Doc::hardline());
105                }
106            }
107        }
108        Doc::concat_all(parts)
109    }
110
111    fn build_comment(&self, comment: &Comment) -> Doc {
112        match comment {
113            Comment::Line(s) => {
114                if s.is_empty() {
115                    Doc::text("//").concat(Doc::hardline())
116                } else {
117                    Doc::text("// ")
118                        .concat(Doc::text(s.clone()))
119                        .concat(Doc::hardline())
120                }
121            }
122            Comment::Block(s) => {
123                if s.is_empty() {
124                    Doc::text("/**/").concat(Doc::hardline())
125                } else {
126                    Doc::text("/* ")
127                        .concat(Doc::text(s.clone()))
128                        .concat(Doc::text(" */"))
129                        .concat(Doc::hardline())
130                }
131            }
132        }
133    }
134
135    /// Build a comment for array elements (no trailing hardline, caller controls line breaks).
136    fn build_array_comment(&self, comment: &Comment) -> Doc {
137        match comment {
138            Comment::Line(s) => {
139                if s.is_empty() {
140                    Doc::text("//")
141                } else {
142                    Doc::text("// ").concat(Doc::text(s.clone()))
143                }
144            }
145            Comment::Block(s) => {
146                if s.is_empty() {
147                    Doc::text("/**/")
148                } else {
149                    Doc::text("/* ")
150                        .concat(Doc::text(s.clone()))
151                        .concat(Doc::text(" */"))
152                }
153            }
154        }
155    }
156
157    /// Build a trailing comment (same line, no hardline at end).
158    fn build_trailing_comment(&self, comment: &Comment) -> Doc {
159        match comment {
160            Comment::Line(s) => {
161                if s.is_empty() {
162                    Doc::text(" //")
163                } else {
164                    Doc::text(" // ").concat(Doc::text(s.clone()))
165                }
166            }
167            Comment::Block(s) => {
168                if s.is_empty() {
169                    Doc::text(" /**/")
170                } else {
171                    Doc::text(" /* ")
172                        .concat(Doc::text(s.clone()))
173                        .concat(Doc::text(" */"))
174                }
175            }
176        }
177    }
178
179    fn build_binding(&self, binding: &BindingSource) -> Doc {
180        let mut parts = Vec::new();
181
182        // Trivia before this binding
183        if !binding.trivia_before.is_empty() {
184            parts.push(self.build_trivia(&binding.trivia_before));
185        }
186
187        let path_doc = self.build_path(&binding.path);
188
189        let body_doc = match &binding.bind {
190            BindSource::Value(node_id) => Doc::text(" = ").concat(self.build_value(*node_id)),
191            BindSource::Array { node, elements } => {
192                Doc::text(" = ").concat(self.build_array_with_trivia(*node, elements))
193            }
194            BindSource::Block(source_id) => {
195                let inner = self.build_eure_source(self.get_source(*source_id));
196                self.build_braced_block(Doc::text(""), inner)
197            }
198        };
199
200        let mut doc = path_doc.concat(body_doc);
201
202        if let Some(comment) = &binding.trailing_comment {
203            doc = doc.concat(self.build_trailing_comment(comment));
204        }
205
206        parts.push(doc.concat(Doc::hardline()));
207        Doc::concat_all(parts)
208    }
209
210    fn build_section(&self, section: &SectionSource) -> Doc {
211        let mut parts = Vec::new();
212
213        // Trivia before this section
214        if !section.trivia_before.is_empty() {
215            parts.push(self.build_trivia(&section.trivia_before));
216        }
217
218        let mut header = Doc::text("@ ").concat(self.build_path(&section.path));
219
220        if let Some(comment) = &section.trailing_comment {
221            header = header.concat(self.build_trailing_comment(comment));
222        }
223
224        let section_doc = match &section.body {
225            SectionBody::Items { value, bindings } => {
226                if let Some(node_id) = value
227                    && bindings.is_empty()
228                {
229                    parts.push(
230                        header
231                            .concat(Doc::text(" = "))
232                            .concat(self.build_value(*node_id))
233                            .concat(Doc::hardline()),
234                    );
235                    return Doc::concat_all(parts);
236                }
237
238                let mut body_parts = Vec::new();
239
240                // Value binding (if present)
241                if let Some(node_id) = value {
242                    body_parts.push(
243                        Doc::text("= ")
244                            .concat(self.build_value(*node_id))
245                            .concat(Doc::hardline()),
246                    );
247                }
248
249                // Bindings in section
250                for binding in bindings {
251                    body_parts.push(self.build_binding(binding));
252                }
253
254                header
255                    .concat(Doc::hardline())
256                    .concat(Doc::concat_all(body_parts))
257            }
258            SectionBody::Block(source_id) => {
259                let inner = self.build_eure_source(self.get_source(*source_id));
260                self.build_braced_block(header, inner)
261                    .concat(Doc::hardline())
262            }
263        };
264
265        parts.push(section_doc);
266        Doc::concat_all(parts)
267    }
268
269    fn build_braced_block(&self, header: Doc, inner: Doc) -> Doc {
270        let (inner, removed) = strip_one_trailing_hardline(inner);
271        if !removed || matches!(inner, Doc::Nil) {
272            return header.concat(Doc::text(" {}"));
273        }
274
275        header
276            .concat(Doc::text(" {"))
277            .concat(Doc::indent(Doc::hardline().concat(inner)))
278            .concat(Doc::hardline())
279            .concat(Doc::text("}"))
280    }
281
282    fn build_path(&self, path: &[SourcePathSegment]) -> Doc {
283        let mut result = Doc::Nil;
284        for (i, segment) in path.iter().enumerate() {
285            if i > 0 {
286                result = result.concat(Doc::text("."));
287            }
288            result = result.concat(self.build_key(&segment.key));
289            if let Some(index) = &segment.array {
290                result = result.concat(Doc::text("["));
291                match index {
292                    ArrayIndexKind::Push => {}
293                    ArrayIndexKind::Current => {
294                        result = result.concat(Doc::text("^"));
295                    }
296                    ArrayIndexKind::Specific(n) => {
297                        result = result.concat(Doc::text(n.to_string()));
298                    }
299                }
300                result = result.concat(Doc::text("]"));
301            }
302        }
303        result
304    }
305
306    fn build_key(&self, key: &SourceKey) -> Doc {
307        match key {
308            SourceKey::Ident(s) => Doc::text(s.as_ref()),
309            SourceKey::Extension(s) => Doc::text("$").concat(Doc::text(s.as_ref())),
310            SourceKey::Hole(None) => Doc::text("!"),
311            SourceKey::Hole(Some(label)) => Doc::text("!").concat(Doc::text(label.as_ref())),
312            SourceKey::String(s, style) => match style {
313                StringStyle::Quoted => Doc::text("\"")
314                    .concat(Doc::text(escape_string(s)))
315                    .concat(Doc::text("\"")),
316                StringStyle::Literal => Doc::text("'")
317                    .concat(Doc::text(s.clone()))
318                    .concat(Doc::text("'")),
319                StringStyle::DelimitedLitStr(level) => {
320                    let delim_open: String = "<".repeat(*level as usize);
321                    let delim_close: String = ">".repeat(*level as usize);
322                    Doc::text(format!("{delim_open}'{s}'{delim_close}"))
323                }
324                StringStyle::DelimitedCode(level) => {
325                    let delim_open: String = "<".repeat(*level as usize);
326                    let delim_close: String = ">".repeat(*level as usize);
327                    Doc::text(format!("{delim_open}`{s}`{delim_close}"))
328                }
329            },
330            SourceKey::Integer(n) => Doc::text(n.to_string()),
331            SourceKey::Tuple(keys) => {
332                let inner = Doc::join(keys.iter().map(|k| self.build_key(k)), Doc::text(", "));
333                Doc::text("(").concat(inner).concat(Doc::text(")"))
334            }
335            SourceKey::TupleIndex(n) => Doc::text("#").concat(Doc::text(n.to_string())),
336        }
337    }
338
339    fn build_value(&self, node_id: NodeId) -> Doc {
340        let node = self.doc().node(node_id);
341        match &node.content {
342            NodeValue::Hole(_) => Doc::text("null"),
343            NodeValue::Primitive(prim) => self.build_primitive(prim),
344            NodeValue::Array(arr) => self.build_array(node_id, arr),
345            NodeValue::Tuple(tuple) => self.build_tuple(tuple),
346            NodeValue::Map(map) => self.build_map(map),
347            NodeValue::PartialMap(map) => self.build_partial_map(map),
348        }
349    }
350
351    fn build_primitive(&self, prim: &PrimitiveValue) -> Doc {
352        match prim {
353            PrimitiveValue::Null => Doc::text("null"),
354            PrimitiveValue::Bool(b) => Doc::text(if *b { "true" } else { "false" }),
355            PrimitiveValue::Integer(n) => Doc::text(n.to_string()),
356            PrimitiveValue::F64(f) => self.build_f64(*f),
357            PrimitiveValue::F32(f) => self.build_f32(*f),
358            PrimitiveValue::Text(text) => self.build_text(text),
359        }
360    }
361
362    fn build_f64(&self, f: f64) -> Doc {
363        if f.is_nan() {
364            Doc::text("nan")
365        } else if f.is_infinite() {
366            if f.is_sign_positive() {
367                Doc::text("inf")
368            } else {
369                Doc::text("-inf")
370            }
371        } else {
372            let s = f.to_string();
373            if !s.contains('.') && !s.contains('e') && !s.contains('E') {
374                Doc::text(s).concat(Doc::text(".0"))
375            } else {
376                Doc::text(s)
377            }
378        }
379    }
380
381    fn build_f32(&self, f: f32) -> Doc {
382        if f.is_nan() {
383            Doc::text("nan")
384        } else if f.is_infinite() {
385            if f.is_sign_positive() {
386                Doc::text("inf")
387            } else {
388                Doc::text("-inf")
389            }
390        } else {
391            Doc::text(f.to_string())
392        }
393    }
394
395    fn build_text(&self, text: &eure_document::text::Text) -> Doc {
396        let is_block = matches!(
397            text.syntax_hint,
398            Some(SyntaxHint::Block)
399                | Some(SyntaxHint::Block3)
400                | Some(SyntaxHint::Block4)
401                | Some(SyntaxHint::Block5)
402                | Some(SyntaxHint::Block6)
403        );
404
405        if is_block {
406            self.build_block_text(text)
407        } else {
408            self.build_inline_text(text)
409        }
410    }
411
412    fn build_block_text(&self, text: &eure_document::text::Text) -> Doc {
413        let backticks = match text.syntax_hint {
414            Some(SyntaxHint::Block6) => "``````",
415            Some(SyntaxHint::Block5) => "`````",
416            Some(SyntaxHint::Block4) => "````",
417            _ => "```",
418        };
419
420        let mut doc = Doc::text(backticks);
421
422        if let Language::Other(lang) = &text.language {
423            doc = doc.concat(Doc::text(lang.clone()));
424        }
425
426        // Note: Block content with internal newlines - we output it literally
427        // The printer doesn't wrap this content since it's already formatted
428        doc = doc.concat(Doc::text("\n"));
429        doc = doc.concat(Doc::text(text.content.clone()));
430        if !text.content.ends_with('\n') {
431            doc = doc.concat(Doc::text("\n"));
432        }
433        doc.concat(Doc::text(backticks))
434    }
435
436    fn build_inline_text(&self, text: &eure_document::text::Text) -> Doc {
437        match &text.language {
438            Language::Plaintext => Doc::text("\"")
439                .concat(Doc::text(escape_string(&text.content)))
440                .concat(Doc::text("\"")),
441            Language::Implicit => Doc::text("`")
442                .concat(Doc::text(text.content.clone()))
443                .concat(Doc::text("`")),
444            Language::Other(lang) => Doc::text(lang.clone())
445                .concat(Doc::text("`"))
446                .concat(Doc::text(text.content.clone()))
447                .concat(Doc::text("`")),
448        }
449    }
450
451    fn build_array(&self, node_id: NodeId, arr: &NodeArray) -> Doc {
452        if arr.is_empty() {
453            return Doc::text("[]");
454        }
455
456        // Check if this array should be forced multi-line (from source tracking)
457        let force_multiline = self.source.is_multiline_array(node_id);
458
459        if force_multiline {
460            // Force multi-line: use hardline, no group
461            let elements = Doc::join(
462                arr.iter()
463                    .map(|&id| self.build_value(id).concat(Doc::text(","))),
464                Doc::hardline(),
465            );
466
467            Doc::text("[")
468                .concat(Doc::indent(Doc::hardline().concat(elements)))
469                .concat(Doc::hardline())
470                .concat(Doc::text("]"))
471        } else {
472            // Smart breaking with Group
473            let elements = Doc::join(
474                arr.iter().map(|&id| self.build_value(id)),
475                Doc::text(",").concat(Doc::line()),
476            );
477
478            Doc::group(
479                Doc::text("[")
480                    .concat(Doc::softline())
481                    .concat(Doc::indent(elements))
482                    .concat(Doc::softline())
483                    .concat(Doc::text("]")),
484            )
485        }
486    }
487
488    /// Build an array with per-element trivia (comments/blank lines).
489    fn build_array_with_trivia(&self, node_id: NodeId, elements: &[ArrayElementSource]) -> Doc {
490        let arr = match &self.doc().node(node_id).content {
491            NodeValue::Array(arr) => arr,
492            _ => return self.build_value(node_id), // Fallback if not an array
493        };
494
495        if elements.is_empty() {
496            // Fallback to regular array formatting
497            return self.build_array(node_id, arr);
498        }
499
500        // Build inner content without per-item indent wrapping
501        let mut inner_parts = Vec::new();
502
503        for (i, elem_source) in elements.iter().enumerate() {
504            // Trivia before element (comments, blank lines)
505            for trivia in &elem_source.trivia_before {
506                match trivia {
507                    Trivia::Comment(comment) => {
508                        // Build comment without trailing hardline (we control line breaks)
509                        inner_parts.push(self.build_array_comment(comment));
510                        inner_parts.push(Doc::hardline());
511                    }
512                    Trivia::BlankLine => {
513                        inner_parts.push(Doc::hardline());
514                    }
515                }
516            }
517
518            // Element value
519            let value_id = arr.get(elem_source.index).unwrap();
520            let mut elem_doc = self.build_value(value_id);
521
522            // Always add trailing comma for Eure style
523            elem_doc = elem_doc.concat(Doc::text(","));
524
525            // Trailing comment
526            if let Some(comment) = &elem_source.trailing_comment {
527                elem_doc = elem_doc.concat(self.build_trailing_comment(comment));
528            }
529
530            inner_parts.push(elem_doc);
531            // Add newline after each element except the last
532            if i < elements.len() - 1 {
533                inner_parts.push(Doc::hardline());
534            }
535        }
536
537        // Wrap all content in indent block
538        // - Hardline inside Indent for proper indentation of first element
539        // - Hardline after Indent for closing bracket at column 0
540        Doc::text("[")
541            .concat(Doc::indent(
542                Doc::hardline().concat(Doc::concat_all(inner_parts)),
543            ))
544            .concat(Doc::hardline())
545            .concat(Doc::text("]"))
546    }
547
548    fn build_tuple(&self, tuple: &NodeTuple) -> Doc {
549        if tuple.is_empty() {
550            return Doc::text("()");
551        }
552
553        let elements = Doc::join(
554            tuple.iter().map(|&id| self.build_value(id)),
555            Doc::text(", "),
556        );
557
558        Doc::text("(").concat(elements).concat(Doc::text(")"))
559    }
560
561    fn build_map(&self, map: &NodeMap) -> Doc {
562        if map.is_empty() {
563            return Doc::text("{}");
564        }
565
566        let entries = Doc::join(
567            map.iter().map(|(key, &child_id)| {
568                self.build_object_key(key)
569                    .concat(Doc::text(" => "))
570                    .concat(self.build_value(child_id))
571            }),
572            Doc::text(", "),
573        );
574
575        Doc::text("{ ").concat(entries).concat(Doc::text(" }"))
576    }
577
578    fn build_partial_map(&self, map: &eure_document::map::PartialNodeMap) -> Doc {
579        if map.is_empty() {
580            return Doc::text("{}");
581        }
582
583        let entries = Doc::join(
584            map.iter().map(|(key, &child_id)| {
585                self.build_partial_object_key(key)
586                    .concat(Doc::text(" => "))
587                    .concat(self.build_value(child_id))
588            }),
589            Doc::text(", "),
590        );
591
592        Doc::text("{ ").concat(entries).concat(Doc::text(" }"))
593    }
594
595    fn build_object_key(&self, key: &ObjectKey) -> Doc {
596        match key {
597            ObjectKey::String(s) => {
598                // Check if the string is a valid identifier
599                // If not (e.g., starts with $, contains spaces, etc.), quote it
600                if s.parse::<Identifier>().is_ok() {
601                    // Valid identifier - output without quotes
602                    Doc::text(s.clone())
603                } else {
604                    // Not a valid identifier - must quote it
605                    Doc::text("\"")
606                        .concat(Doc::text(escape_string(s)))
607                        .concat(Doc::text("\""))
608                }
609            }
610            ObjectKey::Number(n) => Doc::text(n.to_string()),
611            ObjectKey::Tuple(keys) => {
612                let inner = Doc::join(
613                    keys.iter().map(|k| self.build_object_key(k)),
614                    Doc::text(", "),
615                );
616                Doc::text("(").concat(inner).concat(Doc::text(")"))
617            }
618        }
619    }
620
621    fn build_partial_object_key(&self, key: &PartialObjectKey) -> Doc {
622        match key {
623            PartialObjectKey::String(s) => {
624                if s.parse::<Identifier>().is_ok() {
625                    Doc::text(s.clone())
626                } else {
627                    Doc::text("\"")
628                        .concat(Doc::text(escape_string(s)))
629                        .concat(Doc::text("\""))
630                }
631            }
632            PartialObjectKey::Number(n) => Doc::text(n.to_string()),
633            PartialObjectKey::Hole(None) => Doc::text("!"),
634            PartialObjectKey::Hole(Some(label)) => Doc::text("!").concat(Doc::text(label.as_ref())),
635            PartialObjectKey::Tuple(keys) => {
636                let inner = Doc::join(
637                    keys.iter().map(|k| self.build_partial_object_key(k)),
638                    Doc::text(", "),
639                );
640                Doc::text("(").concat(inner).concat(Doc::text(")"))
641            }
642        }
643    }
644}
645
646fn strip_one_trailing_hardline(doc: Doc) -> (Doc, bool) {
647    match doc {
648        Doc::HardLine => (Doc::Nil, true),
649        Doc::Concat(left, right) => {
650            let left = *left;
651            let right = *right;
652
653            let (new_right, removed) = strip_one_trailing_hardline(right.clone());
654            if removed {
655                return (left.concat(new_right), true);
656            }
657
658            let (new_left, removed) = strip_one_trailing_hardline(left);
659            (new_left.concat(right), removed)
660        }
661        other => (other, false),
662    }
663}
664
665/// Escape a string for Eure output
666fn escape_string(s: &str) -> String {
667    let mut result = String::with_capacity(s.len());
668    for c in s.chars() {
669        match c {
670            '"' => result.push_str("\\\""),
671            '\\' => result.push_str("\\\\"),
672            '\n' => result.push_str("\\n"),
673            '\r' => result.push_str("\\r"),
674            '\t' => result.push_str("\\t"),
675            _ => result.push(c),
676        }
677    }
678    result
679}