use crate::utils::regex_cache::HUGO_SHORTCODE_REGEX;
pub(crate) fn shortcode_ranges(content: &str) -> impl Iterator<Item = (usize, usize)> + '_ {
ShortcodeRanges {
content,
cursor: 0,
recover_tail: false,
}
}
struct ShortcodeRanges<'a> {
content: &'a str,
cursor: usize,
recover_tail: bool,
}
impl Iterator for ShortcodeRanges<'_> {
type Item = (usize, usize);
fn next(&mut self) -> Option<Self::Item> {
if !self.recover_tail {
let bytes = self.content.as_bytes();
let mut search = self.cursor;
let start = loop {
let start = search + super::find_ascii_delimiter(&self.content[search..], "{{")?;
if matches!(bytes.get(start + 2), Some(b'<' | b'%')) {
break start;
}
search = start + 1;
};
let marker = if bytes[start + 2] == b'<' { b'>' } else { b'%' };
let mut pos = start + 3;
let mut quote = None;
let mut escaped_outer = false;
let mut parameter_start = true;
let mut has_equals = false;
while pos < bytes.len() {
let byte = bytes[pos];
if let Some(delimiter) = quote {
if byte == delimiter && (delimiter == b'`' || escaped_outer || bytes[pos - 1] != b'\\') {
quote = None;
parameter_start = false;
}
} else if matches!(byte, b'>' | b'%') && bytes.get(pos + 1..pos + 3) == Some(b"}}") {
if byte == marker {
self.cursor = pos + 3;
return Some((start, self.cursor));
}
break;
} else if parameter_start && matches!(byte, b'`' | b'"') {
quote = Some(byte);
escaped_outer = false;
} else if parameter_start && byte == b'\\' && bytes.get(pos + 1) == Some(&b'"') {
quote = Some(b'"');
escaped_outer = true;
pos += 1;
} else {
if byte.is_ascii_whitespace() {
parameter_start = true;
has_equals = false;
} else if byte == b'=' && !has_equals {
parameter_start = true;
has_equals = true;
} else {
parameter_start = false;
}
}
pos += 1;
}
self.cursor = start;
self.recover_tail = true;
}
let found = HUGO_SHORTCODE_REGEX.find(&self.content[self.cursor..])?;
let range = (self.cursor + found.start(), self.cursor + found.end());
self.cursor = range.1;
Some(range)
}
}
#[cfg(test)]
mod tests {
use super::shortcode_ranges;
#[test]
fn malformed_recovery_retains_utf8_boundaries() {
for source in [
"é🦀 {{< broken %}} 🐟 {{% kept %}}",
"é🦀 {{% broken >}} 🐟 {{< kept >}}",
"é🦀 {{< \"unterminated >}} 🐟 {{% kept %}}",
"é🦀 {{% `unterminated %}} 🐟 {{< kept >}}",
] {
let expected: Vec<_> = crate::utils::regex_cache::HUGO_SHORTCODE_REGEX
.find_iter(source)
.map(|found| (found.start(), found.end()))
.collect();
let actual: Vec<_> = shortcode_ranges(source).collect();
assert_eq!(actual, expected, "{source:?}");
assert_eq!(actual.len(), 2, "{source:?}");
assert!(actual.iter().all(|&(start, end)| source.get(start..end).is_some()));
}
}
#[test]
fn overlapping_openers_and_quoted_closers_keep_byte_ranges() {
for (source, expected) in [
("{{{<x>}}", vec![(1, 8)]),
("é🦀{{{{%x%}}", vec![(8, 15)]),
("{{< x \" >}} \" >}}", vec![(0, 17)]),
("{{% x ` %}} ` %}}", vec![(0, 17)]),
("{{< open", vec![]),
("ordinary prose {{ brace", vec![]),
] {
assert_eq!(shortcode_ranges(source).collect::<Vec<_>>(), expected, "{source:?}");
}
}
}