Skip to main content

libmathcat/
pretty_print.rs

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