pub mod entity;
pub mod extract;
pub mod state_machine;
pub mod streaming;
pub mod structural;
pub mod token;
use extract::extract_tokens;
use structural::StructuralIndexer;
use token::Token;
use fhp_core::tag::Tag;
use fhp_simd::AllMasks;
use fhp_simd::dispatch::ops;
pub trait TreeSink {
fn open_tag(&mut self, tag: Tag, name: &str, attr_raw: &str, self_closing: bool);
fn close_tag(&mut self, tag: Tag, name: &str);
fn text(&mut self, content: &str);
fn comment(&mut self, content: &str);
fn doctype(&mut self, content: &str);
fn cdata(&mut self, content: &str);
}
pub fn tokenize<'a>(input: &'a str) -> Vec<Token<'a>> {
let indexer = StructuralIndexer::new();
let index = indexer.index(input.as_bytes());
extract_tokens(input, &index)
}
const BLOCK_SIZE: usize = 64;
pub fn tokenize_with<'a>(input: &'a str, mut on_token: impl FnMut(Token<'a>)) {
let bytes = input.as_bytes();
let len = bytes.len();
if len == 0 {
return;
}
let dispatch = ops();
let compute = dispatch.compute_all_masks;
let mut parser = extract::Parser::new(input);
let mut block_start = 0;
while block_start < len {
let block_end = (block_start + BLOCK_SIZE).min(len);
let chunk = &bytes[block_start..block_end];
let masks: AllMasks = unsafe { compute(chunk) };
let combined = masks.lt | masks.gt | masks.quot | masks.apos;
let valid_bits = block_end - block_start;
let mut bits = if valid_bits < 64 {
combined & ((1u64 << valid_bits) - 1)
} else {
combined
};
while bits != 0 {
let bit_pos = bits.trailing_zeros() as usize;
bits &= bits - 1;
let abs_pos = block_start + bit_pos;
let byte = bytes[abs_pos];
parser.on_delimiter_cb(abs_pos, byte, &mut on_token);
}
block_start = block_end;
}
parser.flush_trailing_cb(&mut on_token);
}
pub fn tokenize_into<S: TreeSink>(input: &str, sink: &mut S) {
let bytes = input.as_bytes();
let len = bytes.len();
if len == 0 {
return;
}
let dispatch = ops();
let compute = dispatch.compute_all_masks;
let mut parser = extract::Parser::new(input);
let mut block_start = 0;
while block_start < len {
let block_end = (block_start + BLOCK_SIZE).min(len);
let chunk = &bytes[block_start..block_end];
let masks: AllMasks = unsafe { compute(chunk) };
let combined = masks.lt | masks.gt | masks.quot | masks.apos;
let valid_bits = block_end - block_start;
let mut bits = if valid_bits < 64 {
combined & ((1u64 << valid_bits) - 1)
} else {
combined
};
while bits != 0 {
let bit_pos = bits.trailing_zeros() as usize;
bits &= bits - 1;
let abs_pos = block_start + bit_pos;
let byte = bytes[abs_pos];
parser.on_delimiter_sink(abs_pos, byte, sink);
}
block_start = block_end;
}
parser.flush_trailing_sink(sink);
}
#[cfg(test)]
mod tokenize_with_tests {
use super::*;
use token::Token;
fn collect_with(input: &str) -> Vec<Token<'_>> {
let mut tokens = Vec::new();
tokenize_with(input, |t| tokens.push(t));
tokens
}
fn assert_equivalent(html: &str) {
let vec_tokens = tokenize(html);
let cb_tokens = collect_with(html);
assert_eq!(
vec_tokens.len(),
cb_tokens.len(),
"token count mismatch for {:?}",
html
);
for (i, (a, b)) in vec_tokens.iter().zip(cb_tokens.iter()).enumerate() {
assert_eq!(
std::mem::discriminant(a),
std::mem::discriminant(b),
"token type mismatch at index {i} for {:?}: {a:?} vs {b:?}",
html
);
}
}
#[test]
fn equiv_simple_div() {
assert_equivalent("<div>hello</div>");
}
#[test]
fn equiv_self_closing() {
assert_equivalent("<br/>");
}
#[test]
fn equiv_attributes() {
assert_equivalent("<a href=\"url\" class=\"link\">text</a>");
}
#[test]
fn equiv_comment() {
assert_equivalent("<!-- hello -->");
}
#[test]
fn equiv_doctype() {
assert_equivalent("<!DOCTYPE html>");
}
#[test]
fn equiv_script() {
assert_equivalent("<script>var x = 1 < 2;</script>");
}
#[test]
fn equiv_entities() {
assert_equivalent("a & b");
}
#[test]
fn equiv_entity_in_attr() {
assert_equivalent("<div title=\"a & b\">x</div>");
}
#[test]
fn equiv_empty() {
assert_equivalent("");
}
#[test]
fn equiv_text_only() {
assert_equivalent("just plain text");
}
#[test]
fn equiv_nested() {
assert_equivalent("<div><span>text</span></div>");
}
#[test]
fn equiv_quotes_spanning_blocks() {
let mut input = String::from("<div data=\"");
while input.len() < 60 {
input.push('a');
}
input.push('<'); while input.len() < 80 {
input.push('b');
}
input.push_str("\">end</div>");
assert_equivalent(&input);
}
#[test]
fn equiv_long_input() {
let mut input = String::new();
for i in 0..100 {
input.push_str(&format!("<p class=\"c{i}\">text {i}</p>"));
}
assert_equivalent(&input);
}
#[test]
fn equiv_mixed_content() {
assert_equivalent(
"<!DOCTYPE html><!-- comment --><div id=\"main\" class='header' disabled>\
<p>Hello & world</p><br/><script>if(a<b){}</script></div>",
);
}
}