## 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
    }