fn opens_tag(xml: &[u8], pos: usize, tag: &str) -> bool {
    let t = tag.as_bytes();
    if pos + 1 + t.len() > xml.len() || xml[pos] != b'<' {
        return false;
    }
    if &xml[pos + 1..pos + 1 + t.len()] != t {
        return false;
    }
    matches!(
        xml.get(pos + 1 + t.len()),
        Some(b'>') | Some(b' ') | Some(b'/') | Some(b'\t') | Some(b'\n') | Some(b'\r')
    )
}

/// Inner text of the first `<tag>…</tag>` at/after `from` and the offset past its close; depth-tracked against nested same-name tags.
pub(crate) fn inner_from<'a>(xml: &'a str, tag: &str, from: usize) -> Option<(&'a str, usize)> {
    let bytes = xml.as_bytes();
    let close = format!("</{tag}>");
    let cb = close.as_bytes();

    let mut i = from;
    let content_start = loop {
        if i >= bytes.len() {
            return None;
        }
        if opens_tag(bytes, i, tag) {
            let after_name = i + 1 + tag.len();
            let gt = after_name + xml[after_name..].find('>')?;
            if bytes[gt - 1] == b'/' {
                return Some(("", gt + 1));
            }
            break gt + 1;
        }
        i += 1;
    };

    let mut depth = 1usize;
    let mut j = content_start;
    while j < bytes.len() {
        if bytes[j] == b'<' {
            if bytes[j..].starts_with(cb) {
                depth -= 1;
                if depth == 0 {
                    return Some((&xml[content_start..j], j + cb.len()));
                }
                j += cb.len();
                continue;
            }
            if opens_tag(bytes, j, tag) {
                let after = j + 1 + tag.len();
                if let Some(rel) = xml[after..].find('>') {
                    let g = after + rel;
                    if bytes[g - 1] != b'/' {
                        depth += 1;
                    }
                    j = g + 1;
                    continue;
                }
            }
        }
        j += 1;
    }
    None
}

pub(crate) fn inner<'a>(xml: &'a str, tag: &str) -> Option<&'a str> {
    inner_from(xml, tag, 0).map(|(s, _)| s)
}

pub(crate) fn inners<'a>(xml: &'a str, tag: &str) -> Vec<&'a str> {
    let mut out = Vec::new();
    let mut from = 0;
    while let Some((s, next)) = inner_from(xml, tag, from) {
        out.push(s);
        from = next;
    }
    out
}

pub(crate) fn unescape(s: &str) -> String {
    if !s.contains('&') {
        return s.to_string();
    }
    s.replace("&lt;", "<")
        .replace("&gt;", ">")
        .replace("&quot;", "\"")
        .replace("&apos;", "'")
        .replace("&amp;", "&")
}
