pub(crate) fn entity_end(bytes: &[u8], start: usize) -> Option<usize> {
if !bytes.get(start..)?.starts_with(b"&") {
return None;
}
entity_name_end(bytes, start + 5)
}
pub(crate) fn entity_name_end(bytes: &[u8], mut end: usize) -> Option<usize> {
let (min, max, digits): (usize, usize, fn(&u8) -> bool) =
if bytes.get(end..)?.starts_with(b"#x") {
end += 2;
(2, 5, u8::is_ascii_hexdigit)
} else if bytes.get(end) == Some(&b'#') {
end += 1;
(2, 6, u8::is_ascii_digit)
} else {
let letters = end;
while bytes.get(end).is_some_and(u8::is_ascii_alphabetic) {
end += 1;
}
if end - letters < 2 {
return None;
}
(0, 2, u8::is_ascii_digit)
};
let beginning = end;
while bytes.get(end).is_some_and(digits) {
end += 1;
}
((min..=max).contains(&(end - beginning)) && bytes.get(end) == Some(&b';')).then_some(end + 1)
}
pub fn character_reference_end(text: &str, at: usize) -> Option<usize> {
let bytes = text.as_bytes();
(bytes.get(at) == Some(&b'&'))
.then(|| entity_name_end(bytes, at + 1))
.flatten()
}
pub(crate) fn decode_protection(lexical: &str) -> String {
let mut decoded = String::with_capacity(lexical.len());
let mut rest = lexical;
while let Some(at) = rest.find('&') {
decoded.push_str(&rest[..at]);
rest = &rest[at..];
let (text, len) = if rest.starts_with("<") {
("<", 4)
} else if rest.starts_with(">") {
(">", 4)
} else if rest.starts_with("&") && entity_end(rest.as_bytes(), 0).is_none() {
("&", 5)
} else {
("&", 1)
};
decoded.push_str(text);
rest = &rest[len..];
}
decoded.push_str(rest);
decoded
}
pub(crate) fn reference_parts(value: &str) -> Vec<crate::AttributePart> {
let mut parts = Vec::new();
let mut copied = 0;
let mut at = 0;
while let Some(offset) = value[at..].find('&') {
let start = at + offset;
match character_reference_end(value, start) {
Some(end) => {
crate::attributes::push_part(&mut parts, &value[copied..start], false);
crate::attributes::push_part(&mut parts, &value[start..end], true);
copied = end;
at = end;
}
None => at = start + 1,
}
}
crate::attributes::push_part(&mut parts, &value[copied..], false);
parts
}
pub(crate) fn reference_inlines(value: &str, span: crate::Span) -> Vec<crate::ast::Inline> {
reference_parts(value)
.into_iter()
.map(|part| match part {
crate::AttributePart::Raw(value) => crate::ast::Inline::Raw { value, span },
part => crate::ast::Inline::Text {
value: part.text().to_owned(),
span,
},
})
.collect()
}
pub(crate) fn lexical_reference_inlines(value: &str, span: crate::Span) -> Vec<crate::ast::Inline> {
let mut out = Vec::new();
let mut rest = value;
while !rest.is_empty() {
if rest.starts_with('<') {
if let Some(end) = rest.find('>') {
out.push(crate::ast::Inline::Raw {
value: rest[..=end].to_owned(),
span,
});
rest = &rest[end + 1..];
continue;
}
}
let end = rest[1..].find('<').map_or(rest.len(), |at| at + 1);
out.extend(reference_inlines(&decode_protection(&rest[..end]), span));
rest = &rest[end..];
}
out
}
pub(crate) fn unknown_reference_label(
xref: &crate::Xref,
id: &str,
lexical: bool,
span: crate::Span,
) -> Vec<crate::ast::Inline> {
let spelling = if lexical {
xref.refid.as_deref()
} else {
xref.spelling.as_deref()
};
match spelling {
Some(spelling) => lexical_reference_inlines(&format!("[{spelling}]"), span),
None => reference_inlines(&format!("[{id}]"), span),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::AttributePart;
#[test]
fn decoding_keeps_references_and_ambiguous_ampersands() {
assert_eq!(decode_protection("a<b>c&d"), "a<b>c&d");
assert_eq!(
decode_protection("& © &"),
"& © &"
);
assert_eq!(
decode_protection("&#38; &copy; &"),
"&#38; &copy; &"
);
assert_eq!(decode_protection("&lt; &a; Ж&"), "&lt; &a; Ж&");
}
#[test]
fn reference_parts_split_only_grammar_references() {
assert_eq!(
reference_parts("Ж&x © &a; &#38;"),
vec![
AttributePart::Text("Ж".into()),
AttributePart::Raw("&".into()),
AttributePart::Text("x © &a; ".into()),
AttributePart::Raw("&".into()),
AttributePart::Text("#38;".into()),
]
);
assert!(reference_parts("").is_empty());
assert_eq!(character_reference_end("xA", 1), Some(7));
assert_eq!(character_reference_end("xA", 1), None);
assert_eq!(character_reference_end("x", 0), None);
}
}