Skip to main content

rig_core/loaders/epub/
text_processors.rs

1use std::{convert::Infallible, error::Error};
2
3use quick_xml::Reader;
4use quick_xml::escape::{resolve_xml_entity, unescape_with};
5use quick_xml::events::Event;
6
7pub trait TextProcessor {
8    type Error: Error + 'static;
9
10    fn process(text: &str) -> Result<String, Self::Error>;
11}
12
13pub struct RawTextProcessor;
14
15impl TextProcessor for RawTextProcessor {
16    type Error = Infallible;
17
18    fn process(text: &str) -> Result<String, Self::Error> {
19        Ok(text.to_string())
20    }
21}
22
23#[derive(thiserror::Error, Debug)]
24pub enum XmlProcessingError {
25    #[error("XML parsing error: {0}")]
26    Xml(#[from] quick_xml::Error),
27
28    #[error("Failed to unescape XML entity: {0}")]
29    Encoding(#[from] quick_xml::encoding::EncodingError),
30
31    #[error("Invalid UTF-8 sequence: {0}")]
32    Utf8(#[from] std::string::FromUtf8Error),
33}
34
35/// Strip markup while resolving predefined XML entities and numeric character references.
36/// Unknown references are errors; DTD entity declarations are not expanded.
37pub struct StripXmlProcessor;
38
39impl TextProcessor for StripXmlProcessor {
40    type Error = XmlProcessingError;
41
42    fn process(xml: &str) -> Result<String, Self::Error> {
43        let mut reader = Reader::from_str(xml.trim());
44
45        let mut result = String::with_capacity(xml.len() / 2);
46        let mut last_was_text = false;
47        let mut text = String::new();
48
49        loop {
50            let event = reader.read_event()?;
51            match &event {
52                Event::Text(e) => {
53                    text.push_str(&e.decode()?);
54                    continue;
55                }
56                Event::GeneralRef(e) => {
57                    let reference = format!("&{};", e.decode()?);
58                    text.push_str(
59                        &unescape_with(&reference, resolve_xml_entity)
60                            .map_err(quick_xml::Error::from)?,
61                    );
62                    continue;
63                }
64                _ => {}
65            }
66
67            // References split a single text node into events, so defer whitespace
68            // filtering until the whole node is available, including spaces around refs.
69            if !text.trim().is_empty() {
70                if last_was_text {
71                    result.push(' ');
72                }
73                result.push_str(&text);
74                last_was_text = true;
75            }
76            text.clear();
77
78            // CDATA stays literal; markup resets text adjacency.
79            match event {
80                Event::CData(e) => {
81                    let text = String::from_utf8(e.into_inner().into_owned())?;
82                    if !text.trim().is_empty() {
83                        if last_was_text {
84                            result.push(' ');
85                        }
86                        result.push_str(&text);
87                        last_was_text = true;
88                    }
89                }
90                Event::Eof => break,
91                _ => {
92                    last_was_text = false;
93                }
94            }
95        }
96
97        Ok(result)
98    }
99}