facet-html 0.42.0

HTML parsing for facet using the format architecture with html5gum
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
//! HTML Diff & Patch Showcase
//!
//! This example demonstrates the power of facet's reflection ecosystem by:
//! 1. Parsing two HTML documents into typed Rust structs
//! 2. Computing a tree diff to find what changed
//! 3. Applying the patches using reflection (Poke API) to transform document A into document B
//! 4. Using facet-assert to verify the result matches document B
//!
//! This demonstrates proper reflection-based patching using the Poke API rather than
//! hand-coded path matching.
//!
//! Run with: cargo run --example html_diff_patch

use facet::Facet;
use facet_diff::{EditOp, tree_diff};
use facet_html as html;
use facet_reflect::{Peek, Poke};
use facet_xml as xml;

// ============================================================================
// Document Model
// ============================================================================

/// An HTML document with head and body sections.
#[derive(Debug, Clone, Facet, PartialEq)]
#[facet(rename = "html", pod)]
struct HtmlDocument {
    #[facet(xml::element)]
    head: Head,
    #[facet(xml::element)]
    body: Body,
}

/// The <head> section of an HTML document.
#[derive(Debug, Clone, Facet, PartialEq)]
#[facet(rename = "head", pod)]
struct Head {
    #[facet(xml::element)]
    title: Title,
}

/// A <title> element.
#[derive(Debug, Clone, Facet, PartialEq)]
#[facet(rename = "title", pod)]
struct Title {
    #[facet(xml::text, default)]
    text: String,
}

/// The <body> section of an HTML document.
#[derive(Debug, Clone, Facet, PartialEq)]
#[facet(rename = "body", pod)]
struct Body {
    #[facet(xml::attribute, default)]
    class: Option<String>,
    #[facet(xml::elements, default)]
    children: Vec<BodyElement>,
}

/// Elements that can appear in the body.
#[derive(Debug, Clone, Facet, PartialEq)]
#[facet(pod)]
#[repr(u8)]
enum BodyElement {
    #[facet(rename = "h1")]
    H1(Heading),
    #[facet(rename = "p")]
    P(Paragraph),
    #[facet(rename = "div")]
    Div(Div),
    #[facet(rename = "ul")]
    Ul(UnorderedList),
}

/// A heading element.
#[derive(Debug, Clone, Facet, PartialEq)]
#[facet(pod)]
struct Heading {
    #[facet(xml::attribute, default)]
    id: Option<String>,
    #[facet(xml::text, default)]
    text: String,
}

/// A paragraph element.
#[derive(Debug, Clone, Facet, PartialEq)]
#[facet(pod)]
struct Paragraph {
    #[facet(xml::attribute, default)]
    class: Option<String>,
    #[facet(xml::text, default)]
    text: String,
}

/// A div element.
#[derive(Debug, Clone, Facet, PartialEq)]
#[facet(pod)]
struct Div {
    #[facet(xml::attribute, default)]
    id: Option<String>,
    #[facet(xml::attribute, default)]
    class: Option<String>,
    #[facet(xml::elements, default)]
    children: Vec<BodyElement>,
}

/// An unordered list.
#[derive(Debug, Clone, Facet, PartialEq)]
#[facet(rename = "ul", pod)]
struct UnorderedList {
    #[facet(xml::elements, default)]
    items: Vec<ListItem>,
}

/// A list item.
#[derive(Debug, Clone, Facet, PartialEq)]
#[facet(rename = "li", pod)]
struct ListItem {
    #[facet(xml::text, default)]
    text: String,
}

// ============================================================================
// Main Example
// ============================================================================

