use fhp_simd::dispatch::{SimdOps, ops};
const BLOCK_SIZE: usize = 64;
#[derive(Clone, Debug, Default)]
pub struct BlockBitmaps {
pub lt: u64,
pub gt: u64,
pub amp: u64,
pub quot: u64,
pub apos: u64,
pub eq: u64,
pub slash: u64,
}
pub struct StructuralIndex {
bitmaps: Vec<BlockBitmaps>,
len: usize,
}
impl StructuralIndex {
pub fn iter_delimiters(&self) -> DelimiterIter<'_> {
let mut iter = DelimiterIter {
bitmaps: &self.bitmaps,
block_idx: 0,
combined: 0,
len: self.len,
};
if !self.bitmaps.is_empty() {
iter.combined = Self::combined_mask(&self.bitmaps[0]);
}
iter
}
pub fn estimated_token_count(&self) -> usize {
let total_lt: u32 = self.bitmaps.iter().map(|b| b.lt.count_ones()).sum();
total_lt as usize + 1
}
pub fn input_len(&self) -> usize {
self.len
}
pub fn block_count(&self) -> usize {
self.bitmaps.len()
}
pub fn block(&self, index: usize) -> &BlockBitmaps {
&self.bitmaps[index]
}
fn combined_mask(block: &BlockBitmaps) -> u64 {
block.lt | block.gt | block.amp | block.quot | block.apos | block.eq | block.slash
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DelimiterEntry {
pub pos: usize,
pub byte: u8,
}
pub struct DelimiterIter<'a> {
bitmaps: &'a [BlockBitmaps],
block_idx: usize,
combined: u64,
len: usize,
}
impl<'a> Iterator for DelimiterIter<'a> {
type Item = DelimiterEntry;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
loop {
if self.combined != 0 {
let bit = self.combined.trailing_zeros() as usize;
self.combined &= self.combined - 1;
let pos = self.block_idx * BLOCK_SIZE + bit;
if pos >= self.len {
return None;
}
let block = &self.bitmaps[self.block_idx];
let byte = determine_byte(block, bit);
return Some(DelimiterEntry { pos, byte });
}
self.block_idx += 1;
if self.block_idx >= self.bitmaps.len() {
return None;
}
self.combined = StructuralIndex::combined_mask(&self.bitmaps[self.block_idx]);
}
}
}
#[inline]
fn determine_byte(block: &BlockBitmaps, bit: usize) -> u8 {
if (block.lt >> bit) & 1 == 1 {
b'<'
} else if (block.gt >> bit) & 1 == 1 {
b'>'
} else if (block.amp >> bit) & 1 == 1 {
b'&'
} else if (block.quot >> bit) & 1 == 1 {
b'"'
} else if (block.apos >> bit) & 1 == 1 {
b'\''
} else if (block.eq >> bit) & 1 == 1 {
b'='
} else {
b'/'
}
}
pub struct StructuralIndexer {
dispatch: &'static SimdOps,
}
impl StructuralIndexer {
pub fn new() -> Self {
Self { dispatch: ops() }
}
pub fn index(&self, input: &[u8]) -> StructuralIndex {
let block_count = input.len().div_ceil(BLOCK_SIZE);
let mut bitmaps = Vec::with_capacity(block_count);
for chunk in input.chunks(BLOCK_SIZE) {
let compute = self.dispatch.compute_byte_mask;
let lt = unsafe { compute(chunk, b'<') };
let gt = unsafe { compute(chunk, b'>') };
let amp = unsafe { compute(chunk, b'&') };
let quot = unsafe { compute(chunk, b'"') };
let apos = unsafe { compute(chunk, b'\'') };
let eq = unsafe { compute(chunk, b'=') };
let slash = unsafe { compute(chunk, b'/') };
bitmaps.push(BlockBitmaps {
lt,
gt,
amp,
quot,
apos,
eq,
slash,
});
}
StructuralIndex {
bitmaps,
len: input.len(),
}
}
}
impl Default for StructuralIndexer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_html_tag() {
let input = b"<div>hello</div>";
let indexer = StructuralIndexer::new();
let index = indexer.index(input);
let delims: Vec<_> = index.iter_delimiters().collect();
assert_eq!(
delims,
vec![
DelimiterEntry { pos: 0, byte: b'<' },
DelimiterEntry { pos: 4, byte: b'>' },
DelimiterEntry {
pos: 10,
byte: b'<'
},
DelimiterEntry {
pos: 11,
byte: b'/'
},
DelimiterEntry {
pos: 15,
byte: b'>'
},
]
);
}
#[test]
fn html_with_attributes() {
let input = b"<div class=\"foo\">bar</div>";
let indexer = StructuralIndexer::new();
let index = indexer.index(input);
let delims: Vec<_> = index.iter_delimiters().collect();
let bytes: Vec<u8> = delims.iter().map(|d| d.byte).collect();
assert!(bytes.contains(&b'<'));
assert!(bytes.contains(&b'>'));
assert!(bytes.contains(&b'='));
assert!(bytes.contains(&b'"'));
}
#[test]
fn delimiters_inside_double_quotes_are_indexed() {
let input = b"<a title=\"x > y < z\">link</a>";
let indexer = StructuralIndexer::new();
let index = indexer.index(input);
let positions: Vec<usize> = index.iter_delimiters().map(|d| d.pos).collect();
assert!(positions.contains(&0)); assert!(positions.contains(&20)); assert!(positions.contains(&25));
assert!(positions.contains(&12), "> inside quotes is indexed");
assert!(positions.contains(&16), "< inside quotes is indexed");
}
#[test]
fn delimiters_inside_single_quotes_are_indexed() {
let input = b"<a title='x > y'>link</a>";
let indexer = StructuralIndexer::new();
let index = indexer.index(input);
let positions: Vec<usize> = index.iter_delimiters().map(|d| d.pos).collect();
assert!(positions.contains(&12), "> inside single quotes is indexed");
}
#[test]
fn single_quote_inside_double_quotes_ignored() {
let input = b"<div title=\"it's\">text</div>";
let indexer = StructuralIndexer::new();
let index = indexer.index(input);
let delims: Vec<_> = index.iter_delimiters().collect();
let bytes: Vec<u8> = delims.iter().map(|d| d.byte).collect();
assert!(bytes.contains(&b'<'));
assert!(bytes.contains(&b'>'));
let gt_positions: Vec<usize> = delims
.iter()
.filter(|d| d.byte == b'>')
.map(|d| d.pos)
.collect();
assert!(
gt_positions.contains(&17),
"closing > at 17 should be structural"
);
}
#[test]
fn empty_input() {
let indexer = StructuralIndexer::new();
let index = indexer.index(b"");
assert_eq!(index.input_len(), 0);
assert_eq!(index.block_count(), 0);
assert_eq!(index.iter_delimiters().count(), 0);
assert_eq!(index.estimated_token_count(), 1);
}
#[test]
fn text_only_no_delimiters() {
let input = b"hello world this is plain text without any tags";
let indexer = StructuralIndexer::new();
let index = indexer.index(input);
assert_eq!(index.iter_delimiters().count(), 0);
assert_eq!(index.estimated_token_count(), 1);
}
#[test]
fn tag_only() {
let input = b"<br/>";
let indexer = StructuralIndexer::new();
let index = indexer.index(input);
let delims: Vec<_> = index.iter_delimiters().collect();
assert_eq!(
delims,
vec![
DelimiterEntry { pos: 0, byte: b'<' },
DelimiterEntry { pos: 3, byte: b'/' },
DelimiterEntry { pos: 4, byte: b'>' },
]
);
}
#[test]
fn entity_reference() {
let input = b"a & b";
let indexer = StructuralIndexer::new();
let index = indexer.index(input);
let amp_entries: Vec<_> = index.iter_delimiters().filter(|d| d.byte == b'&').collect();
assert_eq!(amp_entries.len(), 1);
assert_eq!(amp_entries[0].pos, 2);
}
#[test]
fn long_input_multiple_blocks() {
let mut input = Vec::with_capacity(1200);
input.extend_from_slice(&[b'x'; 100]);
input.extend_from_slice(b"<div>");
input.extend_from_slice(&[b'y'; 200]);
input.extend_from_slice(b"<span class=\"test\">");
input.extend_from_slice(&[b'z'; 700]);
input.extend_from_slice(b"</span></div>");
let indexer = StructuralIndexer::new();
let index = indexer.index(&input);
assert!(input.len() > 1000);
assert!(index.block_count() > 15);
let delims: Vec<_> = index.iter_delimiters().collect();
assert!(
delims.iter().any(|d| d.pos == 100 && d.byte == b'<'),
"should find < at offset 100"
);
assert!(
delims.iter().any(|d| d.byte == b'='),
"should find = in attribute"
);
let last_lt = delims.iter().rev().find(|d| d.byte == b'<');
assert!(last_lt.is_some());
}
#[test]
fn long_input_with_quotes_spanning_blocks() {
let mut input = Vec::new();
input.extend_from_slice(b"<div data=\"");
while input.len() < 60 {
input.push(b'a');
}
input.push(b'<');
while input.len() < 80 {
input.push(b'b');
}
input.extend_from_slice(b"\">end</div>");
let indexer = StructuralIndexer::new();
let index = indexer.index(&input);
let delims: Vec<_> = index.iter_delimiters().collect();
let lt_positions: Vec<usize> = delims
.iter()
.filter(|d| d.byte == b'<')
.map(|d| d.pos)
.collect();
assert!(
lt_positions.contains(&60),
"< at offset 60 is indexed (parser handles quote state)"
);
assert!(lt_positions.contains(&0), "opening < should be structural");
}
#[test]
fn estimated_token_count_basic() {
let input = b"<div>hello</div><p>world</p>";
let indexer = StructuralIndexer::new();
let index = indexer.index(input);
assert_eq!(index.estimated_token_count(), 5);
}
#[test]
fn block_api() {
let input = b"<div>text</div>";
let indexer = StructuralIndexer::new();
let index = indexer.index(input);
assert_eq!(index.input_len(), 15);
assert_eq!(index.block_count(), 1);
let block = index.block(0);
assert_ne!(block.lt, 0, "should have < bits set");
assert_ne!(block.gt, 0, "should have > bits set");
}
}