justhtml 0.24.0__py3-none-any.whl → 0.38.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of justhtml might be problematic. Click here for more details.

@@ -3,7 +3,7 @@
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
- from typing import Any
6
+ from typing import TYPE_CHECKING, Any, Literal
7
7
 
8
8
  from .constants import (
9
9
  FORMAT_MARKER,
@@ -11,16 +11,22 @@ from .constants import (
11
11
  HEADING_ELEMENTS,
12
12
  )
13
13
  from .node import SimpleDomNode, TemplateNode
14
- from .tokens import CharacterTokens, CommentToken, EOFToken, Tag, TokenSinkResult
14
+ from .tokens import AnyToken, CharacterTokens, CommentToken, DoctypeToken, EOFToken, Tag, TokenSinkResult
15
15
  from .treebuilder_utils import (
16
16
  InsertionMode,
17
17
  doctype_error_and_quirks,
18
18
  is_all_whitespace,
19
19
  )
20
20
 
21
+ if TYPE_CHECKING:
22
+ from collections.abc import Callable
23
+
24
+ ModeResultTuple = tuple[str, InsertionMode, AnyToken] | tuple[str, InsertionMode, AnyToken, bool]
25
+ "Result is (instruction, mode, token) or (instruction, mode, token, force_html)"
26
+
21
27
 
22
28
  class TreeBuilderModesMixin:
23
- def _handle_doctype(self, token: Any) -> Any:
29
+ def _handle_doctype(self, token: DoctypeToken) -> Literal[0]:
24
30
  if self.mode != InsertionMode.INITIAL:
25
31
  self._parse_error("unexpected-doctype")
26
32
  return TokenSinkResult.Continue
@@ -38,7 +44,7 @@ class TreeBuilderModesMixin:
38
44
  self.mode = InsertionMode.BEFORE_HTML
39
45
  return TokenSinkResult.Continue
40
46
 
41
- def _mode_initial(self, token: Any) -> Any:
47
+ def _mode_initial(self, token: Any) -> ModeResultTuple | None:
42
48
  if isinstance(token, CharacterTokens):
43
49
  if is_all_whitespace(token.data):
44
50
  return None
@@ -61,7 +67,7 @@ class TreeBuilderModesMixin:
61
67
  self._set_quirks_mode("quirks")
62
68
  return ("reprocess", InsertionMode.BEFORE_HTML, token)
63
69
 
64
- def _mode_before_html(self, token: Any) -> Any:
70
+ def _mode_before_html(self, token: AnyToken) -> ModeResultTuple | None:
65
71
  if isinstance(token, CharacterTokens) and is_all_whitespace(token.data):
66
72
  return None
67
73
  if isinstance(token, CommentToken):
@@ -94,7 +100,7 @@ class TreeBuilderModesMixin:
94
100
  self.mode = InsertionMode.BEFORE_HEAD
95
101
  return ("reprocess", InsertionMode.BEFORE_HEAD, token)
96
102
 
97
- def _mode_before_head(self, token: Any) -> Any:
103
+ def _mode_before_head(self, token: AnyToken) -> ModeResultTuple | None:
98
104
  if isinstance(token, CharacterTokens):
99
105
  data = token.data or ""
100
106
  if "\x00" in data:
@@ -137,7 +143,7 @@ class TreeBuilderModesMixin:
137
143
  self.mode = InsertionMode.IN_HEAD
138
144
  return ("reprocess", InsertionMode.IN_HEAD, token)
139
145
 
140
- def _mode_in_head(self, token: Any) -> Any:
146
+ def _mode_in_head(self, token: AnyToken) -> ModeResultTuple | None:
141
147
  if isinstance(token, CharacterTokens):
142
148
  if is_all_whitespace(token.data):
143
149
  self._append_text(token.data)
@@ -213,7 +219,7 @@ class TreeBuilderModesMixin:
213
219
  self.mode = InsertionMode.AFTER_HEAD
214
220
  return ("reprocess", InsertionMode.AFTER_HEAD, token)
215
221
 
216
- def _mode_in_head_noscript(self, token: Any) -> Any:
222
+ def _mode_in_head_noscript(self, token: AnyToken) -> ModeResultTuple | None:
217
223
  """Handle tokens in 'in head noscript' insertion mode (scripting disabled)."""
218
224
  if isinstance(token, CharacterTokens):
219
225
  data = token.data or ""
@@ -262,7 +268,7 @@ class TreeBuilderModesMixin:
262
268
  # All token types are handled above - CharacterTokens, CommentToken, Tag, EOFToken
263
269
  return None # pragma: no cover
264
270
 
265
- def _mode_after_head(self, token: Any) -> Any:
271
+ def _mode_after_head(self, token: AnyToken) -> ModeResultTuple | None:
266
272
  if isinstance(token, CharacterTokens):
267
273
  data = token.data or ""
268
274
  if "\x00" in data:
@@ -351,7 +357,7 @@ class TreeBuilderModesMixin:
351
357
  self._insert_body_if_missing()
352
358
  return ("reprocess", InsertionMode.IN_BODY, token)
353
359
 
354
- def _mode_text(self, token: Any) -> Any:
360
+ def _mode_text(self, token: AnyToken) -> ModeResultTuple | None:
355
361
  if isinstance(token, CharacterTokens):
356
362
  self._append_text(token.data)
357
363
  return None
@@ -367,11 +373,11 @@ class TreeBuilderModesMixin:
367
373
  self.mode = self.original_mode or InsertionMode.IN_BODY
368
374
  return None
369
375
 
370
- def _mode_in_body(self, token: Any) -> Any:
376
+ def _mode_in_body(self, token: Any) -> ModeResultTuple | None:
371
377
  handler = self._BODY_TOKEN_HANDLERS.get(type(token))
372
378
  return handler(self, token) if handler else None
373
379
 
374
- def _handle_characters_in_body(self, token: Any) -> Any:
380
+ def _handle_characters_in_body(self, token: CharacterTokens) -> None:
375
381
  data = token.data or ""
376
382
  if "\x00" in data:
377
383
  self._parse_error("invalid-codepoint")
@@ -385,11 +391,11 @@ class TreeBuilderModesMixin:
385
391
  self._append_text(data)
386
392
  return
387
393
 
388
- def _handle_comment_in_body(self, token: Any) -> Any:
394
+ def _handle_comment_in_body(self, token: CommentToken) -> None:
389
395
  self._append_comment(token.data)
390
396
  return
391
397
 
392
- def _handle_tag_in_body(self, token: Any) -> Any:
398
+ def _handle_tag_in_body(self, token: Tag) -> ModeResultTuple | None:
393
399
  if token.kind == Tag.START:
394
400
  handler = self._BODY_START_HANDLERS.get(token.name)
395
401
  if handler:
@@ -413,7 +419,7 @@ class TreeBuilderModesMixin:
413
419
  self._any_other_end_tag(token.name)
414
420
  return None
415
421
 
416
- def _handle_eof_in_body(self, token: Any) -> Any:
422
+ def _handle_eof_in_body(self, token: EOFToken) -> ModeResultTuple | None:
417
423
  # If we're in a template, handle EOF in template mode first
418
424
  if self.template_modes:
419
425
  return self._mode_in_template(token)
@@ -448,7 +454,7 @@ class TreeBuilderModesMixin:
448
454
  # Body mode start tag handlers
449
455
  # ---------------------
450
456
 
451
- def _handle_body_start_html(self, token: Any) -> Any:
457
+ def _handle_body_start_html(self, token: Tag) -> None:
452
458
  if self.template_modes:
453
459
  self._parse_error("unexpected-start-tag", tag_name=token.name)
454
460
  return
@@ -460,7 +466,7 @@ class TreeBuilderModesMixin:
460
466
  self._add_missing_attributes(html, token.attrs)
461
467
  return
462
468
 
463
- def _handle_body_start_body(self, token: Any) -> Any:
469
+ def _handle_body_start_body(self, token: Tag) -> None:
464
470
  if self.template_modes:
465
471
  self._parse_error("unexpected-start-tag", tag_name=token.name)
466
472
  return
@@ -474,19 +480,19 @@ class TreeBuilderModesMixin:
474
480
  self.frameset_ok = False
475
481
  return
476
482
 
477
- def _handle_body_start_head(self, token: Any) -> Any:
483
+ def _handle_body_start_head(self, token: Tag) -> None:
478
484
  self._parse_error("unexpected-start-tag", tag_name=token.name)
479
485
  return
480
486
 
481
- def _handle_body_start_in_head(self, token: Any) -> Any:
487
+ def _handle_body_start_in_head(self, token: Tag) -> ModeResultTuple | None:
482
488
  return self._mode_in_head(token)
483
489
 
484
- def _handle_body_start_block_with_p(self, token: Any) -> Any:
490
+ def _handle_body_start_block_with_p(self, token: Tag) -> None:
485
491
  self._close_p_element()
486
492
  self._insert_element(token, push=True)
487
493
  return
488
494
 
489
- def _handle_body_start_heading(self, token: Any) -> Any:
495
+ def _handle_body_start_heading(self, token: Tag) -> None:
490
496
  self._close_p_element()
491
497
  if self.open_elements and self.open_elements[-1].name in HEADING_ELEMENTS:
492
498
  self._parse_error("unexpected-start-tag", tag_name=token.name)
@@ -495,14 +501,14 @@ class TreeBuilderModesMixin:
495
501
  self.frameset_ok = False
496
502
  return
497
503
 
498
- def _handle_body_start_pre_listing(self, token: Any) -> Any:
504
+ def _handle_body_start_pre_listing(self, token: Tag) -> None:
499
505
  self._close_p_element()
500
506
  self._insert_element(token, push=True)
501
507
  self.ignore_lf = True
502
508
  self.frameset_ok = False
503
509
  return
504
510
 
505
- def _handle_body_start_form(self, token: Any) -> Any:
511
+ def _handle_body_start_form(self, token: Tag) -> None:
506
512
  if self.form_element is not None:
507
513
  self._parse_error("unexpected-start-tag", tag_name=token.name)
508
514
  return
@@ -512,7 +518,7 @@ class TreeBuilderModesMixin:
512
518
  self.frameset_ok = False
513
519
  return
514
520
 
515
- def _handle_body_start_button(self, token: Any) -> Any:
521
+ def _handle_body_start_button(self, token: Tag) -> None:
516
522
  if self._has_in_scope("button"):
517
523
  self._parse_error("unexpected-start-tag-implies-end-tag", tag_name=token.name)
518
524
  self._close_element_by_name("button")
@@ -520,19 +526,19 @@ class TreeBuilderModesMixin:
520
526
  self.frameset_ok = False
521
527
  return
522
528
 
523
- def _handle_body_start_paragraph(self, token: Any) -> Any:
529
+ def _handle_body_start_paragraph(self, token: Tag) -> None:
524
530
  self._close_p_element()
525
531
  self._insert_element(token, push=True)
526
532
  return
527
533
 
528
- def _handle_body_start_math(self, token: Any) -> Any:
534
+ def _handle_body_start_math(self, token: Tag) -> None:
529
535
  self._reconstruct_active_formatting_elements()
530
536
  attrs = self._prepare_foreign_attributes("math", token.attrs)
531
537
  new_tag = Tag(Tag.START, token.name, attrs, token.self_closing)
532
538
  self._insert_element(new_tag, push=not token.self_closing, namespace="math")
533
539
  return
534
540
 
535
- def _handle_body_start_svg(self, token: Any) -> Any:
541
+ def _handle_body_start_svg(self, token: Tag) -> None:
536
542
  self._reconstruct_active_formatting_elements()
537
543
  adjusted_name = self._adjust_svg_tag_name(token.name)
538
544
  attrs = self._prepare_foreign_attributes("svg", token.attrs)
@@ -540,7 +546,7 @@ class TreeBuilderModesMixin:
540
546
  self._insert_element(new_tag, push=not token.self_closing, namespace="svg")
541
547
  return
542
548
 
543
- def _handle_body_start_li(self, token: Any) -> Any:
549
+ def _handle_body_start_li(self, token: Tag) -> None:
544
550
  self.frameset_ok = False
545
551
  self._close_p_element()
546
552
  if self._has_in_list_item_scope("li"):
@@ -548,7 +554,7 @@ class TreeBuilderModesMixin:
548
554
  self._insert_element(token, push=True)
549
555
  return
550
556
 
551
- def _handle_body_start_dd_dt(self, token: Any) -> Any:
557
+ def _handle_body_start_dd_dt(self, token: Tag) -> None:
552
558
  self.frameset_ok = False
553
559
  self._close_p_element()
554
560
  name = token.name
@@ -614,7 +620,7 @@ class TreeBuilderModesMixin:
614
620
  if furthest_block is None:
615
621
  # formatting_element is known to be on the stack
616
622
  while True:
617
- popped = self.open_elements.pop()
623
+ popped = self._pop_current()
618
624
  if popped is formatting_element:
619
625
  break
620
626
  self._remove_formatting_entry(formatting_element_index)
@@ -721,7 +727,7 @@ class TreeBuilderModesMixin:
721
727
  furthest_block_index = self.open_elements.index(furthest_block)
722
728
  self.open_elements.insert(furthest_block_index + 1, new_formatting_element)
723
729
 
724
- def _handle_body_start_a(self, token: Any) -> Any:
730
+ def _handle_body_start_a(self, token: Tag) -> None:
725
731
  if self._has_active_formatting_entry("a"):
726
732
  self._parse_error("unexpected-start-tag-implies-end-tag", tag_name=token.name)
727
733
  self._adoption_agency("a")
@@ -732,7 +738,7 @@ class TreeBuilderModesMixin:
732
738
  self._append_active_formatting_entry("a", token.attrs, node)
733
739
  return
734
740
 
735
- def _handle_body_start_formatting(self, token: Any) -> Any:
741
+ def _handle_body_start_formatting(self, token: Tag) -> None:
736
742
  name = token.name
737
743
  if name == "nobr" and self._in_scope("nobr"):
738
744
  self._adoption_agency("nobr")
@@ -746,21 +752,21 @@ class TreeBuilderModesMixin:
746
752
  self._append_active_formatting_entry(name, token.attrs, node)
747
753
  return
748
754
 
749
- def _handle_body_start_applet_like(self, token: Any) -> Any:
755
+ def _handle_body_start_applet_like(self, token: Tag) -> None:
750
756
  self._reconstruct_active_formatting_elements()
751
757
  self._insert_element(token, push=True)
752
758
  self._push_formatting_marker()
753
759
  self.frameset_ok = False
754
760
  return
755
761
 
756
- def _handle_body_start_br(self, token: Any) -> Any:
762
+ def _handle_body_start_br(self, token: Tag) -> None:
757
763
  self._close_p_element()
758
764
  self._reconstruct_active_formatting_elements()
759
765
  self._insert_element(token, push=False)
760
766
  self.frameset_ok = False
761
767
  return
762
768
 
763
- def _handle_body_start_frameset(self, token: Any) -> Any:
769
+ def _handle_body_start_frameset(self, token: Tag) -> None:
764
770
  if not self.frameset_ok:
765
771
  self._parse_error("unexpected-start-tag-ignored", tag_name=token.name)
766
772
  return
@@ -785,17 +791,17 @@ class TreeBuilderModesMixin:
785
791
  # Body mode end tag handlers
786
792
  # ---------------------
787
793
 
788
- def _handle_body_end_body(self, token: Any) -> Any:
794
+ def _handle_body_end_body(self, token: Tag) -> None:
789
795
  if self._in_scope("body"):
790
796
  self.mode = InsertionMode.AFTER_BODY
791
797
  return
792
798
 
793
- def _handle_body_end_html(self, token: Any) -> Any:
799
+ def _handle_body_end_html(self, token: Tag) -> ModeResultTuple | None:
794
800
  if self._in_scope("body"):
795
801
  return ("reprocess", InsertionMode.AFTER_BODY, token)
796
802
  return None
797
803
 
798
- def _handle_body_end_p(self, token: Any) -> Any:
804
+ def _handle_body_end_p(self, token: Tag) -> None:
799
805
  if not self._close_p_element():
800
806
  self._parse_error("unexpected-end-tag", tag_name=token.name)
801
807
  phantom = Tag(Tag.START, "p", {}, False)
@@ -803,21 +809,21 @@ class TreeBuilderModesMixin:
803
809
  self._close_p_element()
804
810
  return
805
811
 
806
- def _handle_body_end_li(self, token: Any) -> Any:
812
+ def _handle_body_end_li(self, token: Tag) -> None:
807
813
  if not self._has_in_list_item_scope("li"):
808
814
  self._parse_error("unexpected-end-tag", tag_name=token.name)
809
815
  return
810
816
  self._pop_until_any_inclusive({"li"})
811
817
  return
812
818
 
813
- def _handle_body_end_dd_dt(self, token: Any) -> Any:
819
+ def _handle_body_end_dd_dt(self, token: Tag) -> None:
814
820
  name = token.name
815
821
  if not self._has_in_definition_scope(name):
816
822
  self._parse_error("unexpected-end-tag", tag_name=name)
817
823
  return
818
824
  self._pop_until_any_inclusive({"dd", "dt"})
819
825
 
820
- def _handle_body_end_form(self, token: Any) -> Any:
826
+ def _handle_body_end_form(self, token: Tag) -> None:
821
827
  if self.form_element is None:
822
828
  self._parse_error("unexpected-end-tag", tag_name=token.name)
823
829
  return
@@ -827,20 +833,20 @@ class TreeBuilderModesMixin:
827
833
  self._parse_error("unexpected-end-tag", tag_name=token.name)
828
834
  return
829
835
 
830
- def _handle_body_end_applet_like(self, token: Any) -> Any:
836
+ def _handle_body_end_applet_like(self, token: Tag) -> None:
831
837
  name = token.name
832
838
  if not self._in_scope(name):
833
839
  self._parse_error("unexpected-end-tag", tag_name=name)
834
840
  return
835
841
  # Element verified in scope above
836
842
  while self.open_elements: # pragma: no branch
837
- popped = self.open_elements.pop()
843
+ popped = self._pop_current()
838
844
  if popped.name == name:
839
845
  break
840
846
  self._clear_active_formatting_up_to_marker()
841
847
  return
842
848
 
843
- def _handle_body_end_heading(self, token: Any) -> Any:
849
+ def _handle_body_end_heading(self, token: Tag) -> None:
844
850
  name = token.name
845
851
  if not self._has_any_in_scope(HEADING_ELEMENTS):
846
852
  self._parse_error("unexpected-end-tag", tag_name=name)
@@ -850,12 +856,12 @@ class TreeBuilderModesMixin:
850
856
  self._parse_error("end-tag-too-early", tag_name=name)
851
857
  # Heading verified in scope by caller
852
858
  while self.open_elements: # pragma: no branch
853
- popped = self.open_elements.pop()
859
+ popped = self._pop_current()
854
860
  if popped.name in HEADING_ELEMENTS:
855
861
  break
856
862
  return
857
863
 
858
- def _handle_body_end_block(self, token: Any) -> Any:
864
+ def _handle_body_end_block(self, token: Tag) -> None:
859
865
  name = token.name
860
866
  if not self._in_scope(name):
861
867
  self._parse_error("unexpected-end-tag", tag_name=name)
@@ -866,7 +872,7 @@ class TreeBuilderModesMixin:
866
872
  self._pop_until_any_inclusive({name})
867
873
  return
868
874
 
869
- def _handle_body_end_template(self, token: Any) -> Any:
875
+ def _handle_body_end_template(self, token: Tag) -> None:
870
876
  has_template = any(node.name == "template" for node in self.open_elements)
871
877
  if not has_template:
872
878
  self._parse_error("unexpected-end-tag", tag_name=token.name)
@@ -880,18 +886,18 @@ class TreeBuilderModesMixin:
880
886
  self._reset_insertion_mode()
881
887
  return
882
888
 
883
- def _handle_body_start_structure_ignored(self, token: Any) -> Any:
889
+ def _handle_body_start_structure_ignored(self, token: Tag) -> None:
884
890
  self._parse_error("unexpected-start-tag-ignored", tag_name=token.name)
885
891
  return
886
892
 
887
- def _handle_body_start_col_or_frame(self, token: Any) -> Any:
893
+ def _handle_body_start_col_or_frame(self, token: Tag) -> None:
888
894
  if self.fragment_context is None:
889
895
  self._parse_error("unexpected-start-tag-ignored", tag_name=token.name)
890
896
  return
891
897
  self._insert_element(token, push=False)
892
898
  return
893
899
 
894
- def _handle_body_start_image(self, token: Any) -> Any:
900
+ def _handle_body_start_image(self, token: Tag) -> None:
895
901
  self._parse_error("image-start-tag", tag_name=token.name)
896
902
  img_token = Tag(Tag.START, "img", token.attrs, token.self_closing)
897
903
  self._reconstruct_active_formatting_elements()
@@ -899,17 +905,17 @@ class TreeBuilderModesMixin:
899
905
  self.frameset_ok = False
900
906
  return
901
907
 
902
- def _handle_body_start_void_with_formatting(self, token: Any) -> Any:
908
+ def _handle_body_start_void_with_formatting(self, token: Tag) -> None:
903
909
  self._reconstruct_active_formatting_elements()
904
910
  self._insert_element(token, push=False)
905
911
  self.frameset_ok = False
906
912
  return
907
913
 
908
- def _handle_body_start_simple_void(self, token: Any) -> Any:
914
+ def _handle_body_start_simple_void(self, token: Tag) -> None:
909
915
  self._insert_element(token, push=False)
910
916
  return
911
917
 
912
- def _handle_body_start_input(self, token: Any) -> Any:
918
+ def _handle_body_start_input(self, token: Tag) -> None:
913
919
  input_type = None
914
920
  for name, value in token.attrs.items():
915
921
  if name == "type":
@@ -920,7 +926,7 @@ class TreeBuilderModesMixin:
920
926
  self.frameset_ok = False
921
927
  return
922
928
 
923
- def _handle_body_start_table(self, token: Any) -> Any:
929
+ def _handle_body_start_table(self, token: Tag) -> None:
924
930
  if self.quirks_mode != "quirks":
925
931
  self._close_p_element()
926
932
  self._insert_element(token, push=True)
@@ -928,7 +934,7 @@ class TreeBuilderModesMixin:
928
934
  self.mode = InsertionMode.IN_TABLE
929
935
  return
930
936
 
931
- def _handle_body_start_plaintext_xmp(self, token: Any) -> Any:
937
+ def _handle_body_start_plaintext_xmp(self, token: Tag) -> None:
932
938
  self._close_p_element()
933
939
  self._insert_element(token, push=True)
934
940
  self.frameset_ok = False
@@ -940,58 +946,58 @@ class TreeBuilderModesMixin:
940
946
  self.mode = InsertionMode.TEXT
941
947
  return
942
948
 
943
- def _handle_body_start_textarea(self, token: Any) -> Any:
949
+ def _handle_body_start_textarea(self, token: Tag) -> None:
944
950
  self._insert_element(token, push=True)
945
951
  self.ignore_lf = True
946
952
  self.frameset_ok = False
947
953
  return
948
954
 
949
- def _handle_body_start_select(self, token: Any) -> Any:
955
+ def _handle_body_start_select(self, token: Tag) -> None:
950
956
  self._reconstruct_active_formatting_elements()
951
957
  self._insert_element(token, push=True)
952
958
  self.frameset_ok = False
953
959
  self._reset_insertion_mode()
954
960
  return
955
961
 
956
- def _handle_body_start_option(self, token: Any) -> Any:
962
+ def _handle_body_start_option(self, token: Tag) -> None:
957
963
  if self.open_elements and self.open_elements[-1].name == "option":
958
- self.open_elements.pop()
964
+ self._pop_current()
959
965
  self._reconstruct_active_formatting_elements()
960
966
  self._insert_element(token, push=True)
961
967
  return
962
968
 
963
- def _handle_body_start_optgroup(self, token: Any) -> Any:
969
+ def _handle_body_start_optgroup(self, token: Tag) -> None:
964
970
  if self.open_elements and self.open_elements[-1].name == "option":
965
- self.open_elements.pop()
971
+ self._pop_current()
966
972
  self._reconstruct_active_formatting_elements()
967
973
  self._insert_element(token, push=True)
968
974
  return
969
975
 
970
- def _handle_body_start_rp_rt(self, token: Any) -> Any:
976
+ def _handle_body_start_rp_rt(self, token: Tag) -> None:
971
977
  self._generate_implied_end_tags(exclude="rtc")
972
978
  self._insert_element(token, push=True)
973
979
  return
974
980
 
975
- def _handle_body_start_rb_rtc(self, token: Any) -> Any:
981
+ def _handle_body_start_rb_rtc(self, token: Tag) -> None:
976
982
  if self.open_elements and self.open_elements[-1].name in {"rb", "rp", "rt", "rtc"}:
977
983
  self._generate_implied_end_tags()
978
984
  self._insert_element(token, push=True)
979
985
  return
980
986
 
981
- def _handle_body_start_table_parse_error(self, token: Any) -> Any:
987
+ def _handle_body_start_table_parse_error(self, token: Tag) -> None:
982
988
  self._parse_error("unexpected-start-tag", tag_name=token.name)
983
989
  return
984
990
 
985
- def _handle_body_start_default(self, token: Any) -> Any:
991
+ def _handle_body_start_default(self, token: Tag) -> ModeResultTuple | None:
986
992
  self._reconstruct_active_formatting_elements()
987
993
  self._insert_element(token, push=True)
988
994
  if token.self_closing:
989
995
  self._parse_error("non-void-html-element-start-tag-with-trailing-solidus", tag_name=token.name)
990
996
  # Elements reaching here have no handler - never in FRAMESET_NEUTRAL/FORMATTING_ELEMENTS
991
997
  self.frameset_ok = False
992
- return
998
+ return None
993
999
 
994
- def _mode_in_table(self, token: Any) -> Any:
1000
+ def _mode_in_table(self, token: AnyToken) -> ModeResultTuple | None:
995
1001
  if isinstance(token, CharacterTokens):
996
1002
  data = token.data or ""
997
1003
  if "\x00" in data:
@@ -1085,14 +1091,14 @@ class TreeBuilderModesMixin:
1085
1091
  if input_type == "hidden":
1086
1092
  self._parse_error("unexpected-hidden-input-in-table")
1087
1093
  self._insert_element(token, push=True)
1088
- self.open_elements.pop() # push=True always adds to stack
1094
+ self._pop_current() # push=True always adds to stack
1089
1095
  return None
1090
1096
  if name == "form":
1091
1097
  self._parse_error("unexpected-form-in-table")
1092
1098
  if self.form_element is None:
1093
1099
  node = self._insert_element(token, push=True)
1094
1100
  self.form_element = node
1095
- self.open_elements.pop() # push=True always adds to stack
1101
+ self._pop_current() # push=True always adds to stack
1096
1102
  return None
1097
1103
  self._parse_error("foster-parenting-start-tag", tag_name=name)
1098
1104
  previous = self.insert_from_table
@@ -1124,7 +1130,7 @@ class TreeBuilderModesMixin:
1124
1130
  self._parse_error("eof-in-table")
1125
1131
  return None
1126
1132
 
1127
- def _mode_in_table_text(self, token: Any) -> Any:
1133
+ def _mode_in_table_text(self, token: AnyToken) -> ModeResultTuple | None:
1128
1134
  if isinstance(token, CharacterTokens):
1129
1135
  # IN_TABLE mode guarantees non-empty data
1130
1136
  data = token.data
@@ -1154,7 +1160,7 @@ class TreeBuilderModesMixin:
1154
1160
  self.mode = original
1155
1161
  return ("reprocess", original, token)
1156
1162
 
1157
- def _mode_in_caption(self, token: Any) -> Any:
1163
+ def _mode_in_caption(self, token: AnyToken) -> ModeResultTuple | None:
1158
1164
  if isinstance(token, CharacterTokens):
1159
1165
  return self._mode_in_body(token)
1160
1166
  if isinstance(token, CommentToken):
@@ -1200,14 +1206,14 @@ class TreeBuilderModesMixin:
1200
1206
  self._generate_implied_end_tags()
1201
1207
  # Caption verified in scope above
1202
1208
  while self.open_elements: # pragma: no branch
1203
- node = self.open_elements.pop()
1209
+ node = self._pop_current()
1204
1210
  if node.name == "caption":
1205
1211
  break
1206
1212
  self._clear_active_formatting_up_to_marker()
1207
1213
  self.mode = InsertionMode.IN_TABLE
1208
1214
  return True
1209
1215
 
1210
- def _mode_in_column_group(self, token: Any) -> Any:
1216
+ def _mode_in_column_group(self, token: AnyToken) -> ModeResultTuple | None:
1211
1217
  current = self.open_elements[-1] if self.open_elements else None
1212
1218
  if isinstance(token, CharacterTokens):
1213
1219
  data = token.data or ""
@@ -1244,7 +1250,7 @@ class TreeBuilderModesMixin:
1244
1250
  return self._mode_in_body(token)
1245
1251
  if name == "col":
1246
1252
  self._insert_element(token, push=True)
1247
- self.open_elements.pop() # push=True always adds to stack
1253
+ self._pop_current() # push=True always adds to stack
1248
1254
  return None
1249
1255
  if name == "template":
1250
1256
  # Template is handled by delegating to IN_HEAD
@@ -1302,7 +1308,7 @@ class TreeBuilderModesMixin:
1302
1308
  return None
1303
1309
  # Per spec: EOF when current is html - implicit None return
1304
1310
 
1305
- def _mode_in_table_body(self, token: Any) -> Any:
1311
+ def _mode_in_table_body(self, token: AnyToken) -> ModeResultTuple | None:
1306
1312
  if isinstance(token, CharacterTokens) or isinstance(token, CommentToken):
1307
1313
  return self._mode_in_table(token)
1308
1314
  if isinstance(token, Tag):
@@ -1337,7 +1343,7 @@ class TreeBuilderModesMixin:
1337
1343
  return None
1338
1344
  # Pop tbody/tfoot/thead (stack always has elements here in normal parsing)
1339
1345
  if self.open_elements:
1340
- self.open_elements.pop()
1346
+ self._pop_current()
1341
1347
  self.mode = InsertionMode.IN_TABLE
1342
1348
  return ("reprocess", InsertionMode.IN_TABLE, token)
1343
1349
  # Empty stack edge case - go directly to IN_TABLE without reprocess
@@ -1368,7 +1374,7 @@ class TreeBuilderModesMixin:
1368
1374
  self._parse_error("unexpected-end-tag", tag_name=token.name)
1369
1375
  return None
1370
1376
  if current and current.name in {"tbody", "tfoot", "thead"}:
1371
- self.open_elements.pop()
1377
+ self._pop_current()
1372
1378
  self.mode = InsertionMode.IN_TABLE
1373
1379
  return ("reprocess", InsertionMode.IN_TABLE, token)
1374
1380
  if name in {"caption", "col", "colgroup", "td", "th", "tr"}:
@@ -1378,7 +1384,7 @@ class TreeBuilderModesMixin:
1378
1384
  assert isinstance(token, EOFToken), f"Unexpected token type: {type(token)}"
1379
1385
  return self._mode_in_table(token)
1380
1386
 
1381
- def _mode_in_row(self, token: Any) -> Any:
1387
+ def _mode_in_row(self, token: AnyToken) -> ModeResultTuple | None:
1382
1388
  if isinstance(token, CharacterTokens) or isinstance(token, CommentToken):
1383
1389
  return self._mode_in_table(token)
1384
1390
  if isinstance(token, Tag):
@@ -1431,14 +1437,14 @@ class TreeBuilderModesMixin:
1431
1437
  self._clear_stack_until({"tr", "template", "html"})
1432
1438
  # Pop tr if on top (may not be if stack was exhausted)
1433
1439
  if self.open_elements and self.open_elements[-1].name == "tr":
1434
- self.open_elements.pop()
1440
+ self._pop_current()
1435
1441
  # When in a template, restore template mode; otherwise use IN_TABLE_BODY
1436
1442
  if self.template_modes:
1437
1443
  self.mode = self.template_modes[-1]
1438
1444
  else:
1439
1445
  self.mode = InsertionMode.IN_TABLE_BODY
1440
1446
 
1441
- def _mode_in_cell(self, token: Any) -> Any:
1447
+ def _mode_in_cell(self, token: AnyToken) -> ModeResultTuple | None:
1442
1448
  if isinstance(token, CharacterTokens):
1443
1449
  previous = self.insert_from_table
1444
1450
  self.insert_from_table = False
@@ -1492,7 +1498,7 @@ class TreeBuilderModesMixin:
1492
1498
  return ("reprocess", self.mode, token)
1493
1499
  return self._mode_in_table(token)
1494
1500
 
1495
- def _mode_in_select(self, token: Any) -> Any:
1501
+ def _mode_in_select(self, token: AnyToken) -> ModeResultTuple | None:
1496
1502
  if isinstance(token, CharacterTokens):
1497
1503
  data = token.data or ""
1498
1504
  if "\x00" in data:
@@ -1511,15 +1517,15 @@ class TreeBuilderModesMixin:
1511
1517
  return ("reprocess", InsertionMode.IN_BODY, token)
1512
1518
  if name == "option":
1513
1519
  if self.open_elements and self.open_elements[-1].name == "option":
1514
- self.open_elements.pop()
1520
+ self._pop_current()
1515
1521
  self._reconstruct_active_formatting_elements()
1516
1522
  self._insert_element(token, push=True)
1517
1523
  return None
1518
1524
  if name == "optgroup":
1519
1525
  if self.open_elements and self.open_elements[-1].name == "option":
1520
- self.open_elements.pop()
1526
+ self._pop_current()
1521
1527
  if self.open_elements and self.open_elements[-1].name == "optgroup":
1522
- self.open_elements.pop()
1528
+ self._pop_current()
1523
1529
  self._reconstruct_active_formatting_elements()
1524
1530
  self._insert_element(token, push=True)
1525
1531
  return None
@@ -1561,9 +1567,9 @@ class TreeBuilderModesMixin:
1561
1567
  self._parse_error("unexpected-start-tag-in-select", tag_name=name)
1562
1568
  # Per spec: pop option and optgroup before inserting hr (makes hr sibling, not child)
1563
1569
  if self.open_elements and self.open_elements[-1].name == "option":
1564
- self.open_elements.pop()
1570
+ self._pop_current()
1565
1571
  if self.open_elements and self.open_elements[-1].name == "optgroup":
1566
- self.open_elements.pop()
1572
+ self._pop_current()
1567
1573
  self._reconstruct_active_formatting_elements()
1568
1574
  self._insert_element(token, push=False)
1569
1575
  return None
@@ -1594,15 +1600,15 @@ class TreeBuilderModesMixin:
1594
1600
  return None
1595
1601
  if name == "optgroup":
1596
1602
  if self.open_elements and self.open_elements[-1].name == "option":
1597
- self.open_elements.pop()
1603
+ self._pop_current()
1598
1604
  if self.open_elements and self.open_elements[-1].name == "optgroup":
1599
- self.open_elements.pop()
1605
+ self._pop_current()
1600
1606
  else:
1601
1607
  self._parse_error("unexpected-end-tag-in-select", tag_name=token.name)
1602
1608
  return None
1603
1609
  if name == "option":
1604
1610
  if self.open_elements and self.open_elements[-1].name == "option":
1605
- self.open_elements.pop()
1611
+ self._pop_current()
1606
1612
  else:
1607
1613
  self._parse_error("unexpected-end-tag-in-select", tag_name=token.name)
1608
1614
  return None
@@ -1643,7 +1649,7 @@ class TreeBuilderModesMixin:
1643
1649
  # i.e., the target is inside the select or there's no select
1644
1650
  if target_idx is not None and (select_idx is None or target_idx > select_idx):
1645
1651
  while True:
1646
- popped = self.open_elements.pop()
1652
+ popped = self._pop_current()
1647
1653
  if popped.name == name:
1648
1654
  break
1649
1655
  return None
@@ -1659,7 +1665,7 @@ class TreeBuilderModesMixin:
1659
1665
  assert isinstance(token, EOFToken), f"Unexpected token type: {type(token)}"
1660
1666
  return self._mode_in_body(token)
1661
1667
 
1662
- def _mode_in_template(self, token: Any) -> Any:
1668
+ def _mode_in_template(self, token: AnyToken) -> ModeResultTuple | None:
1663
1669
  # § The "in template" insertion mode
1664
1670
  # https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-intemplate
1665
1671
  if isinstance(token, CharacterTokens):
@@ -1738,7 +1744,7 @@ class TreeBuilderModesMixin:
1738
1744
  return ("reprocess", self.mode, token)
1739
1745
  return None
1740
1746
 
1741
- def _mode_after_body(self, token: Any) -> Any:
1747
+ def _mode_after_body(self, token: AnyToken) -> ModeResultTuple | None:
1742
1748
  if isinstance(token, CharacterTokens):
1743
1749
  if is_all_whitespace(token.data):
1744
1750
  # Whitespace is processed using InBody rules (appended to body)
@@ -1759,7 +1765,7 @@ class TreeBuilderModesMixin:
1759
1765
  assert isinstance(token, EOFToken), f"Unexpected token type: {type(token)}"
1760
1766
  return None
1761
1767
 
1762
- def _mode_after_after_body(self, token: Any) -> Any:
1768
+ def _mode_after_after_body(self, token: AnyToken) -> ModeResultTuple | None:
1763
1769
  if isinstance(token, CharacterTokens):
1764
1770
  if is_all_whitespace(token.data):
1765
1771
  # Per spec: whitespace characters are inserted using the rules for the "in body" mode
@@ -1786,7 +1792,7 @@ class TreeBuilderModesMixin:
1786
1792
  assert isinstance(token, EOFToken), f"Unexpected token type: {type(token)}"
1787
1793
  return None
1788
1794
 
1789
- def _mode_in_frameset(self, token: Any) -> Any:
1795
+ def _mode_in_frameset(self, token: AnyToken) -> ModeResultTuple | None:
1790
1796
  # Per HTML5 spec §13.2.6.4.16: In frameset insertion mode
1791
1797
  if isinstance(token, CharacterTokens):
1792
1798
  # Only whitespace characters allowed; ignore all others
@@ -1807,13 +1813,13 @@ class TreeBuilderModesMixin:
1807
1813
  if self.open_elements and self.open_elements[-1].name == "html":
1808
1814
  self._parse_error("unexpected-end-tag", tag_name=token.name)
1809
1815
  return None
1810
- self.open_elements.pop()
1816
+ self._pop_current()
1811
1817
  if self.open_elements and self.open_elements[-1].name != "frameset":
1812
1818
  self.mode = InsertionMode.AFTER_FRAMESET
1813
1819
  return None
1814
1820
  if token.kind == Tag.START and token.name == "frame":
1815
1821
  self._insert_element(token, push=True)
1816
- self.open_elements.pop()
1822
+ self._pop_current()
1817
1823
  return None
1818
1824
  if token.kind == Tag.START and token.name == "noframes":
1819
1825
  # Per spec: use IN_HEAD rules but preserve current mode for TEXT restoration
@@ -1828,7 +1834,7 @@ class TreeBuilderModesMixin:
1828
1834
  self._parse_error("unexpected-token-in-frameset")
1829
1835
  return None
1830
1836
 
1831
- def _mode_after_frameset(self, token: Any) -> Any:
1837
+ def _mode_after_frameset(self, token: AnyToken) -> ModeResultTuple | None:
1832
1838
  # Per HTML5 spec §13.2.6.4.17: After frameset insertion mode
1833
1839
  if isinstance(token, CharacterTokens):
1834
1840
  # Only whitespace characters allowed; non-whitespace is a parse error.
@@ -1863,7 +1869,7 @@ class TreeBuilderModesMixin:
1863
1869
  self.mode = InsertionMode.IN_FRAMESET
1864
1870
  return ("reprocess", InsertionMode.IN_FRAMESET, token)
1865
1871
 
1866
- def _mode_after_after_frameset(self, token: Any) -> Any:
1872
+ def _mode_after_after_frameset(self, token: AnyToken) -> ModeResultTuple | None:
1867
1873
  # Per HTML5 spec §13.2.6.4.18: After after frameset insertion mode
1868
1874
  if isinstance(token, CharacterTokens):
1869
1875
  # Whitespace is processed using InBody rules
@@ -1894,7 +1900,7 @@ class TreeBuilderModesMixin:
1894
1900
 
1895
1901
  # Helpers ----------------------------------------------------------------
1896
1902
 
1897
- _MODE_HANDLERS = [
1903
+ _MODE_HANDLERS: list[Callable[[TreeBuilderModesMixin, AnyToken], ModeResultTuple | None]] = [
1898
1904
  _mode_initial,
1899
1905
  _mode_before_html,
1900
1906
  _mode_before_head,
@@ -1919,14 +1925,14 @@ class TreeBuilderModesMixin:
1919
1925
  _mode_in_template,
1920
1926
  ]
1921
1927
 
1922
- _BODY_TOKEN_HANDLERS = {
1928
+ _BODY_TOKEN_HANDLERS: dict[type[AnyToken], Callable[[TreeBuilderModesMixin, Any], ModeResultTuple | None]] = {
1923
1929
  CharacterTokens: _handle_characters_in_body,
1924
1930
  CommentToken: _handle_comment_in_body,
1925
1931
  Tag: _handle_tag_in_body,
1926
1932
  EOFToken: _handle_eof_in_body,
1927
1933
  }
1928
1934
 
1929
- _BODY_START_HANDLERS = {
1935
+ _BODY_START_HANDLERS: dict[str, Callable[[TreeBuilderModesMixin, Tag], ModeResultTuple | None]] = {
1930
1936
  "a": _handle_body_start_a,
1931
1937
  "address": _handle_body_start_block_with_p,
1932
1938
  "applet": _handle_body_start_applet_like,
@@ -2031,7 +2037,7 @@ class TreeBuilderModesMixin:
2031
2037
  "wbr": _handle_body_start_void_with_formatting,
2032
2038
  "xmp": _handle_body_start_plaintext_xmp,
2033
2039
  }
2034
- _BODY_END_HANDLERS = {
2040
+ _BODY_END_HANDLERS: dict[str, Callable[[TreeBuilderModesMixin, Tag], ModeResultTuple | None]] = {
2035
2041
  "address": _handle_body_end_block,
2036
2042
  "applet": _handle_body_end_applet_like,
2037
2043
  "article": _handle_body_end_block,