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}