mod fast_path;
mod interpolation;
mod tags;
use self::tags::{
find_opening_tag_end, find_raw_text_element_end, is_opening_tag_named, raw_text_tag_name,
};
use self::{fast_path::find_flat_template_end, interpolation::skip_template_interpolation};
use super::block::{
BlockEndSearch, BlockParseResult, TAG_TEMPLATE, advance_line, build_malformed_error,
find_closing_tag_end,
};
use alloc::borrow::Cow;
use memchr::{memchr2, memmem};
const MAX_FAILED_INTERPOLATION_SCANS: usize = 8;
const BOUNDED_INTERPOLATION_SCAN_WINDOW: usize = 4096;
pub(super) fn find_template_block_end<'a>(search: BlockEndSearch<'a>) -> BlockParseResult<'a> {
let BlockEndSearch {
bytes,
source,
tag_name,
mut pos,
content_start,
start_line,
start_column,
initial_last_newline,
attrs,
} = search;
let len = bytes.len();
let mut line = start_line;
let mut last_newline = initial_last_newline;
let mut depth = 1usize;
let mut interpolation_close_exhausted = false;
let mut failed_interpolation_scans = 0usize;
if let Some((content_end, end_pos)) = find_flat_template_end(bytes, content_start, len) {
advance_line(
bytes.get(content_start..content_end).unwrap_or_default(),
content_start,
&mut line,
&mut last_newline,
);
let col = if line == start_line {
start_column + content_end - content_start
} else {
content_end - last_newline
};
let content = Cow::Borrowed(source.get(content_start..content_end).unwrap_or_default());
return Ok(Some((
tag_name,
attrs,
content,
content_start,
content_end,
end_pos,
line,
col,
)));
}
while pos < len {
let Some(candidate_offset) = memchr2(b'<', b'{', bytes.get(pos..).unwrap_or_default())
else {
advance_line(
bytes.get(pos..).unwrap_or_default(),
pos,
&mut line,
&mut last_newline,
);
break;
};
advance_line(
bytes.get(pos..pos + candidate_offset).unwrap_or_default(),
pos,
&mut line,
&mut last_newline,
);
pos += candidate_offset;
if bytes.get(pos) == Some(&b'{') {
if pos + 1 < len && bytes.get(pos + 1) == Some(&b'{') {
let close_ahead = if interpolation_close_exhausted {
None
} else {
let close_ahead = memmem::find(bytes.get(pos + 2..).unwrap_or_default(), b"}}");
interpolation_close_exhausted = close_ahead.is_none();
close_ahead
};
let scan_limit = if failed_interpolation_scans < MAX_FAILED_INTERPOLATION_SCANS {
len
} else {
(pos + 2)
.saturating_add(BOUNDED_INTERPOLATION_SCAN_WINDOW)
.min(len)
};
let close_within_limit =
close_ahead.is_some_and(|offset| pos + 2 + offset + 2 <= scan_limit);
if !close_within_limit {
pos += 2;
} else if let Some(interpolation_end) = skip_template_interpolation(
bytes.get(..scan_limit).unwrap_or_default(),
pos,
scan_limit,
&mut line,
&mut last_newline,
) {
pos = interpolation_end;
} else {
failed_interpolation_scans += 1;
pos += 2;
}
continue;
}
pos += 1;
continue;
}
if bytes.get(pos..).unwrap_or_default().starts_with(b"<!--") {
let comment_body_start = pos + 4;
if let Some(comment_end_offset) =
memmem::find(bytes.get(comment_body_start..).unwrap_or_default(), b"-->")
{
let comment_end = comment_body_start + comment_end_offset + 3;
advance_line(
bytes.get(pos..comment_end).unwrap_or_default(),
pos,
&mut line,
&mut last_newline,
);
pos = comment_end;
continue;
}
break;
}
if bytes
.get(pos..)
.unwrap_or_default()
.starts_with(b"<![CDATA[")
{
let cdata_body_start = pos + 9;
if let Some(cdata_end_offset) =
memmem::find(bytes.get(cdata_body_start..).unwrap_or_default(), b"]]>")
{
let cdata_end = cdata_body_start + cdata_end_offset + 3;
advance_line(
bytes.get(pos..cdata_end).unwrap_or_default(),
pos,
&mut line,
&mut last_newline,
);
pos = cdata_end;
continue;
}
break;
}
if pos + 1 < len && matches!(bytes.get(pos + 1), Some(b'!' | b'?')) {
if let Some((declaration_end, _)) = find_opening_tag_end(bytes, pos, len) {
advance_line(
bytes.get(pos..declaration_end).unwrap_or_default(),
pos,
&mut line,
&mut last_newline,
);
pos = declaration_end;
continue;
}
break;
}
if let Some(end_tag_pos) = find_closing_tag_end(bytes, pos, len, TAG_TEMPLATE) {
depth -= 1;
if depth == 0 {
let content_end = pos;
let col = if line == start_line {
start_column + content_end - content_start
} else {
content_end - last_newline
};
let content =
Cow::Borrowed(source.get(content_start..content_end).unwrap_or_default());
return Ok(Some((
tag_name,
attrs,
content,
content_start,
content_end,
end_tag_pos,
line,
col,
)));
}
advance_line(
bytes.get(pos..end_tag_pos).unwrap_or_default(),
pos,
&mut line,
&mut last_newline,
);
pos = end_tag_pos;
continue;
}
if pos + 1 < len && bytes.get(pos + 1).is_some_and(u8::is_ascii_alphabetic) {
let nested_template = is_opening_tag_named(bytes, pos, len, TAG_TEMPLATE);
let raw_text_tag = raw_text_tag_name(bytes, pos, len);
if let Some((tag_end, self_closing)) = find_opening_tag_end(bytes, pos, len) {
let scan_end = if !self_closing && let Some(raw_text_tag) = raw_text_tag {
let Some(raw_text_end) =
find_raw_text_element_end(bytes, tag_end, len, raw_text_tag)
else {
break;
};
raw_text_end
} else {
tag_end
};
advance_line(
bytes.get(pos..scan_end).unwrap_or_default(),
pos,
&mut line,
&mut last_newline,
);
if nested_template && !self_closing {
depth += 1;
}
pos = scan_end;
continue;
}
}
pos += 1;
}
Err(build_malformed_error(
tag_name,
"the closing tag is missing",
))
}