facet_diff_core/layout/
build.rs

1//! Build a Layout from a Diff.
2//!
3//! This module converts a `Diff<'mem, 'facet>` into a `Layout` that can be rendered.
4//!
5//! # Architecture
6//!
7//! The build process walks the Diff tree while simultaneously navigating the original
8//! `from` and `to` Peek values. This allows us to:
9//! - Look up unchanged field values from the original structs
10//! - Decide whether to show unchanged fields or collapse them
11//!
12//! The Diff itself only stores what changed - the original Peeks provide context.
13
14use std::borrow::Cow;
15
16use tracing::debug;
17
18use facet_core::{Def, NumericType, PrimitiveType, Shape, StructKind, TextualType, Type, UserType};
19use facet_reflect::Peek;
20use indextree::{Arena, NodeId};
21
22use super::{
23    Attr, DiffFlavor, ElementChange, FormatArena, FormattedValue, Layout, LayoutNode, ValueType,
24    group_changed_attrs,
25};
26use crate::{Diff, ReplaceGroup, Updates, UpdatesGroup, Value};
27
28/// Get the display name for a shape, respecting the `rename` attribute.
29fn get_shape_display_name(shape: &Shape) -> &'static str {
30    if let Some(renamed) = shape.get_builtin_attr_value::<&str>("rename") {
31        return renamed;
32    }
33    shape.type_identifier
34}
35
36/// Check if a shape has any XML namespace attributes (ns_all, rename in xml namespace, etc.)
37/// Shapes without XML attributes are "proxy types" - Rust implementation details
38/// that wouldn't exist in actual XML output.
39fn shape_has_xml_attrs(shape: &Shape) -> bool {
40    shape.attributes.iter().any(|attr| attr.ns == Some("xml"))
41}
42
43/// Get display name for XML output, prefixing proxy types with `@`.
44/// Proxy types are structs without XML namespace attributes - they're Rust
45/// implementation details (like PathData) that represent something that would
46/// be different in actual XML (like a string attribute).
47fn get_xml_display_name(shape: &Shape) -> Cow<'static, str> {
48    let base_name = get_shape_display_name(shape);
49
50    // Check if this is a struct without XML attributes (a proxy type)
51    if let Type::User(UserType::Struct(_)) = shape.ty
52        && !shape_has_xml_attrs(shape)
53    {
54        return Cow::Owned(format!("@{}", base_name));
55    }
56
57    Cow::Borrowed(base_name)
58}
59
60/// Get the display name for an enum variant, respecting the `rename` attribute.
61fn get_variant_display_name(variant: &facet_core::Variant) -> &'static str {
62    if let Some(attr) = variant.get_builtin_attr("rename")
63        && let Some(renamed) = attr.get_as::<&'static str>()
64    {
65        return renamed;
66    }
67    variant.name
68}
69
70/// Check if a value should be skipped in diff output.
71///
72/// Returns true for "falsy" values like `Option::None`, empty strings, empty vecs, etc.
73/// This is used to avoid cluttering diff output with unchanged `None` fields.
74fn should_skip_falsy(peek: Peek<'_, '_>) -> bool {
75    let shape = peek.shape();
76    // Check if the type has a truthiness function (Option<T>, Vec<T>, str, etc.)
77    if let Some(truthy_fn) = shape.truthiness_fn() {
78        // If the value is falsy, skip it
79        let is_truthy = unsafe { truthy_fn(peek.data()) };
80        let should_skip = !is_truthy;
81        debug!(
82            type_id = %shape.type_identifier,
83            is_truthy,
84            should_skip,
85            "should_skip_falsy check"
86        );
87        return should_skip;
88    }
89    false
90}
91
92/// Determine the type of a value for coloring purposes.
93fn determine_value_type(peek: Peek<'_, '_>) -> ValueType {
94    let shape = peek.shape();
95
96    // Check the Def first for special types like Option
97    if let Def::Option(_) = shape.def {
98        // Check if it's None
99        if let Ok(opt) = peek.into_option() {
100            if opt.is_none() {
101                return ValueType::Null;
102            }
103            // If Some, recurse to get inner type
104            if let Some(inner) = opt.value() {
105                return determine_value_type(inner);
106            }
107        }
108        return ValueType::Other;
109    }
110
111    // Check the Type for primitives
112    match shape.ty {
113        Type::Primitive(p) => match p {
114            PrimitiveType::Boolean => ValueType::Boolean,
115            PrimitiveType::Numeric(NumericType::Integer { .. })
116            | PrimitiveType::Numeric(NumericType::Float) => ValueType::Number,
117            PrimitiveType::Textual(TextualType::Char)
118            | PrimitiveType::Textual(TextualType::Str) => ValueType::String,
119            PrimitiveType::Never => ValueType::Null,
120        },
121        _ => ValueType::Other,
122    }
123}
124
125/// Options for building a layout from a diff.
126#[derive(Clone, Debug)]
127pub struct BuildOptions {
128    /// Maximum line width for attribute grouping.
129    pub max_line_width: usize,
130    /// Maximum number of unchanged fields to show inline.
131    /// If more than this many unchanged fields exist, collapse to "N unchanged".
132    pub max_unchanged_fields: usize,
133    /// Minimum run length to collapse unchanged sequence elements.
134    pub collapse_threshold: usize,
135    /// Precision for formatting floating-point numbers.
136    /// If set, floats are formatted with this many decimal places.
137    /// Useful when using float tolerance in comparisons.
138    pub float_precision: Option<usize>,
139}
140
141impl Default for BuildOptions {
142    fn default() -> Self {
143        Self {
144            max_line_width: 80,
145            max_unchanged_fields: 5,
146            collapse_threshold: 3,
147            float_precision: None,
148        }
149    }
150}
151
152impl BuildOptions {
153    /// Set the float precision for formatting.
154    ///
155    /// When set, all floating-point numbers will be formatted with this many
156    /// decimal places. This is useful when using float tolerance in comparisons
157    /// to ensure the display matches the tolerance level.
158    pub fn with_float_precision(mut self, precision: usize) -> Self {
159        self.float_precision = Some(precision);
160        self
161    }
162}
163
164/// Build a Layout from a Diff.
165///
166/// This is the main entry point for converting a diff into a renderable layout.
167///
168/// # Arguments
169///
170/// * `diff` - The diff to render
171/// * `from` - The original "from" value (for looking up unchanged fields)
172/// * `to` - The original "to" value (for looking up unchanged fields)
173/// * `opts` - Build options
174/// * `flavor` - The output flavor (Rust, JSON, XML)
175pub fn build_layout<'mem, 'facet, F: DiffFlavor>(
176    diff: &Diff<'mem, 'facet>,
177    from: Peek<'mem, 'facet>,
178    to: Peek<'mem, 'facet>,
179    opts: &BuildOptions,
180    flavor: &F,
181) -> Layout {
182    let mut builder = LayoutBuilder::new(opts.clone(), flavor);
183    let root_id = builder.build(diff, Some(from), Some(to));
184    builder.finish(root_id)
185}
186
187/// Internal builder state.
188struct LayoutBuilder<'f, F: DiffFlavor> {
189    /// Arena for formatted strings.
190    strings: FormatArena,
191    /// Arena for layout nodes.
192    tree: Arena<LayoutNode>,
193    /// Build options.
194    opts: BuildOptions,
195    /// Output flavor for formatting.
196    flavor: &'f F,
197}
198
199impl<'f, F: DiffFlavor> LayoutBuilder<'f, F> {
200    fn new(opts: BuildOptions, flavor: &'f F) -> Self {
201        Self {
202            strings: FormatArena::new(),
203            tree: Arena::new(),
204            opts,
205            flavor,
206        }
207    }
208
209    /// Build the layout from a diff, with optional context Peeks.
210    fn build<'mem, 'facet>(
211        &mut self,
212        diff: &Diff<'mem, 'facet>,
213        from: Option<Peek<'mem, 'facet>>,
214        to: Option<Peek<'mem, 'facet>>,
215    ) -> NodeId {
216        self.build_diff(diff, from, to, ElementChange::None)
217    }
218
219    /// Build a node from a diff with a given element change type.
220    fn build_diff<'mem, 'facet>(
221        &mut self,
222        diff: &Diff<'mem, 'facet>,
223        from: Option<Peek<'mem, 'facet>>,
224        to: Option<Peek<'mem, 'facet>>,
225        change: ElementChange,
226    ) -> NodeId {
227        match diff {
228            Diff::Equal { value } => {
229                // For equal values, render as unchanged text
230                if let Some(peek) = value {
231                    self.build_peek(*peek, ElementChange::None)
232                } else {
233                    // No value available, create a placeholder
234                    let (span, width) = self.strings.push_str("(equal)");
235                    let value = FormattedValue::new(span, width);
236                    self.tree.new_node(LayoutNode::Text {
237                        value,
238                        change: ElementChange::None,
239                    })
240                }
241            }
242            Diff::Replace { from, to } => {
243                // Create a container element with deleted and inserted children
244                let root = self.tree.new_node(LayoutNode::element("_replace"));
245
246                let from_node = self.build_peek(*from, ElementChange::Deleted);
247                let to_node = self.build_peek(*to, ElementChange::Inserted);
248
249                root.append(from_node, &mut self.tree);
250                root.append(to_node, &mut self.tree);
251
252                root
253            }
254            Diff::User {
255                from: from_shape,
256                to: _to_shape,
257                variant,
258                value,
259            } => {
260                // Handle Option<T> transparently - don't create an <Option> element wrapper
261                // Option is a Rust implementation detail that shouldn't leak into XML diff output
262                if matches!(from_shape.def, Def::Option(_))
263                    && let Value::Tuple { updates } = value
264                {
265                    // Unwrap from/to to get inner Option values
266                    let inner_from =
267                        from.and_then(|p| p.into_option().ok().and_then(|opt| opt.value()));
268                    let inner_to =
269                        to.and_then(|p| p.into_option().ok().and_then(|opt| opt.value()));
270
271                    // Build updates without an Option wrapper
272                    // Use a transparent container that just holds the children
273                    return self.build_tuple_transparent(updates, inner_from, inner_to, change);
274                }
275
276                // Handle enum variants transparently - use variant name as tag
277                // This makes enums like SvgNode::Path render as <path> not <SvgNode><Path>
278                if let Some(variant_name) = *variant
279                    && let Type::User(UserType::Enum(enum_ty)) = from_shape.ty
280                {
281                    // Look up the variant to get the rename attribute
282                    let tag =
283                        if let Some(v) = enum_ty.variants.iter().find(|v| v.name == variant_name) {
284                            Cow::Borrowed(get_variant_display_name(v))
285                        } else {
286                            Cow::Borrowed(variant_name)
287                        };
288                    debug!(
289                        tag = tag.as_ref(),
290                        variant_name, "Diff::User enum variant - using variant tag"
291                    );
292
293                    // For tuple variants (newtypes), make them transparent
294                    if let Value::Tuple { updates } = value {
295                        // Unwrap from/to to get inner enum values
296                        let inner_from = from.and_then(|p| {
297                            p.into_enum().ok().and_then(|e| e.field(0).ok().flatten())
298                        });
299                        let inner_to = to.and_then(|p| {
300                            p.into_enum().ok().and_then(|e| e.field(0).ok().flatten())
301                        });
302
303                        // Build the inner content with the variant tag
304                        return self
305                            .build_enum_tuple_variant(tag, updates, inner_from, inner_to, change);
306                    }
307
308                    // For struct variants, use the variant tag directly
309                    if let Value::Struct {
310                        updates,
311                        deletions,
312                        insertions,
313                        unchanged,
314                    } = value
315                    {
316                        return self.build_struct(
317                            tag, None, updates, deletions, insertions, unchanged, from, to, change,
318                        );
319                    }
320                }
321
322                // Get type name for the tag, respecting `rename` attribute
323                // Use get_xml_display_name to prefix proxy types with `@`
324                let tag = get_xml_display_name(from_shape);
325                debug!(tag = tag.as_ref(), variant = ?variant, value_type = ?std::mem::discriminant(value), "Diff::User");
326
327                match value {
328                    Value::Struct {
329                        updates,
330                        deletions,
331                        insertions,
332                        unchanged,
333                    } => self.build_struct(
334                        tag, *variant, updates, deletions, insertions, unchanged, from, to, change,
335                    ),
336                    Value::Tuple { updates } => {
337                        debug!(tag = tag.as_ref(), "Value::Tuple - building tuple");
338                        self.build_tuple(tag, *variant, updates, from, to, change)
339                    }
340                }
341            }
342            Diff::Sequence {
343                from: _seq_shape_from,
344                to: _seq_shape_to,
345                updates,
346            } => {
347                // Get item type from the from/to Peek values passed to build_diff
348                let item_type = from
349                    .and_then(|p| p.into_list_like().ok())
350                    .and_then(|list| list.iter().next())
351                    .or_else(|| {
352                        to.and_then(|p| p.into_list_like().ok())
353                            .and_then(|list| list.iter().next())
354                    })
355                    .map(|item| get_shape_display_name(item.shape()))
356                    .unwrap_or("item");
357                self.build_sequence(updates, change, item_type)
358            }
359        }
360    }
361
362    /// Build a node from a Peek value.
363    fn build_peek(&mut self, peek: Peek<'_, '_>, change: ElementChange) -> NodeId {
364        let shape = peek.shape();
365        debug!(
366            type_id = %shape.type_identifier,
367            def = ?shape.def,
368            change = ?change,
369            "build_peek"
370        );
371
372        // Check if this is a struct we can recurse into
373        match (shape.def, shape.ty) {
374            // Handle Option<T> by unwrapping to the inner value
375            (Def::Option(_), _) => {
376                if let Ok(opt) = peek.into_option()
377                    && let Some(inner) = opt.value()
378                {
379                    // Recurse into the inner value
380                    return self.build_peek(inner, change);
381                }
382                // None - render as null text
383                let (span, width) = self.strings.push_str("null");
384                return self.tree.new_node(LayoutNode::Text {
385                    value: FormattedValue::with_type(span, width, ValueType::Null),
386                    change,
387                });
388            }
389            (_, Type::User(UserType::Struct(ty))) if ty.kind == StructKind::Struct => {
390                // Build as element with fields as attributes
391                if let Ok(struct_peek) = peek.into_struct() {
392                    let tag = get_xml_display_name(shape);
393                    let mut attrs = Vec::new();
394
395                    for (i, field) in ty.fields.iter().enumerate() {
396                        if let Ok(field_value) = struct_peek.field(i) {
397                            // Skip falsy values (e.g., Option::None)
398                            if should_skip_falsy(field_value) {
399                                continue;
400                            }
401                            let formatted_value = self.format_peek(field_value);
402                            let attr = match change {
403                                ElementChange::None => {
404                                    Attr::unchanged(field.name, field.name.len(), formatted_value)
405                                }
406                                ElementChange::Deleted => {
407                                    Attr::deleted(field.name, field.name.len(), formatted_value)
408                                }
409                                ElementChange::Inserted => {
410                                    Attr::inserted(field.name, field.name.len(), formatted_value)
411                                }
412                                ElementChange::MovedFrom | ElementChange::MovedTo => {
413                                    // For moved elements, show fields as unchanged
414                                    Attr::unchanged(field.name, field.name.len(), formatted_value)
415                                }
416                            };
417                            attrs.push(attr);
418                        }
419                    }
420
421                    let changed_groups = group_changed_attrs(&attrs, self.opts.max_line_width, 0);
422
423                    return self.tree.new_node(LayoutNode::Element {
424                        tag,
425                        field_name: None,
426                        attrs,
427                        changed_groups,
428                        change,
429                    });
430                }
431            }
432            (_, Type::User(UserType::Enum(_))) => {
433                // Build enum as element with variant name as tag (respecting rename attribute)
434                debug!(type_id = %shape.type_identifier, "processing enum");
435                if let Ok(enum_peek) = peek.into_enum()
436                    && let Ok(variant) = enum_peek.active_variant()
437                {
438                    let tag_str = get_variant_display_name(variant);
439                    let fields = &variant.data.fields;
440                    debug!(
441                        variant_name = tag_str,
442                        fields_count = fields.len(),
443                        "enum variant"
444                    );
445
446                    // If variant has fields, build as element with those fields
447                    if !fields.is_empty() {
448                        // Check for newtype pattern: single field with same-named inner type
449                        // e.g., `Circle(Circle)` where we want to show Circle's fields directly
450                        if fields.len() == 1
451                            && let Ok(Some(inner_value)) = enum_peek.field(0)
452                        {
453                            let inner_shape = inner_value.shape();
454                            // If it's a struct, recurse into it but use the variant name
455                            if let Type::User(UserType::Struct(s)) = inner_shape.ty
456                                && s.kind == StructKind::Struct
457                                && let Ok(struct_peek) = inner_value.into_struct()
458                            {
459                                let mut attrs = Vec::new();
460
461                                for (i, field) in s.fields.iter().enumerate() {
462                                    if let Ok(field_value) = struct_peek.field(i) {
463                                        // Skip falsy values (e.g., Option::None)
464                                        if should_skip_falsy(field_value) {
465                                            continue;
466                                        }
467                                        let formatted_value = self.format_peek(field_value);
468                                        let attr = match change {
469                                            ElementChange::None => Attr::unchanged(
470                                                field.name,
471                                                field.name.len(),
472                                                formatted_value,
473                                            ),
474                                            ElementChange::Deleted => Attr::deleted(
475                                                field.name,
476                                                field.name.len(),
477                                                formatted_value,
478                                            ),
479                                            ElementChange::Inserted => Attr::inserted(
480                                                field.name,
481                                                field.name.len(),
482                                                formatted_value,
483                                            ),
484                                            ElementChange::MovedFrom | ElementChange::MovedTo => {
485                                                Attr::unchanged(
486                                                    field.name,
487                                                    field.name.len(),
488                                                    formatted_value,
489                                                )
490                                            }
491                                        };
492                                        attrs.push(attr);
493                                    }
494                                }
495
496                                let changed_groups =
497                                    group_changed_attrs(&attrs, self.opts.max_line_width, 0);
498
499                                return self.tree.new_node(LayoutNode::Element {
500                                    tag: Cow::Borrowed(tag_str),
501                                    field_name: None,
502                                    attrs,
503                                    changed_groups,
504                                    change,
505                                });
506                            }
507                        }
508
509                        // General case: show variant fields directly
510                        let mut attrs = Vec::new();
511
512                        for (i, field) in fields.iter().enumerate() {
513                            if let Ok(Some(field_value)) = enum_peek.field(i) {
514                                // Skip falsy values (e.g., Option::None)
515                                if should_skip_falsy(field_value) {
516                                    continue;
517                                }
518                                let formatted_value = self.format_peek(field_value);
519                                let attr = match change {
520                                    ElementChange::None => Attr::unchanged(
521                                        field.name,
522                                        field.name.len(),
523                                        formatted_value,
524                                    ),
525                                    ElementChange::Deleted => {
526                                        Attr::deleted(field.name, field.name.len(), formatted_value)
527                                    }
528                                    ElementChange::Inserted => Attr::inserted(
529                                        field.name,
530                                        field.name.len(),
531                                        formatted_value,
532                                    ),
533                                    ElementChange::MovedFrom | ElementChange::MovedTo => {
534                                        Attr::unchanged(
535                                            field.name,
536                                            field.name.len(),
537                                            formatted_value,
538                                        )
539                                    }
540                                };
541                                attrs.push(attr);
542                            }
543                        }
544
545                        let changed_groups =
546                            group_changed_attrs(&attrs, self.opts.max_line_width, 0);
547
548                        return self.tree.new_node(LayoutNode::Element {
549                            tag: Cow::Borrowed(tag_str),
550                            field_name: None,
551                            attrs,
552                            changed_groups,
553                            change,
554                        });
555                    } else {
556                        // Unit variant - just show the variant name as text
557                        let (span, width) = self.strings.push_str(tag_str);
558                        return self.tree.new_node(LayoutNode::Text {
559                            value: FormattedValue::new(span, width),
560                            change,
561                        });
562                    }
563                }
564            }
565            _ => {}
566        }
567
568        // Default: format as text
569        let formatted = self.format_peek(peek);
570        self.tree.new_node(LayoutNode::Text {
571            value: formatted,
572            change,
573        })
574    }
575
576    /// Build a struct diff as an element with attributes.
577    #[allow(clippy::too_many_arguments)]
578    fn build_struct<'mem, 'facet>(
579        &mut self,
580        tag: Cow<'static, str>,
581        variant: Option<&'static str>,
582        updates: &std::collections::HashMap<Cow<'static, str>, Diff<'mem, 'facet>>,
583        deletions: &std::collections::HashMap<Cow<'static, str>, Peek<'mem, 'facet>>,
584        insertions: &std::collections::HashMap<Cow<'static, str>, Peek<'mem, 'facet>>,
585        unchanged: &std::collections::HashSet<Cow<'static, str>>,
586        from: Option<Peek<'mem, 'facet>>,
587        to: Option<Peek<'mem, 'facet>>,
588        change: ElementChange,
589    ) -> NodeId {
590        let element_tag = tag;
591
592        // If there's a variant, we should indicate it somehow.
593        // TODO: LayoutNode::Element should have an optional variant: Option<&'static str>
594        if variant.is_some() {
595            // For now, just use the tag
596        }
597
598        let mut attrs = Vec::new();
599        let mut child_nodes = Vec::new();
600
601        // Handle unchanged fields - try to get values from the original Peek
602        debug!(
603            unchanged_count = unchanged.len(),
604            updates_count = updates.len(),
605            deletions_count = deletions.len(),
606            insertions_count = insertions.len(),
607            unchanged_fields = ?unchanged.iter().collect::<Vec<_>>(),
608            updates_fields = ?updates.keys().collect::<Vec<_>>(),
609            "build_struct"
610        );
611        if !unchanged.is_empty() {
612            let unchanged_count = unchanged.len();
613
614            if unchanged_count <= self.opts.max_unchanged_fields {
615                // Show unchanged fields with their values (if we have the original Peek)
616                if let Some(from_peek) = from {
617                    if let Ok(struct_peek) = from_peek.into_struct() {
618                        let mut sorted_unchanged: Vec<_> = unchanged.iter().collect();
619                        sorted_unchanged.sort();
620
621                        for field_name in sorted_unchanged {
622                            if let Ok(field_value) = struct_peek.field_by_name(field_name) {
623                                let field_shape = field_value.shape();
624                                debug!(
625                                    field_name = %field_name,
626                                    field_type = %field_shape.type_identifier,
627                                    "processing unchanged field"
628                                );
629                                // Skip falsy values (e.g., Option::None) in unchanged fields
630                                if should_skip_falsy(field_value) {
631                                    debug!(field_name = %field_name, "skipping falsy field");
632                                    continue;
633                                }
634                                let formatted = self.format_peek(field_value);
635                                let name_width = field_name.len();
636                                let attr =
637                                    Attr::unchanged(field_name.clone(), name_width, formatted);
638                                attrs.push(attr);
639                            }
640                        }
641                    }
642                } else {
643                    // No original Peek available - add a collapsed placeholder
644                    // We'll handle this after building the element
645                }
646            }
647            // If more than max_unchanged_fields, we'll add a collapsed node as a child
648        }
649
650        // Process updates - these become changed attributes or nested children
651        let mut sorted_updates: Vec<_> = updates.iter().collect();
652        sorted_updates.sort_by(|(a, _), (b, _)| a.cmp(b));
653
654        for (field_name, field_diff) in sorted_updates {
655            // Navigate into the field in from/to Peeks for nested context
656            let field_from = from.and_then(|p| {
657                p.into_struct()
658                    .ok()
659                    .and_then(|s| s.field_by_name(field_name).ok())
660            });
661            let field_to = to.and_then(|p| {
662                p.into_struct()
663                    .ok()
664                    .and_then(|s| s.field_by_name(field_name).ok())
665            });
666
667            match field_diff {
668                Diff::Replace { from, to } => {
669                    // Check if this is a complex type that should be built as children
670                    let from_shape = from.shape();
671                    let is_complex = match from_shape.ty {
672                        Type::User(UserType::Enum(_)) => true,
673                        Type::User(UserType::Struct(s)) if s.kind == StructKind::Struct => true,
674                        _ => false,
675                    };
676
677                    if is_complex {
678                        // Build from/to as separate child elements
679                        let from_node = self.build_peek(*from, ElementChange::Deleted);
680                        let to_node = self.build_peek(*to, ElementChange::Inserted);
681
682                        // Set field name on both nodes
683                        if let Cow::Borrowed(name) = field_name {
684                            if let Some(node) = self.tree.get_mut(from_node)
685                                && let LayoutNode::Element { field_name, .. } = node.get_mut()
686                            {
687                                *field_name = Some(name);
688                            }
689                            if let Some(node) = self.tree.get_mut(to_node)
690                                && let LayoutNode::Element { field_name, .. } = node.get_mut()
691                            {
692                                *field_name = Some(name);
693                            }
694                        }
695
696                        child_nodes.push(from_node);
697                        child_nodes.push(to_node);
698                    } else {
699                        // Scalar replacement - show as changed attribute
700                        let old_value = self.format_peek(*from);
701                        let new_value = self.format_peek(*to);
702                        let name_width = field_name.len();
703                        let attr =
704                            Attr::changed(field_name.clone(), name_width, old_value, new_value);
705                        attrs.push(attr);
706                    }
707                }
708                // Handle Option<scalar> as attribute changes, not children
709                Diff::User {
710                    from: shape,
711                    value: Value::Tuple { .. },
712                    ..
713                } if matches!(shape.def, Def::Option(_)) => {
714                    // Check if we can get scalar values from the Option
715                    if let (Some(from_peek), Some(to_peek)) = (field_from, field_to) {
716                        // Unwrap Option to get inner values
717                        let inner_from = from_peek.into_option().ok().and_then(|opt| opt.value());
718                        let inner_to = to_peek.into_option().ok().and_then(|opt| opt.value());
719
720                        if let (Some(from_val), Some(to_val)) = (inner_from, inner_to) {
721                            // Check if inner type is scalar (not struct/enum)
722                            let is_scalar = match from_val.shape().ty {
723                                Type::User(UserType::Enum(_)) => false,
724                                Type::User(UserType::Struct(s)) if s.kind == StructKind::Struct => {
725                                    false
726                                }
727                                _ => true,
728                            };
729
730                            if is_scalar {
731                                // Treat as scalar attribute change
732                                let old_value = self.format_peek(from_val);
733                                let new_value = self.format_peek(to_val);
734                                let name_width = field_name.len();
735                                let attr = Attr::changed(
736                                    field_name.clone(),
737                                    name_width,
738                                    old_value,
739                                    new_value,
740                                );
741                                attrs.push(attr);
742                                continue;
743                            }
744                        }
745                    }
746
747                    // Fall through to child handling if not a simple scalar Option
748                    let child =
749                        self.build_diff(field_diff, field_from, field_to, ElementChange::None);
750                    if let Cow::Borrowed(name) = field_name
751                        && let Some(node) = self.tree.get_mut(child)
752                    {
753                        match node.get_mut() {
754                            LayoutNode::Element { field_name, .. } => {
755                                *field_name = Some(name);
756                            }
757                            LayoutNode::Sequence { field_name, .. } => {
758                                *field_name = Some(name);
759                            }
760                            _ => {}
761                        }
762                    }
763                    child_nodes.push(child);
764                }
765                // Handle single-field wrapper structs (like SvgStyle) as inline attributes
766                // instead of nested child elements
767                Diff::User {
768                    from: inner_shape,
769                    value:
770                        Value::Struct {
771                            updates: inner_updates,
772                            deletions: inner_deletions,
773                            insertions: inner_insertions,
774                            unchanged: inner_unchanged,
775                        },
776                    ..
777                } if inner_updates.len() == 1
778                    && inner_deletions.is_empty()
779                    && inner_insertions.is_empty()
780                    && inner_unchanged.is_empty() =>
781                {
782                    // Single-field struct with one update - check if it's a scalar change
783                    let (inner_field_name, inner_field_diff) = inner_updates.iter().next().unwrap();
784
785                    // Check if the inner field's change is a scalar Replace
786                    if let Diff::Replace {
787                        from: inner_from,
788                        to: inner_to,
789                    } = inner_field_diff
790                    {
791                        // Check if inner type is scalar (not struct/enum)
792                        let is_scalar = match inner_from.shape().ty {
793                            Type::User(UserType::Enum(_)) => false,
794                            Type::User(UserType::Struct(s)) if s.kind == StructKind::Struct => {
795                                false
796                            }
797                            _ => true,
798                        };
799
800                        if is_scalar {
801                            // Inline as attribute change using the parent field name
802                            debug!(
803                                field_name = %field_name,
804                                inner_type = %inner_shape.type_identifier,
805                                inner_field = %inner_field_name,
806                                "inlining single-field wrapper as attribute"
807                            );
808                            let old_value = self.format_peek(*inner_from);
809                            let new_value = self.format_peek(*inner_to);
810                            let name_width = field_name.len();
811                            let attr =
812                                Attr::changed(field_name.clone(), name_width, old_value, new_value);
813                            attrs.push(attr);
814                            continue;
815                        }
816                    }
817
818                    // Fall through to default child handling
819                    let child =
820                        self.build_diff(field_diff, field_from, field_to, ElementChange::None);
821                    if let Cow::Borrowed(name) = field_name
822                        && let Some(node) = self.tree.get_mut(child)
823                    {
824                        match node.get_mut() {
825                            LayoutNode::Element { field_name, .. } => {
826                                *field_name = Some(name);
827                            }
828                            LayoutNode::Sequence { field_name, .. } => {
829                                *field_name = Some(name);
830                            }
831                            _ => {}
832                        }
833                    }
834                    child_nodes.push(child);
835                }
836                _ => {
837                    // Nested diff - build as child element or sequence
838                    let child =
839                        self.build_diff(field_diff, field_from, field_to, ElementChange::None);
840
841                    // Set the field name on the child (only for borrowed names for now)
842                    // TODO: Support owned field names for nested elements
843                    if let Cow::Borrowed(name) = field_name
844                        && let Some(node) = self.tree.get_mut(child)
845                    {
846                        match node.get_mut() {
847                            LayoutNode::Element { field_name, .. } => {
848                                *field_name = Some(name);
849                            }
850                            LayoutNode::Sequence { field_name, .. } => {
851                                *field_name = Some(name);
852                            }
853                            _ => {}
854                        }
855                    }
856
857                    child_nodes.push(child);
858                }
859            }
860        }
861
862        // Process deletions
863        let mut sorted_deletions: Vec<_> = deletions.iter().collect();
864        sorted_deletions.sort_by(|(a, _), (b, _)| a.cmp(b));
865
866        for (field_name, value) in sorted_deletions {
867            let formatted = self.format_peek(*value);
868            let name_width = field_name.len();
869            let attr = Attr::deleted(field_name.clone(), name_width, formatted);
870            attrs.push(attr);
871        }
872
873        // Process insertions
874        let mut sorted_insertions: Vec<_> = insertions.iter().collect();
875        sorted_insertions.sort_by(|(a, _), (b, _)| a.cmp(b));
876
877        for (field_name, value) in sorted_insertions {
878            let formatted = self.format_peek(*value);
879            let name_width = field_name.len();
880            let attr = Attr::inserted(field_name.clone(), name_width, formatted);
881            attrs.push(attr);
882        }
883
884        // Group changed attributes for alignment
885        let changed_groups = group_changed_attrs(&attrs, self.opts.max_line_width, 0);
886
887        // Create the element node
888        let node = self.tree.new_node(LayoutNode::Element {
889            tag: element_tag,
890            field_name: None, // Will be set by parent if this is a struct field
891            attrs,
892            changed_groups,
893            change,
894        });
895
896        // Add children
897        for child in child_nodes {
898            node.append(child, &mut self.tree);
899        }
900
901        // Add collapsed unchanged fields indicator if needed
902        let unchanged_count = unchanged.len();
903        if unchanged_count > self.opts.max_unchanged_fields
904            || (unchanged_count > 0 && from.is_none())
905        {
906            let collapsed = self.tree.new_node(LayoutNode::collapsed(unchanged_count));
907            node.append(collapsed, &mut self.tree);
908        }
909
910        node
911    }
912
913    /// Build a tuple diff.
914    fn build_tuple<'mem, 'facet>(
915        &mut self,
916        tag: Cow<'static, str>,
917        variant: Option<&'static str>,
918        updates: &Updates<'mem, 'facet>,
919        _from: Option<Peek<'mem, 'facet>>,
920        _to: Option<Peek<'mem, 'facet>>,
921        change: ElementChange,
922    ) -> NodeId {
923        // Same variant issue as build_struct
924        if variant.is_some() {
925            // TODO: LayoutNode::Element should support variant display
926        }
927
928        // Create element for the tuple
929        let node = self.tree.new_node(LayoutNode::Element {
930            tag,
931            field_name: None,
932            attrs: Vec::new(),
933            changed_groups: Vec::new(),
934            change,
935        });
936
937        // Build children from updates (tuple items don't have specific type names)
938        self.build_updates_children(node, updates, "item");
939
940        node
941    }
942
943    /// Build a tuple diff without a wrapper element (for transparent types like Option).
944    ///
945    /// This builds the updates directly without creating a containing element.
946    /// If there's a single child, returns it directly. Otherwise returns
947    /// a transparent wrapper element.
948    fn build_tuple_transparent<'mem, 'facet>(
949        &mut self,
950        updates: &Updates<'mem, 'facet>,
951        _from: Option<Peek<'mem, 'facet>>,
952        _to: Option<Peek<'mem, 'facet>>,
953        change: ElementChange,
954    ) -> NodeId {
955        // Create a temporary container to collect children
956        let temp = self.tree.new_node(LayoutNode::Element {
957            tag: Cow::Borrowed("_transparent"),
958            field_name: None,
959            attrs: Vec::new(),
960            changed_groups: Vec::new(),
961            change,
962        });
963
964        // Build children into the temporary container
965        self.build_updates_children(temp, updates, "item");
966
967        // Check how many children we have
968        let children: Vec<_> = temp.children(&self.tree).collect();
969
970        if children.len() == 1 {
971            // Single child - detach it from temp and return it directly
972            let child = children[0];
973            child.detach(&mut self.tree);
974            // Remove the temporary node
975            temp.remove(&mut self.tree);
976            child
977        } else {
978            // Multiple children or none - return the container
979            // (it will render as transparent due to the "_transparent" tag)
980            temp
981        }
982    }
983
984    /// Build an enum tuple variant (newtype pattern) with the variant tag.
985    ///
986    /// For enums like `SvgNode::Path(Path)`, this:
987    /// 1. Uses the variant's renamed tag (e.g., "path") as the element name
988    /// 2. Extracts the inner struct's fields as element attributes
989    ///
990    /// This makes enum variants transparent in the diff output.
991    fn build_enum_tuple_variant<'mem, 'facet>(
992        &mut self,
993        tag: Cow<'static, str>,
994        updates: &Updates<'mem, 'facet>,
995        inner_from: Option<Peek<'mem, 'facet>>,
996        inner_to: Option<Peek<'mem, 'facet>>,
997        change: ElementChange,
998    ) -> NodeId {
999        // Check if this is a single-element tuple (newtype pattern)
1000        // For newtype variants, the updates should contain a single diff for the inner value
1001        let interspersed = &updates.0;
1002
1003        // Check for a single replacement (1 removal + 1 addition) in the first update group
1004        // This handles cases where the inner struct is fully replaced
1005        if let Some(update_group) = &interspersed.first {
1006            let group_interspersed = &update_group.0;
1007
1008            // Check the first ReplaceGroup for a single replacement
1009            if let Some(replace_group) = &group_interspersed.first
1010                && replace_group.removals.len() == 1
1011                && replace_group.additions.len() == 1
1012            {
1013                let from = replace_group.removals[0];
1014                let to = replace_group.additions[0];
1015
1016                // Compare fields and only show those that actually differ
1017                let mut attrs = Vec::new();
1018
1019                if let (Ok(from_struct), Ok(to_struct)) = (from.into_struct(), to.into_struct())
1020                    && let Type::User(UserType::Struct(ty)) = from.shape().ty
1021                {
1022                    for (i, field) in ty.fields.iter().enumerate() {
1023                        let from_value = from_struct.field(i).ok();
1024                        let to_value = to_struct.field(i).ok();
1025
1026                        match (from_value, to_value) {
1027                            (Some(fv), Some(tv)) => {
1028                                // Both present - compare formatted values
1029                                let from_formatted = self.format_peek(fv);
1030                                let to_formatted = self.format_peek(tv);
1031
1032                                if self.strings.get(from_formatted.span)
1033                                    != self.strings.get(to_formatted.span)
1034                                {
1035                                    // Values differ - show as changed
1036                                    attrs.push(Attr::changed(
1037                                        Cow::Borrowed(field.name),
1038                                        field.name.len(),
1039                                        from_formatted,
1040                                        to_formatted,
1041                                    ));
1042                                } else {
1043                                    // Values same - show as unchanged (if not falsy)
1044                                    if !should_skip_falsy(fv) {
1045                                        attrs.push(Attr::unchanged(
1046                                            Cow::Borrowed(field.name),
1047                                            field.name.len(),
1048                                            from_formatted,
1049                                        ));
1050                                    }
1051                                }
1052                            }
1053                            (Some(fv), None) => {
1054                                // Only in from - deleted
1055                                if !should_skip_falsy(fv) {
1056                                    let formatted = self.format_peek(fv);
1057                                    attrs.push(Attr::deleted(
1058                                        Cow::Borrowed(field.name),
1059                                        field.name.len(),
1060                                        formatted,
1061                                    ));
1062                                }
1063                            }
1064                            (None, Some(tv)) => {
1065                                // Only in to - inserted
1066                                if !should_skip_falsy(tv) {
1067                                    let formatted = self.format_peek(tv);
1068                                    attrs.push(Attr::inserted(
1069                                        Cow::Borrowed(field.name),
1070                                        field.name.len(),
1071                                        formatted,
1072                                    ));
1073                                }
1074                            }
1075                            (None, None) => {
1076                                // Neither present - skip
1077                            }
1078                        }
1079                    }
1080                }
1081
1082                let changed_groups = group_changed_attrs(&attrs, self.opts.max_line_width, 0);
1083
1084                return self.tree.new_node(LayoutNode::Element {
1085                    tag,
1086                    field_name: None,
1087                    attrs,
1088                    changed_groups,
1089                    change,
1090                });
1091            }
1092        }
1093
1094        // Try to find the single nested diff
1095        let single_diff = {
1096            let mut found_diff: Option<&Diff<'mem, 'facet>> = None;
1097
1098            // Check first update group
1099            if let Some(update_group) = &interspersed.first {
1100                let group_interspersed = &update_group.0;
1101
1102                // Check for nested diffs in the first group
1103                if let Some(diffs) = &group_interspersed.last
1104                    && diffs.len() == 1
1105                    && found_diff.is_none()
1106                {
1107                    found_diff = Some(&diffs[0]);
1108                }
1109                for (diffs, _replace) in &group_interspersed.values {
1110                    if diffs.len() == 1 && found_diff.is_none() {
1111                        found_diff = Some(&diffs[0]);
1112                    }
1113                }
1114            }
1115
1116            found_diff
1117        };
1118
1119        // If we have a single nested diff, handle it with our variant tag
1120        if let Some(diff) = single_diff {
1121            match diff {
1122                Diff::User {
1123                    value:
1124                        Value::Struct {
1125                            updates,
1126                            deletions,
1127                            insertions,
1128                            unchanged,
1129                        },
1130                    ..
1131                } => {
1132                    // Build the struct with our variant tag
1133                    return self.build_struct(
1134                        tag.clone(),
1135                        None,
1136                        updates,
1137                        deletions,
1138                        insertions,
1139                        unchanged,
1140                        inner_from,
1141                        inner_to,
1142                        change,
1143                    );
1144                }
1145                Diff::Replace { from, to } => {
1146                    // For replacements, show both values as attributes with change markers
1147                    // This handles cases where the inner struct is fully different
1148                    let mut attrs = Vec::new();
1149
1150                    // Build attrs from the "from" struct (deleted)
1151                    if let Ok(struct_peek) = from.into_struct()
1152                        && let Type::User(UserType::Struct(ty)) = from.shape().ty
1153                    {
1154                        for (i, field) in ty.fields.iter().enumerate() {
1155                            if let Ok(field_value) = struct_peek.field(i) {
1156                                if should_skip_falsy(field_value) {
1157                                    continue;
1158                                }
1159                                let formatted = self.format_peek(field_value);
1160                                attrs.push(Attr::deleted(
1161                                    Cow::Borrowed(field.name),
1162                                    field.name.len(),
1163                                    formatted,
1164                                ));
1165                            }
1166                        }
1167                    }
1168
1169                    // Build attrs from the "to" struct (inserted)
1170                    if let Ok(struct_peek) = to.into_struct()
1171                        && let Type::User(UserType::Struct(ty)) = to.shape().ty
1172                    {
1173                        for (i, field) in ty.fields.iter().enumerate() {
1174                            if let Ok(field_value) = struct_peek.field(i) {
1175                                if should_skip_falsy(field_value) {
1176                                    continue;
1177                                }
1178                                let formatted = self.format_peek(field_value);
1179                                attrs.push(Attr::inserted(
1180                                    Cow::Borrowed(field.name),
1181                                    field.name.len(),
1182                                    formatted,
1183                                ));
1184                            }
1185                        }
1186                    }
1187
1188                    let changed_groups = group_changed_attrs(&attrs, self.opts.max_line_width, 0);
1189
1190                    return self.tree.new_node(LayoutNode::Element {
1191                        tag: tag.clone(),
1192                        field_name: None,
1193                        attrs,
1194                        changed_groups,
1195                        change,
1196                    });
1197                }
1198                _ => {}
1199            }
1200        }
1201
1202        // Fallback: create element with tag and build children normally
1203        let node = self.tree.new_node(LayoutNode::Element {
1204            tag,
1205            field_name: None,
1206            attrs: Vec::new(),
1207            changed_groups: Vec::new(),
1208            change,
1209        });
1210
1211        // Build children from updates
1212        self.build_updates_children(node, updates, "item");
1213
1214        node
1215    }
1216
1217    /// Build a sequence diff.
1218    fn build_sequence(
1219        &mut self,
1220        updates: &Updates<'_, '_>,
1221        change: ElementChange,
1222        item_type: &'static str,
1223    ) -> NodeId {
1224        // Create sequence node with item type info
1225        let node = self.tree.new_node(LayoutNode::Sequence {
1226            change,
1227            item_type,
1228            field_name: None,
1229        });
1230
1231        // Build children from updates
1232        self.build_updates_children(node, updates, item_type);
1233
1234        node
1235    }
1236
1237    /// Build children from an Updates structure and append to parent.
1238    ///
1239    /// This groups consecutive items by their change type (unchanged, deleted, inserted)
1240    /// and renders them on single lines with optional collapsing for long runs.
1241    /// Nested diffs (struct items with internal changes) are built as full child nodes.
1242    fn build_updates_children(
1243        &mut self,
1244        parent: NodeId,
1245        updates: &Updates<'_, '_>,
1246        _item_type: &'static str,
1247    ) {
1248        // Collect simple items (adds/removes) and nested diffs separately
1249        let mut items: Vec<(Peek<'_, '_>, ElementChange)> = Vec::new();
1250        let mut nested_diffs: Vec<&Diff<'_, '_>> = Vec::new();
1251
1252        let interspersed = &updates.0;
1253
1254        // Process first update group if present
1255        if let Some(update_group) = &interspersed.first {
1256            self.collect_updates_group_items(&mut items, &mut nested_diffs, update_group);
1257        }
1258
1259        // Process interleaved (unchanged, update) pairs
1260        for (unchanged_items, update_group) in &interspersed.values {
1261            // Add unchanged items
1262            for item in unchanged_items {
1263                items.push((*item, ElementChange::None));
1264            }
1265
1266            self.collect_updates_group_items(&mut items, &mut nested_diffs, update_group);
1267        }
1268
1269        // Process trailing unchanged items
1270        if let Some(unchanged_items) = &interspersed.last {
1271            for item in unchanged_items {
1272                items.push((*item, ElementChange::None));
1273            }
1274        }
1275
1276        tracing::debug!(
1277            items_count = items.len(),
1278            nested_diffs_count = nested_diffs.len(),
1279            "collected sequence items"
1280        );
1281
1282        // Build nested diffs as full child nodes (struct items with internal changes)
1283        for diff in nested_diffs {
1284            debug!(diff_type = ?std::mem::discriminant(diff), "building nested diff");
1285            // Get from/to Peek from the diff for context
1286            let (from_peek, to_peek) = match diff {
1287                Diff::User { .. } => {
1288                    // For User diffs, we need the actual Peek values
1289                    // The diff contains the shapes but we need to find the corresponding Peeks
1290                    // For now, pass None - the build_diff will use the shape info
1291                    (None, None)
1292                }
1293                Diff::Replace { from, to } => (Some(*from), Some(*to)),
1294                _ => (None, None),
1295            };
1296            let child = self.build_diff(diff, from_peek, to_peek, ElementChange::None);
1297            parent.append(child, &mut self.tree);
1298        }
1299
1300        // Render simple items (unchanged, adds, removes)
1301        for (item_peek, item_change) in items {
1302            let child = self.build_peek(item_peek, item_change);
1303            parent.append(child, &mut self.tree);
1304        }
1305    }
1306
1307    /// Collect items from an UpdatesGroup into the items list.
1308    /// Also returns nested diffs that need to be built as full child nodes.
1309    fn collect_updates_group_items<'a, 'mem: 'a, 'facet: 'a>(
1310        &self,
1311        items: &mut Vec<(Peek<'mem, 'facet>, ElementChange)>,
1312        nested_diffs: &mut Vec<&'a Diff<'mem, 'facet>>,
1313        group: &'a UpdatesGroup<'mem, 'facet>,
1314    ) {
1315        let interspersed = &group.0;
1316
1317        // Process first replace group if present
1318        if let Some(replace) = &interspersed.first {
1319            self.collect_replace_group_items(items, replace);
1320        }
1321
1322        // Process interleaved (diffs, replace) pairs
1323        for (diffs, replace) in &interspersed.values {
1324            // Collect nested diffs - these are struct items with internal changes
1325            for diff in diffs {
1326                nested_diffs.push(diff);
1327            }
1328            self.collect_replace_group_items(items, replace);
1329        }
1330
1331        // Process trailing diffs (if any)
1332        if let Some(diffs) = &interspersed.last {
1333            for diff in diffs {
1334                nested_diffs.push(diff);
1335            }
1336        }
1337    }
1338
1339    /// Collect items from a ReplaceGroup into the items list.
1340    fn collect_replace_group_items<'a, 'mem: 'a, 'facet: 'a>(
1341        &self,
1342        items: &mut Vec<(Peek<'mem, 'facet>, ElementChange)>,
1343        group: &'a ReplaceGroup<'mem, 'facet>,
1344    ) {
1345        // Add removals as deleted
1346        for removal in &group.removals {
1347            items.push((*removal, ElementChange::Deleted));
1348        }
1349
1350        // Add additions as inserted
1351        for addition in &group.additions {
1352            items.push((*addition, ElementChange::Inserted));
1353        }
1354    }
1355
1356    /// Format a Peek value into the arena using the flavor.
1357    fn format_peek(&mut self, peek: Peek<'_, '_>) -> FormattedValue {
1358        let shape = peek.shape();
1359        debug!(
1360            type_id = %shape.type_identifier,
1361            def = ?shape.def,
1362            "format_peek"
1363        );
1364
1365        // Unwrap Option types to format the inner value
1366        if let Def::Option(_) = shape.def
1367            && let Ok(opt) = peek.into_option()
1368        {
1369            if let Some(inner) = opt.value() {
1370                return self.format_peek(inner);
1371            }
1372            // None - format as null
1373            let (span, width) = self.strings.push_str("null");
1374            return FormattedValue::with_type(span, width, ValueType::Null);
1375        }
1376
1377        // Handle float formatting with precision if configured
1378        if let Some(precision) = self.opts.float_precision
1379            && let Type::Primitive(PrimitiveType::Numeric(NumericType::Float)) = shape.ty
1380        {
1381            // Try f64 first, then f32
1382            if let Ok(v) = peek.get::<f64>() {
1383                let formatted = format!("{:.prec$}", v, prec = precision);
1384                // Trim trailing zeros and decimal point for cleaner output
1385                let formatted = formatted.trim_end_matches('0').trim_end_matches('.');
1386                let (span, width) = self.strings.push_str(formatted);
1387                return FormattedValue::with_type(span, width, ValueType::Number);
1388            }
1389            if let Ok(v) = peek.get::<f32>() {
1390                let formatted = format!("{:.prec$}", v, prec = precision);
1391                let formatted = formatted.trim_end_matches('0').trim_end_matches('.');
1392                let (span, width) = self.strings.push_str(formatted);
1393                return FormattedValue::with_type(span, width, ValueType::Number);
1394            }
1395        }
1396
1397        let (span, width) = self.strings.format(|w| self.flavor.format_value(peek, w));
1398        let value_type = determine_value_type(peek);
1399        FormattedValue::with_type(span, width, value_type)
1400    }
1401
1402    /// Finish building and return the Layout.
1403    fn finish(self, root: NodeId) -> Layout {
1404        Layout {
1405            strings: self.strings,
1406            tree: self.tree,
1407            root,
1408        }
1409    }
1410}
1411
1412#[cfg(test)]
1413mod tests {
1414    use super::*;
1415    use crate::layout::render::{RenderOptions, render_to_string};
1416    use crate::layout::{RustFlavor, XmlFlavor};
1417
1418    #[test]
1419    fn test_build_equal_diff() {
1420        let value = 42i32;
1421        let peek = Peek::new(&value);
1422        let diff = Diff::Equal { value: Some(peek) };
1423
1424        let layout = build_layout(&diff, peek, peek, &BuildOptions::default(), &RustFlavor);
1425
1426        // Should produce a single text node
1427        let root = layout.get(layout.root).unwrap();
1428        assert!(matches!(root, LayoutNode::Text { .. }));
1429    }
1430
1431    #[test]
1432    fn test_build_replace_diff() {
1433        let from = 10i32;
1434        let to = 20i32;
1435        let diff = Diff::Replace {
1436            from: Peek::new(&from),
1437            to: Peek::new(&to),
1438        };
1439
1440        let layout = build_layout(
1441            &diff,
1442            Peek::new(&from),
1443            Peek::new(&to),
1444            &BuildOptions::default(),
1445            &RustFlavor,
1446        );
1447
1448        // Should produce an element with two children
1449        let root = layout.get(layout.root).unwrap();
1450        match root {
1451            LayoutNode::Element { tag, .. } => assert_eq!(tag.as_ref(), "_replace"),
1452            _ => panic!("expected Element node"),
1453        }
1454
1455        let children: Vec<_> = layout.children(layout.root).collect();
1456        assert_eq!(children.len(), 2);
1457    }
1458
1459    #[test]
1460    fn test_build_and_render_replace() {
1461        let from = 10i32;
1462        let to = 20i32;
1463        let diff = Diff::Replace {
1464            from: Peek::new(&from),
1465            to: Peek::new(&to),
1466        };
1467
1468        let layout = build_layout(
1469            &diff,
1470            Peek::new(&from),
1471            Peek::new(&to),
1472            &BuildOptions::default(),
1473            &RustFlavor,
1474        );
1475        let output = render_to_string(&layout, &RenderOptions::plain(), &XmlFlavor);
1476
1477        // Should contain both values with appropriate markers
1478        assert!(
1479            output.contains("10"),
1480            "output should contain old value: {}",
1481            output
1482        );
1483        assert!(
1484            output.contains("20"),
1485            "output should contain new value: {}",
1486            output
1487        );
1488    }
1489
1490    #[test]
1491    fn test_build_options_default() {
1492        let opts = BuildOptions::default();
1493        assert_eq!(opts.max_line_width, 80);
1494        assert_eq!(opts.max_unchanged_fields, 5);
1495        assert_eq!(opts.collapse_threshold, 3);
1496    }
1497}