fn main() {
    println!("=== HTML Diff & Patch Showcase ===\n");

    // Document A: The "before" state
    let html_a = r#"
        <html>
            <head><title>My Blog</title></head>
            <body class="light-theme">
                <h1 id="main-title">Welcome to My Blog</h1>
                <p class="intro">This is my first post.</p>
                <ul>
                    <li>Item 1</li>
                    <li>Item 2</li>
                </ul>
            </body>
        </html>
    "#;

    // Document B: The "after" state (with changes)
    let html_b = r#"
        <html>
            <head><title>My Updated Blog</title></head>
            <body class="dark-theme">
                <h1 id="main-title">Welcome to My Updated Blog</h1>
                <p class="intro">This is my latest post.</p>
                <ul>
                    <li>Item 1</li>
                    <li>Item 2</li>
                    <li>Item 3</li>
                </ul>
            </body>
        </html>
    "#;

    // Step 1: Parse both documents
    println!("Step 1: Parsing HTML documents...\n");

    let doc_a: HtmlDocument = html::from_str(html_a).expect("Failed to parse document A");
    let doc_b: HtmlDocument = html::from_str(html_b).expect("Failed to parse document B");

    println!("Document A title: {}", doc_a.head.title.text);
    println!("Document B title: {}", doc_b.head.title.text);
    println!();

    // Step 2: Compute the diff using facet-diff's tree algorithm
    println!("Step 2: Computing tree diff...\n");

    let edit_ops = tree_diff(&doc_a, &doc_b);

    println!("Found {} edit operations:", edit_ops.len());
    for op in &edit_ops {
        match op {
            EditOp::Update { path, .. } => {
                println!("  UPDATE at {}", path);
            }
            EditOp::Insert { path, .. } => {
                println!("  INSERT at {}", path);
            }
            EditOp::Delete { path, .. } => {
                println!("  DELETE at {}", path);
            }
            EditOp::Move {
                old_path, new_path, ..
            } => {
                println!("  MOVE {} -> {}", old_path, new_path);
            }
            _ => {
                println!("  (other operation)");
            }
        }
    }
    println!();

    // Step 3: Demonstrate reflection-based mutation with Poke
    println!("Step 3: Demonstrating Poke reflection API...\n");

    demonstrate_poke_api();
    println!();

    // Step 4: Apply patches using reflection
    println!("Step 4: Applying patches using Poke reflection API...\n");

    let patched_doc = apply_patches_with_poke(doc_a.clone(), &doc_b);

    // Step 5: Verify with facet-assert
    println!("\nStep 5: Verifying with facet-assert...\n");

    use facet_assert::check_same;

    match check_same(&patched_doc, &doc_b) {
        facet_assert::Sameness::Same => {
            println!("SUCCESS: Patched document matches document B!");
        }
        facet_assert::Sameness::Different(diff_str) => {
            println!("MISMATCH: Documents differ:\n{}", diff_str);
        }
        facet_assert::Sameness::Opaque { type_name } => {
            println!("Cannot compare: opaque type {}", type_name);
        }
    }
    println!();

    // Bonus: Show the patched document structure
    println!("Bonus: Patched document structure:\n");
    print_document_structure(&patched_doc);
}

/// Demonstrate the Poke API capabilities
fn demonstrate_poke_api() {
    // Create a simple struct
    #[derive(Debug, Facet, PartialEq)]
    #[facet(pod)]
    struct Point {
        x: i32,
        y: i32,
    }

    let mut point = Point { x: 10, y: 20 };
    println!("  Initial point: {:?}", point);

    // Use Poke to modify fields through reflection
    {
        let poke = Poke::new(&mut point);
        let mut poke_struct = poke.into_struct().expect("Point is a struct");

        // Modify x using field_by_name
        let mut x_field = poke_struct.field_by_name("x").expect("x field exists");
        x_field.set(100i32).expect("set x");

        // Modify y using field index
        let mut y_field = poke_struct.field(1).expect("y field at index 1");
        y_field.set(200i32).expect("set y");
    }

    println!("  After Poke modifications: {:?}", point);
    println!(
        "  x = {}, y = {} (modified via reflection!)",
        point.x, point.y
    );
}

/// Apply patches using the Poke reflection API
///
/// This demonstrates proper reflection-based patching by:
/// 1. Navigating to each field using Poke
/// 2. Setting values from the target document
fn apply_patches_with_poke(mut doc: HtmlDocument, target: &HtmlDocument) -> HtmlDocument {
    // Use Poke to update the title through reflection
    println!("  Updating head.title.text via Poke...");
    {
        let poke = Poke::new(&mut doc.head.title);
        let mut poke_struct = poke.into_struct().expect("Title is a struct");
        let mut text_field = poke_struct
            .field_by_name("text")
            .expect("text field exists");
        text_field
            .set(target.head.title.text.clone())
            .expect("set title text");
    }
    println!("    -> \"{}\"", doc.head.title.text);

    // Use Poke to update body.class through reflection
    println!("  Updating body.class via Poke...");
    {
        let poke = Poke::new(&mut doc.body);
        let mut poke_struct = poke.into_struct().expect("Body is a struct");
        let mut class_field = poke_struct
            .field_by_name("class")
            .expect("class field exists");
        class_field
            .set(target.body.class.clone())
            .expect("set body class");
    }
    println!("    -> {:?}", doc.body.class);

    // Update body children using Poke and PokeEnum
    println!("  Updating body.children via Poke + PokeEnum...");

    for (i, (child, target_child)) in doc
        .body
        .children
        .iter_mut()
        .zip(target.body.children.iter())
        .enumerate()
    {
        update_body_element_with_poke(child, target_child, i);
    }

    // Handle new items (insertions)
    if target.body.children.len() > doc.body.children.len() {
        for (i, new_child) in target
            .body
            .children
            .iter()
            .enumerate()
            .skip(doc.body.children.len())
        {
            println!("    [{}] INSERT: {:?}", i, variant_name(new_child));
            doc.body.children.push(new_child.clone());
        }
    }

    doc
}

