Skip to main content

eure_toml/
lib.rs

1//! TOML conversion support for Eure format.
2//!
3//! This crate provides conversion from TOML documents to Eure's [`SourceDocument`],
4//! preserving section ordering.
5//!
6//! # Example
7//!
8//! ```
9//! use eure_toml::to_source_document;
10//!
11//! let toml_str = r#"
12//! [server]
13//! host = "localhost"
14//! port = 8080
15//! "#;
16//!
17//! let source_doc = to_source_document(toml_str).unwrap();
18//! ```
19
20mod error;
21mod query;
22
23pub use error::TomlToEureError;
24pub use query::{TomlToEureDocument, TomlToEureSource};
25
26use eure_document::document::constructor::{DocumentConstructor, Scope};
27use eure_document::identifier::Identifier;
28use eure_document::path::PathSegment;
29use eure_document::source::{
30    ArrayElementSource, BindSource, BindingSource, Comment, EureSource, SectionBody,
31    SourceDocument, SourceKey, SourcePathSegment, Trivia,
32};
33use eure_document::text::{Language, Text};
34use eure_document::value::ObjectKey;
35use eure_document::value::PrimitiveValue;
36use num_bigint::BigInt;
37use toml_parser::decoder::Encoding;
38use toml_parser::decoder::ScalarKind;
39use toml_parser::parser::EventReceiver;
40use toml_parser::{ErrorSink, ParseError, Source, Span};
41
42/// Convert a TOML string to a SourceDocument.
43///
44/// This preserves:
45/// - Section ordering (including interleaved `[table]` and `[[array]]` sections)
46/// - All TOML values
47pub fn to_source_document(toml_str: &str) -> Result<SourceDocument, TomlToEureError> {
48    let source = Source::new(toml_str);
49    let tokens: Vec<_> = source.lex().collect();
50
51    let mut converter = TomlParserConverter::new(source);
52    let mut errors = ErrorCollector::new();
53
54    toml_parser::parser::parse_document(&tokens, &mut converter, &mut errors);
55
56    if let Some(err) = errors.first_error() {
57        return Err(err);
58    }
59
60    converter.finish()
61}
62
63/// Error collector for toml_parser
64struct ErrorCollector {
65    errors: Vec<TomlToEureError>,
66}
67
68impl ErrorCollector {
69    fn new() -> Self {
70        Self { errors: Vec::new() }
71    }
72
73    fn first_error(&self) -> Option<TomlToEureError> {
74        self.errors.first().cloned()
75    }
76}
77
78impl ErrorSink for ErrorCollector {
79    fn report_error(&mut self, error: ParseError) {
80        self.errors.push(TomlToEureError::ParseError {
81            message: format!("{:?}", error),
82        });
83    }
84}
85
86/// State for tracking current parsing context
87#[derive(Debug, Clone)]
88enum ValueContext {
89    /// At the root document level
90    Root,
91    /// Inside a [table] section
92    StdTable {
93        /// Trivia (comments/blank lines) before this section
94        trivia_before: Vec<Trivia>,
95        /// Path segments for this section
96        path: Vec<SourcePathSegment>,
97        /// Bindings collected for this section
98        bindings: Vec<BindingSource>,
99        /// Scope for the DocumentConstructor
100        scope: Scope,
101    },
102    /// Inside a [[array_table]] section
103    ArrayTable {
104        /// Trivia (comments/blank lines) before this section
105        trivia_before: Vec<Trivia>,
106        /// Path segments for this section
107        path: Vec<SourcePathSegment>,
108        /// Bindings collected for this section
109        bindings: Vec<BindingSource>,
110        /// Scope for the DocumentConstructor
111        scope: Scope,
112    },
113    /// Inside an inline table { }
114    InlineTable {
115        /// Scope for the DocumentConstructor
116        scope: Scope,
117        /// Binding path for this inline table (if it's a value)
118        binding_path: Vec<SourcePathSegment>,
119    },
120    /// Inside an array [ ]
121    Array {
122        /// Scope for the DocumentConstructor
123        scope: Scope,
124        /// Current element index
125        element_index: usize,
126        /// Binding path for this array (if it's a value)
127        binding_path: Vec<SourcePathSegment>,
128        /// Per-element trivia collected during parsing
129        element_sources: Vec<ArrayElementSource>,
130        /// Pending trivia for the next element
131        element_pending_trivia: Vec<Trivia>,
132        /// Whether the array was multi-line in the original TOML
133        is_multiline: bool,
134        /// Span end of the last element (for trailing comment detection)
135        last_element_span_end: Option<usize>,
136    },
137}
138
139/// Main converter from TOML to SourceDocument
140struct TomlParserConverter<'a> {
141    /// The source TOML string
142    source: Source<'a>,
143    /// Document constructor for building EureDocument
144    constructor: DocumentConstructor,
145    /// Arena for EureSource blocks
146    sources: Vec<EureSource>,
147
148    /// Stack of parsing contexts
149    context_stack: Vec<ValueContext>,
150
151    /// Current key path being built (for dotted keys like `a.b.c`)
152    current_keys: Vec<(String, Option<Encoding>)>,
153    /// Whether we're currently parsing a key (before `=`)
154    parsing_key: bool,
155
156    /// Pending trivia to attach to the next item
157    pending_trivia: Vec<Trivia>,
158    /// Flag to track blank lines (consecutive newlines)
159    saw_newline: bool,
160
161    /// Array nodes that should be formatted multi-line
162    multiline_arrays: std::collections::HashSet<eure_document::document::NodeId>,
163}
164
165impl<'a> TomlParserConverter<'a> {
166    fn new(source: Source<'a>) -> Self {
167        // Create root EureSource
168        let sources = vec![EureSource::default()];
169        Self {
170            source,
171            constructor: DocumentConstructor::new(),
172            sources,
173            context_stack: vec![ValueContext::Root],
174            current_keys: Vec::new(),
175            parsing_key: false,
176            pending_trivia: Vec::new(),
177            saw_newline: false,
178            multiline_arrays: std::collections::HashSet::new(),
179        }
180    }
181
182    fn finish(mut self) -> Result<SourceDocument, TomlToEureError> {
183        // Close any remaining sections
184        self.close_current_section();
185        // Any remaining pending trivia becomes trailing trivia of the root source
186        if !self.pending_trivia.is_empty() {
187            self.sources[0].trailing_trivia = std::mem::take(&mut self.pending_trivia);
188        }
189        let mut source_doc = SourceDocument::new(self.constructor.finish(), self.sources);
190        source_doc.multiline_arrays = self.multiline_arrays;
191        Ok(source_doc)
192    }
193
194    fn current_context(&self) -> &ValueContext {
195        self.context_stack.last().unwrap()
196    }
197
198    fn current_context_mut(&mut self) -> &mut ValueContext {
199        self.context_stack.last_mut().unwrap()
200    }
201
202    /// Close the current section and add it to sources
203    fn close_current_section(&mut self) {
204        if let Some(context) = self.context_stack.pop() {
205            match context {
206                ValueContext::StdTable {
207                    trivia_before,
208                    path,
209                    bindings,
210                    scope,
211                } => {
212                    self.constructor.end_scope(scope).expect("scope mismatch");
213
214                    // Add section to root source
215                    self.sources[0]
216                        .sections
217                        .push(eure_document::source::SectionSource {
218                            trivia_before,
219                            path,
220                            body: SectionBody::Items {
221                                value: None,
222                                bindings,
223                            },
224                            trailing_comment: None,
225                        });
226                }
227                ValueContext::ArrayTable {
228                    trivia_before,
229                    path,
230                    bindings,
231                    scope,
232                } => {
233                    self.constructor.end_scope(scope).expect("scope mismatch");
234
235                    // Add section to root source
236                    self.sources[0]
237                        .sections
238                        .push(eure_document::source::SectionSource {
239                            trivia_before,
240                            path,
241                            body: SectionBody::Items {
242                                value: None,
243                                bindings,
244                            },
245                            trailing_comment: None,
246                        });
247                }
248                ValueContext::Root => {
249                    // Don't pop root, push it back
250                    self.context_stack.push(ValueContext::Root);
251                }
252                _ => {}
253            }
254        }
255    }
256
257    /// Decode a key from span
258    fn decode_key(&self, span: Span, encoding: Option<Encoding>) -> String {
259        let raw = self.source.get(span).expect("valid span");
260        let raw = toml_parser::Raw::new_unchecked(raw.as_str(), encoding, span);
261        let mut output = String::new();
262        let mut errors = ErrorCollector::new();
263        raw.decode_key(&mut output, &mut errors);
264        output
265    }
266
267    /// Decode a scalar value from span
268    fn decode_scalar(&self, span: Span, encoding: Option<Encoding>) -> (ScalarKind, String) {
269        let raw = self.source.get(span).expect("valid span");
270        let raw = toml_parser::Raw::new_unchecked(raw.as_str(), encoding, span);
271        let mut output = String::new();
272        let mut errors = ErrorCollector::new();
273        let kind = raw.decode_scalar(&mut output, &mut errors);
274        (kind, output)
275    }
276
277    /// Parse a key string into SourceKey and PathSegment
278    fn parse_key(&self, key: &str) -> (SourceKey, PathSegment) {
279        match key.parse::<Identifier>() {
280            Ok(id) => (SourceKey::Ident(id.clone()), PathSegment::Ident(id)),
281            Err(_) => (
282                SourceKey::quoted(key.to_string()),
283                PathSegment::Value(ObjectKey::String(key.to_string())),
284            ),
285        }
286    }
287
288    /// Create a SourcePathSegment from a SourceKey
289    fn source_path_segment(&self, key: SourceKey) -> SourcePathSegment {
290        SourcePathSegment { key, array: None }
291    }
292
293    /// Check if there's a newline between two byte positions in the source
294    fn has_newline_between(&self, start: usize, end: usize) -> bool {
295        if let Some(raw) = self.source.get(Span::new_unchecked(start, end)) {
296            raw.as_str().contains('\n')
297        } else {
298            // If we can't get the slice, assume there's a newline to be safe
299            true
300        }
301    }
302
303    /// Navigate to the key path and bind a value
304    fn bind_value(&mut self, value: PrimitiveValue) {
305        self.constructor
306            .bind_primitive(value)
307            .expect("binding should succeed");
308    }
309
310    /// Add a binding to the current context
311    fn add_binding(&mut self, path: Vec<SourcePathSegment>, node: eure_document::document::NodeId) {
312        // Don't consume trivia when in inline contexts (it should go to the outer binding)
313        match self.current_context() {
314            ValueContext::InlineTable { .. } | ValueContext::Array { .. } => {
315                // Inline structures don't track bindings in source
316                return;
317            }
318            _ => {}
319        }
320
321        // Attach pending trivia to this binding
322        let trivia_before = std::mem::take(&mut self.pending_trivia);
323        let binding = BindingSource {
324            trivia_before,
325            path,
326            bind: BindSource::Value(node),
327            trailing_comment: None,
328        };
329
330        match self.current_context_mut() {
331            ValueContext::Root => {
332                self.sources[0].bindings.push(binding);
333            }
334            ValueContext::StdTable { bindings, .. } | ValueContext::ArrayTable { bindings, .. } => {
335                bindings.push(binding);
336            }
337            ValueContext::InlineTable { .. } | ValueContext::Array { .. } => {
338                // Already handled above
339            }
340        }
341    }
342
343    /// Add an array binding with per-element trivia to the current context
344    fn add_array_binding(
345        &mut self,
346        path: Vec<SourcePathSegment>,
347        node: eure_document::document::NodeId,
348        elements: Vec<ArrayElementSource>,
349    ) {
350        // Don't consume trivia when in inline contexts (it should go to the outer binding)
351        match self.current_context() {
352            ValueContext::InlineTable { .. } | ValueContext::Array { .. } => {
353                // Inline structures don't track bindings in source
354                return;
355            }
356            _ => {}
357        }
358
359        // Attach pending trivia to this binding
360        let trivia_before = std::mem::take(&mut self.pending_trivia);
361        let binding = BindingSource {
362            trivia_before,
363            path,
364            bind: BindSource::Array { node, elements },
365            trailing_comment: None,
366        };
367
368        match self.current_context_mut() {
369            ValueContext::Root => {
370                self.sources[0].bindings.push(binding);
371            }
372            ValueContext::StdTable { bindings, .. } | ValueContext::ArrayTable { bindings, .. } => {
373                bindings.push(binding);
374            }
375            ValueContext::InlineTable { .. } | ValueContext::Array { .. } => {
376                // Already handled above
377            }
378        }
379    }
380
381    /// Convert a scalar value to PrimitiveValue
382    fn scalar_to_primitive(
383        &self,
384        kind: ScalarKind,
385        value: &str,
386        encoding: Option<Encoding>,
387    ) -> PrimitiveValue {
388        match kind {
389            ScalarKind::String => {
390                // Check if this is a multi-line string (TOML """ or ''')
391                let is_multiline = matches!(
392                    encoding,
393                    Some(Encoding::MlBasicString) | Some(Encoding::MlLiteralString)
394                );
395
396                if is_multiline {
397                    // Use block text for multi-line strings
398                    // Determine appropriate block level based on content
399                    use eure_document::text::SyntaxHint;
400
401                    let mut content = value.to_string();
402                    if !content.ends_with('\n') {
403                        content.push('\n');
404                    }
405
406                    // Find the minimum block level needed
407                    let syntax_hint = if content.contains("``````") {
408                        // Content has 6 backticks, can't safely delimit
409                        // Use Block6 and hope for the best
410                        SyntaxHint::Block6
411                    } else if content.contains("`````") {
412                        SyntaxHint::Block6
413                    } else if content.contains("````") {
414                        SyntaxHint::Block5
415                    } else if content.contains("```") {
416                        SyntaxHint::Block4
417                    } else {
418                        SyntaxHint::Block3
419                    };
420
421                    PrimitiveValue::Text(Text {
422                        content,
423                        language: Language::Implicit,
424                        syntax_hint: Some(syntax_hint),
425                    })
426                } else {
427                    // Use plaintext for single-line strings
428                    let text = Text::plaintext(value.to_string());
429                    PrimitiveValue::Text(text)
430                }
431            }
432            ScalarKind::Boolean(b) => PrimitiveValue::Bool(b),
433            ScalarKind::Integer(_radix) => {
434                // Parse the integer, handling underscores
435                let clean: String = value.chars().filter(|c| *c != '_').collect();
436                let parsed = if clean.starts_with("0x") || clean.starts_with("0X") {
437                    i64::from_str_radix(&clean[2..], 16)
438                } else if clean.starts_with("0o") || clean.starts_with("0O") {
439                    i64::from_str_radix(&clean[2..], 8)
440                } else if clean.starts_with("0b") || clean.starts_with("0B") {
441                    i64::from_str_radix(&clean[2..], 2)
442                } else {
443                    clean.parse::<i64>()
444                };
445                match parsed {
446                    Ok(n) => PrimitiveValue::Integer(BigInt::from(n)),
447                    Err(_) => {
448                        // i64 overflow: try parsing as BigInt for very large numbers
449                        let n = clean.parse::<BigInt>().unwrap_or_else(|e| {
450                            panic!("TOML parser validated integer '{clean}' failed to parse: {e}")
451                        });
452                        PrimitiveValue::Integer(n)
453                    }
454                }
455            }
456            ScalarKind::Float => {
457                let clean: String = value.chars().filter(|c| *c != '_').collect();
458                if clean == "inf" || clean == "+inf" {
459                    PrimitiveValue::F64(f64::INFINITY)
460                } else if clean == "-inf" {
461                    PrimitiveValue::F64(f64::NEG_INFINITY)
462                } else if clean == "nan" || clean == "+nan" || clean == "-nan" {
463                    PrimitiveValue::F64(f64::NAN)
464                } else {
465                    let f = clean.parse::<f64>().unwrap_or_else(|e| {
466                        panic!("TOML parser validated float '{clean}' failed to parse: {e}")
467                    });
468                    PrimitiveValue::F64(f)
469                }
470            }
471            ScalarKind::DateTime => {
472                // Determine the datetime type and create appropriate Text with language tag
473                let lang = if value.contains('T') || value.contains(' ') {
474                    // Has date and time component (datetime)
475                    "datetime"
476                } else if value.contains(':') {
477                    // Time only
478                    "time"
479                } else {
480                    // Date only
481                    "date"
482                };
483                PrimitiveValue::Text(Text::new(value.to_string(), Language::Other(lang.into())))
484            }
485        }
486    }
487}
488
489impl<'a> EventReceiver for TomlParserConverter<'a> {
490    fn std_table_open(&mut self, _span: Span, _error: &mut dyn ErrorSink) {
491        // Reset newline tracking when we see new content
492        self.saw_newline = false;
493
494        // Close previous section if any
495        self.close_current_section();
496
497        // Reset key state
498        self.current_keys.clear();
499        self.parsing_key = true;
500    }
501
502    fn std_table_close(&mut self, _span: Span, _error: &mut dyn ErrorSink) {
503        // Capture pending trivia for this section
504        let trivia_before = std::mem::take(&mut self.pending_trivia);
505
506        // Collect keys first to avoid borrow issues
507        let keys: Vec<_> = self.current_keys.drain(..).collect();
508
509        // Build the path from collected keys
510        let path: Vec<SourcePathSegment> = keys
511            .iter()
512            .map(|(key, _)| {
513                let (source_key, _) = self.parse_key(key);
514                self.source_path_segment(source_key)
515            })
516            .collect();
517
518        // Navigate to this path in the document
519        let scope = self.constructor.begin_scope();
520
521        // Navigate for each segment
522        for seg in &path {
523            let path_seg = match &seg.key {
524                SourceKey::Ident(id) => PathSegment::Ident(id.clone()),
525                SourceKey::String(s, _) => PathSegment::Value(ObjectKey::String(s.clone())),
526                _ => continue,
527            };
528            self.constructor
529                .navigate(path_seg)
530                .expect("navigation should succeed");
531        }
532
533        // Ensure it's a map
534        if self.constructor.current_node().content.is_hole() {
535            self.constructor
536                .bind_empty_map()
537                .expect("binding should succeed");
538        }
539
540        self.context_stack.push(ValueContext::StdTable {
541            trivia_before,
542            path,
543            bindings: Vec::new(),
544            scope,
545        });
546
547        self.parsing_key = false;
548    }
549
550    fn array_table_open(&mut self, _span: Span, _error: &mut dyn ErrorSink) {
551        // Reset newline tracking when we see new content
552        self.saw_newline = false;
553
554        // Close previous section if any
555        self.close_current_section();
556
557        // Reset key state
558        self.current_keys.clear();
559        self.parsing_key = true;
560    }
561
562    fn array_table_close(&mut self, _span: Span, _error: &mut dyn ErrorSink) {
563        // Capture pending trivia for this section
564        let trivia_before = std::mem::take(&mut self.pending_trivia);
565
566        // Build the path from collected keys with array marker
567        let keys: Vec<_> = self.current_keys.drain(..).collect();
568        let mut path: Vec<SourcePathSegment> = Vec::new();
569
570        for (i, (key, _)) in keys.iter().enumerate() {
571            let (source_key, _) = self.parse_key(key);
572            let mut seg = self.source_path_segment(source_key);
573            // Add array marker to last segment
574            if i == keys.len() - 1 {
575                seg = seg.with_array_push();
576            }
577            path.push(seg);
578        }
579
580        // Navigate to this path in the document
581        let scope = self.constructor.begin_scope();
582
583        for (i, (key, _)) in keys.iter().enumerate() {
584            let (_, path_seg) = self.parse_key(key);
585            self.constructor
586                .navigate(path_seg)
587                .expect("navigation should succeed");
588
589            if i == keys.len() - 1 {
590                // Last key - ensure it's an array and push new element
591                if self.constructor.current_node().content.is_hole() {
592                    self.constructor
593                        .bind_empty_array()
594                        .expect("binding should succeed");
595                }
596                self.constructor
597                    .navigate(PathSegment::ArrayIndex(None))
598                    .expect("array navigation should succeed");
599            }
600        }
601
602        // Ensure current position is a map
603        if self.constructor.current_node().content.is_hole() {
604            self.constructor
605                .bind_empty_map()
606                .expect("binding should succeed");
607        }
608
609        self.context_stack.push(ValueContext::ArrayTable {
610            trivia_before,
611            path,
612            bindings: Vec::new(),
613            scope,
614        });
615
616        self.parsing_key = false;
617    }
618
619    fn inline_table_open(&mut self, _span: Span, _error: &mut dyn ErrorSink) -> bool {
620        let scope = self.constructor.begin_scope();
621
622        // Build binding path before clearing keys
623        let binding_path: Vec<SourcePathSegment> = self
624            .current_keys
625            .iter()
626            .map(|(key, _)| {
627                let (source_key, _) = self.parse_key(key);
628                self.source_path_segment(source_key)
629            })
630            .collect();
631
632        // Navigate to the key path first
633        for (key, _) in &self.current_keys {
634            let (_, path_seg) = self.parse_key(key);
635            self.constructor
636                .navigate(path_seg)
637                .expect("navigation should succeed");
638        }
639
640        // Check if we're in an array context (values don't have keys)
641        if let Some(ValueContext::Array {
642            element_index,
643            element_pending_trivia,
644            element_sources,
645            ..
646        }) = self.context_stack.last_mut()
647        {
648            self.constructor
649                .navigate(PathSegment::ArrayIndex(None))
650                .expect("array navigation should succeed");
651
652            // Capture pending trivia for this element
653            let trivia = std::mem::take(element_pending_trivia);
654            let idx = *element_index;
655            element_sources.push(ArrayElementSource {
656                trivia_before: trivia,
657                index: idx,
658                trailing_comment: None,
659            });
660
661            *element_index += 1;
662            // Reset newline tracking - element newline shouldn't count as blank line
663            self.saw_newline = false;
664        }
665
666        self.constructor
667            .bind_empty_map()
668            .expect("binding should succeed");
669        self.context_stack.push(ValueContext::InlineTable {
670            scope,
671            binding_path,
672        });
673        self.current_keys.clear();
674        true
675    }
676
677    fn inline_table_close(&mut self, _span: Span, _error: &mut dyn ErrorSink) {
678        if let Some(ValueContext::InlineTable {
679            scope,
680            binding_path,
681        }) = self.context_stack.pop()
682        {
683            let node_id = self.constructor.current_node_id();
684            self.constructor.end_scope(scope).expect("scope mismatch");
685
686            // Add binding if we have a path
687            if !binding_path.is_empty() {
688                self.add_binding(binding_path, node_id);
689            }
690        }
691    }
692
693    fn array_open(&mut self, _span: Span, _error: &mut dyn ErrorSink) -> bool {
694        let scope = self.constructor.begin_scope();
695
696        // Build binding path before clearing keys
697        let binding_path: Vec<SourcePathSegment> = self
698            .current_keys
699            .iter()
700            .map(|(key, _)| {
701                let (source_key, _) = self.parse_key(key);
702                self.source_path_segment(source_key)
703            })
704            .collect();
705
706        // Navigate to the key path first
707        for (key, _) in &self.current_keys {
708            let (_, path_seg) = self.parse_key(key);
709            self.constructor
710                .navigate(path_seg)
711                .expect("navigation should succeed");
712        }
713
714        // Check if we're in an array context (nested arrays)
715        // Handle pending trivia for this element from parent array
716        if let Some(ValueContext::Array {
717            element_index,
718            element_pending_trivia,
719            element_sources,
720            ..
721        }) = self.context_stack.last_mut()
722        {
723            self.constructor
724                .navigate(PathSegment::ArrayIndex(None))
725                .expect("array navigation should succeed");
726            let trivia = std::mem::take(element_pending_trivia);
727            let idx = *element_index;
728            *element_index += 1;
729            // Create element source for this nested array element
730            element_sources.push(ArrayElementSource {
731                trivia_before: trivia,
732                index: idx,
733                trailing_comment: None,
734            });
735            // Reset newline tracking - element newline shouldn't count as blank line
736            self.saw_newline = false;
737        }
738
739        self.constructor
740            .bind_empty_array()
741            .expect("binding should succeed");
742        self.context_stack.push(ValueContext::Array {
743            scope,
744            element_index: 0,
745            binding_path,
746            element_sources: Vec::new(),
747            element_pending_trivia: Vec::new(),
748            is_multiline: false,
749            last_element_span_end: None,
750        });
751        self.current_keys.clear();
752        true
753    }
754
755    fn array_close(&mut self, _span: Span, _error: &mut dyn ErrorSink) {
756        // Reset newline tracking - newline after ] shouldn't count as blank line
757        self.saw_newline = false;
758
759        if let Some(ValueContext::Array {
760            scope,
761            binding_path,
762            element_sources,
763            is_multiline,
764            ..
765        }) = self.context_stack.pop()
766        {
767            let node_id = self.constructor.current_node_id();
768            self.constructor.end_scope(scope).expect("scope mismatch");
769
770            // Track multiline arrays for formatting (even when inside inline contexts)
771            if is_multiline {
772                self.multiline_arrays.insert(node_id);
773            }
774
775            // Add binding if we have a path
776            if !binding_path.is_empty() {
777                // Use array binding if multiline or has element trivia to preserve formatting
778                let has_element_trivia = element_sources
779                    .iter()
780                    .any(|e| !e.trivia_before.is_empty() || e.trailing_comment.is_some());
781
782                if is_multiline || has_element_trivia {
783                    self.add_array_binding(binding_path, node_id, element_sources);
784                } else {
785                    self.add_binding(binding_path, node_id);
786                }
787            }
788        }
789    }
790
791    fn simple_key(&mut self, span: Span, kind: Option<Encoding>, _error: &mut dyn ErrorSink) {
792        let key = self.decode_key(span, kind);
793        self.current_keys.push((key, kind));
794    }
795
796    fn key_sep(&mut self, _span: Span, _error: &mut dyn ErrorSink) {
797        // Dot separator between keys - keys are already being collected
798    }
799
800    fn key_val_sep(&mut self, _span: Span, _error: &mut dyn ErrorSink) {
801        // Reset newline tracking when we see new content (key = value)
802        self.saw_newline = false;
803
804        // = separator - now we'll receive the value
805        self.parsing_key = false;
806    }
807
808    fn scalar(&mut self, span: Span, kind: Option<Encoding>, _error: &mut dyn ErrorSink) {
809        let (scalar_kind, value) = self.decode_scalar(span, kind);
810        let primitive = self.scalar_to_primitive(scalar_kind, &value, kind);
811
812        // Build path from current_keys
813        let path: Vec<SourcePathSegment> = self
814            .current_keys
815            .iter()
816            .map(|(key, _)| {
817                let (source_key, _) = self.parse_key(key);
818                self.source_path_segment(source_key)
819            })
820            .collect();
821
822        // Navigate to the path
823        let scope = self.constructor.begin_scope();
824        for (key, _) in &self.current_keys {
825            let (_, path_seg) = self.parse_key(key);
826            self.constructor
827                .navigate(path_seg)
828                .expect("navigation should succeed");
829        }
830
831        // Check if we're in an array context
832        if let Some(ValueContext::Array {
833            element_index,
834            element_pending_trivia,
835            element_sources,
836            ..
837        }) = self.context_stack.last_mut()
838        {
839            // Navigate to array index
840            self.constructor
841                .navigate(PathSegment::ArrayIndex(None))
842                .expect("array navigation should succeed");
843
844            // Capture pending trivia for this element
845            let trivia = std::mem::take(element_pending_trivia);
846            let idx = *element_index;
847            element_sources.push(ArrayElementSource {
848                trivia_before: trivia,
849                index: idx,
850                trailing_comment: None,
851            });
852
853            *element_index += 1;
854            // Reset newline tracking - element newline shouldn't count as blank line
855            self.saw_newline = false;
856        }
857
858        // Set span end for trailing comment detection
859        if let Some(ValueContext::Array {
860            last_element_span_end,
861            ..
862        }) = self.context_stack.last_mut()
863        {
864            *last_element_span_end = Some(span.end());
865        }
866
867        let node_id = self.constructor.current_node_id();
868        self.bind_value(primitive);
869        self.constructor.end_scope(scope).expect("scope mismatch");
870
871        // Only add binding if we have a path (not in array context without keys)
872        if !path.is_empty() {
873            self.add_binding(path, node_id);
874            self.current_keys.clear();
875        }
876    }
877
878    fn value_sep(&mut self, _span: Span, _error: &mut dyn ErrorSink) {
879        // Comma separator - clear keys for next item in inline table
880        if matches!(self.current_context(), ValueContext::InlineTable { .. }) {
881            self.current_keys.clear();
882        }
883    }
884
885    fn comment(&mut self, span: Span, _error: &mut dyn ErrorSink) {
886        // Decode the comment text
887        if let Some(raw) = self.source.get(span) {
888            let text = raw.as_str();
889            // Strip the leading # character
890            let content = text
891                .strip_prefix('#')
892                .map(|s| s.trim_start().to_string())
893                .unwrap_or_else(|| text.to_string());
894            let comment = Comment::Line(content);
895
896            // Check if we're in array context and if this is a trailing comment
897            let is_trailing_comment = if let Some(ValueContext::Array {
898                last_element_span_end: Some(elem_end),
899                element_sources,
900                ..
901            }) = self.context_stack.last()
902            {
903                !element_sources.is_empty() && !self.has_newline_between(*elem_end, span.start())
904            } else {
905                false
906            };
907
908            if is_trailing_comment
909                // Set as trailing comment on the last array element
910                && let Some(ValueContext::Array {
911                    element_sources,
912                    last_element_span_end,
913                    ..
914                }) = self.context_stack.last_mut()
915                    && let Some(last_elem) = element_sources.last_mut()
916            {
917                last_elem.trailing_comment = Some(comment);
918                *last_element_span_end = None; // Clear to prevent double assignment
919                self.saw_newline = false;
920                return;
921            }
922
923            // Route to element trivia if in array context
924            if let Some(ValueContext::Array {
925                element_pending_trivia,
926                ..
927            }) = self.context_stack.last_mut()
928            {
929                element_pending_trivia.push(Trivia::Comment(comment));
930            } else {
931                self.pending_trivia.push(Trivia::Comment(comment));
932            }
933        }
934        self.saw_newline = false;
935    }
936
937    fn whitespace(&mut self, _span: Span, _error: &mut dyn ErrorSink) {
938        // Ignore whitespace (but don't reset saw_newline)
939    }
940
941    fn newline(&mut self, _span: Span, _error: &mut dyn ErrorSink) {
942        // Track blank lines (consecutive newlines)
943        if self.saw_newline {
944            let trivia = Trivia::BlankLine;
945
946            // Route to element trivia if in array context
947            if let Some(ValueContext::Array {
948                element_pending_trivia,
949                ..
950            }) = self.context_stack.last_mut()
951            {
952                element_pending_trivia.push(trivia);
953            } else {
954                self.pending_trivia.push(trivia);
955            }
956        }
957
958        // Mark array as multiline when we see a newline inside it
959        if let Some(ValueContext::Array { is_multiline, .. }) = self.context_stack.last_mut() {
960            *is_multiline = true;
961        }
962
963        self.saw_newline = true;
964    }
965
966    fn error(&mut self, _span: Span, _error: &mut dyn ErrorSink) {
967        // Errors are collected by ErrorCollector
968    }
969}
970
971// Re-export formatting functions from eure-fmt
972pub use eure_fmt::{build_source_doc, format_source_document};
973
974#[cfg(test)]
975mod tests {
976    use super::*;
977
978    #[test]
979    fn test_simple_key_value() {
980        let toml = r#"key = "value""#;
981        let result = to_source_document(toml);
982        assert!(result.is_ok());
983
984        let source = result.expect("conversion should succeed");
985        assert_eq!(source.root_source().bindings.len(), 1);
986    }
987
988    #[test]
989    fn test_section() {
990        let toml = r#"
991[server]
992host = "localhost"
993port = 8080
994"#;
995        let result = to_source_document(toml);
996        assert!(result.is_ok());
997
998        let source = result.expect("conversion should succeed");
999        // Should have one section
1000        assert_eq!(source.root_source().sections.len(), 1);
1001    }
1002
1003    #[test]
1004    fn test_array_of_tables() {
1005        let toml = r#"
1006[[items]]
1007name = "first"
1008
1009[[items]]
1010name = "second"
1011"#;
1012        let result = to_source_document(toml);
1013        assert!(result.is_ok());
1014
1015        let source = result.expect("conversion should succeed");
1016        // Should have two sections (one for each [[items]])
1017        assert_eq!(source.root_source().sections.len(), 2);
1018    }
1019
1020    #[test]
1021    fn test_interleaved_sections() {
1022        // With toml_parser, we should preserve the source order!
1023        let toml = r#"
1024[[example]]
1025name = "first"
1026
1027[metadata.first]
1028description = "First example"
1029
1030[[example]]
1031name = "second"
1032
1033[metadata.second]
1034description = "Second example"
1035"#;
1036        let result = to_source_document(toml);
1037        assert!(result.is_ok());
1038
1039        let source = result.expect("conversion should succeed");
1040        // toml_parser preserves order: [[example]], [metadata.first], [[example]], [metadata.second]
1041        assert_eq!(source.root_source().sections.len(), 4);
1042    }
1043
1044    #[test]
1045    fn test_quoted_string_key() {
1046        // Keys that are not valid identifiers should be converted to quoted strings
1047        let toml = r#""invalid key with spaces" = "value""#;
1048        let result = to_source_document(toml);
1049        assert!(result.is_ok());
1050
1051        // Verify the source document uses a quoted string key
1052        let source_doc = result.unwrap();
1053        let formatted = format_source_document(&source_doc);
1054        assert!(
1055            formatted.contains(r#""invalid key with spaces""#),
1056            "Expected quoted key in output: {}",
1057            formatted
1058        );
1059    }
1060
1061    #[test]
1062    fn test_numeric_key() {
1063        // Keys starting with numbers should be converted to quoted strings
1064        let toml = r#"[features]
10652d = ["value"]"#;
1066        let result = to_source_document(toml);
1067        assert!(result.is_ok());
1068
1069        let source_doc = result.unwrap();
1070        let formatted = format_source_document(&source_doc);
1071        assert!(
1072            formatted.contains(r#""2d""#),
1073            "Expected quoted key in output: {}",
1074            formatted
1075        );
1076    }
1077}