1use fhp_core::tag::Tag;
8use fhp_tokenizer::token::Token;
9
10use crate::arena::Arena;
11use crate::node::NodeId;
12
13const MAX_DEPTH: u16 = 512;
15
16const IMPLICIT_CLOSE_SIZE: usize = 8;
24static IMPLICIT_CLOSE: [[bool; IMPLICIT_CLOSE_SIZE]; IMPLICIT_CLOSE_SIZE] = {
25 let mut table = [[false; IMPLICIT_CLOSE_SIZE]; IMPLICIT_CLOSE_SIZE];
26
27 let p = 0usize;
29 let li = 1;
30 let td = 2;
31 let th = 3;
32 let tr = 4;
33 let thead = 5;
34 let tbody = 6;
35 let option = 7;
36
37 table[p][p] = true;
40
41 table[li][li] = true;
43
44 table[td][td] = true;
46 table[td][th] = true;
47
48 table[th][td] = true;
50 table[th][th] = true;
51
52 table[tr][tr] = true;
54
55 table[thead][tbody] = true;
57
58 table[tbody][tbody] = true;
60
61 table[option][option] = true;
63
64 table
65};
66
67fn implicit_close_index(tag: Tag) -> Option<usize> {
70 match tag {
71 Tag::P => Some(0),
72 Tag::Li => Some(1),
73 Tag::Td => Some(2),
74 Tag::Th => Some(3),
75 Tag::Tr => Some(4),
76 Tag::Thead => Some(5),
77 Tag::Tbody => Some(6),
78 _ => None,
80 }
81}
82
83#[inline]
85fn should_implicit_close(open_tag: Tag, new_tag: Tag) -> bool {
86 if let (Some(open_idx), Some(new_idx)) = (
87 implicit_close_index(open_tag),
88 implicit_close_index(new_tag),
89 ) {
90 IMPLICIT_CLOSE[open_idx][new_idx]
91 } else {
92 false
93 }
94}
95
96pub struct TreeBuilder {
101 pub(crate) arena: Arena,
103 open_elements: Vec<(NodeId, Tag, u16)>,
105 root: NodeId,
107 source_base: usize,
109 source_len: usize,
111 suppressed_opens: u32,
114}
115
116impl TreeBuilder {
117 pub fn new() -> Self {
119 Self::with_capacity_hint(0)
120 }
121
122 pub fn with_capacity_hint(input_len: usize) -> Self {
127 let node_cap = (input_len / 32).max(256);
128 let text_cap = (input_len / 16).max(4096);
130 let attr_cap = (input_len / 128).max(64);
131 let mut arena = Arena::with_capacity(node_cap, text_cap, attr_cap);
132 let root = arena.new_element(Tag::Unknown, 0);
134 let mut open_elements = Vec::with_capacity(32);
135 open_elements.push((root, Tag::Unknown, 0));
136 Self {
137 arena,
138 open_elements,
139 root,
140 source_base: 0,
141 source_len: 0,
142 suppressed_opens: 0,
143 }
144 }
145
146 pub fn enable_tag_index(&mut self) {
152 self.arena.enable_tag_index();
153 }
154
155 pub fn set_source(&mut self, input: &str) {
161 self.source_base = input.as_ptr() as usize;
162 self.source_len = input.len();
163 self.arena.set_source(input);
164 }
165
166 pub fn set_source_ptr(&mut self, input: &str) {
171 self.source_base = input.as_ptr() as usize;
172 self.source_len = input.len();
173 }
174
175 #[inline]
180 pub fn process(&mut self, token: &Token<'_>) -> Option<NodeId> {
181 match token {
182 Token::OpenTag {
183 tag,
184 name,
185 attributes,
186 self_closing,
187 ..
188 } => self.handle_open_tag(*tag, name.as_ref(), attributes, *self_closing),
189 Token::CloseTag { tag, name } => {
190 self.handle_close_tag(*tag, name.as_ref());
191 None
192 }
193 Token::Text { content } => self.handle_text(content.as_ref()),
194 Token::Comment { content } => self.handle_comment(content.as_ref()),
195 Token::Doctype { content } => self.handle_doctype(content.as_ref()),
196 Token::CData { content } => {
197 self.handle_text(content.as_ref())
199 }
200 }
201 }
202
203 pub fn finish(self) -> (Arena, NodeId) {
205 (self.arena, self.root)
207 }
208
209 #[inline]
211 fn current_parent(&self) -> NodeId {
212 self.open_elements
213 .last()
214 .map(|&(id, _, _)| id)
215 .unwrap_or(self.root)
216 }
217
218 #[inline]
220 fn current_depth(&self) -> u16 {
221 (self.open_elements.len() as u16).min(MAX_DEPTH)
222 }
223
224 fn handle_open_tag(
226 &mut self,
227 tag: Tag,
228 name: &str,
229 attributes: &[fhp_tokenizer::token::Attribute<'_>],
230 self_closing: bool,
231 ) -> Option<NodeId> {
232 self.apply_implicit_close(tag);
234
235 if self.current_depth() >= MAX_DEPTH {
237 if !tag.is_void() && !self_closing {
241 self.suppressed_opens += 1;
242 }
243 return None;
244 }
245
246 let depth = self.current_depth();
247 let parent = self.current_parent();
248 let node = self.arena.new_element(tag, depth);
249 if tag == Tag::Unknown {
250 self.arena.set_unknown_tag_name(node, name);
251 }
252
253 if !attributes.is_empty() {
255 self.arena.set_attrs(node, attributes);
256 }
257
258 self.arena.append_child(parent, node);
260 if let Some(parent_entry) = self.open_elements.last_mut() {
261 parent_entry.2 = parent_entry.2.saturating_add(1);
266 self.arena.set_element_index(node, parent_entry.2);
267 }
268
269 if tag.is_void() || self_closing {
271 if self_closing {
272 self.arena.set_self_closing(node);
273 }
274 if tag.is_void() {
276 self.arena.set_self_closing(node);
277 }
278 } else {
279 self.open_elements.push((node, tag, 0));
280 }
281
282 Some(node)
283 }
284
285 fn handle_close_tag(&mut self, tag: Tag, name: &str) {
287 if tag.is_void() {
289 return;
290 }
291
292 if self.suppressed_opens > 0 {
296 self.suppressed_opens -= 1;
297 return;
298 }
299
300 let mut match_idx = None;
303 for i in (1..self.open_elements.len()).rev() {
304 let (open_id, open_tag, _) = self.open_elements[i];
305 if open_tag == tag {
306 if tag == Tag::Unknown {
307 let open_name = self.arena.unknown_tag_name(open_id).unwrap_or("");
308 if !open_name.eq_ignore_ascii_case(name) {
309 continue;
310 }
311 }
312 match_idx = Some(i);
313 break;
314 }
315 }
316
317 if let Some(idx) = match_idx {
318 self.open_elements.truncate(idx);
320 }
321 }
323
324 fn handle_text(&mut self, content: &str) -> Option<NodeId> {
329 if content.is_empty() {
330 return None;
331 }
332 let depth = self.current_depth();
333 let parent = self.current_parent();
334 let node = self.try_source_ref(depth, content);
335 self.arena.append_child(parent, node);
336 Some(node)
337 }
338
339 fn handle_raw_text(&mut self, raw: &str) -> Option<NodeId> {
344 if raw.is_empty() {
345 return None;
346 }
347 let depth = self.current_depth();
348 let parent = self.current_parent();
349
350 #[cfg(feature = "entity-decode")]
351 let node = {
352 let decoded = fhp_tokenizer::entity::decode_entities(raw);
353 match decoded {
354 std::borrow::Cow::Borrowed(s) => self.try_source_ref(depth, s),
355 std::borrow::Cow::Owned(s) => self.arena.new_text(depth, &s),
356 }
357 };
358
359 #[cfg(not(feature = "entity-decode"))]
360 let node = self.try_source_ref(depth, raw);
361
362 self.arena.append_child(parent, node);
363 Some(node)
364 }
365
366 #[inline]
369 fn try_source_ref(&mut self, depth: u16, content: &str) -> NodeId {
370 if self.source_len > 0 {
371 let ptr = content.as_ptr() as usize;
372 if ptr >= self.source_base && ptr + content.len() <= self.source_base + self.source_len
373 {
374 let offset = ptr - self.source_base;
375 return self
376 .arena
377 .new_text_ref(depth, offset as u32, content.len() as u32);
378 }
379 }
380 self.arena.new_text(depth, content)
381 }
382
383 fn handle_comment(&mut self, content: &str) -> Option<NodeId> {
385 let depth = self.current_depth();
386 let parent = self.current_parent();
387 let node = self.arena.new_comment(depth, content);
388 self.arena.append_child(parent, node);
389 Some(node)
390 }
391
392 fn handle_doctype(&mut self, content: &str) -> Option<NodeId> {
394 let depth = self.current_depth();
395 let parent = self.current_parent();
396 let node = self.arena.new_doctype(depth, content);
397 self.arena.append_child(parent, node);
398 Some(node)
399 }
400
401 fn apply_implicit_close(&mut self, new_tag: Tag) {
403 while self.open_elements.len() > 1 {
406 let (_, current_tag, _) = *self.open_elements.last().unwrap();
407
408 if should_implicit_close(current_tag, new_tag) {
409 self.open_elements.pop();
410 } else {
411 break;
412 }
413 }
414 }
415}
416
417impl fhp_tokenizer::TreeSink for TreeBuilder {
418 fn open_tag(&mut self, tag: Tag, name: &str, attr_raw: &str, self_closing: bool) {
419 self.apply_implicit_close(tag);
420
421 if self.current_depth() >= MAX_DEPTH {
422 if !tag.is_void() && !self_closing {
425 self.suppressed_opens += 1;
426 }
427 return;
428 }
429
430 let depth = self.current_depth();
431 let parent = self.current_parent();
432 let node = self.arena.new_element(tag, depth);
433 if tag == Tag::Unknown {
434 self.arena.set_unknown_tag_name(node, name);
435 }
436
437 self.arena.set_attrs_from_raw(node, attr_raw);
439
440 self.arena.append_child(parent, node);
441 if let Some(parent_entry) = self.open_elements.last_mut() {
442 parent_entry.2 = parent_entry.2.saturating_add(1);
447 self.arena.set_element_index(node, parent_entry.2);
448 }
449
450 if tag.is_void() || self_closing {
451 if self_closing {
452 self.arena.set_self_closing(node);
453 }
454 if tag.is_void() {
455 self.arena.set_self_closing(node);
456 }
457 } else {
458 self.open_elements.push((node, tag, 0));
459 }
460 }
461
462 fn close_tag(&mut self, tag: Tag, name: &str) {
463 self.handle_close_tag(tag, name);
464 }
465
466 fn text(&mut self, raw: &str) {
467 self.handle_raw_text(raw);
468 }
469
470 fn comment(&mut self, content: &str) {
471 self.handle_comment(content);
472 }
473
474 fn doctype(&mut self, content: &str) {
475 self.handle_doctype(content);
476 }
477
478 fn cdata(&mut self, content: &str) {
479 self.handle_raw_text(content);
481 }
482}
483
484impl Default for TreeBuilder {
485 fn default() -> Self {
486 Self::new()
487 }
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493 use crate::node::NodeFlags;
494 use std::borrow::Cow;
495
496 fn make_open(tag: Tag) -> Token<'static> {
497 Token::OpenTag {
498 tag,
499 name: Cow::Borrowed(tag.as_str().unwrap_or("unknown")),
500 attributes: vec![],
501 self_closing: false,
502 }
503 }
504
505 fn make_close(tag: Tag) -> Token<'static> {
506 Token::CloseTag {
507 tag,
508 name: Cow::Borrowed(tag.as_str().unwrap_or("unknown")),
509 }
510 }
511
512 fn make_text(content: &'static str) -> Token<'static> {
513 Token::Text {
514 content: Cow::Borrowed(content),
515 }
516 }
517
518 #[test]
519 fn simple_tree() {
520 let mut builder = TreeBuilder::new();
521 builder.process(&make_open(Tag::Div));
522 builder.process(&make_text("hello"));
523 builder.process(&make_close(Tag::Div));
524
525 let (arena, root) = builder.finish();
526
527 let div = arena.get(root).first_child;
529 assert!(!div.is_null());
530 assert_eq!(arena.get(div).tag, Tag::Div);
531
532 let text = arena.get(div).first_child;
534 assert!(!text.is_null());
535 assert!(arena.get(text).flags.has(NodeFlags::IS_TEXT));
536 assert_eq!(arena.text(text), "hello");
537 }
538
539 #[test]
540 fn void_element_not_pushed() {
541 let mut builder = TreeBuilder::new();
542 builder.process(&make_open(Tag::Div));
543 builder.process(&Token::OpenTag {
544 tag: Tag::Br,
545 name: Cow::Borrowed("br"),
546 attributes: vec![],
547 self_closing: false,
548 });
549 builder.process(&make_text("after br"));
550 builder.process(&make_close(Tag::Div));
551
552 let (arena, root) = builder.finish();
553 let div = arena.get(root).first_child;
554 let br = arena.get(div).first_child;
555 assert_eq!(arena.get(br).tag, Tag::Br);
556
557 let text = arena.get(br).next_sibling;
559 assert!(!text.is_null());
560 assert_eq!(arena.text(text), "after br");
561 }
562
563 #[test]
564 fn implicit_close_p() {
565 let mut builder = TreeBuilder::new();
566 builder.process(&make_open(Tag::P));
567 builder.process(&make_text("first"));
568 builder.process(&make_open(Tag::P));
569 builder.process(&make_text("second"));
570 builder.process(&make_close(Tag::P));
571
572 let (arena, root) = builder.finish();
573
574 let p1 = arena.get(root).first_child;
576 assert_eq!(arena.get(p1).tag, Tag::P);
577
578 let p2 = arena.get(p1).next_sibling;
579 assert!(!p2.is_null());
580 assert_eq!(arena.get(p2).tag, Tag::P);
581
582 assert_eq!(arena.text(arena.get(p1).first_child), "first");
584 assert_eq!(arena.text(arena.get(p2).first_child), "second");
585 }
586
587 #[test]
588 fn mismatched_close_finds_nearest() {
589 let mut builder = TreeBuilder::new();
591 builder.process(&make_open(Tag::Div));
592 builder.process(&make_open(Tag::Span));
593 builder.process(&make_text("hi"));
594 builder.process(&make_close(Tag::Div));
595
596 let (arena, root) = builder.finish();
597 let div = arena.get(root).first_child;
598 assert_eq!(arena.get(div).tag, Tag::Div);
599 }
600
601 #[test]
602 fn extra_close_tag_ignored() {
603 let mut builder = TreeBuilder::new();
604 builder.process(&make_close(Tag::Div)); builder.process(&make_open(Tag::P));
606 builder.process(&make_text("ok"));
607 builder.process(&make_close(Tag::P));
608
609 let (arena, root) = builder.finish();
610 let p = arena.get(root).first_child;
611 assert_eq!(arena.get(p).tag, Tag::P);
612 }
613
614 #[test]
615 fn unknown_close_matches_by_name() {
616 let mut builder = TreeBuilder::new();
617 builder.process(&Token::OpenTag {
618 tag: Tag::Unknown,
619 name: Cow::Borrowed("my-widget"),
620 attributes: vec![],
621 self_closing: false,
622 });
623 builder.process(&Token::OpenTag {
624 tag: Tag::Unknown,
625 name: Cow::Borrowed("x-item"),
626 attributes: vec![],
627 self_closing: false,
628 });
629 builder.process(&Token::CloseTag {
630 tag: Tag::Unknown,
631 name: Cow::Borrowed("my-widget"),
632 });
633
634 let (arena, root) = builder.finish();
635 let my_widget = arena.get(root).first_child;
636 let x_item = arena.get(my_widget).first_child;
637 assert_eq!(arena.unknown_tag_name(my_widget), Some("my-widget"));
638 assert_eq!(arena.unknown_tag_name(x_item), Some("x-item"));
639 }
640
641 #[test]
642 fn should_implicit_close_rules() {
643 assert!(should_implicit_close(Tag::P, Tag::P));
644 assert!(should_implicit_close(Tag::Li, Tag::Li));
645 assert!(should_implicit_close(Tag::Td, Tag::Td));
646 assert!(should_implicit_close(Tag::Td, Tag::Th));
647 assert!(should_implicit_close(Tag::Th, Tag::Td));
648 assert!(should_implicit_close(Tag::Tr, Tag::Tr));
649
650 assert!(!should_implicit_close(Tag::Div, Tag::Div));
651 assert!(!should_implicit_close(Tag::Span, Tag::Span));
652 assert!(!should_implicit_close(Tag::P, Tag::Span));
653 }
654}