#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(in crate::formats) enum Kind {
Open,
Close,
Empty,
}
#[derive(Debug, Clone)]
pub(in crate::formats) struct Tag<'a> {
pub(in crate::formats) name: &'a str,
pub(in crate::formats) kind: Kind,
pub(in crate::formats) start: usize,
pub(in crate::formats) end: usize,
pub(in crate::formats) raw: &'a str,
}
impl<'a> Tag<'a> {
pub(in crate::formats) fn attribute(&self, name: &str) -> Option<&'a str> {
attributes(self.raw)
.into_iter()
.find(|attr| attr.name == name)
.map(|attr| attr.value)
}
}
pub(in crate::formats) struct Attribute<'a> {
pub(in crate::formats) name: &'a str,
pub(in crate::formats) value: &'a str,
pub(in crate::formats) start: usize,
pub(in crate::formats) end: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(in crate::formats) enum NonElementKind {
Comment,
Cdata,
ProcessingInstruction,
Doctype,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(in crate::formats) struct NonElement {
pub(in crate::formats) kind: NonElementKind,
pub(in crate::formats) start: usize,
pub(in crate::formats) end: usize,
}
pub(in crate::formats) struct Scan<'a> {
pub(in crate::formats) tags: Vec<Tag<'a>>,
pub(in crate::formats) others: Vec<NonElement>,
}
pub(in crate::formats) fn tags(src: &str) -> Vec<Tag<'_>> {
scan(src).tags
}
pub(in crate::formats) fn scan(src: &str) -> Scan<'_> {
let bytes = src.as_bytes();
let mut tags = Vec::new();
let mut others = Vec::new();
let mut i = 0usize;
while i < bytes.len() {
let Some(open) = find_from(bytes, i, b'<') else {
break;
};
let rest = src.get(open..).unwrap_or_default();
if let Some((kind, skipped)) = skip_non_element(rest, open) {
others.push(NonElement {
kind,
start: open,
end: skipped,
});
i = skipped;
continue;
}
let Some(close) = find_tag_end(bytes, open) else {
break;
};
let end = close.saturating_add(1);
let raw = src.get(open..end).unwrap_or_default();
if let Some(tag) = parse_tag(raw, open, end) {
tags.push(tag);
}
i = end;
}
Scan { tags, others }
}
fn skip_non_element(rest: &str, at: usize) -> Option<(NonElementKind, usize)> {
let (kind, prefix, terminator): (NonElementKind, &str, &str) = if rest.starts_with("<!--") {
(NonElementKind::Comment, "<!--", "-->")
} else if rest.starts_with("<![CDATA[") {
(NonElementKind::Cdata, "<![CDATA[", "]]>")
} else if rest.starts_with("<?") {
(NonElementKind::ProcessingInstruction, "<?", "?>")
} else if rest.starts_with("<!") {
(NonElementKind::Doctype, "<!", ">")
} else {
return None;
};
let after_prefix = rest.get(prefix.len()..).unwrap_or_default();
let found = after_prefix.find(terminator);
let offset = found.map_or(rest.len(), |n| {
prefix
.len()
.saturating_add(n)
.saturating_add(terminator.len())
});
Some((kind, at.saturating_add(offset)))
}
fn find_tag_end(bytes: &[u8], open: usize) -> Option<usize> {
let mut i = open.checked_add(1)?;
let mut quote: Option<u8> = None;
while let Some(&b) = bytes.get(i) {
match quote {
Some(q) => {
if b == q {
quote = None;
}
}
None => match b {
b'"' | b'\'' => quote = Some(b),
b'>' => return Some(i),
_ => {}
},
}
i = i.checked_add(1)?;
}
None
}
fn parse_tag(raw: &str, start: usize, end: usize) -> Option<Tag<'_>> {
let inner = raw.strip_prefix('<')?.strip_suffix('>')?;
let (kind, body) = match inner.strip_prefix('/') {
Some(rest) => (Kind::Close, rest),
None => match inner.strip_suffix('/') {
Some(rest) => (Kind::Empty, rest),
None => (Kind::Open, inner),
},
};
let name_end = body
.find(|c: char| c.is_whitespace() || c == '/')
.unwrap_or(body.len());
let name = body.get(0..name_end)?;
if name.is_empty() {
return None;
}
Some(Tag {
name,
kind,
start,
end,
raw,
})
}
pub(in crate::formats) fn attributes(raw: &str) -> Vec<Attribute<'_>> {
let bytes = raw.as_bytes();
let mut out = Vec::new();
let Some(mut i) = raw.find(|c: char| c.is_whitespace()) else {
return out;
};
while i < bytes.len() {
let span_start = i;
while matches!(bytes.get(i), Some(b) if b.is_ascii_whitespace()) {
i = i.saturating_add(1);
}
let name_start = i;
while matches!(bytes.get(i), Some(b) if !b.is_ascii_whitespace() && *b != b'=' && *b != b'>' && *b != b'/')
{
i = i.saturating_add(1);
}
let Some(name) = raw.get(name_start..i) else {
break;
};
if name.is_empty() {
break;
}
while matches!(bytes.get(i), Some(b) if b.is_ascii_whitespace()) {
i = i.saturating_add(1);
}
if bytes.get(i) != Some(&b'=') {
break;
}
i = i.saturating_add(1);
while matches!(bytes.get(i), Some(b) if b.is_ascii_whitespace()) {
i = i.saturating_add(1);
}
let Some("e) = bytes.get(i) else { break };
if quote != b'"' && quote != b'\'' {
break;
}
i = i.saturating_add(1);
let value_start = i;
while matches!(bytes.get(i), Some(b) if *b != quote) {
i = i.saturating_add(1);
}
let Some(value) = raw.get(value_start..i) else {
break;
};
if bytes.get(i) != Some("e) {
break;
}
i = i.saturating_add(1);
out.push(Attribute {
name,
value,
start: span_start,
end: i,
});
}
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(in crate::formats) struct Cut {
pub(in crate::formats) start: usize,
pub(in crate::formats) end: usize,
}
impl Cut {
pub(in crate::formats) const fn is_empty(self) -> bool {
self.start >= self.end
}
pub(in crate::formats) const fn len(self) -> usize {
self.end.saturating_sub(self.start)
}
}
pub(in crate::formats) fn apply(src: &str, mut cuts: Vec<Cut>) -> Option<String> {
if cuts.is_empty() {
return None;
}
cuts.sort_unstable();
let mut out = String::with_capacity(src.len());
let mut copied = 0usize;
for cut in cuts {
if cut.start < copied {
continue;
}
out.push_str(src.get(copied..cut.start)?);
copied = cut.end;
}
out.push_str(src.get(copied..)?);
Some(out)
}
fn find_from(bytes: &[u8], from: usize, needle: u8) -> Option<usize> {
let rest = bytes.get(from..)?;
rest.iter()
.position(|b| *b == needle)
.and_then(|n| from.checked_add(n))
}
pub(in crate::formats) fn element_span(tags: &[Tag<'_>], index: usize) -> Option<Cut> {
let start = tags.get(index)?;
if start.kind == Kind::Empty {
return Some(Cut {
start: start.start,
end: start.end,
});
}
let mut depth = 1usize;
let mut i = index.checked_add(1)?;
while let Some(tag) = tags.get(i) {
if tag.name == start.name {
match tag.kind {
Kind::Open => depth = depth.saturating_add(1),
Kind::Close => {
depth = depth.saturating_sub(1);
if depth == 0 {
return Some(Cut {
start: start.start,
end: tag.end,
});
}
}
Kind::Empty => {}
}
}
i = i.checked_add(1)?;
}
None
}
pub(in crate::formats) struct ElementText<'a> {
pub(in crate::formats) range: Cut,
pub(in crate::formats) value: &'a str,
}
pub(in crate::formats) fn element_text<'a>(
tags: &[Tag<'a>],
index: usize,
src: &'a str,
) -> Option<ElementText<'a>> {
let start = tags.get(index)?;
let next = tags.get(index.checked_add(1)?)?;
if next.kind != Kind::Close || next.name != start.name {
return None;
}
let range = Cut {
start: start.end,
end: next.start,
};
Some(ElementText {
range,
value: src.get(range.start..range.end)?,
})
}
pub(in crate::formats) struct TextElement<'a> {
pub(in crate::formats) name: &'a str,
pub(in crate::formats) text: &'a str,
}
pub(in crate::formats) fn elements_with_text(src: &str) -> Vec<TextElement<'_>> {
let tags = tags(src);
let mut out = Vec::new();
for (index, tag) in tags.iter().enumerate() {
if tag.kind != Kind::Open {
continue;
}
if let Some(text) = element_text(&tags, index, src)
&& !text.value.is_empty()
{
out.push(TextElement {
name: tag.name,
text: text.value,
});
}
}
out
}
pub(in crate::formats) fn drop_tags(
src: &str,
should_drop: impl Fn(&Tag<'_>) -> bool,
) -> Option<String> {
let mut cuts = Vec::new();
for tag in tags(src) {
if should_drop(&tag) {
cuts.push(Cut {
start: tag.start,
end: tag.end,
});
}
}
apply(src, cuts)
}
pub(in crate::formats) struct OpenElements<'a> {
stack: Vec<&'a str>,
}
const MAX_ELEMENT_DEPTH: usize = 256;
impl<'a> OpenElements<'a> {
pub(in crate::formats) const fn new() -> Self {
Self { stack: Vec::new() }
}
pub(in crate::formats) fn observe(&mut self, tag: &Tag<'a>) -> bool {
match tag.kind {
Kind::Open => {
if self.stack.len() >= MAX_ELEMENT_DEPTH {
return false;
}
self.stack.push(tag.name);
}
Kind::Close => {
if self.stack.last() != Some(&tag.name) {
return false;
}
self.stack.pop();
}
Kind::Empty => {}
}
true
}
pub(in crate::formats) fn inside(&self, name: &str) -> bool {
self.stack.contains(&name)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::indexing_slicing)]
use super::*;
#[test]
fn a_quoted_angle_bracket_does_not_split_a_tag() {
let found = tags(r#"<w:p w:rsidR="a>b" other="x"/>"#);
assert_eq!(found.len(), 1);
assert_eq!(found[0].name, "w:p");
assert_eq!(found[0].attribute("w:rsidR"), Some("a>b"));
assert_eq!(found[0].attribute("other"), Some("x"));
}
#[test]
fn comments_and_cdata_are_not_read_as_markup() {
let src = r"<a><!-- <w:p w:rsidR='1'/> --><![CDATA[<b/>]]><c/></a>";
let names: Vec<&str> = tags(src).iter().map(|t| t.name).collect();
assert_eq!(names, vec!["a", "c", "a"]);
}
#[test]
fn an_unterminated_comment_stops_the_scan_rather_than_resuming_inside_it() {
let found = tags("<a/><!-- <b/>");
assert_eq!(found.len(), 1);
assert_eq!(found[0].name, "a");
}
#[test]
fn open_elements_tracks_context_and_gives_up_on_mismatched_markup() {
let src = "<office:annotation><dc:creator>x</dc:creator></office:annotation>";
let mut open = OpenElements::new();
let mut seen_inside = false;
for tag in tags(src) {
if tag.name == "dc:creator" && tag.kind == Kind::Open {
seen_inside = open.inside("office:annotation");
}
assert!(open.observe(&tag));
}
assert!(seen_inside, "context must be visible to the rule");
let mut open = OpenElements::new();
let mismatched = tags("<a></b>");
assert!(open.observe(&mismatched[0]));
assert!(!open.observe(&mismatched[1]));
}
#[test]
fn arbitrary_text_does_not_panic_the_scanner() {
let cases = [
"<",
"<a",
"<a=",
"</",
"<!",
"<!--",
"<![CDATA[",
"<?",
"<a b=",
"<a b='",
"<a b='c",
"<<<<>>>>",
"<a/><//>",
"<w:rsids>",
"<author>",
];
for case in cases {
let found = tags(case);
for (index, tag) in found.iter().enumerate() {
let _ = attributes(tag.raw);
let _ = element_span(&found, index);
let _ = element_text(&found, index, case);
}
let _ = elements_with_text(case);
let _ = drop_tags(case, |_| true);
}
}
}