Skip to main content

eure_document/
layout.rs

1//! Generic layout projection for Eure documents.
2//!
3//! `DocLayout` is a source-agnostic, declarative plan describing how document
4//! paths should be projected to source constructs.
5
6use alloc::string::{String, ToString};
7use alloc::vec::Vec;
8
9use crate::document::node::NodeValue;
10use crate::document::{EureDocument, NodeId};
11use crate::identifier::Identifier;
12use crate::parse::union::VARIANT;
13use crate::parse::variant_path::VariantPath;
14use crate::path::PathSegment;
15use crate::source::{
16    BindSource, BindingSource, EureSource, SectionBody, SectionSource, SourceDocument, SourceId,
17    SourceKey, SourcePath, SourcePathSegment,
18};
19use crate::value::{ObjectKey, PartialObjectKey};
20
21/// Preferred layout style for a document path.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
23pub enum LayoutStyle {
24    /// Automatically determine the best representation.
25    #[default]
26    Auto,
27    /// Pass through; emit children at the current level with the path prefix.
28    Passthrough,
29    /// Create a new section (`@ a.b.c`).
30    Section,
31    /// Create a nested section (`@ a.b.c { ... }`).
32    Nested,
33    /// Bind value (`a.b.c = value`).
34    Binding,
35    /// Bind a block (`a.b.c { ... }`).
36    SectionBinding,
37    /// Section with root value binding (`@ a.b.c = value`).
38    SectionRootBinding,
39}
40
41/// Declarative layout plan for projecting a document to source.
42#[derive(Debug, Clone, PartialEq, Eq, Default)]
43pub struct DocLayout {
44    /// Exact-path style rules.
45    pub style_rules: Vec<LayoutStyleRule>,
46    /// Ordering directives by parent path.
47    pub order_rules: Vec<LayoutOrderRule>,
48    /// Fallback style when no style rule matches.
49    pub fallback_style: LayoutStyle,
50}
51
52/// Style directive at a path with optional union constraints.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct LayoutStyleRule {
55    pub path: Vec<PathSegment>,
56    pub style: LayoutStyle,
57    pub variant_constraints: Vec<VariantConstraint>,
58}
59
60/// Ordering directive for direct children of a parent path.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct LayoutOrderRule {
63    pub parent_path: Vec<PathSegment>,
64    pub child_order: Vec<PathSegment>,
65    pub append_unlisted: bool,
66}
67
68/// Constraint for style applicability under a union branch.
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct VariantConstraint {
71    /// Path to the union node whose variant selection constrains this rule.
72    pub union_path: Vec<PathSegment>,
73    /// Variant name that must be selected.
74    pub variant: String,
75    /// Whether this branch requires an explicit variant tag.
76    pub requires_explicit_tag: bool,
77}
78
79impl DocLayout {
80    pub fn new() -> Self {
81        Self::default()
82    }
83
84    pub fn add_style_rule(&mut self, path: Vec<PathSegment>, style: LayoutStyle) {
85        self.style_rules.push(LayoutStyleRule {
86            path,
87            style,
88            variant_constraints: Vec::new(),
89        });
90    }
91
92    pub fn add_style_rule_with_constraints(
93        &mut self,
94        path: Vec<PathSegment>,
95        style: LayoutStyle,
96        variant_constraints: Vec<VariantConstraint>,
97    ) {
98        self.style_rules.push(LayoutStyleRule {
99            path,
100            style,
101            variant_constraints,
102        });
103    }
104
105    pub fn add_order_rule(
106        &mut self,
107        parent_path: Vec<PathSegment>,
108        child_order: Vec<PathSegment>,
109        append_unlisted: bool,
110    ) {
111        self.order_rules.push(LayoutOrderRule {
112            parent_path,
113            child_order,
114            append_unlisted,
115        });
116    }
117
118    fn order_rule_for_parent(&self, path: &[PathSegment]) -> Option<&LayoutOrderRule> {
119        self.order_rules
120            .iter()
121            .rev()
122            .find(|rule| rule.parent_path == path)
123    }
124
125    fn style_for_path(&self, doc: &EureDocument, path: &[PathSegment]) -> LayoutStyle {
126        let mut exact_style: Option<LayoutStyle> = None;
127        let mut exact_conflict = false;
128
129        let mut potential_style: Option<LayoutStyle> = None;
130        let mut potential_conflict = false;
131
132        for rule in self.style_rules.iter().filter(|rule| rule.path == path) {
133            match rule_match_status(doc, rule) {
134                RuleMatchStatus::NoMatch => {}
135                RuleMatchStatus::Exact(style) => {
136                    if let Some(existing) = exact_style {
137                        if existing != style {
138                            exact_conflict = true;
139                        }
140                    } else {
141                        exact_style = Some(style);
142                    }
143                }
144                RuleMatchStatus::Potential(style) => {
145                    if let Some(existing) = potential_style {
146                        if existing != style {
147                            potential_conflict = true;
148                        }
149                    } else {
150                        potential_style = Some(style);
151                    }
152                }
153            }
154        }
155
156        if exact_conflict {
157            return LayoutStyle::Auto;
158        }
159        if let Some(style) = exact_style {
160            return style;
161        }
162
163        if potential_conflict {
164            return LayoutStyle::Auto;
165        }
166        if let Some(style) = potential_style {
167            return style;
168        }
169
170        self.fallback_style
171    }
172}
173
174/// Project a runtime document to source using a generic layout plan.
175pub fn project_with_layout(doc: &EureDocument, layout: &DocLayout) -> SourceDocument {
176    let projected = LayoutBuilder::new(doc, layout).build();
177    projected.into_source_document(doc.clone())
178}
179
180#[derive(Debug, Clone)]
181struct ProjectedDoc {
182    root: ProjectedBlock,
183}
184
185#[derive(Debug, Clone)]
186struct ProjectedBlock {
187    value: Option<NodeId>,
188    bindings: Vec<ProjectedBinding>,
189    sections: Vec<ProjectedSection>,
190}
191
192#[derive(Debug, Clone)]
193struct ProjectedBinding {
194    path: Vec<PathSegment>,
195    body: ProjectedBindingBody,
196}
197
198#[derive(Debug, Clone)]
199enum ProjectedBindingBody {
200    Value(NodeId),
201    Block(ProjectedBlock),
202}
203
204#[derive(Debug, Clone)]
205struct ProjectedSection {
206    path: Vec<PathSegment>,
207    body: ProjectedSectionBody,
208}
209
210#[derive(Debug, Clone)]
211enum ProjectedSectionBody {
212    Items {
213        value: Option<NodeId>,
214        bindings: Vec<ProjectedBinding>,
215    },
216    Block(ProjectedBlock),
217}
218
219impl ProjectedDoc {
220    fn into_source_document(self, doc: EureDocument) -> SourceDocument {
221        let mut sources = Vec::new();
222        let root_id = build_source_block(&self.root, &mut sources);
223        debug_assert_eq!(root_id.0, 0, "root source must be index 0");
224        SourceDocument::new(doc, sources)
225    }
226}
227
228struct LayoutBuilder<'a> {
229    doc: &'a EureDocument,
230    layout: &'a DocLayout,
231}
232
233struct EntryContext<'a> {
234    bindings: &'a mut Vec<ProjectedBinding>,
235    sections: &'a mut Vec<ProjectedSection>,
236    node_path: &'a [PathSegment],
237    path_prefix: &'a [PathSegment],
238    allow_sections: bool,
239}
240
241#[derive(Debug, Clone)]
242struct ChildEntry {
243    segment: PathSegment,
244    node_id: NodeId,
245}
246
247impl<'a> LayoutBuilder<'a> {
248    fn new(doc: &'a EureDocument, layout: &'a DocLayout) -> Self {
249        Self { doc, layout }
250    }
251
252    fn build(&self) -> ProjectedDoc {
253        let root_id = self.doc.get_root_id();
254        let root_node = self.doc.node(root_id);
255        let root_is_map = is_map_like(&root_node.content);
256        let value = if root_is_map { None } else { Some(root_id) };
257
258        let (bindings, sections) = self.build_entries(root_id, &[], &[], root_is_map, true);
259
260        ProjectedDoc {
261            root: ProjectedBlock {
262                value,
263                bindings,
264                sections,
265            },
266        }
267    }
268
269    fn build_block(&self, node_id: NodeId, node_path: &[PathSegment]) -> ProjectedBlock {
270        let node_is_map = is_map_like(&self.doc.node(node_id).content);
271        let (bindings, sections) = self.build_entries(node_id, node_path, &[], node_is_map, true);
272        ProjectedBlock {
273            value: None,
274            bindings,
275            sections,
276        }
277    }
278
279    fn build_entries(
280        &self,
281        node_id: NodeId,
282        node_path: &[PathSegment],
283        path_prefix: &[PathSegment],
284        emit_map_fields: bool,
285        allow_sections: bool,
286    ) -> (Vec<ProjectedBinding>, Vec<ProjectedSection>) {
287        let mut bindings = Vec::new();
288        let mut sections = Vec::new();
289        let mut ctx = EntryContext {
290            bindings: &mut bindings,
291            sections: &mut sections,
292            node_path,
293            path_prefix,
294            allow_sections,
295        };
296
297        let node = self.doc.node(node_id);
298        let mut children = Vec::new();
299
300        for (ident, &child_id) in node.extensions.iter() {
301            children.push(ChildEntry {
302                segment: PathSegment::Extension(ident.clone()),
303                node_id: child_id,
304            });
305        }
306
307        if emit_map_fields {
308            children.extend(map_like_children(node));
309        }
310
311        let children = self.order_children(node_path, children);
312        for child in children {
313            self.append_child_entries(&mut ctx, child.segment, child.node_id);
314        }
315
316        (bindings, sections)
317    }
318
319    fn order_children(
320        &self,
321        parent_path: &[PathSegment],
322        children: Vec<ChildEntry>,
323    ) -> Vec<ChildEntry> {
324        let Some(rule) = self.layout.order_rule_for_parent(parent_path) else {
325            return children;
326        };
327
328        if !rule.append_unlisted {
329            let mut out = children;
330            let listed: Vec<(usize, ChildEntry)> = out
331                .iter()
332                .enumerate()
333                .filter(|(_, entry)| {
334                    rule.child_order
335                        .iter()
336                        .any(|segment| segment == &entry.segment)
337                })
338                .map(|(idx, entry)| (idx, entry.clone()))
339                .collect();
340
341            let mut sorted = listed
342                .iter()
343                .map(|(_, entry)| entry.clone())
344                .collect::<Vec<_>>();
345            sorted.sort_by_key(|entry| {
346                rule.child_order
347                    .iter()
348                    .position(|segment| segment == &entry.segment)
349                    .unwrap_or(usize::MAX)
350            });
351
352            for ((idx, _), replacement) in listed.into_iter().zip(sorted.into_iter()) {
353                out[idx] = replacement;
354            }
355
356            return out;
357        }
358
359        let mut remaining = children;
360        let mut ordered = Vec::new();
361
362        for segment in &rule.child_order {
363            if let Some(pos) = remaining.iter().position(|entry| &entry.segment == segment) {
364                ordered.push(remaining.remove(pos));
365            }
366        }
367
368        ordered.extend(remaining);
369
370        ordered
371    }
372
373    fn append_child_entries(
374        &self,
375        ctx: &mut EntryContext<'_>,
376        child_seg: PathSegment,
377        child_id: NodeId,
378    ) {
379        let child_node_path = concat_path(ctx.node_path, &child_seg);
380        let child_print_path = concat_path(ctx.path_prefix, &child_seg);
381
382        let style = self.layout.style_for_path(self.doc, &child_node_path);
383
384        if style == LayoutStyle::Passthrough {
385            let child_is_map = is_map_like(&self.doc.node(child_id).content);
386            let (b, s) = self.build_entries(
387                child_id,
388                &child_node_path,
389                &child_print_path,
390                child_is_map,
391                ctx.allow_sections,
392            );
393            ctx.bindings.extend(b);
394            ctx.sections.extend(s);
395            return;
396        }
397
398        let child_is_map = is_map_like(&self.doc.node(child_id).content);
399        let mut style = self.normalize_style(style, child_is_map, ctx.allow_sections);
400
401        if style == LayoutStyle::Binding
402            && child_is_map
403            && self.inline_binding_hides_descendant_entries(child_id, &child_node_path)
404        {
405            style = LayoutStyle::SectionBinding;
406        }
407
408        if style == LayoutStyle::Section && self.has_section_entries(child_id, &child_node_path) {
409            style = LayoutStyle::Nested;
410        }
411
412        match style {
413            LayoutStyle::Binding | LayoutStyle::Auto | LayoutStyle::Passthrough => {
414                ctx.bindings.push(ProjectedBinding {
415                    path: child_print_path.clone(),
416                    body: ProjectedBindingBody::Value(child_id),
417                });
418
419                let (b, s) = self.build_entries(
420                    child_id,
421                    &child_node_path,
422                    &child_print_path,
423                    false,
424                    ctx.allow_sections,
425                );
426                ctx.bindings.extend(b);
427                ctx.sections.extend(s);
428            }
429            LayoutStyle::SectionBinding => {
430                if child_is_map {
431                    let block = self.build_block(child_id, &child_node_path);
432                    ctx.bindings.push(ProjectedBinding {
433                        path: child_print_path,
434                        body: ProjectedBindingBody::Block(block),
435                    });
436                } else {
437                    ctx.bindings.push(ProjectedBinding {
438                        path: child_print_path.clone(),
439                        body: ProjectedBindingBody::Value(child_id),
440                    });
441                    let (b, s) = self.build_entries(
442                        child_id,
443                        &child_node_path,
444                        &child_print_path,
445                        false,
446                        ctx.allow_sections,
447                    );
448                    ctx.bindings.extend(b);
449                    ctx.sections.extend(s);
450                }
451            }
452            LayoutStyle::Section => {
453                let (child_bindings, child_sections) =
454                    self.build_entries(child_id, &child_node_path, &[], child_is_map, false);
455                debug_assert!(child_sections.is_empty());
456                ctx.sections.push(ProjectedSection {
457                    path: child_print_path,
458                    body: ProjectedSectionBody::Items {
459                        value: None,
460                        bindings: child_bindings,
461                    },
462                });
463            }
464            LayoutStyle::SectionRootBinding => {
465                if child_is_map {
466                    let (child_bindings, child_sections) =
467                        self.build_entries(child_id, &child_node_path, &[], true, false);
468                    debug_assert!(child_sections.is_empty());
469                    ctx.sections.push(ProjectedSection {
470                        path: child_print_path,
471                        body: ProjectedSectionBody::Items {
472                            value: None,
473                            bindings: child_bindings,
474                        },
475                    });
476                } else {
477                    let (child_bindings, child_sections) =
478                        self.build_entries(child_id, &child_node_path, &[], false, false);
479                    debug_assert!(child_sections.is_empty());
480                    ctx.sections.push(ProjectedSection {
481                        path: child_print_path,
482                        body: ProjectedSectionBody::Items {
483                            value: Some(child_id),
484                            bindings: child_bindings,
485                        },
486                    });
487                }
488            }
489            LayoutStyle::Nested => {
490                if child_is_map {
491                    let block = self.build_block(child_id, &child_node_path);
492                    ctx.sections.push(ProjectedSection {
493                        path: child_print_path,
494                        body: ProjectedSectionBody::Block(block),
495                    });
496                } else {
497                    ctx.bindings.push(ProjectedBinding {
498                        path: child_print_path.clone(),
499                        body: ProjectedBindingBody::Value(child_id),
500                    });
501                    let (b, s) = self.build_entries(
502                        child_id,
503                        &child_node_path,
504                        &child_print_path,
505                        false,
506                        ctx.allow_sections,
507                    );
508                    ctx.bindings.extend(b);
509                    ctx.sections.extend(s);
510                }
511            }
512        }
513    }
514
515    fn normalize_style(
516        &self,
517        style: LayoutStyle,
518        node_is_map: bool,
519        allow_sections: bool,
520    ) -> LayoutStyle {
521        let mut style = match style {
522            LayoutStyle::Auto | LayoutStyle::Passthrough => LayoutStyle::Binding,
523            other => other,
524        };
525
526        if !node_is_map {
527            style = match style {
528                LayoutStyle::SectionRootBinding => LayoutStyle::SectionRootBinding,
529                _ => LayoutStyle::Binding,
530            };
531        } else if style == LayoutStyle::SectionRootBinding {
532            style = LayoutStyle::Section;
533        }
534
535        if !allow_sections
536            && matches!(
537                style,
538                LayoutStyle::Section | LayoutStyle::Nested | LayoutStyle::SectionRootBinding
539            )
540        {
541            style = if node_is_map {
542                LayoutStyle::SectionBinding
543            } else {
544                LayoutStyle::Binding
545            };
546        }
547
548        style
549    }
550
551    fn inline_binding_hides_descendant_entries(
552        &self,
553        node_id: NodeId,
554        node_path: &[PathSegment],
555    ) -> bool {
556        for ChildEntry {
557            segment,
558            node_id: child_id,
559        } in map_like_children(self.doc.node(node_id))
560        {
561            let child_node_path = concat_path(node_path, &segment);
562            if self.subtree_has_deferred_entries(child_id, &child_node_path) {
563                return true;
564            }
565        }
566
567        false
568    }
569
570    fn subtree_has_deferred_entries(&self, node_id: NodeId, node_path: &[PathSegment]) -> bool {
571        let node = self.doc.node(node_id);
572        if !node.extensions.is_empty() {
573            return true;
574        }
575
576        if self.has_section_entries(node_id, node_path) {
577            return true;
578        }
579
580        if !is_map_like(&node.content) {
581            return false;
582        }
583
584        for ChildEntry {
585            segment,
586            node_id: child_id,
587        } in map_like_children(node)
588        {
589            let child_node_path = concat_path(node_path, &segment);
590            if self.subtree_has_deferred_entries(child_id, &child_node_path) {
591                return true;
592            }
593        }
594
595        false
596    }
597
598    fn has_section_entries(&self, node_id: NodeId, node_path: &[PathSegment]) -> bool {
599        let node = self.doc.node(node_id);
600        let node_is_map = is_map_like(&node.content);
601
602        for (ident, &child_id) in node.extensions.iter() {
603            let seg = PathSegment::Extension(ident.clone());
604            let child_node_path = concat_path(node_path, &seg);
605            if self.child_is_section(child_id, &child_node_path) {
606                return true;
607            }
608        }
609
610        if node_is_map {
611            for ChildEntry {
612                segment,
613                node_id: child_id,
614            } in map_like_children(node)
615            {
616                let child_node_path = concat_path(node_path, &segment);
617                if self.child_is_section(child_id, &child_node_path) {
618                    return true;
619                }
620            }
621        }
622
623        false
624    }
625
626    fn child_is_section(&self, child_id: NodeId, child_node_path: &[PathSegment]) -> bool {
627        let style = self.layout.style_for_path(self.doc, child_node_path);
628        if style == LayoutStyle::Passthrough {
629            return self.has_section_entries(child_id, child_node_path);
630        }
631
632        let child_is_map = is_map_like(&self.doc.node(child_id).content);
633        let style = self.normalize_style(style, child_is_map, true);
634        matches!(
635            style,
636            LayoutStyle::Section | LayoutStyle::Nested | LayoutStyle::SectionRootBinding
637        )
638    }
639}
640
641fn is_map_like(content: &NodeValue) -> bool {
642    matches!(content, NodeValue::Map(_) | NodeValue::PartialMap(_))
643}
644
645fn map_like_children(node: &crate::document::node::Node) -> Vec<ChildEntry> {
646    match &node.content {
647        NodeValue::Map(map) => map
648            .iter()
649            .map(|(key, &child_id)| ChildEntry {
650                segment: PathSegment::Value(key.clone()),
651                node_id: child_id,
652            })
653            .collect(),
654        NodeValue::PartialMap(map) => map
655            .iter()
656            .map(|(key, &child_id)| ChildEntry {
657                segment: PathSegment::from_partial_object_key(key.clone()),
658                node_id: child_id,
659            })
660            .collect(),
661        _ => Vec::new(),
662    }
663}
664
665#[derive(Debug, Clone, Copy, PartialEq, Eq)]
666enum RuleMatchStatus {
667    NoMatch,
668    Exact(LayoutStyle),
669    Potential(LayoutStyle),
670}
671
672fn rule_match_status(doc: &EureDocument, rule: &LayoutStyleRule) -> RuleMatchStatus {
673    if rule.variant_constraints.is_empty() {
674        return RuleMatchStatus::Exact(rule.style);
675    }
676
677    let mut inherited_variant: Option<VariantPath> = None;
678    let mut uncertain = false;
679
680    for constraint in &rule.variant_constraints {
681        let Some(union_node_id) = node_id_at_path(doc, &constraint.union_path) else {
682            return RuleMatchStatus::NoMatch;
683        };
684
685        let explicit_variant = if let Some(vp) = inherited_variant.as_ref()
686            && !vp.is_empty()
687        {
688            Some(vp.clone())
689        } else {
690            match parse_variant_extension(doc, union_node_id) {
691                Ok(path) => path,
692                Err(()) => return RuleMatchStatus::NoMatch,
693            }
694        };
695
696        match explicit_variant {
697            Some(vp) if !vp.is_empty() => {
698                let Some(first) = vp.first() else {
699                    return RuleMatchStatus::NoMatch;
700                };
701                if first.as_ref() != constraint.variant {
702                    return RuleMatchStatus::NoMatch;
703                }
704                inherited_variant = vp.rest();
705            }
706            _ => {
707                inherited_variant = None;
708                if constraint.requires_explicit_tag {
709                    return RuleMatchStatus::NoMatch;
710                }
711                uncertain = true;
712            }
713        }
714    }
715
716    if uncertain {
717        RuleMatchStatus::Potential(rule.style)
718    } else {
719        RuleMatchStatus::Exact(rule.style)
720    }
721}
722
723fn parse_variant_extension(doc: &EureDocument, node_id: NodeId) -> Result<Option<VariantPath>, ()> {
724    let node = doc.node(node_id);
725    let Some(&variant_node_id) = node.extensions.get(&VARIANT) else {
726        return Ok(None);
727    };
728
729    let Ok(variant_text) = doc.parse::<&str>(variant_node_id) else {
730        return Err(());
731    };
732
733    VariantPath::parse(variant_text).map(Some).map_err(|_| ())
734}
735
736fn node_id_at_path(doc: &EureDocument, path: &[PathSegment]) -> Option<NodeId> {
737    let mut current = doc.get_root_id();
738    for segment in path {
739        current = child_node_id(doc, current, segment)?;
740    }
741    Some(current)
742}
743
744fn build_source_block(block: &ProjectedBlock, sources: &mut Vec<EureSource>) -> SourceId {
745    let id = SourceId(sources.len());
746    sources.push(EureSource::default());
747    let mut eure = EureSource {
748        value: block.value,
749        ..Default::default()
750    };
751
752    for binding in &block.bindings {
753        let path = to_source_path(&binding.path);
754        let bind = match &binding.body {
755            ProjectedBindingBody::Value(node_id) => BindSource::Value(*node_id),
756            ProjectedBindingBody::Block(inner) => {
757                let inner_id = build_source_block(inner, sources);
758                BindSource::Block(inner_id)
759            }
760        };
761        eure.bindings.push(BindingSource {
762            trivia_before: Vec::new(),
763            path,
764            bind,
765            trailing_comment: None,
766        });
767    }
768
769    for section in &block.sections {
770        let path = to_source_path(&section.path);
771        let body = match &section.body {
772            ProjectedSectionBody::Items { value, bindings } => {
773                let mut items = Vec::new();
774                for binding in bindings {
775                    let path = to_source_path(&binding.path);
776                    let bind = match &binding.body {
777                        ProjectedBindingBody::Value(node_id) => BindSource::Value(*node_id),
778                        ProjectedBindingBody::Block(inner) => {
779                            let inner_id = build_source_block(inner, sources);
780                            BindSource::Block(inner_id)
781                        }
782                    };
783                    items.push(BindingSource {
784                        trivia_before: Vec::new(),
785                        path,
786                        bind,
787                        trailing_comment: None,
788                    });
789                }
790                SectionBody::Items {
791                    value: *value,
792                    bindings: items,
793                }
794            }
795            ProjectedSectionBody::Block(inner) => {
796                let inner_id = build_source_block(inner, sources);
797                SectionBody::Block(inner_id)
798            }
799        };
800        eure.sections.push(SectionSource {
801            trivia_before: Vec::new(),
802            path,
803            body,
804            trailing_comment: None,
805        });
806    }
807
808    sources[id.0] = eure;
809    id
810}
811
812fn to_source_path(path: &[PathSegment]) -> SourcePath {
813    let mut out: Vec<SourcePathSegment> = Vec::new();
814    for seg in path {
815        match seg {
816            PathSegment::Ident(id) => out.push(SourcePathSegment::ident(id.clone())),
817            PathSegment::Extension(id) => out.push(SourcePathSegment::extension(id.clone())),
818            PathSegment::PartialValue(key) => out.push(SourcePathSegment {
819                key: partial_object_key_to_source_key(key),
820                array: None,
821            }),
822            PathSegment::HoleKey(label) => out.push(SourcePathSegment {
823                key: SourceKey::hole(label.clone()),
824                array: None,
825            }),
826            PathSegment::Value(key) => out.push(SourcePathSegment {
827                key: object_key_to_source_key(key),
828                array: None,
829            }),
830            PathSegment::TupleIndex(index) => out.push(SourcePathSegment {
831                key: SourceKey::TupleIndex(*index),
832                array: None,
833            }),
834            PathSegment::ArrayIndex(index) => {
835                if let Some(last) = out.last_mut() {
836                    last.array = Some(*index);
837                }
838            }
839        }
840    }
841    out
842}
843
844fn object_key_to_source_key(key: &ObjectKey) -> SourceKey {
845    match key {
846        ObjectKey::String(s) => {
847            if let Ok(id) = s.parse::<Identifier>() {
848                SourceKey::Ident(id)
849            } else {
850                SourceKey::quoted(s.clone())
851            }
852        }
853        ObjectKey::Number(n) => {
854            if let Ok(n64) = i64::try_from(n) {
855                SourceKey::Integer(n64)
856            } else {
857                SourceKey::quoted(n.to_string())
858            }
859        }
860        ObjectKey::Tuple(keys) => {
861            SourceKey::Tuple(keys.iter().map(object_key_to_source_key).collect())
862        }
863    }
864}
865
866fn partial_object_key_to_source_key(key: &PartialObjectKey) -> SourceKey {
867    match key {
868        PartialObjectKey::String(s) => {
869            if let Ok(id) = s.parse::<Identifier>() {
870                SourceKey::Ident(id)
871            } else {
872                SourceKey::quoted(s.clone())
873            }
874        }
875        PartialObjectKey::Number(n) => {
876            if let Ok(n64) = i64::try_from(n) {
877                SourceKey::Integer(n64)
878            } else {
879                SourceKey::quoted(n.to_string())
880            }
881        }
882        PartialObjectKey::Hole(label) => SourceKey::hole(label.clone()),
883        PartialObjectKey::Tuple(keys) => {
884            SourceKey::Tuple(keys.iter().map(partial_object_key_to_source_key).collect())
885        }
886    }
887}
888
889fn concat_path(prefix: &[PathSegment], seg: &PathSegment) -> Vec<PathSegment> {
890    let mut out = Vec::with_capacity(prefix.len() + 1);
891    out.extend_from_slice(prefix);
892    out.push(seg.clone());
893    out
894}
895
896fn child_node_id(doc: &EureDocument, parent_id: NodeId, segment: &PathSegment) -> Option<NodeId> {
897    let parent = doc.node(parent_id);
898    match segment {
899        PathSegment::Extension(ext) => parent.extensions.get(ext).copied(),
900        PathSegment::Ident(ident) => match &parent.content {
901            NodeValue::Map(map) => map.get(&ObjectKey::String(ident.to_string())).copied(),
902            NodeValue::PartialMap(map) => map
903                .find(&PartialObjectKey::String(ident.to_string()))
904                .copied(),
905            _ => None,
906        },
907        PathSegment::Value(key) => match &parent.content {
908            NodeValue::Map(map) => map.get(key).copied(),
909            NodeValue::PartialMap(map) => map.find(&PartialObjectKey::from(key.clone())).copied(),
910            _ => None,
911        },
912        PathSegment::PartialValue(key) => match &parent.content {
913            NodeValue::Map(map) => ObjectKey::try_from(key.clone())
914                .ok()
915                .and_then(|object_key| map.get(&object_key))
916                .copied(),
917            NodeValue::PartialMap(map) => map.find(key).copied(),
918            _ => None,
919        },
920        PathSegment::ArrayIndex(index) => match &parent.content {
921            NodeValue::Array(array) => index.and_then(|i| array.get(i)),
922            _ => None,
923        },
924        PathSegment::TupleIndex(index) => match &parent.content {
925            NodeValue::Tuple(tuple) => tuple.get(*index as usize),
926            _ => None,
927        },
928        PathSegment::HoleKey(label) => match &parent.content {
929            NodeValue::PartialMap(map) => map.find(&PartialObjectKey::Hole(label.clone())).copied(),
930            _ => None,
931        },
932    }
933}
934
935#[cfg(test)]
936mod tests {
937    use super::*;
938    use crate::document::constructor::DocumentConstructor;
939    use crate::value::ObjectKey;
940    use alloc::vec;
941
942    fn make_doc() -> EureDocument {
943        let mut c = DocumentConstructor::new();
944        c.bind_empty_map().unwrap();
945
946        let scope = c.begin_scope();
947        c.navigate(PathSegment::Value(ObjectKey::String("a".to_string())))
948            .unwrap();
949        c.bind_empty_map().unwrap();
950        let inner = c.begin_scope();
951        c.navigate(PathSegment::Value(ObjectKey::String("x".to_string())))
952            .unwrap();
953        c.bind_primitive(crate::value::PrimitiveValue::Integer(1.into()))
954            .unwrap();
955        c.end_scope(inner).unwrap();
956        c.end_scope(scope).unwrap();
957
958        let scope = c.begin_scope();
959        c.navigate(PathSegment::Value(ObjectKey::String("b".to_string())))
960            .unwrap();
961        c.bind_empty_map().unwrap();
962        c.end_scope(scope).unwrap();
963
964        c.finish()
965    }
966
967    #[test]
968    fn applies_section_binding_rule() {
969        let doc = make_doc();
970        let mut layout = DocLayout::new();
971        layout.add_style_rule(
972            vec![PathSegment::Value(ObjectKey::String("a".to_string()))],
973            LayoutStyle::SectionBinding,
974        );
975
976        let source = project_with_layout(&doc, &layout);
977        let root = source.root_source();
978
979        assert_eq!(root.bindings.len(), 2);
980        assert!(matches!(root.bindings[0].bind, BindSource::Block(_)));
981    }
982
983    #[test]
984    fn applies_order_rule() {
985        let doc = make_doc();
986        let mut layout = DocLayout::new();
987        layout.add_order_rule(
988            Vec::new(),
989            vec![PathSegment::Value(ObjectKey::String("b".to_string()))],
990            true,
991        );
992
993        let source = project_with_layout(&doc, &layout);
994        let root = source.root_source();
995
996        let first = &root.bindings[0].path[0];
997        assert!(matches!(
998            first,
999            SourcePathSegment {
1000                key: SourceKey::Ident(id),
1001                ..
1002            } if id.as_ref() == "b"
1003        ));
1004    }
1005
1006    #[test]
1007    fn passthrough_flattens_parent_binding() {
1008        let doc = make_doc();
1009        let mut layout = DocLayout::new();
1010        layout.add_style_rule(
1011            vec![PathSegment::Value(ObjectKey::String("a".to_string()))],
1012            LayoutStyle::Passthrough,
1013        );
1014
1015        let source = project_with_layout(&doc, &layout);
1016        let root = source.root_source();
1017
1018        assert!(!root.bindings.iter().any(|b| {
1019            matches!(
1020                b.path.as_slice(),
1021                [SourcePathSegment {
1022                    key: SourceKey::Ident(id),
1023                    ..
1024                }] if id.as_ref() == "a"
1025            )
1026        }));
1027        assert!(root.bindings.iter().any(|b| {
1028            matches!(
1029                b.path.as_slice(),
1030                [
1031                    SourcePathSegment {
1032                        key: SourceKey::Ident(first),
1033                        ..
1034                    },
1035                    SourcePathSegment {
1036                        key: SourceKey::Ident(second),
1037                        ..
1038                    }
1039                ] if first.as_ref() == "a" && second.as_ref() == "x"
1040            )
1041        }));
1042    }
1043
1044    #[test]
1045    fn auto_promotes_inline_map_when_nested_extensions_would_be_lost() {
1046        let mut c = DocumentConstructor::new();
1047        c.bind_empty_map().unwrap();
1048
1049        let outer_scope = c.begin_scope();
1050        c.navigate(PathSegment::Value(ObjectKey::String("outer".to_string())))
1051            .unwrap();
1052        c.bind_empty_map().unwrap();
1053
1054        let inner_scope = c.begin_scope();
1055        c.navigate(PathSegment::Value(ObjectKey::String("inner".to_string())))
1056            .unwrap();
1057        c.bind_primitive(crate::value::PrimitiveValue::Integer(1.into()))
1058            .unwrap();
1059        c.set_extension("flag", true).unwrap();
1060        c.end_scope(inner_scope).unwrap();
1061        c.end_scope(outer_scope).unwrap();
1062
1063        let doc = c.finish();
1064        let source = project_with_layout(&doc, &DocLayout::new());
1065        let root = source.root_source();
1066
1067        assert_eq!(root.bindings.len(), 1);
1068        let BindSource::Block(outer_block_id) = &root.bindings[0].bind else {
1069            panic!("expected outer map to be promoted to a block binding");
1070        };
1071        let outer_block = source.source(*outer_block_id);
1072        assert_eq!(outer_block.bindings.len(), 2);
1073    }
1074}