Coverage Report

Created: 2026-05-25 08:50

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/runner/work/MathCAT/MathCAT/src/pretty_print.rs
Line
Count
Source
1
//! Useful functions for debugging and error messages.
2
#![allow(clippy::needless_return)]
3
4
use sxd_document::dom::{Element, ChildOfElement, Attribute};
5
6
// #[allow(dead_code)]
7
// pub fn pp_doc(doc: &Document) {
8
//     for root_child in doc.root().children() {
9
//         if let ChildOfRoot::Element(e) = root_child {
10
//             format_element(&e, 0);
11
//             break;
12
//         }
13
//     };
14
// }
15
16
/// Pretty-print the MathML represented by `element`.
17
4.90k
pub fn mml_to_string(e: Element) -> String {
18
4.90k
    return format_element(e, 0);
19
4.90k
}
20
21
/// Pretty-print the MathML represented by `element`.
22
/// * `indent` -- the amount of indentation to start with
23
57.6k
pub fn format_element(e: Element, indent: usize) -> String {
24
    // let namespace = match e.name().namespace_uri() {
25
    //     None => "".to_string(),
26
    //     Some(prefix) => prefix.to_string() + ":",
27
    // };
28
    // let namespace = namespace.as_str();
29
57.6k
    let namespace = "";
30
57.6k
    let mut answer = format!("{:in$}<{ns}{name}{attrs}>", " ", in=2*indent, ns=namespace, name=e.name().local_part(), attrs=format_attrs(&e.attributes()));
31
57.6k
    let children = e.children();
32
57.6k
    let has_element = children.iter().find(|&&c| matches!(
c57.1k
, ChildOfElement::Element(
_x21.6k
)));
33
57.6k
    if has_element.is_none() {
34
        // print text content
35
35.9k
        let content = children.iter()
36
35.9k
                .map(|c| if let ChildOfElement::Text(
t35.4k
) =
c35.4k
{
t35.4k
.
text35.4k
()} else {
""0
}35.4k
)
37
35.9k
                .collect::<Vec<&str>>()
38
35.9k
                .join("");
39
35.9k
        return format!("{}{}</{}{}>\n", answer, &handle_special_chars(&content), namespace, e.name().local_part());
40
        // for child in children {
41
        //     if let ChildOfElement::Text(t) = child {
42
        //         return format!("{}{}</{}{}>\n", answer, &make_invisible_chars_visible(t.text()), namespace, e.name().local_part());
43
        //     }
44
        // };
45
    } else {
46
21.6k
       answer += "\n";        // tag with children should start on new line
47
        // recurse on each Element child
48
52.7k
        for c in 
e21.6k
.
children21.6k
() {
49
52.7k
            if let ChildOfElement::Element(e) = c {
50
52.7k
                answer += &format_element(e, indent+1);
51
52.7k
            
}0
52
        }
53
    }
54
21.6k
    return answer + &format!("{:in$}</{ns}{name}>\n", " ", in=2*indent, ns=namespace, name=e.name().local_part());
55
56
    // Use the &#x....; representation for invisible chars when printing
57
57.6k
}
58
59
/// Format a vector of attributes as a string with a leading space
60
57.6k
pub fn format_attrs(attrs: &[Attribute]) -> String {
61
57.6k
    let mut result = String::new();
62
134k
    for attr in 
attrs57.6k
{
63
134k
        result += format!(" {}='{}'", attr.name().local_part(), &handle_special_chars(attr.value())).as_str();
64
134k
    }
65
57.6k
    result
66
57.6k
}
67
68
170k
fn handle_special_chars(text: &str) -> String {
69
    // Pre-allocate a buffer. We guess the size is roughly the same as input, maybe slightly larger.
70
170k
    let mut s = String::with_capacity(text.len());
71
963k
    for ch in 
text170k
.
chars170k
() {
72
963k
        match ch {
73
32
            '"' => s.push_str("&quot;"),
74
5
            '&' => s.push_str("&amp;"),
75
277
            '\'' => s.push_str("&apos;"),
76
367
            '<' => s.push_str("&lt;"),
77
410
            '>' => s.push_str("&gt;"),
78
724
            '\u{2061}' => s.push_str("&#x2061;"),
79
3.46k
            '\u{2062}' => s.push_str("&#x2062;"),
80
571
            '\u{2063}' => s.push_str("&#x2063;"),
81
76
            '\u{2064}' => s.push_str("&#x2064;"),
82
957k
            _ => s.push(ch),
83
        }
84
    }
85
170k
    s
86
170k
}
87
88
89
// /// Pretty print an xpath value.
90
// /// If the value is a `NodeSet`, the MathML for the node/element is returned.
91
// pub fn pp_xpath_value(value: Value) {
92
//     use sxd_xpath::Value;
93
//     use sxd_xpath::nodeset::Node;
94
//     debug!("XPath value:");
95
//     if let Value::Nodeset(nodeset) = &value {
96
//         for node in nodeset.document_order() {
97
//             match node {
98
//                 Node::Element(el) => {debug!("{}", crate::pretty_print::format_element(&el, 1))},
99
//                 Node::Text(t) =>  {debug!("found Text value: {}", t.text())},
100
//                 _ => {debug!("found unexpected node type")}
101
//             }
102
//         }
103
//     }
104
// }
105
106
/// Convert YAML to a string using with `indent` amount of space.
107
2.42M
pub fn yaml_to_string(yaml: &Yaml, indent: usize) -> String {
108
2.42M
    let mut result = String::new();
109
2.42M
    {
110
2.42M
        let mut emitter = YamlEmitter::new(&mut result);
111
2.42M
        emitter.compact(true);
112
2.42M
        emitter.emit_node(yaml).unwrap(); // dump the YAML object to a String
113
2.42M
    }
114
2.42M
    if indent == 0 {
115
2.42M
        return result;
116
0
    }
117
0
    let indent_str = format!("{:in$}", " ", in=2*indent);
118
0
    result = result.replace('\n',&("\n".to_string() + &indent_str)); // add indentation to all but first line
119
0
    return indent_str + result.trim_end();  // add indent to first line and remove an extra indent at end
120
2.42M
}
121
122
/* --------------------- Tweaked pretty printer for YAML (from YAML code) --------------------- */
123
124
// Changed: new function to determine if more compact notation can be used (when child is a one entry simple array/hash). Writes
125
// -foo [bar: bletch]
126
// -foo {bar: bletch}
127
20.1k
fn is_scalar(v: &Yaml) -> bool {
128
20.1k
    return !matches!(v, Yaml::Hash(_) | Yaml::Array(_));
129
20.1k
}
130
131
20.1k
fn is_complex(v: &Yaml) -> bool {
132
20.1k
    return match v {
133
1
        Yaml::Hash(h) => {
134
1
            return match h.len() {
135
0
                0 => false,
136
                1 => {
137
1
                    let (key,val) = h.iter().next().unwrap();
138
1
                    return !(is_scalar(key) && is_scalar(val))
139
                },
140
0
                _ => true,
141
            }
142
        },
143
0
        Yaml::Array(v) => {
144
0
            return match v.len() {
145
0
                0 => false,
146
                1 => {
147
0
                    let hash = v[0].as_hash();
148
0
                    if let Some(hash) = hash {
149
0
                        return match hash.len() {
150
0
                            0 => false,
151
                            1 => {
152
0
                                let (key, val) = hash.iter().next().unwrap();
153
0
                                return !(is_scalar(key) && is_scalar(val));
154
                            },
155
0
                            _ => true,
156
                        }
157
                    } else {
158
0
                        return !is_scalar(&v[0]);
159
                    }    
160
                },
161
0
                _ => true,
162
            }
163
        },
164
20.1k
        _ => false,
165
    }
166
20.1k
}
167
168
use std::error::Error;
169
use std::fmt::{self, Display};
170
use yaml_rust::{Yaml, yaml::Hash};
171
172
//use crate::yaml::{Hash, Yaml};
173
174
#[derive(Copy, Clone, Debug)]
175
#[allow(dead_code)] // from original YAML code (isn't used here)
176
enum EmitError {
177
    FmtError(fmt::Error),
178
    BadHashmapKey,
179
}
180
181
impl Error for EmitError {
182
0
    fn cause(&self) -> Option<&dyn Error> {
183
0
        None
184
0
    }
185
}
186
187
impl Display for EmitError {
188
0
    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
189
0
        match *self {
190
0
            EmitError::FmtError(ref err) => Display::fmt(err, formatter),
191
0
            EmitError::BadHashmapKey => formatter.write_str("bad hashmap key"),
192
        }
193
0
    }