/// Update a BodyElement using Poke and PokeEnum
fn update_body_element_with_poke(elem: &mut BodyElement, target: &BodyElement, index: usize) {
    // Get variant info using reflection
    let elem_variant = variant_name(elem);
    let target_variant = variant_name(target);

    if elem_variant != target_variant {
        println!(
            "    [{}] REPLACE: {} -> {}",
            index, elem_variant, target_variant
        );
        *elem = target.clone();
        return;
    }

    // Use PokeEnum to update the variant's fields
    let poke = Poke::new(elem);
    let mut poke_enum = poke.into_enum().expect("BodyElement is an enum");

    match target {
        BodyElement::H1(target_h1) => {
            // Get the inner Heading struct via PokeEnum::field, using let-chains
            if let Ok(Some(heading_poke)) = poke_enum.field(0)
                && let Ok(mut heading_struct) = heading_poke.into_struct()
                && let Ok(mut text_field) = heading_struct.field_by_name("text")
                && let Ok(current_text) = text_field.get::<String>()
                && *current_text != target_h1.text
            {
                println!(
                    "    [{}] UPDATE H1.text: \"{}\" -> \"{}\"",
                    index, current_text, target_h1.text
                );
                text_field.set(target_h1.text.clone()).expect("set H1 text");
            }
        }
        BodyElement::P(target_p) => {
            if let Ok(Some(p_poke)) = poke_enum.field(0)
                && let Ok(mut p_struct) = p_poke.into_struct()
                && let Ok(mut text_field) = p_struct.field_by_name("text")
                && let Ok(current_text) = text_field.get::<String>()
                && *current_text != target_p.text
            {
                println!(
                    "    [{}] UPDATE P.text: \"{}\" -> \"{}\"",
                    index, current_text, target_p.text
                );
                text_field.set(target_p.text.clone()).expect("set P text");
            }
        }
        BodyElement::Ul(target_ul) => {
            // For list modifications, we replace the whole list
            // (A more sophisticated approach would update individual items)
            if let Ok(Some(ul_poke)) = poke_enum.field(0)
                && let Ok(mut ul_struct) = ul_poke.into_struct()
                && let Ok(mut items_field) = ul_struct.field_by_name("items")
                && let Ok(current_items) = items_field.get::<Vec<ListItem>>()
                && (current_items.len() != target_ul.items.len()
                    || current_items
                        .iter()
                        .zip(target_ul.items.iter())
                        .any(|(a, b)| a.text != b.text))
            {
                println!(
                    "    [{}] UPDATE UL.items: {} items -> {} items",
                    index,
                    current_items.len(),
                    target_ul.items.len()
                );
                items_field
                    .set(target_ul.items.clone())
                    .expect("set UL items");
            }
        }
        BodyElement::Div(_target_div) => {
            println!("    [{}] (Div update not implemented in this demo)", index);
        }
    }
}

/// Get the variant name of a BodyElement for display using reflection
fn variant_name(content: &BodyElement) -> &'static str {
    let peek = Peek::new(content);
    if let Ok(e) = peek.into_enum() {
        e.variant_name_active().unwrap_or("unknown")
    } else {
        "unknown"
    }
}

/// Print the document structure
fn print_document_structure(doc: &HtmlDocument) {
    println!("  Title: {}", doc.head.title.text);
    println!("  Body class: {:?}", doc.body.class);
    println!("  Children: {} elements", doc.body.children.len());

    for (i, child) in doc.body.children.iter().enumerate() {
        match child {
            BodyElement::H1(h) => println!("    [{}] H1: \"{}\"", i, h.text),
            BodyElement::P(p) => println!("    [{}] P: \"{}\"", i, p.text),
            BodyElement::Ul(ul) => {
                println!("    [{}] UL: {} items", i, ul.items.len());
                for (j, item) in ul.items.iter().enumerate() {
                    println!("      [{}] LI: \"{}\"", j, item.text);
                }
            }
            BodyElement::Div(d) => println!("    [{}] DIV: id={:?}", i, d.id),
        }
    }
}