eure_document/source.rs
1//! Source-level document representation for programmatic construction and formatting.
2//!
3//! This module provides types for representing Eure source structure with layout metadata,
4//! while actual values are referenced via [`NodeId`] into an [`EureDocument`].
5//!
6//! # Design
7//!
8//! ```text
9//! ┌─────────────────────────────────────────────┐
10//! │ SourceDocument │
11//! │ ┌─────────────────┐ ┌──────────────────┐ │
12//! │ │ EureDocument │ │ Layout │ │
13//! │ │ ┌───────────┐ │ │ │ │
14//! │ │ │ NodeId(0) │◄─┼──┼─ Binding.node │ │
15//! │ │ │ NodeId(1) │◄─┼──┼─ Binding.node │ │
16//! │ │ │ NodeId(2) │◄─┼──┼─ Binding.node │ │
17//! │ │ └───────────┘ │ │ │ │
18//! │ └─────────────────┘ └──────────────────┘ │
19//! └─────────────────────────────────────────────┘
20//! ```
21//!
22//! - **EureDocument**: Holds semantic data (values)
23//! - **Layout**: Holds presentation metadata (comments, ordering, section structure)
24//!
25//! # Example
26//!
27//! ```ignore
28//! // Convert from TOML, preserving comments and section ordering
29//! let source = eure_toml::to_source_document(&toml_doc);
30//!
31//! // Modify values (layout is preserved)
32//! let node = source.find_binding(&["server", "port"]).unwrap();
33//! source.document.node_mut(node).set_primitive(8080.into());
34//!
35//! // Format to Eure string
36//! let output = eure_fmt::format_source(&source, &config);
37//! ```
38
39use std::collections::HashSet;
40
41use crate::document::{EureDocument, NodeId};
42use crate::prelude_internal::*;
43
44/// A document with layout/presentation metadata.
45///
46/// Combines semantic data ([`EureDocument`]) with presentation information ([`Layout`])
47/// for round-trip conversions from formats like TOML, preserving comments and ordering.
48#[derive(Debug, Clone)]
49pub struct SourceDocument {
50 /// The semantic data (values, structure)
51 pub document: EureDocument,
52 /// The presentation layout (comments, ordering, sections)
53 pub layout: Layout,
54}
55
56impl SourceDocument {
57 /// Create a new source document with the given document and layout.
58 pub fn new(document: EureDocument, layout: Layout) -> Self {
59 Self { document, layout }
60 }
61
62 /// Create an empty source document.
63 pub fn empty() -> Self {
64 Self {
65 document: EureDocument::new_empty(),
66 layout: Layout::new(),
67 }
68 }
69}
70
71/// Layout information describing how to render the document.
72#[derive(Debug, Clone, Default)]
73pub struct Layout {
74 /// Top-level items in order
75 pub items: Vec<LayoutItem>,
76 /// Nodes that should be formatted with multiple lines
77 pub multiline_nodes: HashSet<NodeId>,
78}
79
80impl Layout {
81 /// Create an empty layout.
82 pub fn new() -> Self {
83 Self {
84 items: Vec::new(),
85 multiline_nodes: HashSet::new(),
86 }
87 }
88
89 /// Add an item to the layout.
90 pub fn push(&mut self, item: LayoutItem) {
91 self.items.push(item);
92 }
93}
94
95/// An item in the layout.
96#[derive(Debug, Clone, PartialEq)]
97pub enum LayoutItem {
98 /// A comment (line or block)
99 Comment(Comment),
100
101 /// A blank line for visual separation
102 BlankLine,
103
104 /// A key-value binding: `path.to.key = <value from NodeId>`
105 Binding {
106 /// Path to the binding target
107 path: SourcePath,
108 /// Reference to the value node in EureDocument
109 node: NodeId,
110 /// Optional trailing comment: `key = value // comment`
111 trailing_comment: Option<String>,
112 },
113
114 /// A section header: `@ path.to.section`
115 Section {
116 /// Path to the section
117 path: SourcePath,
118 /// Optional trailing comment: `@ section // comment`
119 trailing_comment: Option<String>,
120 /// Section body
121 body: SectionBody,
122 },
123
124 /// An array binding with per-element layout information.
125 ///
126 /// Used when an array has comments between elements that need to be preserved.
127 /// ```eure
128 /// items = [
129 /// // First item
130 /// "one",
131 /// // Second item
132 /// "two",
133 /// ]
134 /// ```
135 ArrayBinding {
136 /// Path to the binding target
137 path: SourcePath,
138 /// Reference to the array node in EureDocument
139 node: NodeId,
140 /// Per-element layout information (comments before each element)
141 elements: Vec<ArrayElementLayout>,
142 /// Optional trailing comment
143 trailing_comment: Option<String>,
144 },
145}
146
147/// The body of a section.
148#[derive(Debug, Clone, PartialEq)]
149pub enum SectionBody {
150 /// Items following the section header (newline-separated)
151 /// ```eure
152 /// @ section
153 /// key1 = value1
154 /// key2 = value2
155 /// ```
156 Items(Vec<LayoutItem>),
157
158 /// Block syntax with braces
159 /// ```eure
160 /// @ section {
161 /// key1 = value1
162 /// key2 = value2
163 /// }
164 /// ```
165 Block(Vec<LayoutItem>),
166}
167
168/// A path in source representation.
169pub type SourcePath = Vec<SourcePathSegment>;
170
171/// A segment in a source path.
172#[derive(Debug, Clone, PartialEq, Eq)]
173pub struct SourcePathSegment {
174 /// The key part of the segment
175 pub key: SourceKey,
176 /// Optional array marker:
177 /// - `None` = no marker
178 /// - `Some(None)` = `[]` (push to array)
179 /// - `Some(Some(n))` = `[n]` (index into array)
180 pub array: Option<Option<usize>>,
181}
182
183impl SourcePathSegment {
184 /// Create a simple identifier segment without array marker.
185 pub fn ident(name: Identifier) -> Self {
186 Self {
187 key: SourceKey::Ident(name),
188 array: None,
189 }
190 }
191
192 /// Create an extension segment without array marker.
193 pub fn extension(name: Identifier) -> Self {
194 Self {
195 key: SourceKey::Extension(name),
196 array: None,
197 }
198 }
199
200 /// Create a segment with array push marker (`[]`).
201 pub fn with_array_push(mut self) -> Self {
202 self.array = Some(None);
203 self
204 }
205
206 /// Create a segment with array index marker (`[n]`).
207 pub fn with_array_index(mut self, index: usize) -> Self {
208 self.array = Some(Some(index));
209 self
210 }
211}
212
213/// A key in source representation.
214///
215/// This determines how the key should be rendered in the output.
216#[derive(Debug, Clone, PartialEq, Eq)]
217pub enum SourceKey {
218 /// Bare identifier: `foo`, `bar_baz`
219 Ident(Identifier),
220
221 /// Extension namespace: `$variant`, `$eure`
222 Extension(Identifier),
223
224 /// Quoted string key: `"hello world"`
225 String(String),
226
227 /// Integer key: `123`
228 Integer(i64),
229
230 /// Tuple key: `(1, "a")`
231 Tuple(Vec<SourceKey>),
232
233 /// Tuple index: `#0`, `#1`
234 TupleIndex(u8),
235}
236
237impl From<Identifier> for SourceKey {
238 fn from(id: Identifier) -> Self {
239 SourceKey::Ident(id)
240 }
241}
242
243impl From<i64> for SourceKey {
244 fn from(n: i64) -> Self {
245 SourceKey::Integer(n)
246 }
247}
248
249/// A comment in the source.
250#[derive(Debug, Clone, PartialEq, Eq)]
251pub enum Comment {
252 /// Line comment: `// comment`
253 Line(String),
254 /// Block comment: `/* comment */`
255 Block(String),
256}
257
258impl Comment {
259 /// Create a line comment.
260 pub fn line(s: impl Into<String>) -> Self {
261 Comment::Line(s.into())
262 }
263
264 /// Create a block comment.
265 pub fn block(s: impl Into<String>) -> Self {
266 Comment::Block(s.into())
267 }
268
269 /// Get the comment text content.
270 pub fn text(&self) -> &str {
271 match self {
272 Comment::Line(s) | Comment::Block(s) => s,
273 }
274 }
275}
276
277/// Layout information for an array element.
278///
279/// Used to preserve comments that appear before array elements when converting
280/// from formats like TOML.
281#[derive(Debug, Clone, PartialEq, Eq)]
282pub struct ArrayElementLayout {
283 /// Comments that appear before this element in the source
284 pub comments_before: Vec<Comment>,
285 /// Trailing comment on the same line as this element
286 pub trailing_comment: Option<String>,
287 /// The index of this element in the array (corresponds to NodeArray)
288 pub index: usize,
289}
290
291// ============================================================================
292// Builder helpers
293// ============================================================================
294
295impl LayoutItem {
296 /// Create a line comment item.
297 pub fn line_comment(s: impl Into<String>) -> Self {
298 LayoutItem::Comment(Comment::Line(s.into()))
299 }
300
301 /// Create a block comment item.
302 pub fn block_comment(s: impl Into<String>) -> Self {
303 LayoutItem::Comment(Comment::Block(s.into()))
304 }
305
306 /// Create a binding item.
307 pub fn binding(path: SourcePath, node: NodeId) -> Self {
308 LayoutItem::Binding {
309 path,
310 node,
311 trailing_comment: None,
312 }
313 }
314
315 /// Create a binding item with trailing comment.
316 pub fn binding_with_comment(
317 path: SourcePath,
318 node: NodeId,
319 comment: impl Into<String>,
320 ) -> Self {
321 LayoutItem::Binding {
322 path,
323 node,
324 trailing_comment: Some(comment.into()),
325 }
326 }
327
328 /// Create a section item with items body.
329 pub fn section(path: SourcePath, items: Vec<LayoutItem>) -> Self {
330 LayoutItem::Section {
331 path,
332 trailing_comment: None,
333 body: SectionBody::Items(items),
334 }
335 }
336
337 /// Create a section item with block body.
338 pub fn section_block(path: SourcePath, items: Vec<LayoutItem>) -> Self {
339 LayoutItem::Section {
340 path,
341 trailing_comment: None,
342 body: SectionBody::Block(items),
343 }
344 }
345
346 /// Create a section item with trailing comment.
347 pub fn section_with_comment(
348 path: SourcePath,
349 comment: impl Into<String>,
350 items: Vec<LayoutItem>,
351 ) -> Self {
352 LayoutItem::Section {
353 path,
354 trailing_comment: Some(comment.into()),
355 body: SectionBody::Items(items),
356 }
357 }
358
359 /// Create an array binding item with per-element layout.
360 pub fn array_binding(
361 path: SourcePath,
362 node: NodeId,
363 elements: Vec<ArrayElementLayout>,
364 ) -> Self {
365 LayoutItem::ArrayBinding {
366 path,
367 node,
368 elements,
369 trailing_comment: None,
370 }
371 }
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377
378 #[test]
379 fn test_source_path_segment_ident() {
380 let actual = SourcePathSegment::ident(Identifier::new_unchecked("foo"));
381 let expected = SourcePathSegment {
382 key: SourceKey::Ident(Identifier::new_unchecked("foo")),
383 array: None,
384 };
385 assert_eq!(actual, expected);
386 }
387
388 #[test]
389 fn test_source_path_segment_with_array_push() {
390 let actual = SourcePathSegment::ident(Identifier::new_unchecked("items")).with_array_push();
391 let expected = SourcePathSegment {
392 key: SourceKey::Ident(Identifier::new_unchecked("items")),
393 array: Some(None),
394 };
395 assert_eq!(actual, expected);
396 }
397
398 #[test]
399 fn test_source_path_segment_with_array_index() {
400 let actual =
401 SourcePathSegment::ident(Identifier::new_unchecked("items")).with_array_index(0);
402 let expected = SourcePathSegment {
403 key: SourceKey::Ident(Identifier::new_unchecked("items")),
404 array: Some(Some(0)),
405 };
406 assert_eq!(actual, expected);
407 }
408
409 #[test]
410 fn test_layout_item_binding() {
411 let path = vec![SourcePathSegment::ident(Identifier::new_unchecked("foo"))];
412 let actual = LayoutItem::binding(path.clone(), NodeId(0));
413 let expected = LayoutItem::Binding {
414 path,
415 node: NodeId(0),
416 trailing_comment: None,
417 };
418 assert_eq!(actual, expected);
419 }
420
421 #[test]
422 fn test_layout_item_section_with_comment() {
423 let path = vec![SourcePathSegment::ident(Identifier::new_unchecked(
424 "config",
425 ))];
426 let actual = LayoutItem::section_with_comment(path.clone(), "this is config", vec![]);
427 let expected = LayoutItem::Section {
428 path,
429 trailing_comment: Some("this is config".into()),
430 body: SectionBody::Items(vec![]),
431 };
432 assert_eq!(actual, expected);
433 }
434
435 #[test]
436 fn test_source_document_empty() {
437 let doc = SourceDocument::empty();
438 assert!(doc.layout.items.is_empty());
439 assert!(doc.layout.multiline_nodes.is_empty());
440 }
441}