Skip to main content

fop_core/xml/
parser.rs

1//! XML parser wrapper around quick-xml with enhanced features
2
3use crate::xml::Namespace;
4use fop_types::{FopError, Location, Result};
5use quick_xml::events::{BytesStart, Event};
6use quick_xml::Reader;
7use std::collections::HashMap;
8use std::io::BufRead;
9
10/// Entity resolver for handling XML entities
11#[derive(Debug, Clone)]
12pub struct EntityResolver {
13    entities: HashMap<String, String>,
14}
15
16impl EntityResolver {
17    /// Create a new entity resolver with built-in entities
18    pub fn new() -> Self {
19        let mut entities = HashMap::new();
20
21        // Built-in XML entities
22        entities.insert("amp".to_string(), "&".to_string());
23        entities.insert("lt".to_string(), "<".to_string());
24        entities.insert("gt".to_string(), ">".to_string());
25        entities.insert("quot".to_string(), "\"".to_string());
26        entities.insert("apos".to_string(), "'".to_string());
27
28        Self { entities }
29    }
30
31    /// Add a custom entity
32    pub fn add_entity(&mut self, name: String, value: String) {
33        self.entities.insert(name, value);
34    }
35
36    /// Resolve an entity reference
37    pub fn resolve(&self, entity: &str, location: Location) -> Result<String> {
38        // Handle numeric character references
39        if let Some(hex_str) = entity
40            .strip_prefix("#x")
41            .or_else(|| entity.strip_prefix("#X"))
42        {
43            // Hexadecimal
44            if let Ok(code) = u32::from_str_radix(hex_str, 16) {
45                if let Some(ch) = char::from_u32(code) {
46                    return Ok(ch.to_string());
47                }
48            }
49            return Err(FopError::EntityError {
50                message: format!("Invalid hexadecimal character reference: {}", entity),
51                location,
52            });
53        } else if let Some(dec_str) = entity.strip_prefix('#') {
54            // Decimal
55            if let Ok(code) = dec_str.parse::<u32>() {
56                if let Some(ch) = char::from_u32(code) {
57                    return Ok(ch.to_string());
58                }
59            }
60            return Err(FopError::EntityError {
61                message: format!("Invalid decimal character reference: {}", entity),
62                location,
63            });
64        }
65
66        // Named entity
67        self.entities
68            .get(entity)
69            .cloned()
70            .ok_or_else(|| FopError::EntityError {
71                message: format!("Unknown entity: &{};", entity),
72                location,
73            })
74    }
75
76    /// Resolve all entities in a string
77    pub fn resolve_entities(&self, text: &str, location: Location) -> Result<String> {
78        let mut result = String::new();
79        let mut chars = text.chars().peekable();
80
81        while let Some(ch) = chars.next() {
82            if ch == '&' {
83                // Find the end of the entity reference
84                let mut entity_name = String::new();
85                let mut found_semicolon = false;
86
87                while let Some(&next_ch) = chars.peek() {
88                    if next_ch == ';' {
89                        chars.next(); // consume semicolon
90                        found_semicolon = true;
91                        break;
92                    }
93                    entity_name.push(next_ch);
94                    chars.next();
95                }
96
97                if !found_semicolon {
98                    return Err(FopError::EntityError {
99                        message: format!("Unterminated entity reference: &{}", entity_name),
100                        location,
101                    });
102                }
103
104                let resolved = self.resolve(&entity_name, location)?;
105                result.push_str(&resolved);
106            } else {
107                result.push(ch);
108            }
109        }
110
111        Ok(result)
112    }
113}
114
115impl Default for EntityResolver {
116    fn default() -> Self {
117        Self::new()
118    }
119}
120
121/// Processing instruction data
122#[derive(Debug, Clone, PartialEq)]
123pub struct ProcessingInstruction {
124    pub target: String,
125    pub data: Option<String>,
126}
127
128impl ProcessingInstruction {
129    pub fn new(target: String, data: Option<String>) -> Self {
130        Self { target, data }
131    }
132}
133
134/// One element's worth of namespace declarations (pushed on element open, popped on element close)
135#[derive(Debug, Default)]
136struct NamespaceScope {
137    /// `(prefix, uri)` pairs; empty-string prefix = default `xmlns`
138    decls: Vec<(String, String)>,
139}
140
141/// Wrapper around quick-xml Reader for parsing XSL-FO documents
142pub struct XmlParser<R: BufRead> {
143    reader: Reader<R>,
144    buf: Vec<u8>,
145    /// Namespace scope stack — push on Start/Empty, pop on End/Empty
146    namespace_stack: Vec<NamespaceScope>,
147    /// Entity resolver
148    entity_resolver: EntityResolver,
149    /// Processing instructions encountered
150    processing_instructions: Vec<ProcessingInstruction>,
151}
152
153impl<R: BufRead> XmlParser<R> {
154    /// Create a new XML parser
155    pub fn new(reader: R) -> Self {
156        let mut xml_reader = Reader::from_reader(reader);
157        xml_reader.config_mut().trim_text(true);
158        xml_reader.config_mut().expand_empty_elements = true;
159
160        Self {
161            reader: xml_reader,
162            buf: Vec::new(),
163            namespace_stack: Vec::new(),
164            entity_resolver: EntityResolver::new(),
165            processing_instructions: Vec::new(),
166        }
167    }
168
169    /// Get a reference to the underlying reader
170    pub fn reader(&self) -> &Reader<R> {
171        &self.reader
172    }
173
174    /// Get a mutable reference to the underlying reader
175    pub fn reader_mut(&mut self) -> &mut Reader<R> {
176        &mut self.reader
177    }
178
179    /// Get the entity resolver
180    pub fn entity_resolver(&self) -> &EntityResolver {
181        &self.entity_resolver
182    }
183
184    /// Get a mutable reference to the entity resolver
185    pub fn entity_resolver_mut(&mut self) -> &mut EntityResolver {
186        &mut self.entity_resolver
187    }
188
189    /// Get processing instructions
190    pub fn processing_instructions(&self) -> &[ProcessingInstruction] {
191        &self.processing_instructions
192    }
193
194    /// Get current location (line and column)
195    pub fn location(&self) -> Location {
196        let pos = self.reader.buffer_position();
197        // quick-xml doesn't provide column info directly, so we approximate
198        Location::new(pos as usize, 0)
199    }
200
201    /// Read the next event
202    pub fn read_event(&mut self) -> Result<Event<'static>> {
203        self.buf.clear();
204        let event = self
205            .reader
206            .read_event_into(&mut self.buf)
207            .map(|e| e.into_owned())
208            .map_err(|e| {
209                let location = self.location();
210                FopError::XmlErrorWithLocation {
211                    message: format!("XML parsing error: {}", e),
212                    location,
213                    suggestion: None,
214                }
215            })?;
216
217        // Handle processing instructions
218        if let Event::PI(ref pi) = event {
219            if let Ok(target) = std::str::from_utf8(pi.as_ref()) {
220                // Parse target and data
221                let parts: Vec<&str> = target.splitn(2, ' ').collect();
222                let pi_target = parts[0].to_string();
223                let pi_data = parts.get(1).map(|s| s.to_string());
224
225                self.processing_instructions
226                    .push(ProcessingInstruction::new(pi_target, pi_data));
227            }
228        }
229
230        Ok(event)
231    }
232
233    /// Push a new namespace scope parsed from the element's `xmlns`/`xmlns:*` attributes.
234    /// Call this when entering a Start or Empty element.
235    pub fn push_namespace_scope(&mut self, start: &BytesStart<'_>) {
236        let mut scope = NamespaceScope::default();
237        for attr in start.attributes().with_checks(false).flatten() {
238            let key = match std::str::from_utf8(attr.key.as_ref()) {
239                Ok(k) => k,
240                Err(_) => continue,
241            };
242            if key == "xmlns" {
243                if let Ok(uri) = attr.decode_and_unescape_value(self.reader.decoder()) {
244                    scope.decls.push((String::new(), uri.into_owned()));
245                }
246            } else if let Some(suffix) = key.strip_prefix("xmlns:") {
247                if let Ok(uri) = attr.decode_and_unescape_value(self.reader.decoder()) {
248                    scope.decls.push((suffix.to_string(), uri.into_owned()));
249                }
250            }
251        }
252        self.namespace_stack.push(scope);
253    }
254
255    /// Pop the innermost namespace scope.  No-op if the stack is empty.
256    pub fn pop_namespace_scope(&mut self) {
257        self.namespace_stack.pop();
258    }
259
260    /// Resolve a namespace prefix to a URI, searching from innermost scope outward.
261    /// Returns `None` if the prefix is not in scope.
262    pub fn resolve_prefix<'a>(&'a self, prefix: &str) -> Option<&'a str> {
263        for scope in self.namespace_stack.iter().rev() {
264            for (p, uri) in &scope.decls {
265                if p == prefix {
266                    return Some(uri.as_str());
267                }
268            }
269        }
270        None
271    }
272
273    /// Return all namespaces currently in scope as `(prefix, uri)` pairs,
274    /// with innermost bindings winning over outer ones.  Sorted by prefix
275    /// for determinism.  Empty string prefix = default namespace.
276    pub fn snapshot_in_scope(&self) -> Vec<(String, String)> {
277        let mut map: HashMap<String, String> = HashMap::new();
278        // Iterate outer→inner so inner overwrites outer (innermost binding wins)
279        for scope in self.namespace_stack.iter() {
280            for (prefix, uri) in &scope.decls {
281                map.insert(prefix.clone(), uri.clone());
282            }
283        }
284        let mut result: Vec<(String, String)> = map.into_iter().collect();
285        result.sort_by(|a, b| a.0.cmp(&b.0));
286        result
287    }
288
289    /// Extract element name and namespace from a BytesStart event
290    pub fn extract_name(&self, start: &BytesStart) -> Result<(String, Namespace)> {
291        let location = self.location();
292        let name = start.name();
293
294        // Extract namespace prefix and local name
295        let (ns_prefix, local_name) = if let Some(pos) =
296            name.as_ref().iter().position(|&b| b == b':')
297        {
298            let prefix = std::str::from_utf8(&name.as_ref()[..pos]).map_err(|e| {
299                FopError::XmlErrorWithLocation {
300                    message: format!("Invalid UTF-8 in prefix: {}", e),
301                    location,
302                    suggestion: None,
303                }
304            })?;
305            let local = std::str::from_utf8(&name.as_ref()[pos + 1..]).map_err(|e| {
306                FopError::XmlErrorWithLocation {
307                    message: format!("Invalid UTF-8 in local name: {}", e),
308                    location,
309                    suggestion: None,
310                }
311            })?;
312            (Some(prefix.to_string()), local)
313        } else {
314            let local =
315                std::str::from_utf8(name.as_ref()).map_err(|e| FopError::XmlErrorWithLocation {
316                    message: format!("Invalid UTF-8 in element name: {}", e),
317                    location,
318                    suggestion: None,
319                })?;
320            (None, local)
321        };
322
323        // Look up namespace URI via scope stack
324        let ns_uri = if let Some(ref prefix) = ns_prefix {
325            self.resolve_prefix(prefix)
326                .map(str::to_string)
327                .unwrap_or_default()
328        } else {
329            self.resolve_prefix("")
330                .map(str::to_string)
331                .unwrap_or_default()
332        };
333
334        let namespace = Namespace::from_uri(&ns_uri);
335
336        Ok((local_name.to_string(), namespace))
337    }
338
339    /// Extract attributes from a BytesStart event
340    pub fn extract_attributes(&self, start: &BytesStart) -> Result<Vec<(String, String)>> {
341        let location = self.location();
342        let mut attrs = Vec::new();
343
344        for attr_result in start.attributes() {
345            let attr = attr_result.map_err(|e| FopError::XmlErrorWithLocation {
346                message: format!("Attribute parsing error: {}", e),
347                location,
348                suggestion: None,
349            })?;
350
351            let key = std::str::from_utf8(attr.key.as_ref())
352                .map_err(|e| FopError::XmlErrorWithLocation {
353                    message: format!("Invalid UTF-8 in attribute name: {}", e),
354                    location,
355                    suggestion: None,
356                })?
357                .to_string();
358
359            // Skip xmlns attributes
360            if key.starts_with("xmlns") {
361                continue;
362            }
363
364            // quick-xml already handles entity unescaping in decode_and_unescape_value
365            let value = attr
366                .decode_and_unescape_value(self.reader.decoder())
367                .map_err(|e| FopError::XmlErrorWithLocation {
368                    message: format!("Attribute value decode error: {}", e),
369                    location,
370                    suggestion: None,
371                })?
372                .to_string();
373
374            attrs.push((key, value));
375        }
376
377        Ok(attrs)
378    }
379
380    /// Extract text content from Text event (with entity resolution)
381    pub fn extract_text(&self, text: &[u8]) -> Result<String> {
382        let location = self.location();
383        let text_str = std::str::from_utf8(text).map_err(|e| FopError::XmlErrorWithLocation {
384            message: format!("Invalid UTF-8 in text: {}", e),
385            location,
386            suggestion: None,
387        })?;
388
389        // Resolve entities in text content
390        self.entity_resolver.resolve_entities(text_str, location)
391    }
392
393    /// Extract CDATA content (no entity resolution)
394    pub fn extract_cdata(&self, cdata: &[u8]) -> Result<String> {
395        let location = self.location();
396        std::str::from_utf8(cdata)
397            .map(|s| s.to_string())
398            .map_err(|e| FopError::XmlErrorWithLocation {
399                message: format!("Invalid UTF-8 in CDATA: {}", e),
400                location,
401                suggestion: None,
402            })
403    }
404}
405
406#[cfg(test)]
407mod tests {
408    use super::*;
409    use std::io::Cursor;
410
411    #[test]
412    fn test_parse_simple_fo() {
413        let xml = r#"<?xml version="1.0"?>
414<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
415    <fo:layout-master-set>
416        <fo:simple-page-master master-name="A4">
417        </fo:simple-page-master>
418    </fo:layout-master-set>
419</fo:root>"#;
420
421        let cursor = Cursor::new(xml);
422        let mut parser = XmlParser::new(cursor);
423
424        let mut found_root = false;
425        let mut found_layout_master_set = false;
426
427        loop {
428            let event = parser.read_event();
429            match event {
430                Ok(Event::Start(ref start)) | Ok(Event::Empty(ref start)) => {
431                    parser.push_namespace_scope(start);
432                    let (name, ns) = parser.extract_name(start).expect("test: should succeed");
433
434                    if name == "root" && ns.is_fo() {
435                        found_root = true;
436                    }
437                    if name == "layout-master-set" && ns.is_fo() {
438                        found_layout_master_set = true;
439                    }
440                }
441                Ok(Event::Eof) => break,
442                Err(e) => panic!("Parse error: {}", e),
443                _ => {}
444            }
445        }
446
447        assert!(found_root);
448        assert!(found_layout_master_set);
449    }
450
451    #[test]
452    fn test_extract_attributes() {
453        let xml = r#"<?xml version="1.0"?>
454<fo:simple-page-master xmlns:fo="http://www.w3.org/1999/XSL/Format"
455                       master-name="A4"
456                       page-width="210mm"
457                       page-height="297mm">
458</fo:simple-page-master>"#;
459
460        let cursor = Cursor::new(xml);
461        let mut parser = XmlParser::new(cursor);
462
463        loop {
464            let event = parser.read_event();
465            match event {
466                Ok(Event::Start(ref start)) | Ok(Event::Empty(ref start)) => {
467                    parser.push_namespace_scope(start);
468                    let attrs = parser
469                        .extract_attributes(start)
470                        .expect("test: should succeed");
471
472                    // Find specific attributes
473                    let master_name = attrs
474                        .iter()
475                        .find(|(k, _)| k == "master-name")
476                        .map(|(_, v)| v.as_str());
477
478                    assert_eq!(master_name, Some("A4"));
479
480                    let page_width = attrs
481                        .iter()
482                        .find(|(k, _)| k == "page-width")
483                        .map(|(_, v)| v.as_str());
484
485                    assert_eq!(page_width, Some("210mm"));
486
487                    break;
488                }
489                Ok(Event::Eof) => break,
490                Err(e) => panic!("Parse error: {}", e),
491                _ => {}
492            }
493        }
494    }
495
496    #[test]
497    fn test_cdata_section() {
498        let xml = r#"<?xml version="1.0"?>
499<fo:block xmlns:fo="http://www.w3.org/1999/XSL/Format">
500    <![CDATA[<tag> & "quotes"]]>
501</fo:block>"#;
502
503        let cursor = Cursor::new(xml);
504        let mut parser = XmlParser::new(cursor);
505
506        let mut found_cdata = false;
507        let mut cdata_content = String::new();
508
509        loop {
510            match parser.read_event() {
511                Ok(Event::CData(ref cdata)) => {
512                    found_cdata = true;
513                    cdata_content = parser.extract_cdata(cdata).expect("test: should succeed");
514                }
515                Ok(Event::Eof) => break,
516                Ok(_) => {}
517                Err(e) => panic!("Parse error: {}", e),
518            }
519        }
520
521        assert!(found_cdata);
522        assert_eq!(cdata_content, r#"<tag> & "quotes""#);
523    }
524
525    #[test]
526    fn test_entity_resolution_builtin() {
527        let resolver = EntityResolver::new();
528        let location = Location::new(1, 1);
529
530        assert_eq!(
531            resolver
532                .resolve("amp", location)
533                .expect("test: should succeed"),
534            "&"
535        );
536        assert_eq!(
537            resolver
538                .resolve("lt", location)
539                .expect("test: should succeed"),
540            "<"
541        );
542        assert_eq!(
543            resolver
544                .resolve("gt", location)
545                .expect("test: should succeed"),
546            ">"
547        );
548        assert_eq!(
549            resolver
550                .resolve("quot", location)
551                .expect("test: should succeed"),
552            "\""
553        );
554        assert_eq!(
555            resolver
556                .resolve("apos", location)
557                .expect("test: should succeed"),
558            "'"
559        );
560    }
561
562    #[test]
563    fn test_entity_resolution_numeric_decimal() {
564        let resolver = EntityResolver::new();
565        let location = Location::new(1, 1);
566
567        // &#65; = 'A'
568        assert_eq!(
569            resolver
570                .resolve("#65", location)
571                .expect("test: should succeed"),
572            "A"
573        );
574        // &#36; = '$'
575        assert_eq!(
576            resolver
577                .resolve("#36", location)
578                .expect("test: should succeed"),
579            "$"
580        );
581    }
582
583    #[test]
584    fn test_entity_resolution_numeric_hex() {
585        let resolver = EntityResolver::new();
586        let location = Location::new(1, 1);
587
588        // &#x41; = 'A'
589        assert_eq!(
590            resolver
591                .resolve("#x41", location)
592                .expect("test: should succeed"),
593            "A"
594        );
595        assert_eq!(
596            resolver
597                .resolve("#X41", location)
598                .expect("test: should succeed"),
599            "A"
600        );
601        // &#xA9; = '©'
602        assert_eq!(
603            resolver
604                .resolve("#xA9", location)
605                .expect("test: should succeed"),
606            "©"
607        );
608    }
609
610    #[test]
611    fn test_entity_resolution_custom() {
612        let mut resolver = EntityResolver::new();
613        resolver.add_entity("copy".to_string(), "©".to_string());
614
615        let location = Location::new(1, 1);
616        assert_eq!(
617            resolver
618                .resolve("copy", location)
619                .expect("test: should succeed"),
620            "©"
621        );
622    }
623
624    #[test]
625    fn test_entity_resolution_in_text() {
626        let resolver = EntityResolver::new();
627        let location = Location::new(1, 1);
628
629        let text = "Price: &#36;100 &amp; up";
630        let resolved = resolver
631            .resolve_entities(text, location)
632            .expect("test: should succeed");
633        assert_eq!(resolved, "Price: $100 & up");
634    }
635
636    #[test]
637    fn test_entity_resolution_unknown() {
638        let resolver = EntityResolver::new();
639        let location = Location::new(1, 1);
640
641        let result = resolver.resolve("unknown", location);
642        assert!(result.is_err());
643    }
644
645    #[test]
646    fn test_processing_instruction() {
647        let xml = r#"<?xml version="1.0"?>
648<?xml-stylesheet type="text/xsl" href="style.xsl"?>
649<?fop-renderer backend="pdf"?>
650<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
651</fo:root>"#;
652
653        let cursor = Cursor::new(xml);
654        let mut parser = XmlParser::new(cursor);
655
656        loop {
657            match parser.read_event() {
658                Ok(Event::Eof) => break,
659                Ok(_) => {}
660                Err(e) => panic!("Parse error: {}", e),
661            }
662        }
663
664        let pis = parser.processing_instructions();
665        assert_eq!(pis.len(), 2);
666
667        assert_eq!(pis[0].target, "xml-stylesheet");
668        assert!(pis[0].data.is_some());
669
670        assert_eq!(pis[1].target, "fop-renderer");
671        assert!(pis[1].data.is_some());
672    }
673
674    #[test]
675    fn test_entities_in_attributes() {
676        let xml = r#"<?xml version="1.0"?>
677<fo:block xmlns:fo="http://www.w3.org/1999/XSL/Format" title="Test &amp; More">
678</fo:block>"#;
679
680        let cursor = Cursor::new(xml);
681        let mut parser = XmlParser::new(cursor);
682
683        loop {
684            match parser.read_event() {
685                Ok(Event::Start(ref start)) | Ok(Event::Empty(ref start)) => {
686                    parser.push_namespace_scope(start);
687                    let attrs = parser
688                        .extract_attributes(start)
689                        .expect("test: should succeed");
690
691                    let title = attrs
692                        .iter()
693                        .find(|(k, _)| k == "title")
694                        .map(|(_, v)| v.as_str());
695
696                    assert_eq!(title, Some("Test & More"));
697                    break;
698                }
699                Ok(Event::Eof) => break,
700                Ok(_) => {}
701                Err(e) => panic!("Parse error: {}", e),
702            }
703        }
704    }
705
706    #[test]
707    fn test_cdata_preserves_content() {
708        let xml = r#"<?xml version="1.0"?>
709<fo:block xmlns:fo="http://www.w3.org/1999/XSL/Format">
710    <![CDATA[Code with <tags> & "special" &amp; chars]]>
711</fo:block>"#;
712
713        let cursor = Cursor::new(xml);
714        let mut parser = XmlParser::new(cursor);
715
716        let mut cdata_content = String::new();
717
718        loop {
719            match parser.read_event() {
720                Ok(Event::CData(ref cdata)) => {
721                    cdata_content = parser.extract_cdata(cdata).expect("test: should succeed");
722                }
723                Ok(Event::Eof) => break,
724                Ok(_) => {}
725                Err(e) => panic!("Parse error: {}", e),
726            }
727        }
728
729        // CDATA should preserve everything, including &amp;
730        assert_eq!(cdata_content, r#"Code with <tags> & "special" &amp; chars"#);
731    }
732
733    #[test]
734    fn test_multiple_entities() {
735        let resolver = EntityResolver::new();
736        let location = Location::new(1, 1);
737
738        let text = "&lt;tag&gt; &amp; &quot;text&quot;";
739        let resolved = resolver
740            .resolve_entities(text, location)
741            .expect("test: should succeed");
742        assert_eq!(resolved, r#"<tag> & "text""#);
743    }
744
745    #[test]
746    fn test_unterminated_entity() {
747        let resolver = EntityResolver::new();
748        let location = Location::new(1, 1);
749
750        let text = "&amp no semicolon";
751        let result = resolver.resolve_entities(text, location);
752        assert!(result.is_err());
753    }
754
755    #[test]
756    fn test_location_tracking() {
757        let xml = r#"<?xml version="1.0"?>
758<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
759</fo:root>"#;
760
761        let cursor = Cursor::new(xml);
762        let parser = XmlParser::new(cursor);
763
764        // Location should be available (just verify we can get it)
765        let _location = parser.location();
766    }
767
768    #[test]
769    fn test_error_with_location() {
770        let xml = r#"<?xml version="1.0"?>
771<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
772    <unclosed-tag>
773</fo:root>"#;
774
775        let cursor = Cursor::new(xml);
776        let mut parser = XmlParser::new(cursor);
777
778        let mut error_found = false;
779
780        loop {
781            match parser.read_event() {
782                Ok(Event::Eof) => break,
783                Ok(_) => {}
784                Err(e) => {
785                    error_found = true;
786                    // Error should contain location information
787                    let error_str = format!("{}", e);
788                    assert!(error_str.contains("line") || error_str.contains("XML parsing error"));
789                    break;
790                }
791            }
792        }
793
794        assert!(error_found);
795    }
796}
797
798// ===== ADDITIONAL TESTS (60+ new tests) =====
799
800#[cfg(test)]
801mod additional_tests {
802    use super::*;
803    use std::io::Cursor;
804
805    // ===== ENTITY RESOLVER EDGE CASES =====
806
807    #[test]
808    fn test_entity_resolver_apos() {
809        let resolver = EntityResolver::new();
810        let location = Location::new(1, 1);
811        assert_eq!(
812            resolver
813                .resolve("apos", location)
814                .expect("test: should succeed"),
815            "'"
816        );
817    }
818
819    #[test]
820    fn test_entity_resolver_quot() {
821        let resolver = EntityResolver::new();
822        let location = Location::new(1, 1);
823        assert_eq!(
824            resolver
825                .resolve("quot", location)
826                .expect("test: should succeed"),
827            "\""
828        );
829    }
830
831    #[test]
832    fn test_entity_resolver_gt() {
833        let resolver = EntityResolver::new();
834        let location = Location::new(1, 1);
835        assert_eq!(
836            resolver
837                .resolve("gt", location)
838                .expect("test: should succeed"),
839            ">"
840        );
841    }
842
843    #[test]
844    fn test_entity_resolver_empty_text() {
845        let resolver = EntityResolver::new();
846        let location = Location::new(1, 1);
847        let result = resolver
848            .resolve_entities("", location)
849            .expect("test: should succeed");
850        assert_eq!(result, "");
851    }
852
853    #[test]
854    fn test_entity_resolver_text_without_entities() {
855        let resolver = EntityResolver::new();
856        let location = Location::new(1, 1);
857        let result = resolver
858            .resolve_entities("hello world", location)
859            .expect("test: should succeed");
860        assert_eq!(result, "hello world");
861    }
862
863    #[test]
864    fn test_entity_resolver_only_entity() {
865        let resolver = EntityResolver::new();
866        let location = Location::new(1, 1);
867        let result = resolver
868            .resolve_entities("&amp;", location)
869            .expect("test: should succeed");
870        assert_eq!(result, "&");
871    }
872
873    #[test]
874    fn test_entity_resolver_hex_zero() {
875        // &#x0041; = 'A'
876        let resolver = EntityResolver::new();
877        let location = Location::new(1, 1);
878        let result = resolver
879            .resolve("#x0041", location)
880            .expect("test: should succeed");
881        assert_eq!(result, "A");
882    }
883
884    #[test]
885    fn test_entity_resolver_decimal_newline() {
886        // &#10; = newline
887        let resolver = EntityResolver::new();
888        let location = Location::new(1, 1);
889        let result = resolver
890            .resolve("#10", location)
891            .expect("test: should succeed");
892        assert_eq!(result, "\n");
893    }
894
895    #[test]
896    fn test_entity_resolver_decimal_tab() {
897        // &#9; = tab
898        let resolver = EntityResolver::new();
899        let location = Location::new(1, 1);
900        let result = resolver
901            .resolve("#9", location)
902            .expect("test: should succeed");
903        assert_eq!(result, "\t");
904    }
905
906    #[test]
907    fn test_entity_resolver_unicode_multibyte() {
908        // &#x4e2d; = '中' (U+4E2D, Chinese character)
909        let resolver = EntityResolver::new();
910        let location = Location::new(1, 1);
911        let result = resolver
912            .resolve("#x4e2d", location)
913            .expect("test: should succeed");
914        assert_eq!(result, "中");
915    }
916
917    #[test]
918    fn test_entity_resolver_add_multiple_custom() {
919        let mut resolver = EntityResolver::new();
920        resolver.add_entity("euro".to_string(), "€".to_string());
921        resolver.add_entity("yen".to_string(), "¥".to_string());
922        resolver.add_entity("pound".to_string(), "£".to_string());
923
924        let location = Location::new(1, 1);
925        assert_eq!(
926            resolver
927                .resolve("euro", location)
928                .expect("test: should succeed"),
929            "€"
930        );
931        assert_eq!(
932            resolver
933                .resolve("yen", location)
934                .expect("test: should succeed"),
935            "¥"
936        );
937        assert_eq!(
938            resolver
939                .resolve("pound", location)
940                .expect("test: should succeed"),
941            "£"
942        );
943    }
944
945    #[test]
946    fn test_entity_resolver_override_custom() {
947        let mut resolver = EntityResolver::new();
948        // Override the built-in amp entity
949        resolver.add_entity("amp".to_string(), "AMPERSAND".to_string());
950
951        let location = Location::new(1, 1);
952        assert_eq!(
953            resolver
954                .resolve("amp", location)
955                .expect("test: should succeed"),
956            "AMPERSAND"
957        );
958    }
959
960    #[test]
961    fn test_entity_resolver_resolve_entities_multiple() {
962        let resolver = EntityResolver::new();
963        let location = Location::new(1, 1);
964        let text = "&lt;&gt;&amp;&quot;&apos;";
965        let result = resolver
966            .resolve_entities(text, location)
967            .expect("test: should succeed");
968        assert_eq!(result, "<>&\"'");
969    }
970
971    #[test]
972    fn test_entity_resolver_numeric_in_text() {
973        let resolver = EntityResolver::new();
974        let location = Location::new(1, 1);
975        let text = "A&#65;B&#66;C";
976        let result = resolver
977            .resolve_entities(text, location)
978            .expect("test: should succeed");
979        assert_eq!(result, "AABBC");
980    }
981
982    #[test]
983    fn test_entity_resolver_hex_uppercase() {
984        // &#X41; (uppercase X) should also resolve
985        let resolver = EntityResolver::new();
986        let location = Location::new(1, 1);
987        let result = resolver
988            .resolve("#X41", location)
989            .expect("test: should succeed");
990        assert_eq!(result, "A");
991    }
992
993    // ===== PROCESSING INSTRUCTION TESTS =====
994
995    #[test]
996    fn test_processing_instruction_new() {
997        let pi = ProcessingInstruction::new("target".to_string(), Some("data".to_string()));
998        assert_eq!(pi.target, "target");
999        assert_eq!(pi.data, Some("data".to_string()));
1000    }
1001
1002    #[test]
1003    fn test_processing_instruction_no_data() {
1004        let pi = ProcessingInstruction::new("target".to_string(), None);
1005        assert_eq!(pi.target, "target");
1006        assert!(pi.data.is_none());
1007    }
1008
1009    #[test]
1010    fn test_processing_instruction_equality() {
1011        let pi1 = ProcessingInstruction::new("foo".to_string(), Some("bar".to_string()));
1012        let pi2 = ProcessingInstruction::new("foo".to_string(), Some("bar".to_string()));
1013        assert_eq!(pi1, pi2);
1014    }
1015
1016    #[test]
1017    fn test_processing_instruction_inequality() {
1018        let pi1 = ProcessingInstruction::new("foo".to_string(), Some("bar".to_string()));
1019        let pi2 = ProcessingInstruction::new("baz".to_string(), Some("bar".to_string()));
1020        assert_ne!(pi1, pi2);
1021    }
1022
1023    // ===== NAMESPACE TESTS =====
1024
1025    #[test]
1026    fn test_nested_namespace_declarations() {
1027        let xml = r#"<?xml version="1.0"?>
1028<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format"
1029         xmlns:svg="http://www.w3.org/2000/svg">
1030    <fo:layout-master-set></fo:layout-master-set>
1031</fo:root>"#;
1032
1033        let cursor = Cursor::new(xml);
1034        let mut parser = XmlParser::new(cursor);
1035
1036        let mut found_root = false;
1037        loop {
1038            let event = parser.read_event();
1039            match event {
1040                Ok(Event::Start(ref start)) | Ok(Event::Empty(ref start)) => {
1041                    parser.push_namespace_scope(start);
1042                    let result = parser.extract_name(start);
1043                    if let Ok((name, ns)) = result {
1044                        if name == "root" && ns.is_fo() {
1045                            found_root = true;
1046                        }
1047                    }
1048                }
1049                Ok(Event::Eof) => break,
1050                Err(e) => panic!("Parse error: {}", e),
1051                _ => {}
1052            }
1053        }
1054        assert!(found_root);
1055    }
1056
1057    #[test]
1058    fn test_fox_extension_namespace() {
1059        let xml = r#"<?xml version="1.0"?>
1060<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format"
1061         xmlns:fox="http://xmlgraphics.apache.org/fop/extensions">
1062    <fo:layout-master-set></fo:layout-master-set>
1063</fo:root>"#;
1064
1065        let cursor = Cursor::new(xml);
1066        let mut parser = XmlParser::new(cursor);
1067
1068        let mut found_root = false;
1069        loop {
1070            let event = parser.read_event();
1071            match event {
1072                Ok(Event::Start(ref start)) | Ok(Event::Empty(ref start)) => {
1073                    parser.push_namespace_scope(start);
1074                    if let Ok((name, ns)) = parser.extract_name(start) {
1075                        if name == "root" && ns.is_fo() {
1076                            found_root = true;
1077                        }
1078                    }
1079                }
1080                Ok(Event::Eof) => break,
1081                Err(e) => panic!("Parse error: {}", e),
1082                _ => {}
1083            }
1084        }
1085        assert!(found_root);
1086    }
1087
1088    // ===== XML PARSER EVENT TESTS =====
1089
1090    #[test]
1091    fn test_empty_element_produces_start_end() {
1092        // With expand_empty_elements=true, empty elements produce Start+End
1093        let xml = r#"<?xml version="1.0"?>
1094<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
1095    <fo:layout-master-set>
1096        <fo:simple-page-master master-name="A4">
1097            <fo:region-body/>
1098        </fo:simple-page-master>
1099    </fo:layout-master-set>
1100</fo:root>"#;
1101
1102        let cursor = Cursor::new(xml);
1103        let mut parser = XmlParser::new(cursor);
1104
1105        let mut element_count = 0;
1106        loop {
1107            match parser.read_event() {
1108                Ok(Event::Start(ref start)) => {
1109                    parser.push_namespace_scope(start);
1110                    element_count += 1;
1111                }
1112                Ok(Event::Eof) => break,
1113                Ok(_) => {}
1114                Err(e) => panic!("Parse error: {}", e),
1115            }
1116        }
1117        // root, layout-master-set, simple-page-master, region-body (expanded from empty)
1118        assert!(element_count >= 4);
1119    }
1120
1121    #[test]
1122    fn test_multiple_attributes_preserved_order() {
1123        let xml = r#"<?xml version="1.0"?>
1124<fo:block xmlns:fo="http://www.w3.org/1999/XSL/Format"
1125    font-size="12pt"
1126    font-family="Arial"
1127    color="black"
1128    margin-top="10pt">text</fo:block>"#;
1129
1130        let cursor = Cursor::new(xml);
1131        let mut parser = XmlParser::new(cursor);
1132
1133        loop {
1134            match parser.read_event() {
1135                Ok(Event::Start(ref start)) => {
1136                    parser.push_namespace_scope(start);
1137                    let attrs = parser
1138                        .extract_attributes(start)
1139                        .expect("test: should succeed");
1140                    // xmlns attrs are skipped, so we expect 4 non-namespace attrs
1141                    assert_eq!(attrs.len(), 4);
1142                    break;
1143                }
1144                Ok(Event::Eof) => break,
1145                Ok(_) => {}
1146                Err(e) => panic!("Parse error: {}", e),
1147            }
1148        }
1149    }
1150
1151    #[test]
1152    fn test_text_with_special_chars_in_cdata() {
1153        let xml = r#"<?xml version="1.0"?>
1154<fo:block xmlns:fo="http://www.w3.org/1999/XSL/Format"><![CDATA[a < b && c > d]]></fo:block>"#;
1155
1156        let cursor = Cursor::new(xml);
1157        let mut parser = XmlParser::new(cursor);
1158
1159        let mut cdata_text = String::new();
1160        loop {
1161            match parser.read_event() {
1162                Ok(Event::CData(ref cdata)) => {
1163                    cdata_text = parser.extract_cdata(cdata).expect("test: should succeed");
1164                }
1165                Ok(Event::Eof) => break,
1166                Ok(_) => {}
1167                Err(e) => panic!("Parse error: {}", e),
1168            }
1169        }
1170        assert_eq!(cdata_text, "a < b && c > d");
1171    }
1172
1173    #[test]
1174    fn test_extract_cdata_preserves_angle_brackets() {
1175        let xml = r#"<?xml version="1.0"?>
1176<fo:block xmlns:fo="http://www.w3.org/1999/XSL/Format"><![CDATA[<tag attr="val"/>]]></fo:block>"#;
1177
1178        let cursor = Cursor::new(xml);
1179        let mut parser = XmlParser::new(cursor);
1180
1181        let mut cdata_text = String::new();
1182        loop {
1183            match parser.read_event() {
1184                Ok(Event::CData(ref cdata)) => {
1185                    cdata_text = parser.extract_cdata(cdata).expect("test: should succeed");
1186                }
1187                Ok(Event::Eof) => break,
1188                Ok(_) => {}
1189                Err(e) => panic!("Parse error: {}", e),
1190            }
1191        }
1192        assert_eq!(cdata_text, r#"<tag attr="val"/>"#);
1193    }
1194
1195    #[test]
1196    fn test_comment_does_not_produce_text_event() {
1197        let xml = r#"<?xml version="1.0"?>
1198<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format"><!-- this is a comment --></fo:root>"#;
1199
1200        let cursor = Cursor::new(xml);
1201        let mut parser = XmlParser::new(cursor);
1202
1203        let mut text_events = 0;
1204        loop {
1205            match parser.read_event() {
1206                Ok(Event::Text(_)) => {
1207                    text_events += 1;
1208                }
1209                Ok(Event::Eof) => break,
1210                Ok(_) => {}
1211                Err(e) => panic!("Parse error: {}", e),
1212            }
1213        }
1214        // Comments should not produce text events; trim_text should remove empty whitespace
1215        assert_eq!(text_events, 0);
1216    }
1217
1218    #[test]
1219    fn test_multiple_processing_instructions() {
1220        let xml = r#"<?xml version="1.0"?>
1221<?stylesheet type="text/css"?>
1222<?renderer backend="pdf"?>
1223<?custom-pi data="value"?>
1224<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format"></fo:root>"#;
1225
1226        let cursor = Cursor::new(xml);
1227        let mut parser = XmlParser::new(cursor);
1228
1229        loop {
1230            match parser.read_event() {
1231                Ok(Event::Eof) => break,
1232                Ok(_) => {}
1233                Err(e) => panic!("Parse error: {}", e),
1234            }
1235        }
1236
1237        let pis = parser.processing_instructions();
1238        assert_eq!(pis.len(), 3);
1239        assert_eq!(pis[0].target, "stylesheet");
1240        assert_eq!(pis[1].target, "renderer");
1241        assert_eq!(pis[2].target, "custom-pi");
1242    }
1243
1244    #[test]
1245    fn test_no_processing_instructions_when_none_present() {
1246        let xml = r#"<?xml version="1.0"?>
1247<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format"></fo:root>"#;
1248
1249        let cursor = Cursor::new(xml);
1250        let mut parser = XmlParser::new(cursor);
1251
1252        loop {
1253            match parser.read_event() {
1254                Ok(Event::Eof) => break,
1255                Ok(_) => {}
1256                Err(e) => panic!("Parse error: {}", e),
1257            }
1258        }
1259
1260        let pis = parser.processing_instructions();
1261        assert_eq!(pis.len(), 0);
1262    }
1263
1264    #[test]
1265    fn test_attributes_with_apos_entity() {
1266        let xml = r#"<?xml version="1.0"?>
1267<fo:block xmlns:fo="http://www.w3.org/1999/XSL/Format" title="it&apos;s">text</fo:block>"#;
1268
1269        let cursor = Cursor::new(xml);
1270        let mut parser = XmlParser::new(cursor);
1271
1272        loop {
1273            match parser.read_event() {
1274                Ok(Event::Start(ref start)) => {
1275                    parser.push_namespace_scope(start);
1276                    let attrs = parser
1277                        .extract_attributes(start)
1278                        .expect("test: should succeed");
1279                    let title = attrs
1280                        .iter()
1281                        .find(|(k, _)| k == "title")
1282                        .map(|(_, v)| v.as_str());
1283                    assert_eq!(title, Some("it's"));
1284                    break;
1285                }
1286                Ok(Event::Eof) => break,
1287                Ok(_) => {}
1288                Err(e) => panic!("Parse error: {}", e),
1289            }
1290        }
1291    }
1292
1293    #[test]
1294    fn test_attributes_with_lt_entity() {
1295        let xml = r#"<?xml version="1.0"?>
1296<fo:block xmlns:fo="http://www.w3.org/1999/XSL/Format" title="a &lt; b">text</fo:block>"#;
1297
1298        let cursor = Cursor::new(xml);
1299        let mut parser = XmlParser::new(cursor);
1300
1301        loop {
1302            match parser.read_event() {
1303                Ok(Event::Start(ref start)) => {
1304                    parser.push_namespace_scope(start);
1305                    let attrs = parser
1306                        .extract_attributes(start)
1307                        .expect("test: should succeed");
1308                    let title = attrs
1309                        .iter()
1310                        .find(|(k, _)| k == "title")
1311                        .map(|(_, v)| v.as_str());
1312                    assert_eq!(title, Some("a < b"));
1313                    break;
1314                }
1315                Ok(Event::Eof) => break,
1316                Ok(_) => {}
1317                Err(e) => panic!("Parse error: {}", e),
1318            }
1319        }
1320    }
1321
1322    #[test]
1323    fn test_attribute_with_numeric_entity() {
1324        let xml = r#"<?xml version="1.0"?>
1325<fo:block xmlns:fo="http://www.w3.org/1999/XSL/Format" title="&#65;BC">text</fo:block>"#;
1326
1327        let cursor = Cursor::new(xml);
1328        let mut parser = XmlParser::new(cursor);
1329
1330        loop {
1331            match parser.read_event() {
1332                Ok(Event::Start(ref start)) => {
1333                    parser.push_namespace_scope(start);
1334                    let attrs = parser
1335                        .extract_attributes(start)
1336                        .expect("test: should succeed");
1337                    let title = attrs
1338                        .iter()
1339                        .find(|(k, _)| k == "title")
1340                        .map(|(_, v)| v.as_str());
1341                    assert_eq!(title, Some("ABC"));
1342                    break;
1343                }
1344                Ok(Event::Eof) => break,
1345                Ok(_) => {}
1346                Err(e) => panic!("Parse error: {}", e),
1347            }
1348        }
1349    }
1350
1351    #[test]
1352    fn test_xml_with_utf8_text() {
1353        let xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<fo:root xmlns:fo=\"http://www.w3.org/1999/XSL/Format\"><fo:block>日本語テスト</fo:block></fo:root>";
1354
1355        let cursor = Cursor::new(xml);
1356        let mut parser = XmlParser::new(cursor);
1357
1358        let mut text_content = String::new();
1359        loop {
1360            match parser.read_event() {
1361                Ok(Event::Text(ref text)) => {
1362                    text_content = parser.extract_text(text).expect("test: should succeed");
1363                }
1364                Ok(Event::Eof) => break,
1365                Ok(_) => {}
1366                Err(e) => panic!("Parse error: {}", e),
1367            }
1368        }
1369        assert_eq!(text_content, "日本語テスト");
1370    }
1371
1372    #[test]
1373    fn test_entity_resolver_clone() {
1374        let mut resolver = EntityResolver::new();
1375        resolver.add_entity("test".to_string(), "TEST_VALUE".to_string());
1376        let cloned = resolver.clone();
1377
1378        let location = Location::new(1, 1);
1379        assert_eq!(
1380            cloned
1381                .resolve("test", location)
1382                .expect("test: should succeed"),
1383            "TEST_VALUE"
1384        );
1385        assert_eq!(
1386            cloned
1387                .resolve("amp", location)
1388                .expect("test: should succeed"),
1389            "&"
1390        );
1391    }
1392
1393    #[test]
1394    fn test_entity_resolver_default() {
1395        let resolver = EntityResolver::default();
1396        let location = Location::new(1, 1);
1397        // Default should have all 5 built-in entities
1398        assert_eq!(
1399            resolver
1400                .resolve("amp", location)
1401                .expect("test: should succeed"),
1402            "&"
1403        );
1404        assert_eq!(
1405            resolver
1406                .resolve("lt", location)
1407                .expect("test: should succeed"),
1408            "<"
1409        );
1410        assert_eq!(
1411            resolver
1412                .resolve("gt", location)
1413                .expect("test: should succeed"),
1414            ">"
1415        );
1416        assert_eq!(
1417            resolver
1418                .resolve("quot", location)
1419                .expect("test: should succeed"),
1420            "\""
1421        );
1422        assert_eq!(
1423            resolver
1424                .resolve("apos", location)
1425                .expect("test: should succeed"),
1426            "'"
1427        );
1428    }
1429
1430    #[test]
1431    fn test_xml_deeply_nested_elements() {
1432        // Test parsing with many levels of nesting
1433        let xml = r#"<?xml version="1.0"?>
1434<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
1435    <fo:layout-master-set>
1436        <fo:simple-page-master master-name="p1">
1437            <fo:region-body/>
1438        </fo:simple-page-master>
1439    </fo:layout-master-set>
1440    <fo:page-sequence master-reference="p1">
1441        <fo:flow flow-name="xsl-region-body">
1442            <fo:block>
1443                <fo:inline>
1444                    <fo:inline>
1445                        <fo:inline>deep nesting</fo:inline>
1446                    </fo:inline>
1447                </fo:inline>
1448            </fo:block>
1449        </fo:flow>
1450    </fo:page-sequence>
1451</fo:root>"#;
1452
1453        let cursor = Cursor::new(xml);
1454        let mut parser = XmlParser::new(cursor);
1455        let mut error = None;
1456
1457        loop {
1458            match parser.read_event() {
1459                Ok(Event::Eof) => break,
1460                Ok(_) => {}
1461                Err(e) => {
1462                    error = Some(e);
1463                    break;
1464                }
1465            }
1466        }
1467        assert!(error.is_none(), "Deep nesting should parse without error");
1468    }
1469
1470    #[test]
1471    fn test_xml_empty_text_nodes_trimmed() {
1472        // With trim_text(true), whitespace-only text nodes are trimmed to empty
1473        let xml = r#"<?xml version="1.0"?>
1474<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
1475    <fo:layout-master-set>
1476    </fo:layout-master-set>
1477</fo:root>"#;
1478
1479        let cursor = Cursor::new(xml);
1480        let mut parser = XmlParser::new(cursor);
1481
1482        let mut non_empty_text = 0;
1483        loop {
1484            match parser.read_event() {
1485                Ok(Event::Text(ref text)) => {
1486                    let content = parser.extract_text(text).unwrap_or_default();
1487                    if !content.is_empty() {
1488                        non_empty_text += 1;
1489                    }
1490                }
1491                Ok(Event::Eof) => break,
1492                Ok(_) => {}
1493                Err(e) => panic!("Parse error: {}", e),
1494            }
1495        }
1496        // Whitespace-only nodes should be empty after trim
1497        assert_eq!(non_empty_text, 0);
1498    }
1499
1500    #[test]
1501    fn test_xml_pi_target_with_data() {
1502        let xml = r#"<?xml version="1.0"?>
1503<?fop-config key="value" other="data"?>
1504<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format"></fo:root>"#;
1505
1506        let cursor = Cursor::new(xml);
1507        let mut parser = XmlParser::new(cursor);
1508
1509        loop {
1510            match parser.read_event() {
1511                Ok(Event::Eof) => break,
1512                Ok(_) => {}
1513                Err(e) => panic!("Parse error: {}", e),
1514            }
1515        }
1516
1517        let pis = parser.processing_instructions();
1518        assert_eq!(pis.len(), 1);
1519        assert_eq!(pis[0].target, "fop-config");
1520        assert!(pis[0].data.is_some());
1521        let data = pis[0].data.as_ref().expect("test: should succeed");
1522        assert!(data.contains("key"));
1523    }
1524
1525    #[test]
1526    fn test_entity_resolver_unknown_entity_has_name_in_error() {
1527        let resolver = EntityResolver::new();
1528        let location = Location::new(5, 10);
1529        let result = resolver.resolve("nonexistent", location);
1530        assert!(result.is_err());
1531        let err = result.unwrap_err();
1532        let err_str = format!("{}", err);
1533        assert!(err_str.contains("nonexistent"));
1534    }
1535
1536    #[test]
1537    fn test_entity_resolver_invalid_hex_ref() {
1538        let resolver = EntityResolver::new();
1539        let location = Location::new(1, 1);
1540        // Non-hex characters after #x
1541        let result = resolver.resolve("#xZZZZ", location);
1542        assert!(result.is_err());
1543    }
1544
1545    #[test]
1546    fn test_entity_resolver_invalid_decimal_ref() {
1547        let resolver = EntityResolver::new();
1548        let location = Location::new(1, 1);
1549        // Non-decimal characters after #
1550        let result = resolver.resolve("#abc", location);
1551        assert!(result.is_err());
1552    }
1553
1554    // ===== NAMESPACE SCOPE STACK TESTS =====
1555
1556    #[test]
1557    fn test_namespace_scope_pop_restores_outer() {
1558        let xml = r#"<root xmlns:x="uri:outer"></root>"#;
1559        let cursor = Cursor::new(xml);
1560        let mut parser = XmlParser::new(cursor);
1561
1562        // Manually push scopes to test push/pop semantics
1563        let outer_start = BytesStart::from_content(r#"root xmlns:x="uri:outer""#, 4);
1564        let inner_start = BytesStart::from_content(r#"child xmlns:x="uri:inner""#, 5);
1565        parser.push_namespace_scope(&outer_start);
1566        parser.push_namespace_scope(&inner_start);
1567        assert_eq!(
1568            parser.resolve_prefix("x"),
1569            Some("uri:inner"),
1570            "inner scope should shadow outer"
1571        );
1572        parser.pop_namespace_scope();
1573        assert_eq!(
1574            parser.resolve_prefix("x"),
1575            Some("uri:outer"),
1576            "after pop, outer scope should be visible"
1577        );
1578        parser.pop_namespace_scope();
1579        assert_eq!(
1580            parser.resolve_prefix("x"),
1581            None,
1582            "after all pops, prefix should be unresolvable"
1583        );
1584    }
1585
1586    #[test]
1587    fn test_namespace_scope_sibling_rebind_does_not_leak() {
1588        let xml = r#"<root></root>"#;
1589        let cursor = Cursor::new(xml);
1590        let mut parser = XmlParser::new(cursor);
1591
1592        let sibling_a = BytesStart::from_content(r#"a xmlns:foo="uri:a""#, 1);
1593        parser.push_namespace_scope(&sibling_a);
1594        assert_eq!(parser.resolve_prefix("foo"), Some("uri:a"));
1595        parser.pop_namespace_scope();
1596
1597        // Sibling B has no xmlns:foo — must not inherit A's declaration
1598        let sibling_b = BytesStart::from_content(r#"b"#, 1);
1599        parser.push_namespace_scope(&sibling_b);
1600        assert_eq!(
1601            parser.resolve_prefix("foo"),
1602            None,
1603            "sibling's xmlns:foo must not be visible after pop"
1604        );
1605        parser.pop_namespace_scope();
1606    }
1607
1608    #[test]
1609    fn test_namespace_snapshot_in_scope_innermost_wins() {
1610        let xml = r#"<root></root>"#;
1611        let cursor = Cursor::new(xml);
1612        let mut parser = XmlParser::new(cursor);
1613
1614        let outer = BytesStart::from_content(r#"outer xmlns:x="outer:uri""#, 5);
1615        let inner = BytesStart::from_content(r#"inner xmlns:x="inner:uri""#, 5);
1616        parser.push_namespace_scope(&outer);
1617        parser.push_namespace_scope(&inner);
1618
1619        let snapshot = parser.snapshot_in_scope();
1620        let x_uri = snapshot
1621            .iter()
1622            .find(|(p, _)| p == "x")
1623            .map(|(_, u)| u.as_str());
1624        assert_eq!(
1625            x_uri,
1626            Some("inner:uri"),
1627            "innermost binding should win in snapshot"
1628        );
1629    }
1630
1631    #[test]
1632    fn test_namespace_scope_empty_prefix_default_namespace() {
1633        let xml = r#"<root></root>"#;
1634        let cursor = Cursor::new(xml);
1635        let mut parser = XmlParser::new(cursor);
1636
1637        let start = BytesStart::from_content(r#"root xmlns="http://example.com/""#, 4);
1638        parser.push_namespace_scope(&start);
1639        assert_eq!(
1640            parser.resolve_prefix(""),
1641            Some("http://example.com/"),
1642            "default namespace should be resolvable via empty-string prefix"
1643        );
1644        parser.pop_namespace_scope();
1645    }
1646
1647    #[test]
1648    fn test_namespace_resolve_prefix_returns_none_on_empty_stack() {
1649        let xml = r#"<root></root>"#;
1650        let cursor = Cursor::new(xml);
1651        let parser = XmlParser::new(cursor);
1652        assert_eq!(parser.resolve_prefix("fo"), None);
1653        assert_eq!(parser.resolve_prefix(""), None);
1654    }
1655}