1use fhp_simd::dispatch::{SimdOps, ops};
10
11const BLOCK_SIZE: usize = 64;
13
14#[derive(Clone, Debug, Default)]
20pub struct BlockBitmaps {
21 pub lt: u64,
23 pub gt: u64,
25 pub amp: u64,
27 pub quot: u64,
29 pub apos: u64,
31 pub eq: u64,
33 pub slash: u64,
35}
36
37pub struct StructuralIndex {
40 bitmaps: Vec<BlockBitmaps>,
41 len: usize,
42}
43
44impl StructuralIndex {
45 pub fn iter_delimiters(&self) -> DelimiterIter<'_> {
47 let mut iter = DelimiterIter {
48 bitmaps: &self.bitmaps,
49 block_idx: 0,
50 combined: 0,
51 len: self.len,
52 };
53 if !self.bitmaps.is_empty() {
55 iter.combined = Self::combined_mask(&self.bitmaps[0]);
56 }
57 iter
58 }
59
60 pub fn estimated_token_count(&self) -> usize {
65 let total_lt: u32 = self.bitmaps.iter().map(|b| b.lt.count_ones()).sum();
66 total_lt as usize + 1
68 }
69
70 pub fn input_len(&self) -> usize {
72 self.len
73 }
74
75 pub fn block_count(&self) -> usize {
77 self.bitmaps.len()
78 }
79
80 pub fn block(&self, index: usize) -> &BlockBitmaps {
82 &self.bitmaps[index]
83 }
84
85 fn combined_mask(block: &BlockBitmaps) -> u64 {
87 block.lt | block.gt | block.amp | block.quot | block.apos | block.eq | block.slash
88 }
89}
90
91#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93pub struct DelimiterEntry {
94 pub pos: usize,
96 pub byte: u8,
98}
99
100pub struct DelimiterIter<'a> {
102 bitmaps: &'a [BlockBitmaps],
103 block_idx: usize,
104 combined: u64,
105 len: usize,
106}
107
108impl<'a> Iterator for DelimiterIter<'a> {
109 type Item = DelimiterEntry;
110
111 #[inline]
112 fn next(&mut self) -> Option<Self::Item> {
113 loop {
114 if self.combined != 0 {
115 let bit = self.combined.trailing_zeros() as usize;
116 self.combined &= self.combined - 1;
118
119 let pos = self.block_idx * BLOCK_SIZE + bit;
120 if pos >= self.len {
121 return None;
122 }
123
124 let block = &self.bitmaps[self.block_idx];
125 let byte = determine_byte(block, bit);
126 return Some(DelimiterEntry { pos, byte });
127 }
128
129 self.block_idx += 1;
131 if self.block_idx >= self.bitmaps.len() {
132 return None;
133 }
134 self.combined = StructuralIndex::combined_mask(&self.bitmaps[self.block_idx]);
135 }
136 }
137}
138
139#[inline]
141fn determine_byte(block: &BlockBitmaps, bit: usize) -> u8 {
142 if (block.lt >> bit) & 1 == 1 {
143 b'<'
144 } else if (block.gt >> bit) & 1 == 1 {
145 b'>'
146 } else if (block.amp >> bit) & 1 == 1 {
147 b'&'
148 } else if (block.quot >> bit) & 1 == 1 {
149 b'"'
150 } else if (block.apos >> bit) & 1 == 1 {
151 b'\''
152 } else if (block.eq >> bit) & 1 == 1 {
153 b'='
154 } else {
155 b'/'
156 }
157}
158
159pub struct StructuralIndexer {
177 dispatch: &'static SimdOps,
178}
179
180impl StructuralIndexer {
181 pub fn new() -> Self {
183 Self { dispatch: ops() }
184 }
185
186 pub fn index(&self, input: &[u8]) -> StructuralIndex {
192 let block_count = input.len().div_ceil(BLOCK_SIZE);
193 let mut bitmaps = Vec::with_capacity(block_count);
194
195 for chunk in input.chunks(BLOCK_SIZE) {
196 let compute = self.dispatch.compute_byte_mask;
199 let lt = unsafe { compute(chunk, b'<') };
200 let gt = unsafe { compute(chunk, b'>') };
201 let amp = unsafe { compute(chunk, b'&') };
202 let quot = unsafe { compute(chunk, b'"') };
203 let apos = unsafe { compute(chunk, b'\'') };
204 let eq = unsafe { compute(chunk, b'=') };
205 let slash = unsafe { compute(chunk, b'/') };
206
207 bitmaps.push(BlockBitmaps {
208 lt,
209 gt,
210 amp,
211 quot,
212 apos,
213 eq,
214 slash,
215 });
216 }
217
218 StructuralIndex {
219 bitmaps,
220 len: input.len(),
221 }
222 }
223}
224
225impl Default for StructuralIndexer {
226 fn default() -> Self {
227 Self::new()
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
240 fn basic_html_tag() {
241 let input = b"<div>hello</div>";
242 let indexer = StructuralIndexer::new();
243 let index = indexer.index(input);
244
245 let delims: Vec<_> = index.iter_delimiters().collect();
246
247 assert_eq!(
250 delims,
251 vec![
252 DelimiterEntry { pos: 0, byte: b'<' },
253 DelimiterEntry { pos: 4, byte: b'>' },
254 DelimiterEntry {
255 pos: 10,
256 byte: b'<'
257 },
258 DelimiterEntry {
259 pos: 11,
260 byte: b'/'
261 },
262 DelimiterEntry {
263 pos: 15,
264 byte: b'>'
265 },
266 ]
267 );
268 }
269
270 #[test]
271 fn html_with_attributes() {
272 let input = b"<div class=\"foo\">bar</div>";
273 let indexer = StructuralIndexer::new();
274 let index = indexer.index(input);
275
276 let delims: Vec<_> = index.iter_delimiters().collect();
277
278 let bytes: Vec<u8> = delims.iter().map(|d| d.byte).collect();
282 assert!(bytes.contains(&b'<'));
283 assert!(bytes.contains(&b'>'));
284 assert!(bytes.contains(&b'='));
285 assert!(bytes.contains(&b'"'));
286 }
287
288 #[test]
293 fn delimiters_inside_double_quotes_are_indexed() {
294 let input = b"<a title=\"x > y < z\">link</a>";
299 let indexer = StructuralIndexer::new();
300 let index = indexer.index(input);
301
302 let positions: Vec<usize> = index.iter_delimiters().map(|d| d.pos).collect();
303
304 assert!(positions.contains(&0)); assert!(positions.contains(&20)); assert!(positions.contains(&25)); assert!(positions.contains(&12), "> inside quotes is indexed");
311 assert!(positions.contains(&16), "< inside quotes is indexed");
312 }
313
314 #[test]
315 fn delimiters_inside_single_quotes_are_indexed() {
316 let input = b"<a title='x > y'>link</a>";
317 let indexer = StructuralIndexer::new();
318 let index = indexer.index(input);
319
320 let positions: Vec<usize> = index.iter_delimiters().map(|d| d.pos).collect();
321
322 assert!(positions.contains(&12), "> inside single quotes is indexed");
325 }
326
327 #[test]
328 fn single_quote_inside_double_quotes_ignored() {
329 let input = b"<div title=\"it's\">text</div>";
331 let indexer = StructuralIndexer::new();
332 let index = indexer.index(input);
333
334 let delims: Vec<_> = index.iter_delimiters().collect();
335 let bytes: Vec<u8> = delims.iter().map(|d| d.byte).collect();
336
337 assert!(bytes.contains(&b'<'));
339 assert!(bytes.contains(&b'>'));
340 let gt_positions: Vec<usize> = delims
342 .iter()
343 .filter(|d| d.byte == b'>')
344 .map(|d| d.pos)
345 .collect();
346 assert!(
347 gt_positions.contains(&17),
348 "closing > at 17 should be structural"
349 );
350 }
351
352 #[test]
357 fn empty_input() {
358 let indexer = StructuralIndexer::new();
359 let index = indexer.index(b"");
360
361 assert_eq!(index.input_len(), 0);
362 assert_eq!(index.block_count(), 0);
363 assert_eq!(index.iter_delimiters().count(), 0);
364 assert_eq!(index.estimated_token_count(), 1);
365 }
366
367 #[test]
368 fn text_only_no_delimiters() {
369 let input = b"hello world this is plain text without any tags";
370 let indexer = StructuralIndexer::new();
371 let index = indexer.index(input);
372
373 assert_eq!(index.iter_delimiters().count(), 0);
374 assert_eq!(index.estimated_token_count(), 1);
375 }
376
377 #[test]
378 fn tag_only() {
379 let input = b"<br/>";
380 let indexer = StructuralIndexer::new();
381 let index = indexer.index(input);
382
383 let delims: Vec<_> = index.iter_delimiters().collect();
384 assert_eq!(
385 delims,
386 vec![
387 DelimiterEntry { pos: 0, byte: b'<' },
388 DelimiterEntry { pos: 3, byte: b'/' },
389 DelimiterEntry { pos: 4, byte: b'>' },
390 ]
391 );
392 }
393
394 #[test]
395 fn entity_reference() {
396 let input = b"a & b";
397 let indexer = StructuralIndexer::new();
398 let index = indexer.index(input);
399
400 let amp_entries: Vec<_> = index.iter_delimiters().filter(|d| d.byte == b'&').collect();
401 assert_eq!(amp_entries.len(), 1);
402 assert_eq!(amp_entries[0].pos, 2);
403 }
404
405 #[test]
410 fn long_input_multiple_blocks() {
411 let mut input = Vec::with_capacity(1200);
413 input.extend_from_slice(&[b'x'; 100]);
415 input.extend_from_slice(b"<div>");
417 input.extend_from_slice(&[b'y'; 200]);
419 input.extend_from_slice(b"<span class=\"test\">");
421 input.extend_from_slice(&[b'z'; 700]);
423 input.extend_from_slice(b"</span></div>");
425
426 let indexer = StructuralIndexer::new();
427 let index = indexer.index(&input);
428
429 assert!(input.len() > 1000);
430 assert!(index.block_count() > 15);
431
432 let delims: Vec<_> = index.iter_delimiters().collect();
433
434 assert!(
436 delims.iter().any(|d| d.pos == 100 && d.byte == b'<'),
437 "should find < at offset 100"
438 );
439
440 assert!(
444 delims.iter().any(|d| d.byte == b'='),
445 "should find = in attribute"
446 );
447
448 let last_lt = delims.iter().rev().find(|d| d.byte == b'<');
450 assert!(last_lt.is_some());
451 }
452
453 #[test]
454 fn long_input_with_quotes_spanning_blocks() {
455 let mut input = Vec::new();
457 input.extend_from_slice(b"<div data=\"");
458 while input.len() < 60 {
460 input.push(b'a');
461 }
462 input.push(b'<');
464 while input.len() < 80 {
466 input.push(b'b');
467 }
468 input.extend_from_slice(b"\">end</div>");
470
471 let indexer = StructuralIndexer::new();
472 let index = indexer.index(&input);
473
474 let delims: Vec<_> = index.iter_delimiters().collect();
475 let lt_positions: Vec<usize> = delims
476 .iter()
477 .filter(|d| d.byte == b'<')
478 .map(|d| d.pos)
479 .collect();
480
481 assert!(
485 lt_positions.contains(&60),
486 "< at offset 60 is indexed (parser handles quote state)"
487 );
488
489 assert!(lt_positions.contains(&0), "opening < should be structural");
491 }
492
493 #[test]
498 fn estimated_token_count_basic() {
499 let input = b"<div>hello</div><p>world</p>";
500 let indexer = StructuralIndexer::new();
501 let index = indexer.index(input);
502
503 assert_eq!(index.estimated_token_count(), 5);
505 }
506
507 #[test]
512 fn block_api() {
513 let input = b"<div>text</div>";
514 let indexer = StructuralIndexer::new();
515 let index = indexer.index(input);
516
517 assert_eq!(index.input_len(), 15);
518 assert_eq!(index.block_count(), 1);
519
520 let block = index.block(0);
521 assert_ne!(block.lt, 0, "should have < bits set");
522 assert_ne!(block.gt, 0, "should have > bits set");
523 }
524}