facet_xml_diff/
lib.rs

1//! Diff-aware XML serialization.
2//!
3//! This module provides XML rendering of diffs with `-`/`+` prefixes,
4//! value-only coloring, and proper alignment.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use facet_diff::FacetDiff;
10//!
11//! let old = Rect { fill: "red".into(), x: 10, .. };
12//! let new = Rect { fill: "blue".into(), x: 10, .. };
13//! let diff = old.diff(&new);
14//!
15//! // Render as diff-aware XML
16//! let xml = facet_xml::diff_to_string(&old, &new, &diff)?;
17//! // Output:
18//! // <rect
19//! // ← fill="red"
20//! // → fill="blue"
21//! //   x="10" y="10" width="50" height="50"
22//! // />
23//! ```
24
25use std::fmt::Write;
26
27use facet_diff_core::{
28    AnsiBackend, BuildOptions, Diff, PlainBackend, RenderOptions, XmlFlavor, build_layout, render,
29    render_to_string,
30};
31pub use facet_diff_core::{DiffSymbols, DiffTheme};
32use facet_reflect::Peek;
33
34/// Options for diff-aware XML serialization.
35#[derive(Debug, Clone)]
36pub struct DiffSerializeOptions {
37    /// Symbols to use for diff markers.
38    pub symbols: DiffSymbols,
39
40    /// Color theme for diff rendering.
41    pub theme: DiffTheme,
42
43    /// Whether to emit ANSI color codes.
44    pub colors: bool,
45
46    /// Indentation string (default: 2 spaces).
47    pub indent: &'static str,
48
49    /// Maximum line width for attribute grouping.
50    pub max_line_width: usize,
51
52    /// Maximum number of unchanged fields to show inline.
53    pub max_unchanged_fields: usize,
54
55    /// Collapse runs of unchanged siblings longer than this.
56    pub collapse_threshold: usize,
57}
58
59impl Default for DiffSerializeOptions {
60    fn default() -> Self {
61        Self {
62            symbols: DiffSymbols::default(),
63            theme: DiffTheme::default(),
64            colors: true,
65            indent: "  ",
66            max_line_width: 80,
67            max_unchanged_fields: 5,
68            collapse_threshold: 3,
69        }
70    }
71}
72
73impl DiffSerializeOptions {
74    /// Create new default options (with ANSI colors).
75    pub fn new() -> Self {
76        Self::default()
77    }
78
79    /// Disable ANSI color output.
80    pub const fn no_colors(mut self) -> Self {
81        self.colors = false;
82        self
83    }
84
85    /// Set custom indentation string.
86    pub const fn indent(mut self, indent: &'static str) -> Self {
87        self.indent = indent;
88        self
89    }
90
91    /// Set maximum line width for attribute grouping.
92    pub const fn max_line_width(mut self, width: usize) -> Self {
93        self.max_line_width = width;
94        self
95    }
96
97    /// Set maximum number of unchanged fields to show inline.
98    pub const fn max_unchanged_fields(mut self, count: usize) -> Self {
99        self.max_unchanged_fields = count;
100        self
101    }
102
103    /// Set collapse threshold for unchanged runs.
104    pub const fn collapse_threshold(mut self, threshold: usize) -> Self {
105        self.collapse_threshold = threshold;
106        self
107    }
108}
109
110/// Render a diff as XML to a String with ANSI colors.
111///
112/// # Arguments
113///
114/// * `from` - The original value
115/// * `to` - The new value  
116/// * `diff` - The diff between `from` and `to`
117///
118/// # Example
119///
120/// ```ignore
121/// use facet_diff::FacetDiff;
122///
123/// let old = Point { x: 10, y: 20 };
124/// let new = Point { x: 15, y: 20 };
125/// let diff = old.diff(&new);
126///
127/// let xml = facet_xml::diff_to_string(&old, &new, &diff);
128/// println!("{}", xml);
129/// ```
130pub fn diff_to_string<'mem, 'facet>(
131    from: &'mem impl facet_core::Facet<'facet>,
132    to: &'mem impl facet_core::Facet<'facet>,
133    diff: &Diff<'mem, 'facet>,
134) -> String {
135    diff_to_string_with_options(from, to, diff, &DiffSerializeOptions::default())
136}
137
138/// Render a diff as XML to a String with custom options.
139///
140/// # Arguments
141///
142/// * `from` - The original value
143/// * `to` - The new value
144/// * `diff` - The diff between `from` and `to`
145/// * `options` - Serialization options
146pub fn diff_to_string_with_options<'mem, 'facet>(
147    from: &'mem impl facet_core::Facet<'facet>,
148    to: &'mem impl facet_core::Facet<'facet>,
149    diff: &Diff<'mem, 'facet>,
150    options: &DiffSerializeOptions,
151) -> String {
152    let from_peek = Peek::new(from);
153    let to_peek = Peek::new(to);
154
155    let build_opts = BuildOptions {
156        max_line_width: options.max_line_width,
157        max_unchanged_fields: options.max_unchanged_fields,
158        collapse_threshold: options.collapse_threshold,
159        float_precision: None,
160    };
161
162    let flavor = XmlFlavor;
163    let layout = build_layout(diff, from_peek, to_peek, &build_opts, &flavor);
164
165    if options.colors {
166        let render_opts = RenderOptions {
167            indent: options.indent,
168            symbols: options.symbols.clone(),
169            backend: AnsiBackend::new(options.theme.clone()),
170        };
171        render_to_string(&layout, &render_opts, &flavor)
172    } else {
173        let render_opts = RenderOptions {
174            indent: options.indent,
175            symbols: options.symbols.clone(),
176            backend: PlainBackend,
177        };
178        render_to_string(&layout, &render_opts, &flavor)
179    }
180}
181
182/// Render a diff as XML to a writer.
183///
184/// # Arguments
185///
186/// * `from` - The original value
187/// * `to` - The new value
188/// * `diff` - The diff between `from` and `to`
189/// * `writer` - The output writer
190pub fn diff_to_writer<'mem, 'facet, W: Write>(
191    from: &'mem impl facet_core::Facet<'facet>,
192    to: &'mem impl facet_core::Facet<'facet>,
193    diff: &Diff<'mem, 'facet>,
194    writer: &mut W,
195) -> std::fmt::Result {
196    diff_to_writer_with_options(from, to, diff, writer, &DiffSerializeOptions::default())
197}
198
199/// Render a diff as XML to a writer with custom options.
200///
201/// # Arguments
202///
203/// * `from` - The original value
204/// * `to` - The new value
205/// * `diff` - The diff between `from` and `to`
206/// * `writer` - The output writer
207/// * `options` - Serialization options
208pub fn diff_to_writer_with_options<'mem, 'facet, W: Write>(
209    from: &'mem impl facet_core::Facet<'facet>,
210    to: &'mem impl facet_core::Facet<'facet>,
211    diff: &Diff<'mem, 'facet>,
212    writer: &mut W,
213    options: &DiffSerializeOptions,
214) -> std::fmt::Result {
215    let from_peek = Peek::new(from);
216    let to_peek = Peek::new(to);
217
218    let build_opts = BuildOptions {
219        max_line_width: options.max_line_width,
220        max_unchanged_fields: options.max_unchanged_fields,
221        collapse_threshold: options.collapse_threshold,
222        float_precision: None,
223    };
224
225    let flavor = XmlFlavor;
226    let layout = build_layout(diff, from_peek, to_peek, &build_opts, &flavor);
227
228    if options.colors {
229        let render_opts = RenderOptions {
230            indent: options.indent,
231            symbols: options.symbols.clone(),
232            backend: AnsiBackend::new(options.theme.clone()),
233        };
234        render(&layout, writer, &render_opts, &flavor)
235    } else {
236        let render_opts = RenderOptions {
237            indent: options.indent,
238            symbols: options.symbols.clone(),
239            backend: PlainBackend,
240        };
241        render(&layout, writer, &render_opts, &flavor)
242    }
243}