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 parent_is_raw_text = self.arena.get(parent).tag.is_raw_text();
353 if parent_is_raw_text {
354 self.try_source_ref(depth, raw)
355 } else {
356 let decoded = fhp_tokenizer::entity::decode_entities(raw);
357 match decoded {
358 std::borrow::Cow::Borrowed(s) => self.try_source_ref(depth, s),
359 std::borrow::Cow::Owned(s) => self.arena.new_text(depth, &s),
360 }
361 }
362 };
363
364 #[cfg(not(feature = "entity-decode"))]
365 let node = self.try_source_ref(depth, raw);
366
367 self.arena.append_child(parent, node);
368 Some(node)
369 }
370
371 #[inline]
374 fn try_source_ref(&mut self, depth: u16, content: &str) -> NodeId {
375 if self.source_len > 0 {
376 let ptr = content.as_ptr() as usize;
377 if ptr >= self.source_base && ptr + content.len() <= self.source_base + self.source_len
378 {
379 let offset = ptr - self.source_base;
380 return self
381 .arena
382 .new_text_ref(depth, offset as u32, content.len() as u32);
383 }
384 }
385 self.arena.new_text(depth, content)
386 }
387
388 fn handle_comment(&mut self, content: &str) -> Option<NodeId> {
390 let depth = self.current_depth();
391 let parent = self.current_parent();
392 let node = self.arena.new_comment(depth, content);
393 self.arena.append_child(parent, node);
394 Some(node)
395 }
396
397 fn handle_doctype(&mut self, content: &str) -> Option<NodeId> {
399 let depth = self.current_depth();
400 let parent = self.current_parent();
401 let node = self.arena.new_doctype(depth, content);
402 self.arena.append_child(parent, node);
403 Some(node)
404 }
405
406 fn apply_implicit_close(&mut self, new_tag: Tag) {
408 while self.open_elements.len() > 1 {
411 let (_, current_tag, _) = *self.open_elements.last().unwrap();
412
413 if should_implicit_close(current_tag, new_tag) {
414 self.open_elements.pop();
415 } else {
416 break;
417 }
418 }
419 }
420}
421
422impl fhp_tokenizer::TreeSink for TreeBuilder {
423 fn open_tag(&mut self, tag: Tag, name: &str, attr_raw: &str, self_closing: bool) {
424 self.apply_implicit_close(tag);
425
426 if self.current_depth() >= MAX_DEPTH {
427 if !tag.is_void() && !self_closing {
430 self.suppressed_opens += 1;
431 }
432 return;
433 }
434
435 let depth = self.current_depth();
436 let parent = self.current_parent();
437 let node = self.arena.new_element(tag, depth);
438 if tag == Tag::Unknown {
439 self.arena.set_unknown_tag_name(node, name);
440 }
441
442 self.arena.set_attrs_from_raw(node, attr_raw);
444
445 self.arena.append_child(parent, node);
446 if let Some(parent_entry) = self.open_elements.last_mut() {
447 parent_entry.2 = parent_entry.2.saturating_add(1);
452 self.arena.set_element_index(node, parent_entry.2);
453 }
454
455 if tag.is_void() || self_closing {
456 if self_closing {
457 self.arena.set_self_closing(node);
458 }
459 if tag.is_void() {
460 self.arena.set_self_closing(node);
461 }
462 } else {
463 self.open_elements.push((node, tag, 0));
464 }
465 }
466
467 fn close_tag(&mut self, tag: Tag, name: &str) {
468 self.handle_close_tag(tag, name);
469 }
470
471 fn text(&mut self, raw: &str) {
472 self.handle_raw_text(raw);
473 }
474
475 fn comment(&mut self, content: &str) {
476 self.handle_comment(content);
477 }
478
479 fn doctype(&mut self, content: &str) {
480 self.handle_doctype(content);
481 }
482
483 fn cdata(&mut self, content: &str) {
484 self.handle_raw_text(content);
486 }
487}
488
489impl Default for TreeBuilder {
490 fn default() -> Self {
491 Self::new()
492 }
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498 use crate::node::NodeFlags;
499 use std::borrow::Cow;
500
501 fn make_open(tag: Tag) -> Token<'static> {
502 Token::OpenTag {
503 tag,
504 name: Cow::Borrowed(tag.as_str().unwrap_or("unknown")),
505 attributes: vec![],
506 self_closing: false,
507 }
508 }
509
510 fn make_close(tag: Tag) -> Token<'static> {
511 Token::CloseTag {
512 tag,
513 name: Cow::Borrowed(tag.as_str().unwrap_or("unknown")),
514 }
515 }
516
517 fn make_text(content: &'static str) -> Token<'static> {
518 Token::Text {
519 content: Cow::Borrowed(content),
520 }
521 }
522
523 #[test]
524 fn simple_tree() {
525 let mut builder = TreeBuilder::new();
526 builder.process(&make_open(Tag::Div));
527 builder.process(&make_text("hello"));
528 builder.process(&make_close(Tag::Div));
529
530 let (arena, root) = builder.finish();
531
532 let div = arena.get(root).first_child;
534 assert!(!div.is_null());
535 assert_eq!(arena.get(div).tag, Tag::Div);
536
537 let text = arena.get(div).first_child;
539 assert!(!text.is_null());
540 assert!(arena.get(text).flags.has(NodeFlags::IS_TEXT));
541 assert_eq!(arena.text(text), "hello");
542 }
543
544 #[test]
545 fn void_element_not_pushed() {
546 let mut builder = TreeBuilder::new();
547 builder.process(&make_open(Tag::Div));
548 builder.process(&Token::OpenTag {
549 tag: Tag::Br,
550 name: Cow::Borrowed("br"),
551 attributes: vec![],
552 self_closing: false,
553 });
554 builder.process(&make_text("after br"));
555 builder.process(&make_close(Tag::Div));
556
557 let (arena, root) = builder.finish();
558 let div = arena.get(root).first_child;
559 let br = arena.get(div).first_child;
560 assert_eq!(arena.get(br).tag, Tag::Br);
561
562 let text = arena.get(br).next_sibling;
564 assert!(!text.is_null());
565 assert_eq!(arena.text(text), "after br");
566 }
567
568 #[test]
569 fn implicit_close_p() {
570 let mut builder = TreeBuilder::new();
571 builder.process(&make_open(Tag::P));
572 builder.process(&make_text("first"));
573 builder.process(&make_open(Tag::P));
574 builder.process(&make_text("second"));
575 builder.process(&make_close(Tag::P));
576
577 let (arena, root) = builder.finish();
578
579 let p1 = arena.get(root).first_child;
581 assert_eq!(arena.get(p1).tag, Tag::P);
582
583 let p2 = arena.get(p1).next_sibling;
584 assert!(!p2.is_null());
585 assert_eq!(arena.get(p2).tag, Tag::P);
586
587 assert_eq!(arena.text(arena.get(p1).first_child), "first");
589 assert_eq!(arena.text(arena.get(p2).first_child), "second");
590 }
591
592 #[test]
593 fn mismatched_close_finds_nearest() {
594 let mut builder = TreeBuilder::new();
596 builder.process(&make_open(Tag::Div));
597 builder.process(&make_open(Tag::Span));
598 builder.process(&make_text("hi"));
599 builder.process(&make_close(Tag::Div));
600
601 let (arena, root) = builder.finish();
602 let div = arena.get(root).first_child;
603 assert_eq!(arena.get(div).tag, Tag::Div);
604 }
605
606 #[test]
607 fn extra_close_tag_ignored() {
608 let mut builder = TreeBuilder::new();
609 builder.process(&make_close(Tag::Div)); builder.process(&make_open(Tag::P));
611 builder.process(&make_text("ok"));
612 builder.process(&make_close(Tag::P));
613
614 let (arena, root) = builder.finish();
615 let p = arena.get(root).first_child;
616 assert_eq!(arena.get(p).tag, Tag::P);
617 }
618
619 #[test]
620 fn unknown_close_matches_by_name() {
621 let mut builder = TreeBuilder::new();
622 builder.process(&Token::OpenTag {
623 tag: Tag::Unknown,
624 name: Cow::Borrowed("my-widget"),
625 attributes: vec![],
626 self_closing: false,
627 });
628 builder.process(&Token::OpenTag {
629 tag: Tag::Unknown,
630 name: Cow::Borrowed("x-item"),
631 attributes: vec![],
632 self_closing: false,
633 });
634 builder.process(&Token::CloseTag {
635 tag: Tag::Unknown,
636 name: Cow::Borrowed("my-widget"),
637 });
638
639 let (arena, root) = builder.finish();
640 let my_widget = arena.get(root).first_child;
641 let x_item = arena.get(my_widget).first_child;
642 assert_eq!(arena.unknown_tag_name(my_widget), Some("my-widget"));
643 assert_eq!(arena.unknown_tag_name(x_item), Some("x-item"));
644 }
645
646 #[test]
647 fn should_implicit_close_rules() {
648 assert!(should_implicit_close(Tag::P, Tag::P));
649 assert!(should_implicit_close(Tag::Li, Tag::Li));
650 assert!(should_implicit_close(Tag::Td, Tag::Td));
651 assert!(should_implicit_close(Tag::Td, Tag::Th));
652 assert!(should_implicit_close(Tag::Th, Tag::Td));
653 assert!(should_implicit_close(Tag::Tr, Tag::Tr));
654
655 assert!(!should_implicit_close(Tag::Div, Tag::Div));
656 assert!(!should_implicit_close(Tag::Span, Tag::Span));
657 assert!(!should_implicit_close(Tag::P, Tag::Span));
658 }
659}