extend-docx 0.1.0

Rust parser, document model, and serializer for DOCX and legacy Word documents
Documentation
use crate::xml::get_attribute;

pub fn starts_with_ignore_ascii_case(haystack: &str, prefix: &str) -> bool {
    let haystack_bytes = haystack.as_bytes();
    let prefix_bytes = prefix.as_bytes();
    haystack_bytes.len() >= prefix_bytes.len()
        && haystack_bytes[..prefix_bytes.len()]
            .iter()
            .zip(prefix_bytes.iter())
            .all(|(left, right)| left.eq_ignore_ascii_case(right))
}

fn has_word_boundary_after(xml: &str, index: usize) -> bool {
    xml.as_bytes()
        .get(index)
        .is_none_or(|byte| !byte.is_ascii_alphanumeric() && *byte != b'_')
}

pub(crate) fn find_tag_token_ref<'a>(xml: &'a str, tag_name: &str) -> Option<&'a str> {
    if let Some(tag) = crate::xml_index::tag(xml, tag_name) {
        return tag;
    }
    let name = tag_name.as_bytes();
    let mut cursor = 0;
    while let Some(offset) = xml[cursor..].find('<') {
        let start = cursor + offset;
        let after = start + 1 + name.len();
        if xml
            .as_bytes()
            .get(start + 1..after)
            .is_some_and(|value| value.eq_ignore_ascii_case(name))
            && has_word_boundary_after(xml, after)
        {
            let end = crate::xml::tag_token_end(xml, after)?;
            return Some(&xml[start..end]);
        }
        cursor = start + 1;
    }
    None
}

pub fn find_tag_token(xml: &str, tag_name: &str) -> Option<String> {
    find_tag_token_ref(xml, tag_name).map(str::to_owned)
}

pub fn find_all_tag_tokens(xml: &str, tag_name: &str) -> Vec<String> {
    let mut remaining = xml;
    let mut tags = Vec::new();
    while let Some(tag) = find_tag_token_ref(remaining, tag_name) {
        let end = tag.as_ptr() as usize - remaining.as_ptr() as usize + tag.len();
        tags.push(tag.to_owned());
        remaining = &remaining[end..];
    }
    tags
}

pub fn find_attribute_value_in_tag(xml: &str, tag_name: &str, attribute: &str) -> Option<String> {
    find_tag_token_ref(xml, tag_name).and_then(|tag| get_attribute(tag, attribute))
}

pub fn contains_tag(xml: &str, tag_name: &str) -> bool {
    find_tag_token_ref(xml, tag_name).is_some()
}

pub fn decode_xml_attribute(value: Option<&str>) -> Option<String> {
    value.map(crate::xml::decode_xml_entities)
}

#[cfg(test)]
mod tests {
    use super::find_tag_token;

    #[test]
    fn find_tag_token_does_not_panic_on_short_haystack() {
        assert_eq!(find_tag_token("abc", "w:p"), None);
        assert_eq!(find_tag_token("<w:t>☐</w:t>", "w:p"), None);
    }

    #[test]
    fn find_tag_token_finds_tags_in_unicode_text() {
        let xml = "<w:p><w:r><w:t>☐ choice</w:t></w:r></w:p>";
        assert!(find_tag_token(xml, "w:p").is_some());
    }
}