/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("""), |
74 | 5 | '&' => s.push_str("&"), |
75 | 277 | '\'' => s.push_str("'"), |
76 | 367 | '<' => s.push_str("<"), |
77 | 410 | '>' => s.push_str(">"), |
78 | 724 | '\u{2061}' => s.push_str("⁡"), |
79 | 3.46k | '\u{2062}' => s.push_str("⁢"), |
80 | 571 | '\u{2063}' => s.push_str("⁣"), |
81 | 76 | '\u{2064}' => s.push_str("⁤"), |
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 = "& < > " ' ⁡ ⁢ ⁣ ⁤ 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>&</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>&</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=\"&\" b=\"<\"></math>").unwrap(); |
575 | 1 | let math = first_element(&package); |
576 | 1 | let rendered = format_attrs(&math.attributes()); |
577 | 1 | assert!(rendered.contains(" a='&'")); |
578 | 1 | assert!(rendered.contains(" b='<'")); |
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>𝞪</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>𝞪</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>𝞪</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 | | } |