Skip to main content

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