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;
15use tracing::debug;
16
17use facet_core::{Def, NumericType, PrimitiveType, Shape, StructKind, TextualType, Type, UserType};
18use facet_reflect::Peek;
19use indextree::{Arena, NodeId};
20
21use super::{
22    Attr, DiffFlavor, ElementChange, FormatArena, FormattedValue, Layout, LayoutNode, ValueType,
23    group_changed_attrs,
24};
25use crate::{Diff, ReplaceGroup, Updates, UpdatesGroup, Value};
26
27/// Get the display name for a shape, respecting the `rename` attribute.
28fn get_shape_display_name(shape: &Shape) -> &'static str {
29    if let Some(renamed) = shape.get_builtin_attr_value::<&str>("rename") {
30        return renamed;
31    }
32    shape.type_identifier
33}
34
35/// Check if a shape has any XML namespace attributes (ns_all, rename in xml namespace, etc.)
36/// Shapes without XML attributes are "proxy types" - Rust implementation details
37/// that wouldn't exist in actual XML output.
38fn shape_has_xml_attrs(shape: &Shape) -> bool {
39    shape.attributes.iter().any(|attr| attr.ns == Some("xml"))
40}
41
42/// Get display name for XML output, prefixing proxy types with `@`.
43/// Proxy types are structs without XML namespace attributes - they're Rust
44/// implementation details (like PathData) that represent something that would
45/// be different in actual XML (like a string attribute).
46fn get_xml_display_name(shape: &Shape) -> &'static str {
47    let base_name = get_shape_display_name(shape);
48
49    // Check if this is a struct without XML attributes (a proxy type)
50    if let Type::User(UserType::Struct(_)) = shape.ty
51        && !shape_has_xml_attrs(shape)
52    {
53        // Leak the formatted string - acceptable for diff tags which are few and short-lived
54        return Box::leak(format!("@{}", base_name).into_boxed_str());
55    }
56
57    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                            get_variant_display_name(v)
285                        } else {
286                            variant_name
287                        };
288                    debug!(
289                        tag,
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, 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, "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 = get_variant_display_name(variant);
439                    let fields = &variant.data.fields;
440                    debug!(
441                        variant_name = tag,
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,
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,
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);
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: &'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: &'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: "_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: &'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, None, updates, deletions, insertions, unchanged, inner_from, inner_to,
1135                        change,
1136                    );
1137                }
1138                Diff::Replace { from, to } => {
1139                    // For replacements, show both values as attributes with change markers
1140                    // This handles cases where the inner struct is fully different
1141                    let mut attrs = Vec::new();
1142
1143                    // Build attrs from the "from" struct (deleted)
1144                    if let Ok(struct_peek) = from.into_struct()
1145                        && let Type::User(UserType::Struct(ty)) = from.shape().ty
1146                    {
1147                        for (i, field) in ty.fields.iter().enumerate() {
1148                            if let Ok(field_value) = struct_peek.field(i) {
1149                                if should_skip_falsy(field_value) {
1150                                    continue;
1151                                }
1152                                let formatted = self.format_peek(field_value);
1153                                attrs.push(Attr::deleted(
1154                                    Cow::Borrowed(field.name),
1155                                    field.name.len(),
1156                                    formatted,
1157                                ));
1158                            }
1159                        }
1160                    }
1161
1162                    // Build attrs from the "to" struct (inserted)
1163                    if let Ok(struct_peek) = to.into_struct()
1164                        && let Type::User(UserType::Struct(ty)) = to.shape().ty
1165                    {
1166                        for (i, field) in ty.fields.iter().enumerate() {
1167                            if let Ok(field_value) = struct_peek.field(i) {
1168                                if should_skip_falsy(field_value) {
1169                                    continue;
1170                                }
1171                                let formatted = self.format_peek(field_value);
1172                                attrs.push(Attr::inserted(
1173                                    Cow::Borrowed(field.name),
1174                                    field.name.len(),
1175                                    formatted,
1176                                ));
1177                            }
1178                        }
1179                    }
1180
1181                    let changed_groups = group_changed_attrs(&attrs, self.opts.max_line_width, 0);
1182
1183                    return self.tree.new_node(LayoutNode::Element {
1184                        tag,
1185                        field_name: None,
1186                        attrs,
1187                        changed_groups,
1188                        change,
1189                    });
1190                }
1191                _ => {}
1192            }
1193        }
1194
1195        // Fallback: create element with tag and build children normally
1196        let node = self.tree.new_node(LayoutNode::Element {
1197            tag,
1198            field_name: None,
1199            attrs: Vec::new(),
1200            changed_groups: Vec::new(),
1201            change,
1202        });
1203
1204        // Build children from updates
1205        self.build_updates_children(node, updates, "item");
1206
1207        node
1208    }
1209
1210    /// Build a sequence diff.
1211    fn build_sequence(
1212        &mut self,
1213        updates: &Updates<'_, '_>,
1214        change: ElementChange,
1215        item_type: &'static str,
1216    ) -> NodeId {
1217        // Create sequence node with item type info
1218        let node = self.tree.new_node(LayoutNode::Sequence {
1219            change,
1220            item_type,
1221            field_name: None,
1222        });
1223
1224        // Build children from updates
1225        self.build_updates_children(node, updates, item_type);
1226
1227        node
1228    }
1229
1230    /// Build children from an Updates structure and append to parent.
1231    ///
1232    /// This groups consecutive items by their change type (unchanged, deleted, inserted)
1233    /// and renders them on single lines with optional collapsing for long runs.
1234    /// Nested diffs (struct items with internal changes) are built as full child nodes.
1235    fn build_updates_children(
1236        &mut self,
1237        parent: NodeId,
1238        updates: &Updates<'_, '_>,
1239        _item_type: &'static str,
1240    ) {
1241        // Collect simple items (adds/removes) and nested diffs separately
1242        let mut items: Vec<(Peek<'_, '_>, ElementChange)> = Vec::new();
1243        let mut nested_diffs: Vec<&Diff<'_, '_>> = Vec::new();
1244
1245        let interspersed = &updates.0;
1246
1247        // Process first update group if present
1248        if let Some(update_group) = &interspersed.first {
1249            self.collect_updates_group_items(&mut items, &mut nested_diffs, update_group);
1250        }
1251
1252        // Process interleaved (unchanged, update) pairs
1253        for (unchanged_items, update_group) in &interspersed.values {
1254            // Add unchanged items
1255            for item in unchanged_items {
1256                items.push((*item, ElementChange::None));
1257            }
1258
1259            self.collect_updates_group_items(&mut items, &mut nested_diffs, update_group);
1260        }
1261
1262        // Process trailing unchanged items
1263        if let Some(unchanged_items) = &interspersed.last {
1264            for item in unchanged_items {
1265                items.push((*item, ElementChange::None));
1266            }
1267        }
1268
1269        tracing::debug!(
1270            items_count = items.len(),
1271            nested_diffs_count = nested_diffs.len(),
1272            "collected sequence items"
1273        );
1274
1275        // Build nested diffs as full child nodes (struct items with internal changes)
1276        for diff in nested_diffs {
1277            debug!(diff_type = ?std::mem::discriminant(diff), "building nested diff");
1278            // Get from/to Peek from the diff for context
1279            let (from_peek, to_peek) = match diff {
1280                Diff::User { .. } => {
1281                    // For User diffs, we need the actual Peek values
1282                    // The diff contains the shapes but we need to find the corresponding Peeks
1283                    // For now, pass None - the build_diff will use the shape info
1284                    (None, None)
1285                }
1286                Diff::Replace { from, to } => (Some(*from), Some(*to)),
1287                _ => (None, None),
1288            };
1289            let child = self.build_diff(diff, from_peek, to_peek, ElementChange::None);
1290            parent.append(child, &mut self.tree);
1291        }
1292
1293        // Render simple items (unchanged, adds, removes)
1294        for (item_peek, item_change) in items {
1295            let child = self.build_peek(item_peek, item_change);
1296            parent.append(child, &mut self.tree);
1297        }
1298    }
1299
1300    /// Collect items from an UpdatesGroup into the items list.
1301    /// Also returns nested diffs that need to be built as full child nodes.
1302    fn collect_updates_group_items<'a, 'mem: 'a, 'facet: 'a>(
1303        &self,
1304        items: &mut Vec<(Peek<'mem, 'facet>, ElementChange)>,
1305        nested_diffs: &mut Vec<&'a Diff<'mem, 'facet>>,
1306        group: &'a UpdatesGroup<'mem, 'facet>,
1307    ) {
1308        let interspersed = &group.0;
1309
1310        // Process first replace group if present
1311        if let Some(replace) = &interspersed.first {
1312            self.collect_replace_group_items(items, replace);
1313        }
1314
1315        // Process interleaved (diffs, replace) pairs
1316        for (diffs, replace) in &interspersed.values {
1317            // Collect nested diffs - these are struct items with internal changes
1318            for diff in diffs {
1319                nested_diffs.push(diff);
1320            }
1321            self.collect_replace_group_items(items, replace);
1322        }
1323
1324        // Process trailing diffs (if any)
1325        if let Some(diffs) = &interspersed.last {
1326            for diff in diffs {
1327                nested_diffs.push(diff);
1328            }
1329        }
1330    }
1331
1332    /// Collect items from a ReplaceGroup into the items list.
1333    fn collect_replace_group_items<'a, 'mem: 'a, 'facet: 'a>(
1334        &self,
1335        items: &mut Vec<(Peek<'mem, 'facet>, ElementChange)>,
1336        group: &'a ReplaceGroup<'mem, 'facet>,
1337    ) {
1338        // Add removals as deleted
1339        for removal in &group.removals {
1340            items.push((*removal, ElementChange::Deleted));
1341        }
1342
1343        // Add additions as inserted
1344        for addition in &group.additions {
1345            items.push((*addition, ElementChange::Inserted));
1346        }
1347    }
1348
1349    /// Format a Peek value into the arena using the flavor.
1350    fn format_peek(&mut self, peek: Peek<'_, '_>) -> FormattedValue {
1351        let shape = peek.shape();
1352        debug!(
1353            type_id = %shape.type_identifier,
1354            def = ?shape.def,
1355            "format_peek"
1356        );
1357
1358        // Unwrap Option types to format the inner value
1359        if let Def::Option(_) = shape.def
1360            && let Ok(opt) = peek.into_option()
1361        {
1362            if let Some(inner) = opt.value() {
1363                return self.format_peek(inner);
1364            }
1365            // None - format as null
1366            let (span, width) = self.strings.push_str("null");
1367            return FormattedValue::with_type(span, width, ValueType::Null);
1368        }
1369
1370        // Handle float formatting with precision if configured
1371        if let Some(precision) = self.opts.float_precision
1372            && let Type::Primitive(PrimitiveType::Numeric(NumericType::Float)) = shape.ty
1373        {
1374            // Try f64 first, then f32
1375            if let Ok(v) = peek.get::<f64>() {
1376                let formatted = format!("{:.prec$}", v, prec = precision);
1377                // Trim trailing zeros and decimal point for cleaner output
1378                let formatted = formatted.trim_end_matches('0').trim_end_matches('.');
1379                let (span, width) = self.strings.push_str(formatted);
1380                return FormattedValue::with_type(span, width, ValueType::Number);
1381            }
1382            if let Ok(v) = peek.get::<f32>() {
1383                let formatted = format!("{:.prec$}", v, prec = precision);
1384                let formatted = formatted.trim_end_matches('0').trim_end_matches('.');
1385                let (span, width) = self.strings.push_str(formatted);
1386                return FormattedValue::with_type(span, width, ValueType::Number);
1387            }
1388        }
1389
1390        let (span, width) = self.strings.format(|w| self.flavor.format_value(peek, w));
1391        let value_type = determine_value_type(peek);
1392        FormattedValue::with_type(span, width, value_type)
1393    }
1394
1395    /// Finish building and return the Layout.
1396    fn finish(self, root: NodeId) -> Layout {
1397        Layout {
1398            strings: self.strings,
1399            tree: self.tree,
1400            root,
1401        }
1402    }
1403}
1404
1405#[cfg(test)]
1406mod tests {
1407    use super::*;
1408    use crate::layout::render::{RenderOptions, render_to_string};
1409    use crate::layout::{RustFlavor, XmlFlavor};
1410
1411    #[test]
1412    fn test_build_equal_diff() {
1413        let value = 42i32;
1414        let peek = Peek::new(&value);
1415        let diff = Diff::Equal { value: Some(peek) };
1416
1417        let layout = build_layout(&diff, peek, peek, &BuildOptions::default(), &RustFlavor);
1418
1419        // Should produce a single text node
1420        let root = layout.get(layout.root).unwrap();
1421        assert!(matches!(root, LayoutNode::Text { .. }));
1422    }
1423
1424    #[test]
1425    fn test_build_replace_diff() {
1426        let from = 10i32;
1427        let to = 20i32;
1428        let diff = Diff::Replace {
1429            from: Peek::new(&from),
1430            to: Peek::new(&to),
1431        };
1432
1433        let layout = build_layout(
1434            &diff,
1435            Peek::new(&from),
1436            Peek::new(&to),
1437            &BuildOptions::default(),
1438            &RustFlavor,
1439        );
1440
1441        // Should produce an element with two children
1442        let root = layout.get(layout.root).unwrap();
1443        assert!(matches!(
1444            root,
1445            LayoutNode::Element {
1446                tag: "_replace",
1447                ..
1448            }
1449        ));
1450
1451        let children: Vec<_> = layout.children(layout.root).collect();
1452        assert_eq!(children.len(), 2);
1453    }
1454
1455    #[test]
1456    fn test_build_and_render_replace() {
1457        let from = 10i32;
1458        let to = 20i32;
1459        let diff = Diff::Replace {
1460            from: Peek::new(&from),
1461            to: Peek::new(&to),
1462        };
1463
1464        let layout = build_layout(
1465            &diff,
1466            Peek::new(&from),
1467            Peek::new(&to),
1468            &BuildOptions::default(),
1469            &RustFlavor,
1470        );
1471        let output = render_to_string(&layout, &RenderOptions::plain(), &XmlFlavor);
1472
1473        // Should contain both values with appropriate markers
1474        assert!(
1475            output.contains("10"),
1476            "output should contain old value: {}",
1477            output
1478        );
1479        assert!(
1480            output.contains("20"),
1481            "output should contain new value: {}",
1482            output
1483        );
1484    }
1485
1486    #[test]
1487    fn test_build_options_default() {
1488        let opts = BuildOptions::default();
1489        assert_eq!(opts.max_line_width, 80);
1490        assert_eq!(opts.max_unchanged_fields, 5);
1491        assert_eq!(opts.collapse_threshold, 3);
1492    }
1493}