194
}
195
196
impl From<fmt::Error> for EmitError {
197
0
    fn from(f: fmt::Error) -> Self {
198
0
        EmitError::FmtError(f)
199
0
    }
200
}
201
202
struct YamlEmitter<'a> {
203
    writer: &'a mut dyn fmt::Write,
204
    best_indent: usize,
205
    compact: bool,
206
207
    level: isize,
208
}
209
210
type EmitResult = Result<(), EmitError>;
211
212
// from serialize::json
213
1.13M
fn escape_str(wr: &mut dyn fmt::Write, v: &str) -> Result<(), fmt::Error> {
214
1.13M
    wr.write_str("\"")
?0
;
215
216
1.13M
    let mut start = 0;
217
218
102M
    for (i, byte) in 
v1.13M
.
bytes1.13M
().
enumerate1.13M
() {
219
102M
        let 
escaped0
= match byte {
220
0
            b'"' => "\\\"",
221
0
            b'\\' => "\\\\",
222
0
            b'\x00' => "\\u0000",
223
0
            b'\x01' => "\\u0001",
224
0
            b'\x02' => "\\u0002",
225
0
            b'\x03' => "\\u0003",
226
0
            b'\x04' => "\\u0004",
227
0
            b'\x05' => "\\u0005",
228
0
            b'\x06' => "\\u0006",
229
0
            b'\x07' => "\\u0007",
230
0
            b'\x08' => "\\b",
231
0
            b'\t' => "\\t",
232
0
            b'\n' => "\\n",
233
0
            b'\x0b' => "\\u000b",
234
0
            b'\x0c' => "\\f",
235
0
            b'\r' => "\\r",
236
0
            b'\x0e' => "\\u000e",
237
0
            b'\x0f' => "\\u000f",
238
0
            b'\x10' => "\\u0010",
239
0
            b'\x11' => "\\u0011",
240
0
            b'\x12' => "\\u0012",
241
0
            b'\x13' => "\\u0013",
242
0
            b'\x14' => "\\u0014",
243
0
            b'\x15' => "\\u0015",
244
0
            b'\x16' => "\\u0016",
245
0
            b'\x17' => "\\u0017",
246
0
            b'\x18' => "\\u0018",
247
0
            b'\x19' => "\\u0019",
248
0
            b'\x1a' => "\\u001a",
249
0
            b'\x1b' => "\\u001b",
250
0
            b'\x1c' => "\\u001c",
251
0
            b'\x1d' => "\\u001d",
252
0
            b'\x1e' => "\\u001e",
253
0
            b'\x1f' => "\\u001f",
254
0
            b'\x7f' => "\\u007f",
255
102M
            _ => continue,
256
        };
257
258
0
        if start < i {
259
0
            wr.write_str(&v[start..i])?;
260
0
        }
261
262
0
        wr.write_str(escaped)?;
263
264
0
        start = i + 1;
265
    }
266
267
1.13M
    if start != v.len() {
268
1.13M
        wr.write_str(&v[start..])
?0
;
269
0
    }
270
271
1.13M
    wr.write_str("\"")
?0
;
272
1.13M
    Ok(())
273
1.13M
}
274
275
impl<'a> YamlEmitter<'a> {
276
2.42M
    pub fn new(writer: &'a mut dyn fmt::Write) -> YamlEmitter<'a> {
277
2.42M
        YamlEmitter {
278
2.42M
            writer,
279
2.42M
            best_indent: 2,
280
2.42M
            compact: true,
281
2.42M
            level: -1,
282
2.42M
        }
283
2.42M
    }
284
285
    /// Set 'compact inline notation' on or off, as described for block
286
    /// [sequences](http://www.yaml.org/spec/1.2/spec.html#id2797382)
287
    /// and
288
    /// [mappings](http://www.yaml.org/spec/1.2/spec.html#id2798057).
289
    ///
290
    /// In this form, blocks cannot have any properties (such as anchors
291
    /// or tags), which should be OK, because this emitter doesn't
292
    /// (currently) emit those anyways.
293
2.42M
    pub fn compact(&mut self, compact: bool) {
294
2.42M
        self.compact = compact;
295
2.42M
    }
296
297
    /// Determine if this emitter is using 'compact inline notation'.
298
    #[allow(dead_code)]   // not all fields are used in this program
299
0
    pub fn is_compact(&self) -> bool {
300
0
        self.compact
301
0
    }
302
303
    // fn dump(&mut self, doc: &Yaml) -> EmitResult {
304
    //     // write DocumentStart
305
    //     writeln!(self.writer, "---")?;
306
    //     self.level = -1;
307
    //     self.emit_node(doc)
308
    // }
309
310
15.1k
    fn write_indent(&mut self) -> EmitResult {
311
15.1k
        if self.level <= 0 {
312
15.1k
            return Ok(());
313
0
        }
314
0
        for _ in 0..self.level {
315
0
            for _ in 0..self.best_indent {
316
0
                write!(self.writer, " ")?;
317
            }
318
        }
319
0
        Ok(())
320
15.1k
    }
321
322
2.46M
    fn emit_node(&mut self, node: &Yaml) -> EmitResult {
323
2.46M
        match *node {
324
5.03k
            Yaml::Array(ref v) => self.emit_array(v),
325
0
            Yaml::Hash(ref h) => self.emit_hash(h),
326
2.45M
            Yaml::String(ref v) => {
327
2.45M
                if need_quotes(v) {
328
1.13M
                    escape_str(self.writer, v)
?0
;
329
                } else {
330
1.32M
                    write!(self.writer, "{v}")
?0
;
331
                }
332
2.45M
                Ok(())
333
            }
334
0
            Yaml::Boolean(v) => {
335
0
                if v {
336
0
                    self.writer.write_str("true")?;
337
                } else {
338
0
                    self.writer.write_str("false")?;
339
                }
340
0
                Ok(())
341
            }
342
0
            Yaml::Integer(v) => {
343
0
                write!(self.writer, "{v}")?;
344
0
                Ok(())
345
            }
346
0
            Yaml::Real(ref v) => {
347
0
                write!(self.writer, "{v}")?;
348
0
                Ok(())
349
            }
350
            Yaml::Null | Yaml::BadValue => {
351
0
                write!(self.writer, "~")?;
352
0
                Ok(())
353
            }
354
            // XXX(chenyh) Alias
355
0
            _ => Ok(()),
356
        }
357
2.46M
    }
358
359
5.03k
    fn emit_array(&mut self, v: &[Yaml]) -> EmitResult {
360
5.03k
        if v.is_empty() {
361
0
            write!(self.writer, "[]")?;
362
5.03k
        } else if v.len() == 1 && 
!is_complex(&v[0])1
{
363
            // changed -- for arrays that have only one simple element, make them more compact by using [...] notation
364
1
            write!(self.writer, "[")
?0
;
365
1
            self.emit_val(true, &v[0])
?0
;
366
1
            write!(self.writer, "]")
?0
;
367
        } else {
368
5.03k
            self.level += 1;
369
            
370
20.1k
            for (cnt, x) in 
v5.03k
.
iter5.03k
().
enumerate5.03k
() {
371
20.1k
                if cnt > 0 {
372
15.1k
                    writeln!(self.writer)
?0
;
373
15.1k
                    self.write_indent()
?0
;
374
5.03k
                }
375
20.1k
                write!(self.writer, "- ")
?0
;
376
20.1k
                self.emit_val(true, x)
?0
;
377
            }
378
5.03k
            self.level -= 1;
379
        }
380
5.03k
        return Ok(());
381
5.03k
    }
382
383
20.1k
    fn emit_hash(&mut self, h: &Hash) -> EmitResult {
384
20.1k
        if h.is_empty() {
385
0
            self.writer.write_str("{}")?;
386
        } else {
387
          // changed -- for hashmaps that have only one simple element, make them more compact by using {...}} notation
388
20.1k
            self.level += 1;
389
20.1k
            for (cnt, (k, v)) in h.iter().enumerate() {
390
                // changed: use new function is_scalar()
391
                // let complex_key = match *k {
392
                //     Yaml::Hash(_) | Yaml::Array(_) => true,
393
                //     _ => false,
394
                // };
395
20.1k
                if cnt > 0 {
396
0
                    writeln!(self.writer)?;
397
0
                    self.write_indent()?;
398
20.1k
                }
399
20.1k
                if !is_scalar(k) {
400
0
                    write!(self.writer, "? ")?;
401
0
                    self.emit_val(true, k)?;
402
0
                    writeln!(self.writer)?;
403
0
                    self.write_indent()?;
404
0
                    write!(self.writer, ": ")?;
405
0
                    self.emit_val(true, v)?;
406
                } else {
407
20.1k
                    self.emit_node(k)
?0
;
408
20.1k
                    write!(self.writer, ": ")
?0
;
409
410
                    // changed to use braces in some cases
411
20.1k
                    let complex_value = is_complex(v);
412
20.1k
                    if !complex_value && v.as_hash().is_some() {
413
0
                        write!(self.writer, "{{")?;
414
20.1k
                    }
415
                    // changed to use complex_value from 'false'
416
20.1k
                    self.emit_val(!complex_value, v)
?0
;
417
20.1k
                    if !complex_value && v.as_hash().is_some() {
418
0
                        write!(self.writer, "}}")?;
419
20.1k
                    }
420
                }
421
            }
422
20.1k
            self.level -= 1;
423
        }   
424
20.1k
        Ok(())
425
20.1k
    }
426
427
    /// Emit a yaml as a hash or array value: i.e., which should appear
428
    /// following a ":" or "-", either after a space, or on a new line.
429
    /// If `inline` is true, then the preceding characters are distinct
430
    /// and short enough to respect the compact flag.
431
    // changed: use to always emit ' ' for inline -- that is now handled elsewhere
432
40.2k
    fn emit_val(&mut self, inline: bool, val: &Yaml) -> EmitResult {
433
40.2k
        match *val {
434
0
            Yaml::Array(ref v) => {
435
0
                if !((inline && self.compact) || v.is_empty()) {
436
0
                    writeln!(self.writer)?;
437
0
                    self.level += 1;
438
0
                    self.write_indent()?;
439
0
                    self.level -= 1;
440
0
                }
441
0
                self.emit_array(v)
442
            }
443
20.1k
            Yaml::Hash(ref h) => {
444
20.1k
                if !((inline && self.compact) || 
h0
.
is_empty0
()) {
445
0
                    writeln!(self.writer)?;
446
0
                    self.level += 1;
447
0
                    self.write_indent()?;
448
0
                    self.level -= 1;
449
20.1k
                }
450
20.1k
                self.emit_hash(h)
451
            }
452
            _ => {
453
           //     write!(self.writer, " ")?;
454
20.1k
                self.emit_node(val)
455
            }
456
        }
457
40.2k
    }
458
}
459
460
/// Check if the string requires quoting.
461
/// Strings starting with any of the following characters must be quoted.
462
/// :, &, *, ?, |, -, <, >, =, !, %, @
463
/// Strings containing any of the following characters must be quoted.
464
/// {, }, [, ], ,, #, `
465
///
466
/// If the string contains any of the following control characters, it must be escaped with double quotes:
467
/// \0, \x01, \x02, \x03, \x04, \x05, \x06, \a, \b, \t, \n, \v, \f, \r, \x0e, \x0f, \x10, \x11, \x12, \x13, \x14, \x15, \x16, \x17, \x18, \x19, \x1a, \e, \x1c, \x1d, \x1e, \x1f, \N, \_, \L, \P
468
///
469
/// Finally, there are other cases when the strings must be quoted, no matter if you're using single or double quotes:
470
/// * When the string is true or false (otherwise, it would be treated as a boolean value);
471
/// * When the string is null or ~ (otherwise, it would be considered as a null value);
472
/// * When the string looks like a number, such as integers (e.g. 2, 14, etc.), floats (e.g. 2.6, 14.9) and exponential numbers (e.g. 12e7, etc.) (otherwise, it would be treated as a numeric value);
473
/// * When the string looks like a date (e.g. 2014-12-31) (otherwise it would be automatically converted into a Unix timestamp).
474
2.45M
fn need_quotes(string: &str) -> bool {
475
2.45M
    fn need_quotes_spaces(string: &str) -> bool {
476
2.45M
        string.starts_with(' ') || 
string2.45M
.
ends_with2.45M
(' ')
477
2.45M
    }
478
479
2.45M
    string.is_empty()
480
2.45M
        || need_quotes_spaces(string)
481
2.45M
        || string.starts_with(['&', '*', '?', '|', '-', '<', '>', '=', '!', '%', '@'])
482
19.9M
        || 
string2.27M
.
contains2.27M
(|character: char| matches!(character,
483
            ':'
484
            | '{'
485
            | '}'
486
            | '['
487
            | ']'
488
            | ','
489
            | '#'
490
            | '`'
491
            | '\"'
492
            | '\''
493
            | '\\'
494
19.0M
            | '\0'..='\x06'
495
            | '\t'
496
            | '\n'
497
            | '\r'
498
19.0M
            | '\x0e'..='\x1a'
499
19.0M
            | '\x1c'..='\x1f') )
500
1.32M
        || [
501
1.32M
            // http://yaml.org/type/bool.html
502
1.32M
            // Note: 'y', 'Y', 'n', 'N', is not quoted deliberately, as in libyaml. PyYAML also parse
503
1.32M
            // them as string, not booleans, although it is violating the YAML 1.1 specification.
504
1.32M
            // See https://github.com/dtolnay/serde-yaml/pull/83#discussion_r152628088.
505
1.32M
            "yes", "Yes", "YES", "no", "No", "NO", "True", "TRUE", "true", "False", "FALSE",
506
1.32M
            "false", "on", "On", "ON", "off", "Off", "OFF",
507
1.32M
            // http://yaml.org/type/null.html
508
1.32M
            "null", "Null", "NULL", "~",
509
1.32M
        ]
510
1.32M
        .contains(&string)
511
1.32M
        || string.starts_with('.')
512
1.32M
        || string.starts_with("0x")
513
1.32M
        || string.parse::<i64>().is_ok()
514
1.32M
        || string.parse::<f64>().is_ok()
515
2.45M
}
516
517
#[cfg(test)]
518
mod tests {
519
    use super::*;
520
    use sxd_document::dom::{ChildOfElement, ChildOfRoot};
521
    use sxd_document::parser;
522
523
    /// helper function
524
10
    fn first_element(package: &sxd_document::Package) -> Element<'_> {
525
10
        let doc = package.as_document();
526
10
        for child in doc.root().children() {
527
10
            if let ChildOfRoot::Element(e) = child {
528
10
                return e;
529
0
            }
530
        }
531
0
        panic!("No root element found");
532
10
    }
533
534
    #[test]
535
    /// Escapes XML entities and invisible characters for safe display.
536
    /// Tests the method on a few hardcoded characters.
537
1
    fn handle_special_chars_escapes() {
538
1
        let input = "& < > \" ' \u{2061} \u{2062} \u{2063} \u{2064} x";
539
1
        let expected = "&amp; &lt; &gt; &quot; &apos; &#x2061; &#x2062; &#x2063; &#x2064; x";
540
1
        assert_eq!(handle_special_chars(input), expected);
541
1
    }
542
543
    #[test]
544
    /// Formats a leaf element as a single line with escaped text.
545
1
    fn format_element_leaf_text() {
546
1
        let package = parser::parse("<math><mi>&amp;</mi></math>").unwrap();
547
1
        let math = first_element(&package);
548
1
        let mi = math
549
1
            .children()
550
1
            .iter()
551
1
            .find_map(|c| match c {
552
1
                ChildOfElement::Element(e) => Some(*e),
553
0
                _ => None,
554
1
            })
555
1
            .unwrap();
556
1
        assert_eq!(format_element(mi, 0), " <mi>&amp;</mi>\n");
557
1
    }
558
559
    #[test]
560
    /// Formats a nested element with indentation and newlines.
561
1
    fn format_element_nested() {
562
1
        let package = parser::parse("<math><mi>x</mi><mo>+</mo></math>").unwrap();
563
1
        let math = first_element(&package);
564
1
        let rendered = format_element(math, 0);
565
1
        assert!(rendered.starts_with(" <math>\n"));
566
1
        assert!(rendered.contains("\n  <mi>x</mi>\n"));
567
1
        assert!(rendered.contains("\n  <mo>+</mo>\n"));
568
1
        assert!(rendered.ends_with("</math>\n"));
569
1
    }
570
571
    #[test]
572
    /// Escapes special characters in attribute values.
573
1
    fn format_attrs_escapes() {
574
1
        let package = parser::parse("<math a=\"&amp;\" b=\"&lt;\"></math>").unwrap();
575
1
        let math = first_element(&package);
576
1
        let rendered = format_attrs(&math.attributes());
577
1
        assert!(rendered.contains(" a='&amp;'"));
578
1
        assert!(rendered.contains(" b='&lt;'"));
579
1
    }
580
581
    #[test]
582
    /// Preserves non-BMP characters from a literal XML form.
583
1
    fn format_element_non_bmp_character_literal() {
584
1
        let package = parser::parse("<math><mi>𝞪</mi></math>").unwrap();
585
1
        let math = first_element(&package);
586
1
        let mi = math
587
1
            .children()
588
1
            .iter()
589
1
            .find_map(|c| match c {
590
1
                ChildOfElement::Element(e) => Some(*e),
591
0
                _ => None,
592
1
            })
593
1
            .unwrap();
594
1
        let rendered = format_element(mi, 0);
595
1
        assert!(rendered.contains("𝞪"));
596
1
    }
597
598
    #[test]
599
    /// Preserves non-BMP characters from a numeric XML form.
600
1
    fn format_element_non_bmp_character_numeric() {
601
1
        let package = parser::parse("<math><mi>&#x1d7aa;</mi></math>").unwrap();
602
1
        let math = first_element(&package);
603
1
        let mi = math
604
1
            .children()
605
1
            .iter()
606
1
            .find_map(|c| match c {
607
1
                ChildOfElement::Element(e) => Some(*e),
608
0
                _ => None,
609
1
            })
610
1
            .unwrap();
611
1
        let rendered = format_element(mi, 0);
612
1
        assert!(rendered.contains("𝞪"));
613
1
    }
614
615
    #[test]
616
    /// Evaluates non-BMP literal text through sxd_xpath.
617
1
    fn xpath_non_bmp_literal() {
618
        use sxd_xpath::{Factory, Value};
619
620
1
        let package = parser::parse("<math><mi>𝞪</mi></math>").unwrap();
621
1
        let xpath = Factory::new().build("string(/math/mi)").unwrap().unwrap();
622
1
        let context = sxd_xpath::Context::new();
623
624
1
        let value = xpath.evaluate(&context, first_element(&package)).unwrap();
625
1
        match value {
626
1
            Value::String(s) => assert_eq!(s, "𝞪"),
627
0
            _ => panic!("Expected string value from xpath"),
628
        }
629
1
    }
630
631
    #[test]
632
    /// Evaluates non-BMP numeric text through sxd_xpath.
633
1
    fn xpath_non_bmp_numeric() {
634
        use sxd_xpath::{Factory, Value};
635
636
1
        let package = parser::parse("<math><mi>&#x1d7aa;</mi></math>").unwrap();
637
1
        let xpath = Factory::new().build("string(/math/mi)").unwrap().unwrap();
638
1
        let context = sxd_xpath::Context::new();
639
640
1
        let value = xpath.evaluate(&context, first_element(&package)).unwrap();
641
1
        match value {
642
1
            Value::String(s) => assert_eq!(s, "𝞪"),
643
0
            _ => panic!("Expected string value from xpath"),
644
        }
645
1
    }
646
647
    #[test]
648
    /// Evaluates non-BMP literal text with a MathML namespace-qualified XPath.
649
1
    fn xpath_non_bmp_namespace_literal() {
650
        use sxd_xpath::{Factory, Value};
651
652
1
        let xml = "<math xmlns=\"http://www.w3.org/1998/Math/MathML\"><mi>𝞪</mi></math>";
653
1
        let package = parser::parse(xml).unwrap();
654
1
        let xpath = Factory::new()
655
1
            .build("string(/m:math/m:mi)")
656
1
            .unwrap()
657
1
            .unwrap();
658
1
        let mut context = sxd_xpath::Context::new();
659
1
        context.set_namespace("m", "http://www.w3.org/1998/Math/MathML");
660
661
1
        let value = xpath.evaluate(&context, first_element(&package)).unwrap();
662
1
        match value {
663
1
            Value::String(s) => assert_eq!(s, "𝞪"),
664
0
            _ => panic!("Expected string value from xpath"),
665
        }
666
1
    }
667
668
    #[test]
669
    /// Evaluates non-BMP numeric text with a MathML namespace-qualified XPath.
670
1
    fn xpath_non_bmp_namespace_numeric() {
671
        use sxd_xpath::{Factory, Value};
672
673
1
        let xml = "<math xmlns=\"http://www.w3.org/1998/Math/MathML\"><mi>&#120746;</mi></math>";
674
1
        let package = parser::parse(xml).unwrap();
675
1
        let xpath = Factory::new()
676
1
            .build("string(/m:math/m:mi)")
677
1
            .unwrap()
678
1
            .unwrap();
679
1
        let mut context = sxd_xpath::Context::new();
680
1
        context.set_namespace("m", "http://www.w3.org/1998/Math/MathML");
681
682
1
        let value = xpath.evaluate(&context, first_element(&package)).unwrap();
683
1
        match value {
684
1
            Value::String(s) => assert_eq!(s, "𝞪"),
685
0
            _ => panic!("Expected string value from xpath"),
686
        }
687
1
    }
688
689
    #[test]
690
    /// Extracts a text node via XPath (nodeset result) and verifies the non-BMP character survives.
691
1
    fn xpath_non_bmp_text_nodeset() {
692
        use sxd_xpath::{Factory, Value};
693
694
1
        let xml = "<math xmlns=\"http://www.w3.org/1998/Math/MathML\"><mi>𝞪</mi></math>";
695
1
        let package = parser::parse(xml).unwrap();
696
1
        let xpath = Factory::new().build("/m:math/m:mi/text()").unwrap().unwrap();
697
1
        let mut context = sxd_xpath::Context::new();
698
1
        context.set_namespace("m", "http://www.w3.org/1998/Math/MathML");
699
700
1
        let value = xpath.evaluate(&context, first_element(&package)).unwrap();
701
1
        match value {
702
1
            Value::Nodeset(nodes) => {
703
1
                let ordered = nodes.document_order();
704
1
                let node = ordered.first().expect("Expected one text node");
705
1
                let text = node.text().expect("Expected text node");
706
1
                assert_eq!(text.text(), "𝞪");
707
1
                assert_eq!(ordered.len(), 1);
708
            }
709
0
            _ => panic!("Expected nodeset value from xpath"),
710
        }
711
1
    }
712
}