mod compat;
mod literals;
mod regex;
mod script;
use alloc::borrow::Cow;
use memchr::memchr;
use vize_l0::FxHashMap;
pub(super) use super::{
BlockAttrs, BlockEndSearch, BlockParseResult, advance_line, build_malformed_error,
content_end_column, find_block_end, find_closing_tag_end, newlines,
};
use compat::is_void_block;
pub(super) use literals::{can_start_regex_literal, skip_script_string_literal};
pub(super) use regex::skip_regex_literal;
use script::find_script_block_end;
pub(super) const TAG_TEMPLATE: &[u8] = b"template";
const TAG_SCRIPT: &[u8] = b"script";
const TAG_STYLE: &[u8] = b"style";
#[inline(always)]
pub fn tag_name_eq(name: &[u8], expected: &[u8]) -> bool {
name.len() == expected.len() && name.eq_ignore_ascii_case(expected)
}
#[inline(always)]
pub(super) fn starts_with_bytes(haystack: &[u8], needle: &[u8]) -> bool {
haystack
.get(..needle.len())
.is_some_and(|prefix| prefix.eq_ignore_ascii_case(needle))
}
#[inline(always)]
fn is_tag_name_char_fast(b: u8) -> bool {
matches!(b, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_')
}
#[inline(always)]
pub(super) fn is_whitespace_fast(b: u8) -> bool {
matches!(b, b' ' | b'\t' | b'\n' | b'\r')
}
#[inline]
fn position_after(bytes: &[u8], line: usize, column: usize) -> (usize, usize) {
let (count, last) = newlines(bytes);
let end_column = last.map_or(column + bytes.len(), |offset| bytes.len() - offset);
(line + count, end_column)
}
pub fn parse_block_fast<'a>(
bytes: &'a [u8],
source: &'a str,
start: usize,
start_line: usize,
start_column: usize,
) -> BlockParseResult<'a> {
let len = bytes.len();
let mut pos = start + 1;
if pos >= len {
return Ok(None);
}
let tag_start = pos;
while bytes.get(pos).is_some_and(|&b| is_tag_name_char_fast(b)) {
pos += 1;
}
if pos == tag_start {
return Ok(None);
}
let tag_name = source.as_bytes().get(tag_start..pos).unwrap_or_default();
let mut attrs: BlockAttrs<'a> = FxHashMap::default();
while bytes.get(pos).is_some_and(|&b| b != b'>') {
while bytes.get(pos).is_some_and(|&b| is_whitespace_fast(b)) {
pos += 1;
}
if bytes.get(pos).is_none_or(|&b| b == b'>' || b == b'/') {
break;
}
let attr_start = pos;
while let Some(&c) = bytes.get(pos) {
if c == b'='
|| c == b' '
|| c == b'>'
|| c == b'/'
|| c == b'\t'
|| c == b'\n'
|| c == b'\r'
{
break;
}
pos += 1;
}
if pos == attr_start {
pos += 1;
continue;
}
let attr_name: Cow<'a, str> =
Cow::Borrowed(source.get(attr_start..pos).unwrap_or_default());
while matches!(bytes.get(pos), Some(b' ' | b'\t')) {
pos += 1;
}
let attr_value: Cow<'a, str> = if bytes.get(pos) == Some(&b'=') {
pos += 1;
while matches!(bytes.get(pos), Some(b' ' | b'\t')) {
pos += 1;
}
if let Some("e_char) = bytes.get(pos).filter(|&&b| b == b'"' || b == b'\'') {
pos += 1;
let value_start = pos;
if let Some(quote_pos) = memchr(quote_char, bytes.get(pos..).unwrap_or_default()) {
pos += quote_pos;
let value = Cow::Borrowed(source.get(value_start..pos).unwrap_or_default());
pos += 1; value
} else {
while bytes.get(pos).is_some_and(|&b| b != quote_char) {
pos += 1;
}
let value = Cow::Borrowed(source.get(value_start..pos).unwrap_or_default());
if pos < len {
pos += 1;
}
value
}
} else {
let value_start = pos;
while let Some(&c) = bytes.get(pos) {
if c == b' ' || c == b'>' || c == b'/' || c == b'\t' || c == b'\n' {
break;
}
pos += 1;
}
Cow::Borrowed(source.get(value_start..pos).unwrap_or_default())
}
} else {
Cow::Borrowed("")
};
if !attr_name.is_empty() {
attrs.insert(attr_name, attr_value);
}
}
let is_self_closing = bytes.get(pos) == Some(&b'/');
if is_self_closing {
pos += 1;
while bytes.get(pos).is_some_and(|&b| is_whitespace_fast(b)) {
pos += 1;
}
if bytes.get(pos) != Some(&b'>') {
return Err(build_malformed_error(
tag_name,
"the self-closing tag is incomplete",
));
}
pos += 1;
let (content_line, content_column) = position_after(
bytes.get(start..pos).unwrap_or_default(),
start_line,
start_column,
);
return Ok(Some((
tag_name,
attrs,
Cow::Borrowed(""),
pos,
pos,
pos,
content_line,
content_column,
)));
}
if bytes.get(pos) == Some(&b'>') {
pos += 1;
} else {
return Err(build_malformed_error(
tag_name,
"the opening tag is incomplete",
));
}
let content_start = pos;
let (content_start_line, content_start_column) = position_after(
bytes.get(start..content_start).unwrap_or_default(),
start_line,
start_column,
);
if is_void_block(tag_name) {
return Ok(Some((
tag_name,
attrs,
Cow::Borrowed(""),
content_start,
content_start,
pos,
content_start_line,
content_start_column,
)));
}
if tag_name.eq_ignore_ascii_case(TAG_TEMPLATE) {
return super::template_boundary::find_template_block_end(BlockEndSearch {
bytes,
source,
tag_name,
pos,
content_start,
start_line: content_start_line,
start_column: content_start_column,
initial_last_newline: content_start,
attrs,
});
}
if tag_name.eq_ignore_ascii_case(TAG_STYLE) {
return find_block_end(BlockEndSearch {
bytes,
source,
tag_name,
pos,
content_start,
start_line: content_start_line,
start_column: content_start_column,
initial_last_newline: content_start,
attrs,
});
}
if !tag_name.eq_ignore_ascii_case(TAG_SCRIPT) {
return find_block_end(BlockEndSearch {
bytes,
source,
tag_name,
pos,
content_start,
start_line: content_start_line,
start_column: content_start_column,
initial_last_newline: content_start,
attrs,
});
}
find_script_block_end(BlockEndSearch {
bytes,
source,
tag_name,
pos,
content_start,
start_line: content_start_line,
start_column: content_start_column,
initial_last_newline: content_start,
attrs,
})
}