use crate::consts::*;
use crate::types::Attributes;
use crate::entities::decode_html_entities;
#[inline(always)]
pub(crate) fn is_whitespace(c: u8) -> bool {
if c > 32 { return false; }
const MASK: u64 = (1u64 << 9) | (1u64 << 10) | (1u64 << 12) | (1u64 << 13) | (1u64 << 32);
(MASK >> c) & 1 == 1
}
pub(crate) struct CommentResult {
pub(crate) complete: bool,
pub(crate) new_position: usize,
pub(crate) remaining_start: usize,
}
pub(crate) fn process_comment_or_doctype(html_chunk: &str, position: usize) -> CommentResult {
let mut i = position;
let bytes = html_chunk.as_bytes();
let chunk_length = bytes.len();
if i + 3 < chunk_length && bytes[i + 2] == DASH_CHAR && bytes[i + 3] == DASH_CHAR {
i += 4;
while i < chunk_length - 2 {
if bytes[i] == DASH_CHAR && bytes[i + 1] == DASH_CHAR && bytes[i + 2] == GT_CHAR {
i += 3;
return CommentResult { complete: true, new_position: i, remaining_start: 0 };
}
i += 1;
}
CommentResult { complete: false, new_position: position, remaining_start: position }
} else {
i += 2;
while i < chunk_length {
if bytes[i] == GT_CHAR {
i += 1;
return CommentResult { complete: true, new_position: i, remaining_start: 0 };
}
i += 1;
}
CommentResult { complete: false, new_position: i, remaining_start: position }
}
}
pub(crate) fn process_tag_attributes(html_chunk: &str, position: usize, tag_handler: Option<&crate::types::TagHandler>, skip_attrs: bool) -> (bool, usize, Attributes, bool) {
let mut i = position;
let bytes = html_chunk.as_bytes();
let chunk_length = bytes.len();
let self_closing = tag_handler.is_some_and(|h| h.is_self_closing);
let mut inside_quote = false;
let mut quote_char: u8 = 0;
let attr_start_pos = i;
while i < chunk_length {
let c = bytes[i];
if inside_quote {
if c == quote_char {
inside_quote = false;
}
i += 1;
continue;
} else if c == QUOTE_CHAR || c == APOS_CHAR {
inside_quote = true;
quote_char = c;
} else if c == SLASH_CHAR && i + 1 < chunk_length && bytes[i + 1] == GT_CHAR {
let attrs = if skip_attrs {
Attributes::new()
} else {
parse_attributes(html_chunk[attr_start_pos..i].trim())
};
return (true, i + 2, attrs, true);
} else if c == GT_CHAR {
let attrs = if skip_attrs {
Attributes::new()
} else {
parse_attributes(html_chunk[attr_start_pos..i].trim())
};
return (true, i + 1, attrs, self_closing);
}
i += 1;
}
(false, i, Attributes::new(), false)
}
#[allow(clippy::collapsible_match)]
pub(crate) fn parse_attributes(attr_str: &str) -> Attributes {
let mut result = Attributes::with_capacity(4);
if attr_str.is_empty() {
return result;
}
let bytes = attr_str.as_bytes();
let len = bytes.len();
let mut i = 0;
const WHITESPACE: u8 = 0;
const NAME: u8 = 1;
const AFTER_NAME: u8 = 2;
const BEFORE_VALUE: u8 = 3;
const QUOTED_VALUE: u8 = 4;
const UNQUOTED_VALUE: u8 = 5;
let mut state = WHITESPACE;
let mut name_start = 0;
let mut name_end;
let mut value_start = 0;
let mut quote_char = 0;
let mut name_start_saved = 0;
let mut name_end_saved = 0;
while i < len {
let char_code = bytes[i];
let is_space = is_whitespace(char_code);
match state {
WHITESPACE => {
if !is_space {
state = NAME;
name_start = i;
}
}
NAME => {
if char_code == EQUALS_CHAR || is_space {
name_end = i;
name_start_saved = name_start;
name_end_saved = name_end;
state = if char_code == EQUALS_CHAR { BEFORE_VALUE } else { AFTER_NAME };
}
}
AFTER_NAME => {
if char_code == EQUALS_CHAR {
state = BEFORE_VALUE;
} else if !is_space {
let raw = &attr_str[name_start_saved..name_end_saved];
let name = raw.to_ascii_lowercase();
result.insert(name, String::new());
state = NAME;
name_start = i;
}
}
BEFORE_VALUE => {
if !is_space {
if char_code == QUOTE_CHAR || char_code == APOS_CHAR {
state = QUOTED_VALUE;
quote_char = char_code;
value_start = i + 1;
} else {
state = UNQUOTED_VALUE;
value_start = i;
}
}
}
QUOTED_VALUE => {
if char_code == quote_char {
let raw = &attr_str[name_start_saved..name_end_saved];
let name = raw.to_ascii_lowercase();
result.insert(name, decode_html_entities(&attr_str[value_start..i]).into_owned());
state = WHITESPACE;
}
}
UNQUOTED_VALUE => {
if is_space {
let raw = &attr_str[name_start_saved..name_end_saved];
let name = raw.to_ascii_lowercase();
result.insert(name, decode_html_entities(&attr_str[value_start..i]).into_owned());
state = WHITESPACE;
}
}
_ => {}
}
i += 1;
}
if state == NAME {
let raw = &attr_str[name_start..];
let lc = raw.to_ascii_lowercase();
result.insert(lc, String::new());
} else if state == UNQUOTED_VALUE {
let raw = &attr_str[name_start_saved..name_end_saved];
let name = raw.to_ascii_lowercase();
result.insert(name, decode_html_entities(&attr_str[value_start..]).into_owned());
} else if state == AFTER_NAME || state == BEFORE_VALUE {
let raw = &attr_str[name_start_saved..name_end_saved];
let name = raw.to_ascii_lowercase();
result.insert(name, String::new());
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn whitespace_detection() {
for c in [b' ', b'\t', b'\n', b'\r'] {
assert!(is_whitespace(c));
}
for c in [b'a', b'0', b'-', 0u8] {
assert!(!is_whitespace(c));
}
}
#[test]
fn parses_quoted_and_unquoted_attributes() {
let a = parse_attributes("href=\"/x\" id=main");
assert_eq!(a.get("href").map(String::as_str), Some("/x"));
assert_eq!(a.get("id").map(String::as_str), Some("main"));
}
#[test]
fn parses_valueless_and_empty_attributes() {
let a = parse_attributes("disabled checked");
assert!(a.contains_key("disabled"));
assert!(a.contains_key("checked"));
let empty = parse_attributes("");
assert!(empty.is_empty());
}
#[test]
fn attribute_names_lowercased_values_decoded() {
let a = parse_attributes("DATA-X='a & b'");
assert_eq!(a.get("data-x").map(String::as_str), Some("a & b"));
}
#[test]
fn form_feed_is_whitespace() {
assert!(is_whitespace(0x0C));
}
#[test]
fn valueless_equals_attribute_kept_as_empty() {
let a = parse_attributes("href=");
assert!(a.contains_key("href"));
assert_eq!(a.get("href").map(String::as_str), Some(""));
}
#[test]
fn process_tag_attributes_finds_close() {
let html = "a href=\"x\">rest";
let (complete, new_pos, attrs, self_closing) =
process_tag_attributes(html, 1, None, false);
assert!(complete);
assert!(!self_closing);
assert_eq!(&html[new_pos..], "rest");
assert_eq!(attrs.get("href").map(String::as_str), Some("x"));
}
}