Skip to main content

rust_docx/parse/
scan.rs

1use crate::xml::get_attribute;
2
3pub fn starts_with_ignore_ascii_case(haystack: &str, prefix: &str) -> bool {
4    let haystack_bytes = haystack.as_bytes();
5    let prefix_bytes = prefix.as_bytes();
6    haystack_bytes.len() >= prefix_bytes.len()
7        && haystack_bytes[..prefix_bytes.len()]
8            .iter()
9            .zip(prefix_bytes.iter())
10            .all(|(left, right)| left.eq_ignore_ascii_case(right))
11}
12
13fn has_word_boundary_after(xml: &str, index: usize) -> bool {
14    xml.as_bytes()
15        .get(index)
16        .is_none_or(|byte| !byte.is_ascii_alphanumeric() && *byte != b'_')
17}
18
19pub(crate) fn find_tag_token_ref<'a>(xml: &'a str, tag_name: &str) -> Option<&'a str> {
20    if let Some(tag) = crate::xml_index::tag(xml, tag_name) {
21        return tag;
22    }
23    let name = tag_name.as_bytes();
24    let mut cursor = 0;
25    while let Some(offset) = xml[cursor..].find('<') {
26        let start = cursor + offset;
27        let after = start + 1 + name.len();
28        if xml
29            .as_bytes()
30            .get(start + 1..after)
31            .is_some_and(|value| value.eq_ignore_ascii_case(name))
32            && has_word_boundary_after(xml, after)
33        {
34            let end = crate::xml::tag_token_end(xml, after)?;
35            return Some(&xml[start..end]);
36        }
37        cursor = start + 1;
38    }
39    None
40}
41
42pub fn find_tag_token(xml: &str, tag_name: &str) -> Option<String> {
43    find_tag_token_ref(xml, tag_name).map(str::to_owned)
44}
45
46pub fn find_all_tag_tokens(xml: &str, tag_name: &str) -> Vec<String> {
47    let mut remaining = xml;
48    let mut tags = Vec::new();
49    while let Some(tag) = find_tag_token_ref(remaining, tag_name) {
50        let end = tag.as_ptr() as usize - remaining.as_ptr() as usize + tag.len();
51        tags.push(tag.to_owned());
52        remaining = &remaining[end..];
53    }
54    tags
55}
56
57pub fn find_attribute_value_in_tag(xml: &str, tag_name: &str, attribute: &str) -> Option<String> {
58    find_tag_token_ref(xml, tag_name).and_then(|tag| get_attribute(tag, attribute))
59}
60
61pub fn contains_tag(xml: &str, tag_name: &str) -> bool {
62    find_tag_token_ref(xml, tag_name).is_some()
63}
64
65pub fn decode_xml_attribute(value: Option<&str>) -> Option<String> {
66    value.map(crate::xml::decode_xml_entities)
67}
68
69#[cfg(test)]
70mod tests {
71    use super::find_tag_token;
72
73    #[test]
74    fn find_tag_token_does_not_panic_on_short_haystack() {
75        assert_eq!(find_tag_token("abc", "w:p"), None);
76        assert_eq!(find_tag_token("<w:t>☐</w:t>", "w:p"), None);
77    }
78
79    #[test]
80    fn find_tag_token_finds_tags_in_unicode_text() {
81        let xml = "<w:p><w:r><w:t>☐ choice</w:t></w:r></w:p>";
82        assert!(find_tag_token(xml, "w:p").is_some());
83    }
84}