1use std::collections::VecDeque;
10
11use crate::entities;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct Position {
18 pub line: u32,
20 pub column: u32,
22 pub byte_offset: usize,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct ParseError {
32 pub kind: ParseErrorKind,
33 pub position: Position,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47#[non_exhaustive]
48pub enum ParseErrorKind {
49 CdataInHtmlContent,
51 IncorrectlyOpenedComment,
52 AbruptClosingOfEmptyComment,
53 NestedComment,
54 IncorrectlyClosedComment,
55 EofInComment,
56 InvalidFirstCharacterOfTagName,
58 EofBeforeTagName,
59 MissingEndTagName,
60 EofInTag,
61 EndTagWithAttributes,
62 EndTagWithTrailingSolidus,
63 DuplicateAttribute,
64 UnexpectedNullCharacter,
65 UnexpectedCharacterInAttributeName,
66 MissingAttributeValue,
67 UnexpectedCharacterInUnquotedAttributeValue,
68 MissingWhitespaceBetweenAttributes,
69 UnexpectedSolidusInTag,
70 UnexpectedEqualsSignBeforeAttributeName,
71 UnknownNamedCharacterReference,
73 AbsenceOfDigitsInNumericCharacterReference,
74 MissingSemicolonAfterCharacterReference,
75 NullCharacterReference,
76 CharacterReferenceOutsideUnicodeRange,
77 SurrogateCharacterReference,
78 NoncharacterCharacterReference,
79 ControlCharacterReference,
80 MissingWhitespaceBeforeDoctypeName,
82 MissingDoctypeName,
83 InvalidCharacterSequenceAfterDoctypeName,
84 MissingWhitespaceAfterDoctypePublicKeyword,
85 MissingWhitespaceAfterDoctypeSystemKeyword,
86 MissingDoctypePublicIdentifier,
87 MissingDoctypeSystemIdentifier,
88 MissingQuoteBeforeDoctypePublicIdentifier,
89 MissingQuoteBeforeDoctypeSystemIdentifier,
90 AbruptDoctypePublicIdentifier,
91 AbruptDoctypeSystemIdentifier,
92 MissingWhitespaceBetweenDoctypePublicAndSystemIdentifiers,
93 UnexpectedCharacterAfterDoctypeSystemIdentifier,
94 EofInDoctype,
95 EofInProcessingInstruction,
97 InvalidFirstCharacterOfProcessingInstructionTarget,
98 InvalidProcessingInstructionTarget,
99 DisallowedProcessingInstructionTarget,
100 EofInScriptHtmlCommentLikeText,
102 EofInCdata,
103 NoncharacterInInputStream,
111 ControlCharacterInInputStream,
112}
113
114impl std::fmt::Display for ParseErrorKind {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 let text = match self {
122 Self::CdataInHtmlContent => "a CDATA section outside foreign content",
123 Self::IncorrectlyOpenedComment => "a comment that doesn't start with \"<!--\"",
124 Self::AbruptClosingOfEmptyComment => "an empty comment closed abruptly with \">\"",
125 Self::NestedComment => "a nested \"<!--\" inside a comment",
126 Self::IncorrectlyClosedComment => "a comment closed with \"--!>\" instead of \"-->\"",
127 Self::EofInComment => "end of file inside a comment",
128 Self::InvalidFirstCharacterOfTagName => "an invalid first character of a tag name",
129 Self::EofBeforeTagName => "end of file before a tag name",
130 Self::MissingEndTagName => "an end tag with no name (\"</>\")",
131 Self::EofInTag => "end of file inside a tag",
132 Self::EndTagWithAttributes => "an end tag with attributes",
133 Self::EndTagWithTrailingSolidus => "an end tag with a trailing \"/\"",
134 Self::DuplicateAttribute => "a duplicate attribute on a tag",
135 Self::UnexpectedNullCharacter => "an unexpected U+0000 NULL character",
136 Self::UnexpectedCharacterInAttributeName => {
137 "an unexpected character in an attribute name"
138 }
139 Self::MissingAttributeValue => "a missing attribute value after \"=\"",
140 Self::UnexpectedCharacterInUnquotedAttributeValue => {
141 "an unexpected character in an unquoted attribute value"
142 }
143 Self::MissingWhitespaceBetweenAttributes => "missing whitespace between attributes",
144 Self::UnexpectedSolidusInTag => "an unexpected \"/\" inside a tag",
145 Self::UnexpectedEqualsSignBeforeAttributeName => {
146 "an unexpected \"=\" before an attribute name"
147 }
148 Self::UnknownNamedCharacterReference => "an unknown named character reference",
149 Self::AbsenceOfDigitsInNumericCharacterReference => {
150 "a numeric character reference with no digits"
151 }
152 Self::MissingSemicolonAfterCharacterReference => {
153 "a character reference not terminated by \";\""
154 }
155 Self::NullCharacterReference => "a character reference resolving to U+0000 NULL",
156 Self::CharacterReferenceOutsideUnicodeRange => {
157 "a character reference outside the Unicode range"
158 }
159 Self::SurrogateCharacterReference => "a character reference resolving to a surrogate",
160 Self::NoncharacterCharacterReference => {
161 "a character reference resolving to a noncharacter"
162 }
163 Self::ControlCharacterReference => {
164 "a character reference resolving to a control character"
165 }
166 Self::MissingWhitespaceBeforeDoctypeName => {
167 "missing whitespace before the DOCTYPE name"
168 }
169 Self::MissingDoctypeName => "a DOCTYPE with no name",
170 Self::InvalidCharacterSequenceAfterDoctypeName => {
171 "an invalid character sequence after the DOCTYPE name"
172 }
173 Self::MissingWhitespaceAfterDoctypePublicKeyword => {
174 "missing whitespace after the DOCTYPE \"PUBLIC\" keyword"
175 }
176 Self::MissingWhitespaceAfterDoctypeSystemKeyword => {
177 "missing whitespace after the DOCTYPE \"SYSTEM\" keyword"
178 }
179 Self::MissingDoctypePublicIdentifier => "a missing DOCTYPE public identifier",
180 Self::MissingDoctypeSystemIdentifier => "a missing DOCTYPE system identifier",
181 Self::MissingQuoteBeforeDoctypePublicIdentifier => {
182 "a missing quote before the DOCTYPE public identifier"
183 }
184 Self::MissingQuoteBeforeDoctypeSystemIdentifier => {
185 "a missing quote before the DOCTYPE system identifier"
186 }
187 Self::AbruptDoctypePublicIdentifier => {
188 "a DOCTYPE public identifier closed abruptly with \">\""
189 }
190 Self::AbruptDoctypeSystemIdentifier => {
191 "a DOCTYPE system identifier closed abruptly with \">\""
192 }
193 Self::MissingWhitespaceBetweenDoctypePublicAndSystemIdentifiers => {
194 "missing whitespace between the DOCTYPE public and system identifiers"
195 }
196 Self::UnexpectedCharacterAfterDoctypeSystemIdentifier => {
197 "an unexpected character after the DOCTYPE system identifier"
198 }
199 Self::EofInDoctype => "end of file inside a DOCTYPE",
200 Self::EofInProcessingInstruction => "end of file inside a processing instruction",
201 Self::InvalidFirstCharacterOfProcessingInstructionTarget => {
202 "an invalid first character of a processing instruction target"
203 }
204 Self::InvalidProcessingInstructionTarget => "an invalid processing instruction target",
205 Self::DisallowedProcessingInstructionTarget => {
206 "a disallowed processing instruction target (\"xml\" or \"xml-stylesheet\")"
207 }
208 Self::EofInScriptHtmlCommentLikeText => {
209 "end of file inside a script element's HTML-comment-like text"
210 }
211 Self::EofInCdata => "end of file inside a CDATA section",
212 Self::NoncharacterInInputStream => "a Unicode noncharacter in the input stream",
213 Self::ControlCharacterInInputStream => "a control character in the input stream",
214 };
215 f.write_str(text)
216 }
217}
218
219#[derive(Debug, Clone, PartialEq, Eq)]
221pub(crate) struct Attribute {
222 pub(crate) name: String,
223 pub(crate) value: String,
224}
225
226#[derive(Debug, Clone, PartialEq, Eq)]
230pub(crate) struct TagToken {
231 pub(crate) name: String,
232 pub(crate) self_closing: bool,
233 pub(crate) attributes: Vec<Attribute>,
234}
235
236#[derive(Debug, Clone, Default, PartialEq, Eq)]
238pub(crate) struct DoctypeToken {
239 pub(crate) name: Option<String>,
240 pub(crate) public_identifier: Option<String>,
241 pub(crate) system_identifier: Option<String>,
242 pub(crate) force_quirks: bool,
243}
244
245#[derive(Debug, Clone, PartialEq, Eq)]
251pub(crate) struct ProcessingInstructionToken {
252 pub(crate) target: String,
253 pub(crate) data: String,
254}
255
256#[derive(Debug, Clone, PartialEq, Eq)]
258pub(crate) enum TokenKind {
259 Doctype(DoctypeToken),
260 StartTag(TagToken),
261 EndTag(TagToken),
262 Comment(String),
263 ProcessingInstruction(ProcessingInstructionToken),
264 Character(char),
268 Eof,
269}
270
271#[derive(Debug, Clone, PartialEq, Eq)]
273pub(crate) struct Token {
274 pub(crate) kind: TokenKind,
275 pub(crate) position: Position,
276}
277
278#[derive(Debug, Clone, Copy, PartialEq, Eq)]
282enum State {
283 Data,
284 TagOpen,
285 EndTagOpen,
286 TagName,
287 BeforeAttributeName,
288 AttributeName,
289 AfterAttributeName,
290 BeforeAttributeValue,
291 AttributeValueDoubleQuoted,
292 AttributeValueSingleQuoted,
293 AttributeValueUnquoted,
294 AfterAttributeValueQuoted,
295 SelfClosingStartTag,
296 CharacterReference,
297 NamedCharacterReference,
298 AmbiguousAmpersand,
299 NumericCharacterReference,
300 HexadecimalCharacterReferenceStart,
301 HexadecimalCharacterReference,
302 DecimalCharacterReference,
303 NumericCharacterReferenceEnd,
304 MarkupDeclarationOpen,
305 BogusComment,
306 CommentStart,
307 CommentStartDash,
308 Comment,
309 CommentLessThanSign,
310 CommentLessThanSignBang,
311 CommentLessThanSignBangDash,
312 CommentLessThanSignBangDashDash,
313 CommentEndDash,
314 CommentEnd,
315 CommentEndBang,
316 Doctype,
317 BeforeDoctypeName,
318 DoctypeName,
319 AfterDoctypeName,
320 AfterDoctypePublicKeyword,
321 BeforeDoctypePublicIdentifier,
322 DoctypePublicIdentifierDoubleQuoted,
323 DoctypePublicIdentifierSingleQuoted,
324 AfterDoctypePublicIdentifier,
325 BetweenDoctypePublicAndSystemIdentifiers,
326 AfterDoctypeSystemKeyword,
327 BeforeDoctypeSystemIdentifier,
328 DoctypeSystemIdentifierDoubleQuoted,
329 DoctypeSystemIdentifierSingleQuoted,
330 AfterDoctypeSystemIdentifier,
331 BogusDoctype,
332 ProcessingInstructionOpen,
333 ProcessingInstructionTarget,
334 AfterProcessingInstructionTarget,
335 ProcessingInstructionData,
336 ProcessingInstructionQuestionable,
337 RcData,
338 RcDataLessThanSign,
339 RcDataEndTagOpen,
340 RcDataEndTagName,
341 RawText,
342 RawTextLessThanSign,
343 RawTextEndTagOpen,
344 RawTextEndTagName,
345 PlainText,
346 ScriptData,
347 ScriptDataLessThanSign,
348 ScriptDataEndTagOpen,
349 ScriptDataEndTagName,
350 ScriptDataEscapeStart,
351 ScriptDataEscapeStartDash,
352 ScriptDataEscaped,
353 ScriptDataEscapedDash,
354 ScriptDataEscapedDashDash,
355 ScriptDataEscapedLessThanSign,
356 ScriptDataEscapedEndTagOpen,
357 ScriptDataEscapedEndTagName,
358 ScriptDataDoubleEscapeStart,
359 ScriptDataDoubleEscaped,
360 ScriptDataDoubleEscapedDash,
361 ScriptDataDoubleEscapedDashDash,
362 ScriptDataDoubleEscapedLessThanSign,
363 ScriptDataDoubleEscapeEnd,
364 CdataSection,
365 CdataSectionBracket,
366 CdataSectionEnd,
367}
368
369#[derive(Debug, Clone, Copy, PartialEq, Eq)]
388pub(crate) enum ExternalState {
389 RcData,
390 RawText,
391 ScriptData,
392 PlainText,
393}
394
395#[derive(Debug, Clone, Copy, PartialEq, Eq)]
400enum DoctypeIdentifierKind {
401 Public,
402 System,
403}
404
405#[derive(Debug, Clone, Copy, PartialEq, Eq)]
411enum AttributeValueTarget {
412 Index(usize),
413 Discarded,
414}
415
416pub(crate) struct Tokenizer {
421 chars: Vec<char>,
422 positions: Vec<Position>,
423 index: usize,
424 saved: Option<(Option<char>, Position)>,
427 state: State,
428 current_tag: Option<TagToken>,
429 current_tag_is_end: bool,
430 current_tag_start: Position,
434 slash_position: Position,
438 current_attribute_name: String,
439 attribute_value_target: AttributeValueTarget,
440 return_state: State,
443 character_reference_start: Position,
446 character_reference_start_index: usize,
448 character_reference_code: u32,
450 current_comment_data: String,
455 current_doctype: Option<DoctypeToken>,
458 pi_temporary_buffer: String,
461 current_processing_instruction: Option<ProcessingInstructionToken>,
464 last_start_tag_name: Option<String>,
472 text_end_tag_buffer: String,
477 in_foreign_content: bool,
485 cdata_pending_brackets: Vec<Position>,
493 pending: VecDeque<Token>,
494 eof_returned: bool,
495 errors: Vec<ParseError>,
500}
501
502impl Tokenizer {
503 pub(crate) fn new(input: &str) -> Self {
504 let mut chars = Vec::new();
510 let mut positions = Vec::new();
511 let mut line = 1u32;
512 let mut column = 1u32;
513 let mut input_stream_errors = Vec::new();
520 let mut iter = input.char_indices().peekable();
521 while let Some((byte_offset, c)) = iter.next() {
522 let (emitted, skip_next) = if c == '\r' {
523 ('\n', matches!(iter.peek(), Some((_, '\n'))))
524 } else {
525 (c, false)
526 };
527 let position = Position {
528 line,
529 column,
530 byte_offset,
531 };
532 let code = u32::from(emitted);
533 if is_noncharacter(code) {
534 input_stream_errors.push(ParseError {
535 kind: ParseErrorKind::NoncharacterInInputStream,
536 position,
537 });
538 } else if is_control(code) && code != 0x00 && !is_ascii_whitespace(code) {
539 input_stream_errors.push(ParseError {
540 kind: ParseErrorKind::ControlCharacterInInputStream,
541 position,
542 });
543 }
544 positions.push(position);
545 chars.push(emitted);
546 if emitted == '\n' {
547 line += 1;
548 column = 1;
549 } else {
550 column += 1;
551 }
552 if skip_next {
553 iter.next();
554 }
555 }
556 positions.push(Position {
557 line,
558 column,
559 byte_offset: input.len(),
560 });
561
562 let origin = Position {
563 line: 1,
564 column: 1,
565 byte_offset: 0,
566 };
567 Tokenizer {
568 chars,
569 positions,
570 index: 0,
571 saved: None,
572 state: State::Data,
573 current_tag: None,
574 current_tag_is_end: false,
575 current_tag_start: origin,
576 slash_position: origin,
577 current_attribute_name: String::new(),
578 attribute_value_target: AttributeValueTarget::Discarded,
579 return_state: State::Data,
580 character_reference_start: origin,
581 character_reference_start_index: 0,
582 character_reference_code: 0,
583 current_comment_data: String::new(),
584 current_doctype: None,
585 pi_temporary_buffer: String::new(),
586 current_processing_instruction: None,
587 last_start_tag_name: None,
588 text_end_tag_buffer: String::new(),
589 in_foreign_content: false,
590 cdata_pending_brackets: Vec::new(),
591 pending: VecDeque::new(),
592 eof_returned: false,
593 errors: input_stream_errors,
594 }
595 }
596
597 fn error(&mut self, kind: ParseErrorKind, position: Position) {
602 self.errors.push(ParseError { kind, position });
603 }
604
605 pub(crate) fn take_errors(&mut self) -> Vec<ParseError> {
608 std::mem::take(&mut self.errors)
609 }
610
611 pub(crate) fn switch_to(&mut self, state: ExternalState) {
616 self.state = match state {
617 ExternalState::RcData => State::RcData,
618 ExternalState::RawText => State::RawText,
619 ExternalState::ScriptData => State::ScriptData,
620 ExternalState::PlainText => State::PlainText,
621 };
622 }
623
624 pub(crate) fn set_in_foreign_content(&mut self, in_foreign_content: bool) {
628 self.in_foreign_content = in_foreign_content;
629 }
630
631 fn consume(&mut self) -> (Option<char>, Position) {
632 if let Some(saved) = self.saved.take() {
633 return saved;
634 }
635 let position = self.positions[self.index];
636 let ch = self.chars.get(self.index).copied();
637 if ch.is_some() {
638 self.index += 1;
639 }
640 (ch, position)
641 }
642
643 fn is_whitespace(c: char) -> bool {
644 matches!(c, '\t' | '\n' | '\x0C' | ' ')
645 }
646
647 fn start_tag_token(&mut self, is_end: bool) {
650 self.current_tag = Some(TagToken {
651 name: String::new(),
652 self_closing: false,
653 attributes: Vec::new(),
654 });
655 self.current_tag_is_end = is_end;
656 }
657
658 fn emit_tag(&mut self, out: &mut Vec<Token>) {
659 let tag = self
660 .current_tag
661 .take()
662 .expect("emit_tag called with no tag token in progress");
663 let kind = if self.current_tag_is_end {
664 if !tag.attributes.is_empty() {
669 self.error(ParseErrorKind::EndTagWithAttributes, self.current_tag_start);
670 }
671 if tag.self_closing {
672 self.error(
673 ParseErrorKind::EndTagWithTrailingSolidus,
674 self.current_tag_start,
675 );
676 }
677 TokenKind::EndTag(tag)
678 } else {
679 self.last_start_tag_name = Some(tag.name.clone());
682 TokenKind::StartTag(tag)
683 };
684 out.push(Token {
685 kind,
686 position: self.current_tag_start,
687 });
688 }
689
690 fn close_tag(&mut self, out: &mut Vec<Token>) -> bool {
694 self.state = State::Data;
695 self.emit_tag(out);
696 false
697 }
698
699 fn emit_comment(&mut self, out: &mut Vec<Token>) {
700 let data = std::mem::take(&mut self.current_comment_data);
701 out.push(Token {
702 kind: TokenKind::Comment(data),
703 position: self.current_tag_start,
704 });
705 }
706
707 fn current_doctype_mut(&mut self) -> &mut DoctypeToken {
708 self.current_doctype
709 .as_mut()
710 .expect("doctype state reached with no doctype token in progress")
711 }
712
713 fn doctype_identifier_mut(&mut self, kind: DoctypeIdentifierKind) -> &mut String {
714 let doctype = self.current_doctype_mut();
715 let field = match kind {
716 DoctypeIdentifierKind::Public => &mut doctype.public_identifier,
717 DoctypeIdentifierKind::System => &mut doctype.system_identifier,
718 };
719 field
720 .as_mut()
721 .expect("doctype identifier appended to before being set to Some")
722 }
723
724 fn start_doctype_public_identifier(&mut self, quote_state: State) -> bool {
730 self.current_doctype_mut().public_identifier = Some(String::new());
731 self.state = quote_state;
732 false
733 }
734
735 fn start_doctype_system_identifier(&mut self, quote_state: State) -> bool {
741 self.current_doctype_mut().system_identifier = Some(String::new());
742 self.state = quote_state;
743 false
744 }
745
746 fn emit_doctype(&mut self, out: &mut Vec<Token>) {
747 let doctype = self
748 .current_doctype
749 .take()
750 .expect("emit_doctype called with no doctype token in progress");
751 out.push(Token {
752 kind: TokenKind::Doctype(doctype),
753 position: self.current_tag_start,
754 });
755 }
756
757 fn close_doctype(&mut self, out: &mut Vec<Token>) -> bool {
760 self.state = State::Data;
761 self.emit_doctype(out);
762 false
763 }
764
765 fn close_doctype_with_quirks(&mut self, out: &mut Vec<Token>) -> bool {
768 self.current_doctype_mut().force_quirks = true;
769 self.close_doctype(out)
770 }
771
772 fn eof_in_doctype(&mut self, out: &mut Vec<Token>, position: Position) -> bool {
778 self.error(ParseErrorKind::EofInDoctype, position);
779 self.current_doctype_mut().force_quirks = true;
780 self.emit_doctype(out);
781 push_eof(out, position);
782 false
783 }
784
785 fn bogus_doctype_with_quirks(&mut self) -> bool {
789 self.current_doctype_mut().force_quirks = true;
790 self.state = State::BogusDoctype;
791 true
792 }
793
794 fn emit_processing_instruction(&mut self, out: &mut Vec<Token>) {
795 let pi = self.current_processing_instruction.take().expect(
796 "emit_processing_instruction called with no processing instruction token in progress",
797 );
798 out.push(Token {
799 kind: TokenKind::ProcessingInstruction(pi),
800 position: self.current_tag_start,
801 });
802 }
803
804 fn convert_pi_temporary_buffer_to_comment(&mut self) {
809 let target = std::mem::take(&mut self.pi_temporary_buffer);
810 self.current_comment_data = format!("?{target}");
811 self.state = State::BogusComment;
812 }
813
814 fn peek_matches(&self, literal: &str, case_insensitive: bool) -> bool {
818 self.peek_matches_at(self.index, literal, case_insensitive)
819 }
820
821 fn peek_matches_at(&self, start: usize, literal: &str, case_insensitive: bool) -> bool {
826 literal.chars().enumerate().all(|(offset, expected)| {
827 self.chars.get(start + offset).is_some_and(|&actual| {
828 if case_insensitive {
829 actual.eq_ignore_ascii_case(&expected)
830 } else {
831 actual == expected
832 }
833 })
834 })
835 }
836
837 fn run_markup_declaration_open(&mut self) {
844 if self.peek_matches("--", false) {
845 self.index += 2;
846 self.current_comment_data.clear();
847 self.state = State::CommentStart;
848 return;
849 }
850 if self.peek_matches("DOCTYPE", true) {
851 self.index += 7;
852 self.state = State::Doctype;
853 return;
854 }
855 if self.peek_matches("[CDATA[", false) {
856 self.index += 7;
857 if self.in_foreign_content {
858 self.state = State::CdataSection;
859 } else {
860 self.error(
862 ParseErrorKind::CdataInHtmlContent,
863 self.positions[self.index],
864 );
865 self.current_comment_data = "[CDATA[".to_owned();
866 self.state = State::BogusComment;
867 }
868 return;
869 }
870 self.error(
872 ParseErrorKind::IncorrectlyOpenedComment,
873 self.positions[self.index],
874 );
875 self.current_comment_data.clear();
876 self.state = State::BogusComment;
877 }
878
879 fn commit_attribute_name(&mut self, position: Position) {
885 let name = std::mem::take(&mut self.current_attribute_name);
886 let tag = self
887 .current_tag
888 .as_mut()
889 .expect("commit_attribute_name called with no tag token in progress");
890 if tag
891 .attributes
892 .iter()
893 .any(|attribute| attribute.name == name)
894 {
895 self.error(ParseErrorKind::DuplicateAttribute, position);
898 self.attribute_value_target = AttributeValueTarget::Discarded;
899 } else {
900 tag.attributes.push(Attribute {
901 name,
902 value: String::new(),
903 });
904 self.attribute_value_target = AttributeValueTarget::Index(tag.attributes.len() - 1);
905 }
906 }
907
908 fn push_attribute_value_char(&mut self, c: char) {
909 if let AttributeValueTarget::Index(i) = self.attribute_value_target {
910 self.current_tag
911 .as_mut()
912 .expect("push_attribute_value_char called with no tag token in progress")
913 .attributes[i]
914 .value
915 .push(c);
916 }
917 }
918
919 fn run_until_token(&mut self) {
920 loop {
921 if self.state == State::NamedCharacterReference {
927 self.run_named_character_reference();
928 if !self.pending.is_empty() {
929 return;
930 }
931 continue;
932 }
933 if self.state == State::MarkupDeclarationOpen {
939 self.run_markup_declaration_open();
940 continue;
941 }
942 let (ch, position) = self.consume();
943 let mut out = Vec::new();
944 let reconsume = self.step(ch, position, &mut out);
945 if reconsume {
946 self.saved = Some((ch, position));
947 }
948 if !out.is_empty() {
949 self.pending.extend(out);
950 return;
951 }
952 }
953 }
954
955 fn step(&mut self, ch: Option<char>, position: Position, out: &mut Vec<Token>) -> bool {
959 match self.state {
960 State::Data => match ch {
961 Some('&') => {
962 self.begin_character_reference(State::Data, position);
963 false
964 }
965 Some('<') => {
966 self.current_tag_start = position;
967 self.state = State::TagOpen;
968 false
969 }
970 Some('\0') => {
971 self.error(ParseErrorKind::UnexpectedNullCharacter, position);
975 push_character(out, '\0', position);
976 false
977 }
978 Some(c) => {
979 push_character(out, c, position);
980 false
981 }
982 None => {
983 push_eof(out, position);
984 false
985 }
986 },
987 State::TagOpen => match ch {
988 Some('!') => {
989 self.state = State::MarkupDeclarationOpen;
990 false
991 }
992 Some('/') => {
993 self.slash_position = position;
994 self.state = State::EndTagOpen;
995 false
996 }
997 Some(c) if c.is_ascii_alphabetic() => {
998 self.start_tag_token(false);
999 self.state = State::TagName;
1000 true
1001 }
1002 Some('?') => {
1003 self.pi_temporary_buffer.clear();
1004 self.state = State::ProcessingInstructionOpen;
1005 false
1006 }
1007 Some(_) => {
1008 self.error(ParseErrorKind::InvalidFirstCharacterOfTagName, position);
1010 push_character(out, '<', self.current_tag_start);
1011 self.state = State::Data;
1012 true
1013 }
1014 None => {
1015 self.error(ParseErrorKind::EofBeforeTagName, position);
1017 push_character(out, '<', self.current_tag_start);
1018 push_eof(out, position);
1019 false
1020 }
1021 },
1022 State::EndTagOpen => match ch {
1023 Some(c) if c.is_ascii_alphabetic() => {
1024 self.start_tag_token(true);
1025 self.state = State::TagName;
1026 true
1027 }
1028 Some('>') => {
1029 self.error(ParseErrorKind::MissingEndTagName, position);
1031 self.state = State::Data;
1032 false
1033 }
1034 Some(_) => {
1035 self.error(ParseErrorKind::InvalidFirstCharacterOfTagName, position);
1037 self.current_comment_data.clear();
1038 self.state = State::BogusComment;
1039 true
1040 }
1041 None => {
1042 self.error(ParseErrorKind::EofBeforeTagName, position);
1044 push_character(out, '<', self.current_tag_start);
1045 push_character(out, '/', self.slash_position);
1046 push_eof(out, position);
1047 false
1048 }
1049 },
1050 State::TagName => match ch {
1051 Some(c) if Self::is_whitespace(c) => {
1052 self.state = State::BeforeAttributeName;
1053 false
1054 }
1055 Some('/') => {
1056 self.state = State::SelfClosingStartTag;
1057 false
1058 }
1059 Some('>') => self.close_tag(out),
1060 Some(c) if c.is_ascii_uppercase() => {
1061 self.current_tag_mut().name.push(c.to_ascii_lowercase());
1062 false
1063 }
1064 Some('\0') => {
1065 self.current_tag_mut().name.push('\u{FFFD}');
1066 false
1067 }
1068 Some(c) => {
1069 self.current_tag_mut().name.push(c);
1070 false
1071 }
1072 None => {
1073 self.error(ParseErrorKind::EofInTag, position);
1075 push_eof(out, position);
1076 false
1077 }
1078 },
1079 State::BeforeAttributeName => match ch {
1080 Some(c) if Self::is_whitespace(c) => false,
1081 Some('/') | Some('>') | None => {
1082 self.state = State::AfterAttributeName;
1083 true
1084 }
1085 Some('=') => {
1086 self.error(
1090 ParseErrorKind::UnexpectedEqualsSignBeforeAttributeName,
1091 position,
1092 );
1093 self.current_attribute_name.clear();
1094 self.current_attribute_name.push('=');
1095 self.state = State::AttributeName;
1096 false
1097 }
1098 Some(_) => {
1099 self.current_attribute_name.clear();
1100 self.state = State::AttributeName;
1101 true
1102 }
1103 },
1104 State::AttributeName => match ch {
1105 Some(c) if Self::is_whitespace(c) || c == '/' || c == '>' => {
1106 self.commit_attribute_name(position);
1107 self.state = State::AfterAttributeName;
1108 true
1109 }
1110 None => {
1111 self.commit_attribute_name(position);
1112 self.state = State::AfterAttributeName;
1113 true
1114 }
1115 Some('=') => {
1116 self.commit_attribute_name(position);
1117 self.state = State::BeforeAttributeValue;
1118 false
1119 }
1120 Some(c) if c.is_ascii_uppercase() => {
1121 self.current_attribute_name.push(c.to_ascii_lowercase());
1122 false
1123 }
1124 Some('\0') => {
1125 self.current_attribute_name.push('\u{FFFD}');
1126 false
1127 }
1128 Some(c @ ('"' | '\'' | '<')) => {
1129 self.error(ParseErrorKind::UnexpectedCharacterInAttributeName, position);
1132 self.current_attribute_name.push(c);
1133 false
1134 }
1135 Some(c) => {
1136 self.current_attribute_name.push(c);
1137 false
1138 }
1139 },
1140 State::AfterAttributeName => match ch {
1141 Some(c) if Self::is_whitespace(c) => false,
1142 Some('/') => {
1143 self.state = State::SelfClosingStartTag;
1144 false
1145 }
1146 Some('=') => {
1147 self.state = State::BeforeAttributeValue;
1148 false
1149 }
1150 Some('>') => self.close_tag(out),
1151 None => {
1152 self.error(ParseErrorKind::EofInTag, position);
1154 push_eof(out, position);
1155 false
1156 }
1157 Some(_) => {
1158 self.current_attribute_name.clear();
1159 self.state = State::AttributeName;
1160 true
1161 }
1162 },
1163 State::BeforeAttributeValue => match ch {
1164 Some(c) if Self::is_whitespace(c) => false,
1165 Some('"') => {
1166 self.state = State::AttributeValueDoubleQuoted;
1167 false
1168 }
1169 Some('\'') => {
1170 self.state = State::AttributeValueSingleQuoted;
1171 false
1172 }
1173 Some('>') => {
1174 self.error(ParseErrorKind::MissingAttributeValue, position);
1176 self.close_tag(out)
1177 }
1178 _ => {
1179 self.state = State::AttributeValueUnquoted;
1180 true
1181 }
1182 },
1183 State::AttributeValueDoubleQuoted => {
1184 self.step_attribute_value_quoted(ch, position, out, '"')
1185 }
1186 State::AttributeValueSingleQuoted => {
1187 self.step_attribute_value_quoted(ch, position, out, '\'')
1188 }
1189 State::AttributeValueUnquoted => match ch {
1190 Some(c) if Self::is_whitespace(c) => {
1191 self.state = State::BeforeAttributeName;
1192 false
1193 }
1194 Some('&') => {
1195 self.begin_character_reference(State::AttributeValueUnquoted, position);
1196 false
1197 }
1198 Some('>') => self.close_tag(out),
1199 Some('\0') => {
1200 self.push_attribute_value_char('\u{FFFD}');
1201 false
1202 }
1203 Some(c @ ('"' | '\'' | '<' | '=' | '`')) => {
1204 self.error(
1207 ParseErrorKind::UnexpectedCharacterInUnquotedAttributeValue,
1208 position,
1209 );
1210 self.push_attribute_value_char(c);
1211 false
1212 }
1213 Some(c) => {
1214 self.push_attribute_value_char(c);
1215 false
1216 }
1217 None => {
1218 self.error(ParseErrorKind::EofInTag, position);
1220 push_eof(out, position);
1221 false
1222 }
1223 },
1224 State::AfterAttributeValueQuoted => match ch {
1225 Some(c) if Self::is_whitespace(c) => {
1226 self.state = State::BeforeAttributeName;
1227 false
1228 }
1229 Some('/') => {
1230 self.state = State::SelfClosingStartTag;
1231 false
1232 }
1233 Some('>') => self.close_tag(out),
1234 None => {
1235 self.error(ParseErrorKind::EofInTag, position);
1237 push_eof(out, position);
1238 false
1239 }
1240 Some(_) => {
1241 self.error(ParseErrorKind::MissingWhitespaceBetweenAttributes, position);
1243 self.state = State::BeforeAttributeName;
1244 true
1245 }
1246 },
1247 State::SelfClosingStartTag => match ch {
1248 Some('>') => {
1249 self.current_tag_mut().self_closing = true;
1250 self.close_tag(out)
1251 }
1252 None => {
1253 self.error(ParseErrorKind::EofInTag, position);
1255 push_eof(out, position);
1256 false
1257 }
1258 Some(_) => {
1259 self.error(ParseErrorKind::UnexpectedSolidusInTag, position);
1261 self.state = State::BeforeAttributeName;
1262 true
1263 }
1264 },
1265 State::CharacterReference => match ch {
1266 Some(c) if c.is_ascii_alphanumeric() => {
1267 self.state = State::NamedCharacterReference;
1268 true
1269 }
1270 Some('#') => {
1271 self.state = State::NumericCharacterReference;
1272 false
1273 }
1274 _ => {
1275 let end = self.character_reference_start_index + 1;
1281 self.flush_literal_character_reference_attempt(end, out);
1282 self.state = self.return_state;
1283 true
1284 }
1285 },
1286 State::NamedCharacterReference => {
1289 unreachable!("NamedCharacterReference is dispatched before step() is called")
1290 }
1291 State::AmbiguousAmpersand => match ch {
1292 Some(c) if c.is_ascii_alphanumeric() => {
1293 self.flush_char_as_character_reference(c, position, out);
1294 false
1295 }
1296 Some(';') => {
1297 self.error(ParseErrorKind::UnknownNamedCharacterReference, position);
1299 self.state = self.return_state;
1300 true
1301 }
1302 _ => {
1303 self.state = self.return_state;
1304 true
1305 }
1306 },
1307 State::NumericCharacterReference => {
1308 self.character_reference_code = 0;
1309 match ch {
1310 Some('x') | Some('X') => {
1311 self.state = State::HexadecimalCharacterReferenceStart;
1312 false
1313 }
1314 Some(c) if c.is_ascii_digit() => {
1315 self.state = State::DecimalCharacterReference;
1316 true
1317 }
1318 _ => {
1319 self.error(
1325 ParseErrorKind::AbsenceOfDigitsInNumericCharacterReference,
1326 position,
1327 );
1328 let end = self.character_reference_start_index + 2;
1329 self.flush_literal_character_reference_attempt(end, out);
1330 self.state = self.return_state;
1331 true
1332 }
1333 }
1334 }
1335 State::HexadecimalCharacterReferenceStart => match ch {
1336 Some(c) if c.is_ascii_hexdigit() => {
1337 self.state = State::HexadecimalCharacterReference;
1338 true
1339 }
1340 _ => {
1341 self.error(
1345 ParseErrorKind::AbsenceOfDigitsInNumericCharacterReference,
1346 position,
1347 );
1348 let end = self.character_reference_start_index + 3;
1349 self.flush_literal_character_reference_attempt(end, out);
1350 self.state = self.return_state;
1351 true
1352 }
1353 },
1354 State::HexadecimalCharacterReference => match ch {
1355 Some(c) if c.is_ascii_digit() => {
1356 self.character_reference_code = self
1357 .character_reference_code
1358 .saturating_mul(16)
1359 .saturating_add(u32::from(c) - u32::from('0'));
1360 false
1361 }
1362 Some(c) if ('A'..='F').contains(&c) => {
1363 self.character_reference_code = self
1364 .character_reference_code
1365 .saturating_mul(16)
1366 .saturating_add(u32::from(c) - 0x37);
1367 false
1368 }
1369 Some(c) if ('a'..='f').contains(&c) => {
1370 self.character_reference_code = self
1371 .character_reference_code
1372 .saturating_mul(16)
1373 .saturating_add(u32::from(c) - 0x57);
1374 false
1375 }
1376 Some(';') => {
1377 self.state = State::NumericCharacterReferenceEnd;
1378 false
1379 }
1380 _ => {
1381 self.error(
1384 ParseErrorKind::MissingSemicolonAfterCharacterReference,
1385 position,
1386 );
1387 self.state = State::NumericCharacterReferenceEnd;
1388 true
1389 }
1390 },
1391 State::DecimalCharacterReference => match ch {
1392 Some(c) if c.is_ascii_digit() => {
1393 self.character_reference_code = self
1394 .character_reference_code
1395 .saturating_mul(10)
1396 .saturating_add(u32::from(c) - u32::from('0'));
1397 false
1398 }
1399 Some(';') => {
1400 self.state = State::NumericCharacterReferenceEnd;
1401 false
1402 }
1403 _ => {
1404 self.error(
1407 ParseErrorKind::MissingSemicolonAfterCharacterReference,
1408 position,
1409 );
1410 self.state = State::NumericCharacterReferenceEnd;
1411 true
1412 }
1413 },
1414 State::NumericCharacterReferenceEnd => {
1415 let resolved =
1419 self.resolve_numeric_character_reference_code(self.character_reference_start);
1420 self.flush_char_as_character_reference(
1421 resolved,
1422 self.character_reference_start,
1423 out,
1424 );
1425 self.state = self.return_state;
1426 true
1427 }
1428 State::MarkupDeclarationOpen => {
1431 unreachable!("MarkupDeclarationOpen is dispatched before step() is called")
1432 }
1433 State::BogusComment => match ch {
1434 Some('>') => {
1435 self.state = State::Data;
1436 self.emit_comment(out);
1437 false
1438 }
1439 None => {
1440 self.emit_comment(out);
1441 push_eof(out, position);
1442 false
1443 }
1444 Some('\0') => {
1445 self.current_comment_data.push('\u{FFFD}');
1446 false
1447 }
1448 Some(c) => {
1449 self.current_comment_data.push(c);
1450 false
1451 }
1452 },
1453 State::CommentStart => match ch {
1454 Some('-') => {
1455 self.state = State::CommentStartDash;
1456 false
1457 }
1458 Some('>') => {
1459 self.error(ParseErrorKind::AbruptClosingOfEmptyComment, position);
1461 self.state = State::Data;
1462 self.emit_comment(out);
1463 false
1464 }
1465 _ => {
1466 self.state = State::Comment;
1467 true
1468 }
1469 },
1470 State::CommentStartDash => match ch {
1471 Some('-') => {
1472 self.state = State::CommentEnd;
1473 false
1474 }
1475 Some('>') => {
1476 self.error(ParseErrorKind::AbruptClosingOfEmptyComment, position);
1478 self.state = State::Data;
1479 self.emit_comment(out);
1480 false
1481 }
1482 None => {
1483 self.error(ParseErrorKind::EofInComment, position);
1485 self.emit_comment(out);
1486 push_eof(out, position);
1487 false
1488 }
1489 Some(_) => {
1490 self.current_comment_data.push('-');
1491 self.state = State::Comment;
1492 true
1493 }
1494 },
1495 State::Comment => match ch {
1496 Some('<') => {
1497 self.current_comment_data.push('<');
1498 self.state = State::CommentLessThanSign;
1499 false
1500 }
1501 Some('-') => {
1502 self.state = State::CommentEndDash;
1503 false
1504 }
1505 Some('\0') => {
1506 self.current_comment_data.push('\u{FFFD}');
1507 false
1508 }
1509 None => {
1510 self.error(ParseErrorKind::EofInComment, position);
1512 self.emit_comment(out);
1513 push_eof(out, position);
1514 false
1515 }
1516 Some(c) => {
1517 self.current_comment_data.push(c);
1518 false
1519 }
1520 },
1521 State::CommentLessThanSign => match ch {
1522 Some('!') => {
1523 self.current_comment_data.push('!');
1524 self.state = State::CommentLessThanSignBang;
1525 false
1526 }
1527 Some('<') => {
1528 self.current_comment_data.push('<');
1529 false
1530 }
1531 _ => {
1532 self.state = State::Comment;
1533 true
1534 }
1535 },
1536 State::CommentLessThanSignBang => match ch {
1537 Some('-') => {
1538 self.state = State::CommentLessThanSignBangDash;
1539 false
1540 }
1541 _ => {
1542 self.state = State::Comment;
1543 true
1544 }
1545 },
1546 State::CommentLessThanSignBangDash => match ch {
1547 Some('-') => {
1548 self.state = State::CommentLessThanSignBangDashDash;
1549 false
1550 }
1551 _ => {
1552 self.state = State::CommentEndDash;
1553 true
1554 }
1555 },
1556 State::CommentLessThanSignBangDashDash => {
1557 if !matches!(ch, Some('>') | None) {
1562 self.error(ParseErrorKind::NestedComment, position);
1563 }
1564 self.state = State::CommentEnd;
1565 true
1566 }
1567 State::CommentEndDash => match ch {
1568 Some('-') => {
1569 self.state = State::CommentEnd;
1570 false
1571 }
1572 None => {
1573 self.error(ParseErrorKind::EofInComment, position);
1575 self.emit_comment(out);
1576 push_eof(out, position);
1577 false
1578 }
1579 Some(_) => {
1580 self.current_comment_data.push('-');
1581 self.state = State::Comment;
1582 true
1583 }
1584 },
1585 State::CommentEnd => match ch {
1586 Some('>') => {
1587 self.state = State::Data;
1588 self.emit_comment(out);
1589 false
1590 }
1591 Some('!') => {
1592 self.state = State::CommentEndBang;
1593 false
1594 }
1595 Some('-') => {
1596 self.current_comment_data.push('-');
1597 false
1598 }
1599 None => {
1600 self.error(ParseErrorKind::EofInComment, position);
1602 self.emit_comment(out);
1603 push_eof(out, position);
1604 false
1605 }
1606 Some(_) => {
1607 self.current_comment_data.push_str("--");
1608 self.state = State::Comment;
1609 true
1610 }
1611 },
1612 State::CommentEndBang => match ch {
1613 Some('-') => {
1614 self.current_comment_data.push_str("--!");
1615 self.state = State::CommentEndDash;
1616 false
1617 }
1618 Some('>') => {
1619 self.error(ParseErrorKind::IncorrectlyClosedComment, position);
1621 self.state = State::Data;
1622 self.emit_comment(out);
1623 false
1624 }
1625 None => {
1626 self.error(ParseErrorKind::EofInComment, position);
1628 self.emit_comment(out);
1629 push_eof(out, position);
1630 false
1631 }
1632 Some(_) => {
1633 self.current_comment_data.push_str("--!");
1634 self.state = State::Comment;
1635 true
1636 }
1637 },
1638 State::Doctype => match ch {
1639 Some(c) if Self::is_whitespace(c) => {
1640 self.state = State::BeforeDoctypeName;
1641 false
1642 }
1643 Some('>') => {
1644 self.state = State::BeforeDoctypeName;
1645 true
1646 }
1647 None => {
1648 self.error(ParseErrorKind::EofInDoctype, position);
1653 self.current_doctype = Some(DoctypeToken {
1654 force_quirks: true,
1655 ..Default::default()
1656 });
1657 self.emit_doctype(out);
1658 push_eof(out, position);
1659 false
1660 }
1661 Some(_) => {
1662 self.error(ParseErrorKind::MissingWhitespaceBeforeDoctypeName, position);
1664 self.state = State::BeforeDoctypeName;
1665 true
1666 }
1667 },
1668 State::BeforeDoctypeName => match ch {
1669 Some(c) if Self::is_whitespace(c) => false,
1670 Some(c) if c.is_ascii_uppercase() => {
1671 self.current_doctype = Some(DoctypeToken {
1672 name: Some(c.to_ascii_lowercase().to_string()),
1673 ..Default::default()
1674 });
1675 self.state = State::DoctypeName;
1676 false
1677 }
1678 Some('\0') => {
1679 self.current_doctype = Some(DoctypeToken {
1680 name: Some("\u{FFFD}".to_owned()),
1681 ..Default::default()
1682 });
1683 self.state = State::DoctypeName;
1684 false
1685 }
1686 Some('>') => {
1687 self.error(ParseErrorKind::MissingDoctypeName, position);
1689 self.current_doctype = Some(DoctypeToken {
1690 force_quirks: true,
1691 ..Default::default()
1692 });
1693 self.close_doctype(out)
1694 }
1695 None => {
1696 self.error(ParseErrorKind::EofInDoctype, position);
1698 self.current_doctype = Some(DoctypeToken {
1699 force_quirks: true,
1700 ..Default::default()
1701 });
1702 self.emit_doctype(out);
1703 push_eof(out, position);
1704 false
1705 }
1706 Some(c) => {
1707 self.current_doctype = Some(DoctypeToken {
1708 name: Some(c.to_string()),
1709 ..Default::default()
1710 });
1711 self.state = State::DoctypeName;
1712 false
1713 }
1714 },
1715 State::DoctypeName => match ch {
1716 Some(c) if Self::is_whitespace(c) => {
1717 self.state = State::AfterDoctypeName;
1718 false
1719 }
1720 Some('>') => self.close_doctype(out),
1721 Some(c) if c.is_ascii_uppercase() => {
1722 self.current_doctype_mut()
1723 .name
1724 .as_mut()
1725 .expect("doctype name should already be Some in DoctypeName state")
1726 .push(c.to_ascii_lowercase());
1727 false
1728 }
1729 Some('\0') => {
1730 self.current_doctype_mut()
1731 .name
1732 .as_mut()
1733 .expect("doctype name should already be Some in DoctypeName state")
1734 .push('\u{FFFD}');
1735 false
1736 }
1737 None => self.eof_in_doctype(out, position),
1738 Some(c) => {
1739 self.current_doctype_mut()
1740 .name
1741 .as_mut()
1742 .expect("doctype name should already be Some in DoctypeName state")
1743 .push(c);
1744 false
1745 }
1746 },
1747 State::AfterDoctypeName => match ch {
1748 Some(c) if Self::is_whitespace(c) => false,
1749 Some('>') => self.close_doctype(out),
1750 None => self.eof_in_doctype(out, position),
1751 Some(_) => {
1752 let start = self.index - 1;
1753 if self.peek_matches_at(start, "PUBLIC", true) {
1754 self.index = start + 6;
1755 self.state = State::AfterDoctypePublicKeyword;
1756 false
1757 } else if self.peek_matches_at(start, "SYSTEM", true) {
1758 self.index = start + 6;
1759 self.state = State::AfterDoctypeSystemKeyword;
1760 false
1761 } else {
1762 self.error(
1765 ParseErrorKind::InvalidCharacterSequenceAfterDoctypeName,
1766 position,
1767 );
1768 self.bogus_doctype_with_quirks()
1769 }
1770 }
1771 },
1772 State::AfterDoctypePublicKeyword => match ch {
1773 Some(c) if Self::is_whitespace(c) => {
1774 self.state = State::BeforeDoctypePublicIdentifier;
1775 false
1776 }
1777 Some(c @ ('"' | '\'')) => {
1778 self.error(
1781 ParseErrorKind::MissingWhitespaceAfterDoctypePublicKeyword,
1782 position,
1783 );
1784 let quoted_state = if c == '"' {
1785 State::DoctypePublicIdentifierDoubleQuoted
1786 } else {
1787 State::DoctypePublicIdentifierSingleQuoted
1788 };
1789 self.start_doctype_public_identifier(quoted_state)
1790 }
1791 Some('>') => {
1792 self.error(ParseErrorKind::MissingDoctypePublicIdentifier, position);
1794 self.close_doctype_with_quirks(out)
1795 }
1796 None => self.eof_in_doctype(out, position),
1797 Some(_) => {
1798 self.error(
1801 ParseErrorKind::MissingQuoteBeforeDoctypePublicIdentifier,
1802 position,
1803 );
1804 self.bogus_doctype_with_quirks()
1805 }
1806 },
1807 State::BeforeDoctypePublicIdentifier => match ch {
1808 Some(c) if Self::is_whitespace(c) => false,
1809 Some('"') => {
1810 self.start_doctype_public_identifier(State::DoctypePublicIdentifierDoubleQuoted)
1811 }
1812 Some('\'') => {
1813 self.start_doctype_public_identifier(State::DoctypePublicIdentifierSingleQuoted)
1814 }
1815 Some('>') => {
1816 self.error(ParseErrorKind::MissingDoctypePublicIdentifier, position);
1818 self.close_doctype_with_quirks(out)
1819 }
1820 None => self.eof_in_doctype(out, position),
1821 Some(_) => {
1822 self.error(
1825 ParseErrorKind::MissingQuoteBeforeDoctypePublicIdentifier,
1826 position,
1827 );
1828 self.bogus_doctype_with_quirks()
1829 }
1830 },
1831 State::DoctypePublicIdentifierDoubleQuoted => self.step_doctype_identifier_quoted(
1832 ch,
1833 position,
1834 out,
1835 '"',
1836 DoctypeIdentifierKind::Public,
1837 ),
1838 State::DoctypePublicIdentifierSingleQuoted => self.step_doctype_identifier_quoted(
1839 ch,
1840 position,
1841 out,
1842 '\'',
1843 DoctypeIdentifierKind::Public,
1844 ),
1845 State::AfterDoctypePublicIdentifier => match ch {
1846 Some(c) if Self::is_whitespace(c) => {
1847 self.state = State::BetweenDoctypePublicAndSystemIdentifiers;
1848 false
1849 }
1850 Some('>') => self.close_doctype(out),
1851 Some(c @ ('"' | '\'')) => {
1852 self.error(
1855 ParseErrorKind::MissingWhitespaceBetweenDoctypePublicAndSystemIdentifiers,
1856 position,
1857 );
1858 let quoted_state = if c == '"' {
1859 State::DoctypeSystemIdentifierDoubleQuoted
1860 } else {
1861 State::DoctypeSystemIdentifierSingleQuoted
1862 };
1863 self.start_doctype_system_identifier(quoted_state)
1864 }
1865 None => self.eof_in_doctype(out, position),
1866 Some(_) => self.bogus_doctype_with_quirks(),
1867 },
1868 State::BetweenDoctypePublicAndSystemIdentifiers => match ch {
1869 Some(c) if Self::is_whitespace(c) => false,
1870 Some('>') => self.close_doctype(out),
1871 Some('"') => {
1872 self.start_doctype_system_identifier(State::DoctypeSystemIdentifierDoubleQuoted)
1873 }
1874 Some('\'') => {
1875 self.start_doctype_system_identifier(State::DoctypeSystemIdentifierSingleQuoted)
1876 }
1877 None => self.eof_in_doctype(out, position),
1878 Some(_) => self.bogus_doctype_with_quirks(),
1879 },
1880 State::AfterDoctypeSystemKeyword => match ch {
1881 Some(c) if Self::is_whitespace(c) => {
1882 self.state = State::BeforeDoctypeSystemIdentifier;
1883 false
1884 }
1885 Some(c @ ('"' | '\'')) => {
1886 self.error(
1889 ParseErrorKind::MissingWhitespaceAfterDoctypeSystemKeyword,
1890 position,
1891 );
1892 let quoted_state = if c == '"' {
1893 State::DoctypeSystemIdentifierDoubleQuoted
1894 } else {
1895 State::DoctypeSystemIdentifierSingleQuoted
1896 };
1897 self.start_doctype_system_identifier(quoted_state)
1898 }
1899 Some('>') => {
1900 self.error(ParseErrorKind::MissingDoctypeSystemIdentifier, position);
1902 self.close_doctype_with_quirks(out)
1903 }
1904 None => self.eof_in_doctype(out, position),
1905 Some(_) => {
1906 self.error(
1909 ParseErrorKind::MissingQuoteBeforeDoctypeSystemIdentifier,
1910 position,
1911 );
1912 self.bogus_doctype_with_quirks()
1913 }
1914 },
1915 State::BeforeDoctypeSystemIdentifier => match ch {
1916 Some(c) if Self::is_whitespace(c) => false,
1917 Some('"') => {
1918 self.start_doctype_system_identifier(State::DoctypeSystemIdentifierDoubleQuoted)
1919 }
1920 Some('\'') => {
1921 self.start_doctype_system_identifier(State::DoctypeSystemIdentifierSingleQuoted)
1922 }
1923 Some('>') => {
1924 self.error(ParseErrorKind::MissingDoctypeSystemIdentifier, position);
1926 self.close_doctype_with_quirks(out)
1927 }
1928 None => self.eof_in_doctype(out, position),
1929 Some(_) => {
1930 self.error(
1933 ParseErrorKind::MissingQuoteBeforeDoctypeSystemIdentifier,
1934 position,
1935 );
1936 self.bogus_doctype_with_quirks()
1937 }
1938 },
1939 State::DoctypeSystemIdentifierDoubleQuoted => self.step_doctype_identifier_quoted(
1940 ch,
1941 position,
1942 out,
1943 '"',
1944 DoctypeIdentifierKind::System,
1945 ),
1946 State::DoctypeSystemIdentifierSingleQuoted => self.step_doctype_identifier_quoted(
1947 ch,
1948 position,
1949 out,
1950 '\'',
1951 DoctypeIdentifierKind::System,
1952 ),
1953 State::AfterDoctypeSystemIdentifier => match ch {
1954 Some(c) if Self::is_whitespace(c) => false,
1955 Some('>') => self.close_doctype(out),
1956 None => self.eof_in_doctype(out, position),
1957 Some(_) => {
1958 self.error(
1962 ParseErrorKind::UnexpectedCharacterAfterDoctypeSystemIdentifier,
1963 position,
1964 );
1965 self.state = State::BogusDoctype;
1966 true
1967 }
1968 },
1969 State::BogusDoctype => match ch {
1970 Some('>') => self.close_doctype(out),
1971 Some('\0') => false,
1972 None => {
1973 self.emit_doctype(out);
1974 push_eof(out, position);
1975 false
1976 }
1977 Some(_) => false,
1978 },
1979 State::ProcessingInstructionOpen => match ch {
1980 Some(c) if c.is_ascii_alphabetic() || c == '_' => {
1981 self.state = State::ProcessingInstructionTarget;
1982 true
1983 }
1984 None => {
1985 self.error(ParseErrorKind::EofInProcessingInstruction, position);
1990 push_eof(out, position);
1991 false
1992 }
1993 Some(_) => {
1994 self.error(
1998 ParseErrorKind::InvalidFirstCharacterOfProcessingInstructionTarget,
1999 position,
2000 );
2001 self.convert_pi_temporary_buffer_to_comment();
2002 true
2003 }
2004 },
2005 State::ProcessingInstructionTarget => match ch {
2006 Some(c) if Self::is_whitespace(c) || c == '?' || c == '>' => {
2007 let target = std::mem::take(&mut self.pi_temporary_buffer);
2008 if target.eq_ignore_ascii_case("xml")
2009 || target.eq_ignore_ascii_case("xml-stylesheet")
2010 {
2011 self.error(
2014 ParseErrorKind::DisallowedProcessingInstructionTarget,
2015 position,
2016 );
2017 self.current_comment_data = format!("?{target}");
2018 self.state = State::BogusComment;
2019 } else {
2020 self.current_processing_instruction = Some(ProcessingInstructionToken {
2021 target,
2022 data: String::new(),
2023 });
2024 self.state = State::AfterProcessingInstructionTarget;
2025 }
2026 true
2027 }
2028 Some(c) if c.is_ascii_alphanumeric() || c == '-' || c == '_' => {
2029 self.pi_temporary_buffer.push(c);
2030 false
2031 }
2032 None => {
2033 self.error(ParseErrorKind::EofInProcessingInstruction, position);
2035 push_eof(out, position);
2036 false
2037 }
2038 Some(_) => {
2039 self.error(ParseErrorKind::InvalidProcessingInstructionTarget, position);
2041 self.convert_pi_temporary_buffer_to_comment();
2042 true
2043 }
2044 },
2045 State::AfterProcessingInstructionTarget => match ch {
2046 Some(c) if Self::is_whitespace(c) => false,
2047 _ => {
2048 self.state = State::ProcessingInstructionData;
2049 true
2050 }
2051 },
2052 State::ProcessingInstructionData => match ch {
2053 Some('?') => {
2054 self.state = State::ProcessingInstructionQuestionable;
2055 false
2056 }
2057 Some('>') => {
2058 self.state = State::Data;
2059 self.emit_processing_instruction(out);
2060 false
2061 }
2062 None => {
2063 self.error(ParseErrorKind::EofInProcessingInstruction, position);
2067 self.current_processing_instruction = None;
2068 push_eof(out, position);
2069 false
2070 }
2071 Some(c) => {
2072 self.current_processing_instruction
2073 .as_mut()
2074 .expect("processing instruction token should already exist in data state")
2075 .data
2076 .push(c);
2077 false
2078 }
2079 },
2080 State::ProcessingInstructionQuestionable => match ch {
2081 Some('>') => {
2082 self.state = State::Data;
2083 self.emit_processing_instruction(out);
2084 false
2085 }
2086 None => {
2087 self.current_processing_instruction = None;
2088 push_eof(out, position);
2089 false
2090 }
2091 Some(_) => {
2092 self.current_processing_instruction
2093 .as_mut()
2094 .expect(
2095 "processing instruction token should already exist in questionable state",
2096 )
2097 .data
2098 .push('?');
2099 self.state = State::ProcessingInstructionData;
2100 true
2101 }
2102 },
2103 State::RcData => match ch {
2104 Some('&') => {
2105 self.begin_character_reference(State::RcData, position);
2106 false
2107 }
2108 Some('<') => {
2109 self.current_tag_start = position;
2110 self.state = State::RcDataLessThanSign;
2111 false
2112 }
2113 Some('\0') => {
2114 push_character(out, '\u{FFFD}', position);
2115 false
2116 }
2117 Some(c) => {
2118 push_character(out, c, position);
2119 false
2120 }
2121 None => {
2122 push_eof(out, position);
2123 false
2124 }
2125 },
2126 State::RcDataLessThanSign => self.step_text_less_than_sign(
2127 ch,
2128 position,
2129 out,
2130 State::RcDataEndTagOpen,
2131 State::RcData,
2132 ),
2133 State::RcDataEndTagOpen => self.step_text_end_tag_open(
2134 ch,
2135 position,
2136 out,
2137 State::RcDataEndTagName,
2138 State::RcData,
2139 ),
2140 State::RcDataEndTagName => {
2141 self.step_text_end_tag_name(ch, position, out, State::RcData)
2142 }
2143 State::RawText => match ch {
2144 Some('<') => {
2145 self.current_tag_start = position;
2146 self.state = State::RawTextLessThanSign;
2147 false
2148 }
2149 Some('\0') => {
2150 push_character(out, '\u{FFFD}', position);
2151 false
2152 }
2153 Some(c) => {
2154 push_character(out, c, position);
2155 false
2156 }
2157 None => {
2158 push_eof(out, position);
2159 false
2160 }
2161 },
2162 State::RawTextLessThanSign => self.step_text_less_than_sign(
2163 ch,
2164 position,
2165 out,
2166 State::RawTextEndTagOpen,
2167 State::RawText,
2168 ),
2169 State::RawTextEndTagOpen => self.step_text_end_tag_open(
2170 ch,
2171 position,
2172 out,
2173 State::RawTextEndTagName,
2174 State::RawText,
2175 ),
2176 State::RawTextEndTagName => {
2177 self.step_text_end_tag_name(ch, position, out, State::RawText)
2178 }
2179 State::PlainText => match ch {
2180 Some('\0') => {
2181 push_character(out, '\u{FFFD}', position);
2182 false
2183 }
2184 Some(c) => {
2185 push_character(out, c, position);
2186 false
2187 }
2188 None => {
2189 push_eof(out, position);
2190 false
2191 }
2192 },
2193 State::ScriptData => match ch {
2194 Some('<') => {
2195 self.current_tag_start = position;
2196 self.state = State::ScriptDataLessThanSign;
2197 false
2198 }
2199 Some('\0') => {
2200 push_character(out, '\u{FFFD}', position);
2201 false
2202 }
2203 Some(c) => {
2204 push_character(out, c, position);
2205 false
2206 }
2207 None => {
2208 push_eof(out, position);
2209 false
2210 }
2211 },
2212 State::ScriptDataLessThanSign => match ch {
2213 Some('/') => {
2214 self.text_end_tag_buffer.clear();
2215 self.slash_position = position;
2216 self.state = State::ScriptDataEndTagOpen;
2217 false
2218 }
2219 Some('!') => {
2220 self.state = State::ScriptDataEscapeStart;
2221 push_character(out, '<', self.current_tag_start);
2222 push_character(out, '!', position);
2223 false
2224 }
2225 _ => {
2226 push_character(out, '<', self.current_tag_start);
2227 self.state = State::ScriptData;
2228 true
2229 }
2230 },
2231 State::ScriptDataEndTagOpen => self.step_text_end_tag_open(
2232 ch,
2233 position,
2234 out,
2235 State::ScriptDataEndTagName,
2236 State::ScriptData,
2237 ),
2238 State::ScriptDataEndTagName => {
2239 self.step_text_end_tag_name(ch, position, out, State::ScriptData)
2240 }
2241 State::ScriptDataEscapeStart => match ch {
2242 Some('-') => {
2243 self.state = State::ScriptDataEscapeStartDash;
2244 push_character(out, '-', position);
2245 false
2246 }
2247 _ => {
2248 self.state = State::ScriptData;
2249 true
2250 }
2251 },
2252 State::ScriptDataEscapeStartDash => match ch {
2253 Some('-') => {
2254 self.state = State::ScriptDataEscapedDashDash;
2255 push_character(out, '-', position);
2256 false
2257 }
2258 _ => {
2259 self.state = State::ScriptData;
2260 true
2261 }
2262 },
2263 State::ScriptDataEscaped => match ch {
2264 Some('-') => {
2265 self.state = State::ScriptDataEscapedDash;
2266 push_character(out, '-', position);
2267 false
2268 }
2269 Some('<') => {
2270 self.current_tag_start = position;
2271 self.state = State::ScriptDataEscapedLessThanSign;
2272 false
2273 }
2274 Some('\0') => {
2275 push_character(out, '\u{FFFD}', position);
2276 false
2277 }
2278 Some(c) => {
2279 push_character(out, c, position);
2280 false
2281 }
2282 None => {
2283 self.error(ParseErrorKind::EofInScriptHtmlCommentLikeText, position);
2285 push_eof(out, position);
2286 false
2287 }
2288 },
2289 State::ScriptDataEscapedDash => match ch {
2290 Some('-') => {
2291 self.state = State::ScriptDataEscapedDashDash;
2292 push_character(out, '-', position);
2293 false
2294 }
2295 Some('<') => {
2296 self.current_tag_start = position;
2297 self.state = State::ScriptDataEscapedLessThanSign;
2298 false
2299 }
2300 Some('\0') => {
2301 self.state = State::ScriptDataEscaped;
2302 push_character(out, '\u{FFFD}', position);
2303 false
2304 }
2305 None => {
2306 push_eof(out, position);
2307 false
2308 }
2309 Some(c) => {
2310 self.state = State::ScriptDataEscaped;
2311 push_character(out, c, position);
2312 false
2313 }
2314 },
2315 State::ScriptDataEscapedDashDash => match ch {
2316 Some('-') => {
2317 push_character(out, '-', position);
2318 false
2319 }
2320 Some('<') => {
2321 self.current_tag_start = position;
2322 self.state = State::ScriptDataEscapedLessThanSign;
2323 false
2324 }
2325 Some('>') => {
2326 self.state = State::ScriptData;
2327 push_character(out, '>', position);
2328 false
2329 }
2330 Some('\0') => {
2331 self.state = State::ScriptDataEscaped;
2332 push_character(out, '\u{FFFD}', position);
2333 false
2334 }
2335 None => {
2336 push_eof(out, position);
2337 false
2338 }
2339 Some(c) => {
2340 self.state = State::ScriptDataEscaped;
2341 push_character(out, c, position);
2342 false
2343 }
2344 },
2345 State::ScriptDataEscapedLessThanSign => match ch {
2346 Some('/') => {
2347 self.text_end_tag_buffer.clear();
2348 self.slash_position = position;
2349 self.state = State::ScriptDataEscapedEndTagOpen;
2350 false
2351 }
2352 Some(c) if c.is_ascii_alphabetic() => {
2353 self.text_end_tag_buffer.clear();
2354 push_character(out, '<', self.current_tag_start);
2355 self.state = State::ScriptDataDoubleEscapeStart;
2356 true
2357 }
2358 _ => {
2359 push_character(out, '<', self.current_tag_start);
2360 self.state = State::ScriptDataEscaped;
2361 true
2362 }
2363 },
2364 State::ScriptDataEscapedEndTagOpen => self.step_text_end_tag_open(
2365 ch,
2366 position,
2367 out,
2368 State::ScriptDataEscapedEndTagName,
2369 State::ScriptDataEscaped,
2370 ),
2371 State::ScriptDataEscapedEndTagName => {
2372 self.step_text_end_tag_name(ch, position, out, State::ScriptDataEscaped)
2373 }
2374 State::ScriptDataDoubleEscapeStart => match ch {
2375 Some(c) if Self::is_whitespace(c) || c == '/' || c == '>' => {
2376 self.state = if self.text_end_tag_buffer == "script" {
2377 State::ScriptDataDoubleEscaped
2378 } else {
2379 State::ScriptDataEscaped
2380 };
2381 push_character(out, c, position);
2382 false
2383 }
2384 Some(c) if c.is_ascii_uppercase() => {
2385 self.text_end_tag_buffer.push(c.to_ascii_lowercase());
2386 push_character(out, c, position);
2387 false
2388 }
2389 Some(c) if c.is_ascii_lowercase() => {
2390 self.text_end_tag_buffer.push(c);
2391 push_character(out, c, position);
2392 false
2393 }
2394 _ => {
2395 self.state = State::ScriptDataEscaped;
2396 true
2397 }
2398 },
2399 State::ScriptDataDoubleEscaped => match ch {
2400 Some('-') => {
2401 self.state = State::ScriptDataDoubleEscapedDash;
2402 push_character(out, '-', position);
2403 false
2404 }
2405 Some('<') => {
2406 self.state = State::ScriptDataDoubleEscapedLessThanSign;
2407 push_character(out, '<', position);
2408 false
2409 }
2410 Some('\0') => {
2411 push_character(out, '\u{FFFD}', position);
2412 false
2413 }
2414 Some(c) => {
2415 push_character(out, c, position);
2416 false
2417 }
2418 None => {
2419 push_eof(out, position);
2420 false
2421 }
2422 },
2423 State::ScriptDataDoubleEscapedDash => match ch {
2424 Some('-') => {
2425 self.state = State::ScriptDataDoubleEscapedDashDash;
2426 push_character(out, '-', position);
2427 false
2428 }
2429 Some('<') => {
2430 self.state = State::ScriptDataDoubleEscapedLessThanSign;
2431 push_character(out, '<', position);
2432 false
2433 }
2434 Some('\0') => {
2435 self.state = State::ScriptDataDoubleEscaped;
2436 push_character(out, '\u{FFFD}', position);
2437 false
2438 }
2439 None => {
2440 push_eof(out, position);
2441 false
2442 }
2443 Some(c) => {
2444 self.state = State::ScriptDataDoubleEscaped;
2445 push_character(out, c, position);
2446 false
2447 }
2448 },
2449 State::ScriptDataDoubleEscapedDashDash => match ch {
2450 Some('-') => {
2451 push_character(out, '-', position);
2452 false
2453 }
2454 Some('<') => {
2455 self.state = State::ScriptDataDoubleEscapedLessThanSign;
2456 push_character(out, '<', position);
2457 false
2458 }
2459 Some('>') => {
2460 self.state = State::ScriptData;
2461 push_character(out, '>', position);
2462 false
2463 }
2464 Some('\0') => {
2465 self.state = State::ScriptDataDoubleEscaped;
2466 push_character(out, '\u{FFFD}', position);
2467 false
2468 }
2469 None => {
2470 push_eof(out, position);
2471 false
2472 }
2473 Some(c) => {
2474 self.state = State::ScriptDataDoubleEscaped;
2475 push_character(out, c, position);
2476 false
2477 }
2478 },
2479 State::ScriptDataDoubleEscapedLessThanSign => match ch {
2480 Some('/') => {
2481 self.text_end_tag_buffer.clear();
2482 self.state = State::ScriptDataDoubleEscapeEnd;
2483 push_character(out, '/', position);
2484 false
2485 }
2486 _ => {
2487 self.state = State::ScriptDataDoubleEscaped;
2488 true
2489 }
2490 },
2491 State::ScriptDataDoubleEscapeEnd => match ch {
2492 Some(c) if Self::is_whitespace(c) || c == '/' || c == '>' => {
2493 self.state = if self.text_end_tag_buffer == "script" {
2494 State::ScriptDataEscaped
2495 } else {
2496 State::ScriptDataDoubleEscaped
2497 };
2498 push_character(out, c, position);
2499 false
2500 }
2501 Some(c) if c.is_ascii_uppercase() => {
2502 self.text_end_tag_buffer.push(c.to_ascii_lowercase());
2503 push_character(out, c, position);
2504 false
2505 }
2506 Some(c) if c.is_ascii_lowercase() => {
2507 self.text_end_tag_buffer.push(c);
2508 push_character(out, c, position);
2509 false
2510 }
2511 _ => {
2512 self.state = State::ScriptDataDoubleEscaped;
2513 true
2514 }
2515 },
2516 State::CdataSection => match ch {
2517 Some(']') => {
2518 self.cdata_pending_brackets.push(position);
2519 self.state = State::CdataSectionBracket;
2520 false
2521 }
2522 None => {
2523 self.error(ParseErrorKind::EofInCdata, position);
2525 push_eof(out, position);
2526 false
2527 }
2528 Some(c) => {
2529 push_character(out, c, position);
2534 false
2535 }
2536 },
2537 State::CdataSectionBracket => match ch {
2538 Some(']') => {
2539 self.cdata_pending_brackets.push(position);
2540 self.state = State::CdataSectionEnd;
2541 false
2542 }
2543 _ => {
2544 let bracket_position = self
2549 .cdata_pending_brackets
2550 .pop()
2551 .expect("CdataSectionBracket reached with no withheld ']'");
2552 push_character(out, ']', bracket_position);
2553 self.state = State::CdataSection;
2554 true
2555 }
2556 },
2557 State::CdataSectionEnd => match ch {
2558 Some(']') => {
2559 let oldest = self.cdata_pending_brackets.remove(0);
2566 self.cdata_pending_brackets.push(position);
2567 push_character(out, ']', oldest);
2568 false
2569 }
2570 Some('>') => {
2571 self.cdata_pending_brackets.clear();
2574 self.state = State::Data;
2575 false
2576 }
2577 _ => {
2578 for bracket_position in self.cdata_pending_brackets.drain(..) {
2581 push_character(out, ']', bracket_position);
2582 }
2583 self.state = State::CdataSection;
2584 true
2585 }
2586 },
2587 }
2588 }
2589
2590 fn step_attribute_value_quoted(
2591 &mut self,
2592 ch: Option<char>,
2593 position: Position,
2594 out: &mut Vec<Token>,
2595 quote: char,
2596 ) -> bool {
2597 match ch {
2598 Some(c) if c == quote => {
2599 self.state = State::AfterAttributeValueQuoted;
2600 false
2601 }
2602 Some('&') => {
2603 let quoted_state = self.state;
2604 self.begin_character_reference(quoted_state, position);
2605 false
2606 }
2607 Some('\0') => {
2608 self.push_attribute_value_char('\u{FFFD}');
2609 false
2610 }
2611 Some(c) => {
2612 self.push_attribute_value_char(c);
2613 false
2614 }
2615 None => {
2616 self.error(ParseErrorKind::EofInTag, position);
2618 push_eof(out, position);
2619 false
2620 }
2621 }
2622 }
2623
2624 fn current_tag_mut(&mut self) -> &mut TagToken {
2625 self.current_tag
2626 .as_mut()
2627 .expect("tag-name/self-closing state reached with no tag token in progress")
2628 }
2629
2630 fn is_appropriate_end_tag(&self) -> bool {
2633 let Some(tag) = &self.current_tag else {
2634 return false;
2635 };
2636 self.last_start_tag_name.as_deref() == Some(tag.name.as_str())
2637 }
2638
2639 fn step_text_less_than_sign(
2644 &mut self,
2645 ch: Option<char>,
2646 position: Position,
2647 out: &mut Vec<Token>,
2648 end_tag_open_state: State,
2649 text_state: State,
2650 ) -> bool {
2651 match ch {
2652 Some('/') => {
2653 self.text_end_tag_buffer.clear();
2654 self.slash_position = position;
2655 self.state = end_tag_open_state;
2656 false
2657 }
2658 _ => {
2659 push_character(out, '<', self.current_tag_start);
2660 self.state = text_state;
2661 true
2662 }
2663 }
2664 }
2665
2666 fn step_text_end_tag_open(
2668 &mut self,
2669 ch: Option<char>,
2670 _position: Position,
2671 out: &mut Vec<Token>,
2672 end_tag_name_state: State,
2673 text_state: State,
2674 ) -> bool {
2675 match ch {
2676 Some(c) if c.is_ascii_alphabetic() => {
2677 self.start_tag_token(true);
2678 self.state = end_tag_name_state;
2679 true
2680 }
2681 _ => {
2682 push_character(out, '<', self.current_tag_start);
2683 push_character(out, '/', self.slash_position);
2684 self.state = text_state;
2685 true
2686 }
2687 }
2688 }
2689
2690 fn step_text_end_tag_name(
2696 &mut self,
2697 ch: Option<char>,
2698 _position: Position,
2699 out: &mut Vec<Token>,
2700 text_state: State,
2701 ) -> bool {
2702 match ch {
2703 Some(c) if Self::is_whitespace(c) => {
2704 if self.is_appropriate_end_tag() {
2705 self.state = State::BeforeAttributeName;
2706 false
2707 } else {
2708 self.abandon_text_end_tag(out, text_state)
2709 }
2710 }
2711 Some('/') => {
2712 if self.is_appropriate_end_tag() {
2713 self.state = State::SelfClosingStartTag;
2714 false
2715 } else {
2716 self.abandon_text_end_tag(out, text_state)
2717 }
2718 }
2719 Some('>') => {
2720 if self.is_appropriate_end_tag() {
2721 self.close_tag(out)
2722 } else {
2723 self.abandon_text_end_tag(out, text_state)
2724 }
2725 }
2726 Some(c) if c.is_ascii_uppercase() => {
2727 self.current_tag_mut().name.push(c.to_ascii_lowercase());
2728 self.text_end_tag_buffer.push(c);
2729 false
2730 }
2731 Some(c) if c.is_ascii_lowercase() => {
2732 self.current_tag_mut().name.push(c);
2733 self.text_end_tag_buffer.push(c);
2734 false
2735 }
2736 _ => self.abandon_text_end_tag(out, text_state),
2737 }
2738 }
2739
2740 fn abandon_text_end_tag(&mut self, out: &mut Vec<Token>, text_state: State) -> bool {
2751 push_character(out, '<', self.current_tag_start);
2752 push_character(out, '/', self.slash_position);
2753 let mut position = Position {
2754 line: self.slash_position.line,
2755 column: self.slash_position.column + 1,
2756 byte_offset: self.slash_position.byte_offset + 1,
2757 };
2758 let buffer = std::mem::take(&mut self.text_end_tag_buffer);
2759 for c in buffer.chars() {
2760 push_character(out, c, position);
2761 position.column += 1;
2762 position.byte_offset += 1;
2763 }
2764 self.current_tag = None;
2765 self.state = text_state;
2766 true
2767 }
2768
2769 fn step_doctype_identifier_quoted(
2774 &mut self,
2775 ch: Option<char>,
2776 position: Position,
2777 out: &mut Vec<Token>,
2778 quote: char,
2779 kind: DoctypeIdentifierKind,
2780 ) -> bool {
2781 match ch {
2782 Some(c) if c == quote => {
2783 self.state = match kind {
2784 DoctypeIdentifierKind::Public => State::AfterDoctypePublicIdentifier,
2785 DoctypeIdentifierKind::System => State::AfterDoctypeSystemIdentifier,
2786 };
2787 false
2788 }
2789 Some('\0') => {
2790 self.doctype_identifier_mut(kind).push('\u{FFFD}');
2791 false
2792 }
2793 Some('>') => {
2794 let error_kind = match kind {
2797 DoctypeIdentifierKind::Public => ParseErrorKind::AbruptDoctypePublicIdentifier,
2798 DoctypeIdentifierKind::System => ParseErrorKind::AbruptDoctypeSystemIdentifier,
2799 };
2800 self.error(error_kind, position);
2801 self.close_doctype_with_quirks(out)
2802 }
2803 None => self.eof_in_doctype(out, position),
2804 Some(c) => {
2805 self.doctype_identifier_mut(kind).push(c);
2806 false
2807 }
2808 }
2809 }
2810
2811 fn begin_character_reference(&mut self, return_state: State, position: Position) {
2816 self.return_state = return_state;
2817 self.character_reference_start = position;
2818 self.character_reference_start_index = self.index - 1;
2819 self.state = State::CharacterReference;
2820 }
2821
2822 fn character_reference_in_attribute(&self) -> bool {
2825 matches!(
2826 self.return_state,
2827 State::AttributeValueDoubleQuoted
2828 | State::AttributeValueSingleQuoted
2829 | State::AttributeValueUnquoted
2830 )
2831 }
2832
2833 fn flush_char_as_character_reference(
2838 &mut self,
2839 c: char,
2840 position: Position,
2841 out: &mut Vec<Token>,
2842 ) {
2843 if self.character_reference_in_attribute() {
2844 self.push_attribute_value_char(c);
2845 } else {
2846 push_character(out, c, position);
2847 }
2848 }
2849
2850 fn flush_literal_character_reference_attempt(
2860 &mut self,
2861 end_index: usize,
2862 out: &mut Vec<Token>,
2863 ) {
2864 let chars: Vec<char> = self.chars[self.character_reference_start_index..end_index].to_vec();
2865 let mut position = self.character_reference_start;
2866 for c in chars {
2867 self.flush_char_as_character_reference(c, position, out);
2868 position.column += 1;
2869 position.byte_offset += 1;
2870 }
2871 }
2872
2873 fn lookup_named_character_reference(name: &str) -> Option<&'static str> {
2876 entities::NAMED_CHARACTER_REFERENCES
2877 .binary_search_by(|&(candidate, _)| candidate.cmp(name))
2878 .ok()
2879 .map(|i| entities::NAMED_CHARACTER_REFERENCES[i].1)
2880 }
2881
2882 fn named_character_reference_has_longer_match(prefix: &str) -> bool {
2887 let start =
2888 entities::NAMED_CHARACTER_REFERENCES.partition_point(|&(name, _)| name < prefix);
2889 entities::NAMED_CHARACTER_REFERENCES[start..]
2890 .iter()
2891 .take_while(|&&(name, _)| name.starts_with(prefix))
2892 .any(|&(name, _)| name.len() > prefix.len())
2893 }
2894
2895 fn run_named_character_reference(&mut self) {
2904 let (first_char, _) = self.consume();
2905 let first_char = first_char.expect(
2906 "named character reference state entered without an alphanumeric first character",
2907 );
2908
2909 let mut candidate = String::new();
2910 candidate.push(first_char);
2911 let mut cursor = self.index;
2912 let mut best: Option<usize> = None;
2913 if Self::lookup_named_character_reference(&candidate).is_some() {
2914 best = Some(candidate.len());
2915 }
2916 loop {
2917 if !Self::named_character_reference_has_longer_match(&candidate) {
2918 break;
2919 }
2920 let Some(&c) = self.chars.get(cursor) else {
2921 break;
2922 };
2923 candidate.push(c);
2924 cursor += 1;
2925 if Self::lookup_named_character_reference(&candidate).is_some() {
2926 best = Some(candidate.len());
2927 }
2928 }
2929
2930 let mut out = Vec::new();
2931 match best {
2932 Some(matched_len) => {
2933 self.index = self.character_reference_start_index + 1 + matched_len;
2934 let matched_name = candidate[..matched_len].to_owned();
2935 let next_char = self.chars.get(self.index).copied();
2936 let last_matched_is_semicolon = matched_name.ends_with(';');
2937 let next_is_equals_or_alphanumeric = matches!(next_char, Some('='))
2938 || matches!(next_char, Some(c) if c.is_ascii_alphanumeric());
2939 let historical = self.character_reference_in_attribute()
2940 && !last_matched_is_semicolon
2941 && next_is_equals_or_alphanumeric;
2942 if historical {
2943 self.flush_literal_character_reference_attempt(self.index, &mut out);
2944 } else {
2945 if !last_matched_is_semicolon {
2949 self.error(
2950 ParseErrorKind::MissingSemicolonAfterCharacterReference,
2951 self.character_reference_start,
2952 );
2953 }
2954 let replacement = Self::lookup_named_character_reference(&matched_name)
2955 .expect("matched_name was already verified to be a table entry");
2956 let start = self.character_reference_start;
2957 for c in replacement.chars() {
2958 self.flush_char_as_character_reference(c, start, &mut out);
2959 }
2960 }
2961 self.state = self.return_state;
2962 }
2963 None => {
2964 self.index = cursor;
2969 self.flush_literal_character_reference_attempt(self.index, &mut out);
2970 self.state = State::AmbiguousAmpersand;
2971 }
2972 }
2973 self.pending.extend(out);
2974 }
2975
2976 fn resolve_numeric_character_reference_code(&mut self, position: Position) -> char {
2983 const NUL: u32 = 0x00;
2984 const MAX_UNICODE: u32 = 0x10FFFF;
2985 let code = self.character_reference_code;
2986 let resolved = if code == NUL {
2987 self.error(ParseErrorKind::NullCharacterReference, position);
2988 0xFFFD
2989 } else if code > MAX_UNICODE {
2990 self.error(
2991 ParseErrorKind::CharacterReferenceOutsideUnicodeRange,
2992 position,
2993 );
2994 0xFFFD
2995 } else if is_surrogate(code) {
2996 self.error(ParseErrorKind::SurrogateCharacterReference, position);
2997 0xFFFD
2998 } else if is_noncharacter(code) {
2999 self.error(ParseErrorKind::NoncharacterCharacterReference, position);
3000 code
3001 } else if code == 0x0D || (is_control(code) && !is_ascii_whitespace(code)) {
3002 self.error(ParseErrorKind::ControlCharacterReference, position);
3003 windows_1252_override(code).unwrap_or(code)
3004 } else {
3005 code
3006 };
3007 char::from_u32(resolved).unwrap_or('\u{FFFD}')
3008 }
3009}
3010
3011fn push_eof(out: &mut Vec<Token>, position: Position) {
3017 out.push(Token {
3018 kind: TokenKind::Eof,
3019 position,
3020 });
3021}
3022
3023fn push_character(out: &mut Vec<Token>, c: char, position: Position) {
3026 out.push(Token {
3027 kind: TokenKind::Character(c),
3028 position,
3029 });
3030}
3031
3032fn is_surrogate(code: u32) -> bool {
3033 (0xD800..=0xDFFF).contains(&code)
3034}
3035
3036fn is_noncharacter(code: u32) -> bool {
3040 (0xFDD0..=0xFDEF).contains(&code) || matches!(code & 0xFFFF, 0xFFFE | 0xFFFF)
3041}
3042
3043fn is_control(code: u32) -> bool {
3046 (0x00..=0x1F).contains(&code) || (0x7F..=0x9F).contains(&code)
3047}
3048
3049fn is_ascii_whitespace(code: u32) -> bool {
3051 matches!(code, 0x09 | 0x0A | 0x0C | 0x0D | 0x20)
3052}
3053
3054fn windows_1252_override(code: u32) -> Option<u32> {
3060 const TABLE: &[(u32, u32)] = &[
3061 (0x80, 0x20AC),
3062 (0x82, 0x201A),
3063 (0x83, 0x0192),
3064 (0x84, 0x201E),
3065 (0x85, 0x2026),
3066 (0x86, 0x2020),
3067 (0x87, 0x2021),
3068 (0x88, 0x02C6),
3069 (0x89, 0x2030),
3070 (0x8A, 0x0160),
3071 (0x8B, 0x2039),
3072 (0x8C, 0x0152),
3073 (0x8E, 0x017D),
3074 (0x91, 0x2018),
3075 (0x92, 0x2019),
3076 (0x93, 0x201C),
3077 (0x94, 0x201D),
3078 (0x95, 0x2022),
3079 (0x96, 0x2013),
3080 (0x97, 0x2014),
3081 (0x98, 0x02DC),
3082 (0x99, 0x2122),
3083 (0x9A, 0x0161),
3084 (0x9B, 0x203A),
3085 (0x9C, 0x0153),
3086 (0x9E, 0x017E),
3087 (0x9F, 0x0178),
3088 ];
3089 TABLE
3090 .iter()
3091 .find(|&&(from, _)| from == code)
3092 .map(|&(_, to)| to)
3093}
3094
3095impl Iterator for Tokenizer {
3096 type Item = Token;
3097
3098 fn next(&mut self) -> Option<Token> {
3099 if self.pending.is_empty() {
3100 if self.eof_returned {
3101 return None;
3102 }
3103 self.run_until_token();
3104 }
3105 let token = self.pending.pop_front()?;
3106 if matches!(token.kind, TokenKind::Eof) {
3107 self.eof_returned = true;
3108 }
3109 Some(token)
3110 }
3111}
3112
3113#[cfg(test)]
3114mod tests {
3115 use super::*;
3120
3121 fn tokenize(input: &str) -> Vec<Token> {
3122 Tokenizer::new(input).collect()
3123 }
3124
3125 fn kinds(tokens: &[Token]) -> Vec<&TokenKind> {
3126 tokens.iter().map(|token| &token.kind).collect()
3127 }
3128
3129 fn pos(line: u32, column: u32, byte_offset: usize) -> Position {
3130 Position {
3131 line,
3132 column,
3133 byte_offset,
3134 }
3135 }
3136
3137 fn errors_for(input: &str) -> Vec<ParseErrorKind> {
3142 let mut tokenizer = Tokenizer::new(input);
3143 for _ in tokenizer.by_ref() {}
3144 tokenizer
3145 .take_errors()
3146 .into_iter()
3147 .map(|error| error.kind)
3148 .collect()
3149 }
3150
3151 #[test]
3152 fn plain_text_emits_one_character_token_per_char_then_eof() {
3153 let tokens = tokenize("hi");
3154 assert_eq!(
3155 tokens,
3156 vec![
3157 Token {
3158 kind: TokenKind::Character('h'),
3159 position: pos(1, 1, 0),
3160 },
3161 Token {
3162 kind: TokenKind::Character('i'),
3163 position: pos(1, 2, 1),
3164 },
3165 Token {
3166 kind: TokenKind::Eof,
3167 position: pos(1, 3, 2),
3168 },
3169 ]
3170 );
3171 }
3172
3173 #[test]
3174 fn simple_start_tag_with_no_attributes() {
3175 let tokens = tokenize("<p>");
3176 assert_eq!(
3177 tokens,
3178 vec![
3179 Token {
3180 kind: TokenKind::StartTag(TagToken {
3181 name: "p".to_owned(),
3182 self_closing: false,
3183 attributes: vec![],
3184 }),
3185 position: pos(1, 1, 0),
3186 },
3187 Token {
3188 kind: TokenKind::Eof,
3189 position: pos(1, 4, 3),
3190 },
3191 ]
3192 );
3193 }
3194
3195 #[test]
3196 fn end_tag() {
3197 let tokens = tokenize("</p>");
3198 match &tokens[0].kind {
3199 TokenKind::EndTag(tag) => assert_eq!(tag.name, "p"),
3200 other => panic!("expected end tag, got {other:?}"),
3201 }
3202 }
3203
3204 #[test]
3205 fn self_closing_tag() {
3206 let tokens = tokenize("<br/>");
3207 match &tokens[0].kind {
3208 TokenKind::StartTag(tag) => {
3209 assert_eq!(tag.name, "br");
3210 assert!(tag.self_closing);
3211 }
3212 other => panic!("expected start tag, got {other:?}"),
3213 }
3214 }
3215
3216 #[test]
3217 fn attribute_value_quoting_forms() {
3218 for html in ["<a href=\"x\">", "<a href='x'>", "<a href=x>"] {
3219 let tokens = tokenize(html);
3220 match &tokens[0].kind {
3221 TokenKind::StartTag(tag) => assert_eq!(
3222 tag.attributes,
3223 vec![Attribute {
3224 name: "href".to_owned(),
3225 value: "x".to_owned(),
3226 }],
3227 "input: {html:?}",
3228 ),
3229 other => panic!("expected start tag for {html:?}, got {other:?}"),
3230 }
3231 }
3232 }
3233
3234 #[test]
3235 fn multiple_attributes_separated_by_whitespace() {
3236 let tokens = tokenize(r#"<a href="x" target="y">"#);
3237 match &tokens[0].kind {
3238 TokenKind::StartTag(tag) => assert_eq!(
3239 tag.attributes,
3240 vec![
3241 Attribute {
3242 name: "href".to_owned(),
3243 value: "x".to_owned(),
3244 },
3245 Attribute {
3246 name: "target".to_owned(),
3247 value: "y".to_owned(),
3248 },
3249 ]
3250 ),
3251 other => panic!("expected start tag, got {other:?}"),
3252 }
3253 }
3254
3255 #[test]
3256 fn duplicate_attribute_name_keeps_first_occurrence_only() {
3257 let tokens = tokenize(r#"<a href="x" href="y">"#);
3258 match &tokens[0].kind {
3259 TokenKind::StartTag(tag) => assert_eq!(
3260 tag.attributes,
3261 vec![Attribute {
3262 name: "href".to_owned(),
3263 value: "x".to_owned(),
3264 }]
3265 ),
3266 other => panic!("expected start tag, got {other:?}"),
3267 }
3268 }
3269
3270 #[test]
3271 fn tag_and_attribute_names_are_ascii_lowercased() {
3272 let tokens = tokenize(r#"<DIV CLASS="a">"#);
3273 match &tokens[0].kind {
3274 TokenKind::StartTag(tag) => {
3275 assert_eq!(tag.name, "div");
3276 assert_eq!(tag.attributes[0].name, "class");
3277 }
3278 other => panic!("expected start tag, got {other:?}"),
3279 }
3280 }
3281
3282 #[test]
3283 fn boolean_attribute_with_no_value() {
3284 let tokens = tokenize("<input disabled>");
3285 match &tokens[0].kind {
3286 TokenKind::StartTag(tag) => assert_eq!(
3287 tag.attributes,
3288 vec![Attribute {
3289 name: "disabled".to_owned(),
3290 value: String::new(),
3291 }]
3292 ),
3293 other => panic!("expected start tag, got {other:?}"),
3294 }
3295 }
3296
3297 #[test]
3298 fn null_character_in_tag_name_is_replaced_with_u_fffd() {
3299 let tokens = tokenize("<a\u{0}b>");
3300 match &tokens[0].kind {
3301 TokenKind::StartTag(tag) => assert_eq!(tag.name, "a\u{FFFD}b"),
3302 other => panic!("expected start tag, got {other:?}"),
3303 }
3304 }
3305
3306 #[test]
3307 fn null_character_in_data_state_is_emitted_literally_not_replaced() {
3308 let tokens = tokenize("\u{0}");
3309 assert_eq!(tokens[0].kind, TokenKind::Character('\u{0}'));
3310 }
3311
3312 #[test]
3313 fn eof_inside_a_tag_emits_only_eof_no_tag_token() {
3314 let tokens = tokenize("<div");
3315 assert_eq!(
3316 tokens,
3317 vec![Token {
3318 kind: TokenKind::Eof,
3319 position: pos(1, 5, 4),
3320 }]
3321 );
3322 }
3323
3324 #[test]
3325 fn eof_right_after_solidus_emits_synthesized_lt_and_solidus_then_eof() {
3326 let tokens = tokenize("</");
3327 assert_eq!(
3328 tokens,
3329 vec![
3330 Token {
3331 kind: TokenKind::Character('<'),
3332 position: pos(1, 1, 0),
3333 },
3334 Token {
3335 kind: TokenKind::Character('/'),
3336 position: pos(1, 2, 1),
3337 },
3338 Token {
3339 kind: TokenKind::Eof,
3340 position: pos(1, 3, 2),
3341 },
3342 ]
3343 );
3344 }
3345
3346 #[test]
3347 fn position_tracking_across_a_newline() {
3348 let tokens = tokenize("a\nb");
3349 assert_eq!(tokens[0].position, pos(1, 1, 0));
3350 assert_eq!(tokens[1].position, pos(1, 2, 1)); assert_eq!(tokens[2].position, pos(2, 1, 2));
3352 }
3353
3354 #[test]
3355 fn crlf_and_lone_cr_normalize_to_a_single_lf_character_token() {
3356 let crlf = tokenize("a\r\nb");
3357 assert_eq!(
3358 kinds(&crlf),
3359 vec![
3360 &TokenKind::Character('a'),
3361 &TokenKind::Character('\n'),
3362 &TokenKind::Character('b'),
3363 &TokenKind::Eof,
3364 ]
3365 );
3366 let lone_cr = tokenize("a\rb");
3367 assert_eq!(
3368 kinds(&lone_cr),
3369 vec![
3370 &TokenKind::Character('a'),
3371 &TokenKind::Character('\n'),
3372 &TokenKind::Character('b'),
3373 &TokenKind::Eof,
3374 ]
3375 );
3376 }
3377
3378 #[test]
3379 fn named_character_reference_with_semicolon() {
3380 assert_eq!(
3381 kinds(&tokenize("&")),
3382 vec![&TokenKind::Character('&'), &TokenKind::Eof]
3383 );
3384 }
3385
3386 #[test]
3387 fn named_character_reference_multi_codepoint() {
3388 assert_eq!(
3390 kinds(&tokenize("≂̸")),
3391 vec![
3392 &TokenKind::Character('\u{2242}'),
3393 &TokenKind::Character('\u{338}'),
3394 &TokenKind::Eof,
3395 ]
3396 );
3397 }
3398
3399 #[test]
3400 fn legacy_named_character_reference_without_semicolon_still_resolves_outside_attributes() {
3401 assert_eq!(
3405 kinds(&tokenize("& b")),
3406 vec![
3407 &TokenKind::Character('&'),
3408 &TokenKind::Character(' '),
3409 &TokenKind::Character('b'),
3410 &TokenKind::Eof,
3411 ]
3412 );
3413 }
3414
3415 #[test]
3416 fn unknown_named_character_reference_falls_back_to_ambiguous_ampersand() {
3417 assert_eq!(
3421 kinds(&tokenize("&1;")),
3422 vec![
3423 &TokenKind::Character('&'),
3424 &TokenKind::Character('1'),
3425 &TokenKind::Character(';'),
3426 &TokenKind::Eof,
3427 ]
3428 );
3429 }
3430
3431 #[test]
3432 fn named_character_reference_historical_fallback_in_unquoted_attribute_value() {
3433 let tokens = tokenize("<a href=¬it=1>");
3438 match &tokens[0].kind {
3439 TokenKind::StartTag(tag) => assert_eq!(
3440 tag.attributes,
3441 vec![Attribute {
3442 name: "href".to_owned(),
3443 value: "¬it=1".to_owned(),
3444 }]
3445 ),
3446 other => panic!("expected start tag, got {other:?}"),
3447 }
3448 }
3449
3450 #[test]
3451 fn named_character_reference_historical_fallback_applies_in_double_quoted_attributes_too() {
3452 let tokens = tokenize(r#"<a href="¬it">"#);
3458 match &tokens[0].kind {
3459 TokenKind::StartTag(tag) => assert_eq!(
3460 tag.attributes,
3461 vec![Attribute {
3462 name: "href".to_owned(),
3463 value: "¬it".to_owned(),
3464 }]
3465 ),
3466 other => panic!("expected start tag, got {other:?}"),
3467 }
3468 }
3469
3470 #[test]
3471 fn named_character_reference_ending_in_semicolon_resolves_normally_even_in_an_attribute() {
3472 let tokens = tokenize(r#"<a href="©">"#);
3476 match &tokens[0].kind {
3477 TokenKind::StartTag(tag) => assert_eq!(
3478 tag.attributes,
3479 vec![Attribute {
3480 name: "href".to_owned(),
3481 value: "\u{A9}".to_owned(),
3482 }]
3483 ),
3484 other => panic!("expected start tag, got {other:?}"),
3485 }
3486 }
3487
3488 #[test]
3489 fn decimal_and_hexadecimal_character_references() {
3490 assert_eq!(
3491 kinds(&tokenize("A")),
3492 vec![&TokenKind::Character('A'), &TokenKind::Eof]
3493 );
3494 assert_eq!(
3495 kinds(&tokenize("A")),
3496 vec![&TokenKind::Character('A'), &TokenKind::Eof]
3497 );
3498 assert_eq!(
3499 kinds(&tokenize("A")),
3500 vec![&TokenKind::Character('A'), &TokenKind::Eof]
3501 );
3502 }
3503
3504 #[test]
3505 fn numeric_character_reference_missing_semicolon_still_resolves() {
3506 assert_eq!(
3507 kinds(&tokenize("Ax")),
3508 vec![
3509 &TokenKind::Character('A'),
3510 &TokenKind::Character('x'),
3511 &TokenKind::Eof,
3512 ]
3513 );
3514 }
3515
3516 #[test]
3517 fn numeric_character_reference_null_is_replaced_with_u_fffd() {
3518 assert_eq!(
3519 kinds(&tokenize("�")),
3520 vec![&TokenKind::Character('\u{FFFD}'), &TokenKind::Eof]
3521 );
3522 }
3523
3524 #[test]
3525 fn numeric_character_reference_outside_unicode_range_is_replaced_with_u_fffd() {
3526 assert_eq!(
3527 kinds(&tokenize("�")),
3528 vec![&TokenKind::Character('\u{FFFD}'), &TokenKind::Eof]
3529 );
3530 }
3531
3532 #[test]
3533 fn numeric_character_reference_surrogate_is_replaced_with_u_fffd() {
3534 assert_eq!(
3535 kinds(&tokenize("�")),
3536 vec![&TokenKind::Character('\u{FFFD}'), &TokenKind::Eof]
3537 );
3538 }
3539
3540 #[test]
3541 fn numeric_character_reference_windows_1252_control_override() {
3542 assert_eq!(
3545 kinds(&tokenize("€")),
3546 vec![&TokenKind::Character('\u{20AC}'), &TokenKind::Eof]
3547 );
3548 }
3549
3550 #[test]
3551 fn numeric_character_reference_unmapped_c1_control_is_left_unchanged() {
3552 assert_eq!(
3556 kinds(&tokenize("")),
3557 vec![&TokenKind::Character('\u{81}'), &TokenKind::Eof]
3558 );
3559 }
3560
3561 #[test]
3562 fn absence_of_digits_in_numeric_character_reference_falls_back_to_literal_text() {
3563 assert_eq!(
3564 kinds(&tokenize("&#;")),
3565 vec![
3566 &TokenKind::Character('&'),
3567 &TokenKind::Character('#'),
3568 &TokenKind::Character(';'),
3569 &TokenKind::Eof,
3570 ]
3571 );
3572 assert_eq!(
3573 kinds(&tokenize("&#x;")),
3574 vec![
3575 &TokenKind::Character('&'),
3576 &TokenKind::Character('#'),
3577 &TokenKind::Character('x'),
3578 &TokenKind::Character(';'),
3579 &TokenKind::Eof,
3580 ]
3581 );
3582 }
3583
3584 #[test]
3585 fn lone_ampersand_at_eof_is_emitted_literally() {
3586 assert_eq!(
3587 kinds(&tokenize("&")),
3588 vec![&TokenKind::Character('&'), &TokenKind::Eof]
3589 );
3590 }
3591
3592 #[test]
3593 fn simple_comment() {
3594 assert_eq!(
3595 kinds(&tokenize("<!-- hi -->")),
3596 vec![&TokenKind::Comment(" hi ".to_owned()), &TokenKind::Eof]
3597 );
3598 }
3599
3600 #[test]
3601 fn abrupt_closing_of_empty_comment_still_emits_an_empty_comment_token() {
3602 assert_eq!(
3603 kinds(&tokenize("<!-->")),
3604 vec![&TokenKind::Comment(String::new()), &TokenKind::Eof]
3605 );
3606 }
3607
3608 #[test]
3609 fn comment_containing_a_lone_hyphen() {
3610 assert_eq!(
3611 kinds(&tokenize("<!-- a - b -->")),
3612 vec![&TokenKind::Comment(" a - b ".to_owned()), &TokenKind::Eof]
3613 );
3614 }
3615
3616 #[test]
3617 fn eof_inside_a_comment_still_emits_the_comment_token() {
3618 assert_eq!(
3619 kinds(&tokenize("<!-- unterminated")),
3620 vec![
3621 &TokenKind::Comment(" unterminated".to_owned()),
3622 &TokenKind::Eof,
3623 ]
3624 );
3625 }
3626
3627 #[test]
3628 fn nested_comment_open_sequence_is_absorbed_as_data_not_a_real_nested_comment() {
3629 assert_eq!(
3634 kinds(&tokenize("<!--<!-->")),
3635 vec![&TokenKind::Comment("<!".to_owned()), &TokenKind::Eof]
3636 );
3637 }
3638
3639 #[test]
3640 fn incorrectly_opened_comment_falls_back_to_bogus_comment() {
3641 assert_eq!(
3644 kinds(&tokenize("<!weird>")),
3645 vec![&TokenKind::Comment("weird".to_owned()), &TokenKind::Eof]
3646 );
3647 }
3648
3649 #[test]
3650 fn end_tag_with_invalid_first_character_falls_back_to_bogus_comment() {
3651 assert_eq!(
3654 kinds(&tokenize("</1>")),
3655 vec![&TokenKind::Comment("1".to_owned()), &TokenKind::Eof]
3656 );
3657 }
3658
3659 #[test]
3660 fn cdata_outside_foreign_content_becomes_a_bogus_comment() {
3661 assert_eq!(
3666 kinds(&tokenize("<![CDATA[x]]>")),
3667 vec![
3668 &TokenKind::Comment("[CDATA[x]]".to_owned()),
3669 &TokenKind::Eof,
3670 ]
3671 );
3672 }
3673
3674 fn expect_doctype(tokens: &[Token]) -> &DoctypeToken {
3675 match &tokens[0].kind {
3676 TokenKind::Doctype(doctype) => doctype,
3677 other => panic!("expected DOCTYPE token, got {other:?}"),
3678 }
3679 }
3680
3681 #[test]
3682 fn simple_doctype() {
3683 let tokens = tokenize("<!DOCTYPE html>");
3684 assert_eq!(
3685 expect_doctype(&tokens),
3686 &DoctypeToken {
3687 name: Some("html".to_owned()),
3688 ..Default::default()
3689 }
3690 );
3691 assert_eq!(kinds(&tokens)[1], &TokenKind::Eof);
3692 }
3693
3694 #[test]
3695 fn doctype_keyword_and_name_are_ascii_case_insensitive_lowercased() {
3696 let tokens = tokenize("<!doctype HTML>");
3697 assert_eq!(
3698 expect_doctype(&tokens),
3699 &DoctypeToken {
3700 name: Some("html".to_owned()),
3701 ..Default::default()
3702 }
3703 );
3704 }
3705
3706 #[test]
3707 fn doctype_with_no_name_sets_force_quirks() {
3708 let tokens = tokenize("<!DOCTYPE>");
3709 assert_eq!(
3710 expect_doctype(&tokens),
3711 &DoctypeToken {
3712 force_quirks: true,
3713 ..Default::default()
3714 }
3715 );
3716 }
3717
3718 #[test]
3719 fn doctype_with_public_and_system_identifiers() {
3720 let tokens = tokenize(
3721 r#"<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">"#,
3722 );
3723 assert_eq!(
3724 expect_doctype(&tokens),
3725 &DoctypeToken {
3726 name: Some("html".to_owned()),
3727 public_identifier: Some("-//W3C//DTD HTML 4.01//EN".to_owned()),
3728 system_identifier: Some("http://www.w3.org/TR/html4/strict.dtd".to_owned()),
3729 force_quirks: false,
3730 }
3731 );
3732 }
3733
3734 #[test]
3735 fn eof_inside_doctype_name_sets_force_quirks_but_keeps_the_partial_name() {
3736 let tokens = tokenize("<!DOCTYPE html");
3737 assert_eq!(
3738 tokens,
3739 vec![
3740 Token {
3741 kind: TokenKind::Doctype(DoctypeToken {
3742 name: Some("html".to_owned()),
3743 force_quirks: true,
3744 ..Default::default()
3745 }),
3746 position: pos(1, 1, 0),
3747 },
3748 Token {
3749 kind: TokenKind::Eof,
3750 position: pos(1, 15, 14),
3751 },
3752 ]
3753 );
3754 }
3755
3756 #[test]
3757 fn unrecognized_text_after_doctype_name_falls_back_to_bogus_doctype() {
3758 let tokens = tokenize("<!DOCTYPE html GARBAGE>");
3761 assert_eq!(
3762 expect_doctype(&tokens),
3763 &DoctypeToken {
3764 name: Some("html".to_owned()),
3765 force_quirks: true,
3766 ..Default::default()
3767 }
3768 );
3769 }
3770
3771 #[test]
3772 fn simple_processing_instruction() {
3773 let tokens = tokenize("<?foo bar?>");
3774 assert_eq!(
3775 kinds(&tokens),
3776 vec![
3777 &TokenKind::ProcessingInstruction(ProcessingInstructionToken {
3778 target: "foo".to_owned(),
3779 data: "bar".to_owned(),
3780 }),
3781 &TokenKind::Eof,
3782 ]
3783 );
3784 }
3785
3786 #[test]
3787 fn xml_target_is_disallowed_and_becomes_a_bogus_comment() {
3788 let tokens = tokenize(r#"<?xml version="1.0"?>"#);
3789 assert_eq!(
3790 kinds(&tokens),
3791 vec![
3792 &TokenKind::Comment("?xml version=\"1.0\"?".to_owned()),
3793 &TokenKind::Eof,
3794 ]
3795 );
3796 }
3797
3798 #[test]
3799 fn xml_stylesheet_target_is_disallowed_and_becomes_a_bogus_comment() {
3800 let tokens = tokenize("<?xml-stylesheet foo?>");
3801 assert_eq!(
3802 kinds(&tokens),
3803 vec![
3804 &TokenKind::Comment("?xml-stylesheet foo?".to_owned()),
3805 &TokenKind::Eof,
3806 ]
3807 );
3808 }
3809
3810 #[test]
3811 fn eof_right_after_question_mark_emits_only_eof_no_token_at_all() {
3812 assert_eq!(kinds(&tokenize("<?")), vec![&TokenKind::Eof]);
3816 }
3817
3818 #[test]
3819 fn eof_inside_processing_instruction_data_discards_the_pi_token() {
3820 assert_eq!(kinds(&tokenize("<?foo bar")), vec![&TokenKind::Eof]);
3823 }
3824
3825 #[test]
3826 fn lone_question_marks_inside_processing_instruction_data_are_kept_literally() {
3827 let tokens = tokenize("<?foo a?b?>");
3828 assert_eq!(
3829 kinds(&tokens),
3830 vec![
3831 &TokenKind::ProcessingInstruction(ProcessingInstructionToken {
3832 target: "foo".to_owned(),
3833 data: "a?b".to_owned(),
3834 }),
3835 &TokenKind::Eof,
3836 ]
3837 );
3838 }
3839
3840 fn tokenize_switching_after_start_tag(input: &str, state: ExternalState) -> Vec<Token> {
3845 let mut tokenizer = Tokenizer::new(input);
3846 let mut tokens = Vec::new();
3847 while let Some(token) = tokenizer.next() {
3848 let is_start = matches!(&token.kind, TokenKind::StartTag(_));
3849 tokens.push(token);
3850 if is_start {
3851 tokenizer.switch_to(state);
3852 }
3853 }
3854 tokens
3855 }
3856
3857 fn characters_only(tokens: &[Token]) -> String {
3858 tokens
3859 .iter()
3860 .filter_map(|token| match &token.kind {
3861 TokenKind::Character(c) => Some(*c),
3862 _ => None,
3863 })
3864 .collect()
3865 }
3866
3867 #[test]
3868 fn rcdata_resolves_character_references_and_ends_on_matching_end_tag() {
3869 let tokens =
3870 tokenize_switching_after_start_tag("<title>AT&T</title>", ExternalState::RcData);
3871 assert_eq!(
3872 kinds(&tokens),
3873 vec![
3874 &TokenKind::StartTag(TagToken {
3875 name: "title".to_owned(),
3876 self_closing: false,
3877 attributes: vec![],
3878 }),
3879 &TokenKind::Character('A'),
3880 &TokenKind::Character('T'),
3881 &TokenKind::Character('&'),
3882 &TokenKind::Character('T'),
3883 &TokenKind::EndTag(TagToken {
3884 name: "title".to_owned(),
3885 self_closing: false,
3886 attributes: vec![],
3887 }),
3888 &TokenKind::Eof,
3889 ]
3890 );
3891 }
3892
3893 #[test]
3894 fn rcdata_end_tag_with_wrong_name_is_kept_as_literal_text() {
3895 let tokens =
3898 tokenize_switching_after_start_tag("<title>a</b>c</title>", ExternalState::RcData);
3899 assert_eq!(
3900 characters_only(&tokens[1..tokens.len() - 2]),
3901 "a</b>c".to_owned()
3902 );
3903 match &tokens.last().unwrap().kind {
3904 TokenKind::Eof => {}
3905 other => panic!("expected trailing Eof, got {other:?}"),
3906 }
3907 match &tokens[tokens.len() - 2].kind {
3908 TokenKind::EndTag(tag) => assert_eq!(tag.name, "title"),
3909 other => panic!("expected closing end tag, got {other:?}"),
3910 }
3911 }
3912
3913 #[test]
3914 fn rawtext_does_not_resolve_character_references() {
3915 let tokens =
3916 tokenize_switching_after_start_tag("<style>&</style>", ExternalState::RawText);
3917 assert_eq!(characters_only(&tokens), "&".to_owned());
3918 }
3919
3920 #[test]
3921 fn plaintext_never_recognizes_any_end_tag_ever_again() {
3922 let tokens = tokenize_switching_after_start_tag(
3925 "<plaintext>a</plaintext>b",
3926 ExternalState::PlainText,
3927 );
3928 assert_eq!(characters_only(&tokens), "a</plaintext>b".to_owned());
3929 match &tokens.last().unwrap().kind {
3930 TokenKind::Eof => {}
3931 other => panic!("expected trailing Eof, got {other:?}"),
3932 }
3933 }
3934
3935 #[test]
3936 fn appropriate_end_tag_requires_a_start_tag_to_have_been_emitted_first() {
3937 let mut tokenizer = Tokenizer::new("</title>");
3941 tokenizer.switch_to(ExternalState::RcData);
3942 let tokens: Vec<_> = tokenizer.collect();
3943 assert_eq!(characters_only(&tokens), "</title>".to_owned());
3944 assert_eq!(tokens.last().unwrap().kind, TokenKind::Eof);
3945 }
3946
3947 #[test]
3948 fn script_data_plain_content_no_html_comment_like_wrapper() {
3949 let tokens = tokenize_switching_after_start_tag(
3950 "<script>var x = 1;</script>",
3951 ExternalState::ScriptData,
3952 );
3953 assert_eq!(characters_only(&tokens), "var x = 1;".to_owned());
3954 match &tokens[tokens.len() - 2].kind {
3955 TokenKind::EndTag(tag) => assert_eq!(tag.name, "script"),
3956 other => panic!("expected closing end tag, got {other:?}"),
3957 }
3958 }
3959
3960 #[test]
3961 fn script_data_does_not_resolve_character_references_either() {
3962 let tokens =
3964 tokenize_switching_after_start_tag("<script>&</script>", ExternalState::ScriptData);
3965 assert_eq!(characters_only(&tokens), "&".to_owned());
3966 }
3967
3968 #[test]
3969 fn script_data_html_comment_like_wrapper_round_trips_literally() {
3970 let source = "<!--alert(1);-->";
3973 let tokens = tokenize_switching_after_start_tag(
3974 &format!("<script>{source}</script>"),
3975 ExternalState::ScriptData,
3976 );
3977 assert_eq!(characters_only(&tokens), source.to_owned());
3978 match &tokens[tokens.len() - 2].kind {
3979 TokenKind::EndTag(tag) => assert_eq!(tag.name, "script"),
3980 other => panic!("expected closing end tag, got {other:?}"),
3981 }
3982 }
3983
3984 #[test]
3985 fn nested_script_tags_inside_html_comment_like_wrapper_do_not_end_the_element_early() {
3986 let source = "<!--<script>x</script>-->";
3993 let tokens = tokenize_switching_after_start_tag(
3994 &format!("<script>{source}</script>"),
3995 ExternalState::ScriptData,
3996 );
3997 assert_eq!(characters_only(&tokens), source.to_owned());
3998 let end_tags: Vec<_> = tokens
3999 .iter()
4000 .filter(|token| matches!(&token.kind, TokenKind::EndTag(_)))
4001 .collect();
4002 assert_eq!(
4003 end_tags.len(),
4004 1,
4005 "only the real closing tag should be an end tag token, not the one nested inside the comment-like wrapper"
4006 );
4007 }
4008
4009 #[test]
4010 fn script_data_end_tag_with_wrong_name_is_kept_as_literal_text() {
4011 let tokens = tokenize_switching_after_start_tag(
4012 "<script>a</scriptx>b</script>",
4013 ExternalState::ScriptData,
4014 );
4015 assert_eq!(characters_only(&tokens), "a</scriptx>b".to_owned());
4016 let end_tags: Vec<_> = tokens
4017 .iter()
4018 .filter_map(|token| match &token.kind {
4019 TokenKind::EndTag(tag) => Some(tag.name.as_str()),
4020 _ => None,
4021 })
4022 .collect();
4023 assert_eq!(end_tags, vec!["script"]);
4024 }
4025
4026 fn tokenize_in_foreign_content(input: &str) -> Vec<Token> {
4027 let mut tokenizer = Tokenizer::new(input);
4028 tokenizer.set_in_foreign_content(true);
4029 tokenizer.collect()
4030 }
4031
4032 #[test]
4033 fn cdata_section_outside_foreign_content_still_becomes_a_bogus_comment() {
4034 assert_eq!(
4039 kinds(&tokenize("<![CDATA[x]]>")),
4040 vec![
4041 &TokenKind::Comment("[CDATA[x]]".to_owned()),
4042 &TokenKind::Eof,
4043 ]
4044 );
4045 }
4046
4047 #[test]
4048 fn cdata_section_in_foreign_content_yields_character_tokens() {
4049 assert_eq!(
4050 kinds(&tokenize_in_foreign_content("<![CDATA[hi]]>")),
4051 vec![
4052 &TokenKind::Character('h'),
4053 &TokenKind::Character('i'),
4054 &TokenKind::Eof,
4055 ]
4056 );
4057 }
4058
4059 #[test]
4060 fn cdata_section_null_character_is_kept_literal_not_replaced() {
4061 assert_eq!(
4065 kinds(&tokenize_in_foreign_content("<![CDATA[\u{0}]]>")),
4066 vec![&TokenKind::Character('\u{0}'), &TokenKind::Eof]
4067 );
4068 }
4069
4070 #[test]
4071 fn cdata_section_single_bracket_not_followed_by_another_is_literal_content() {
4072 let tokens = tokenize_in_foreign_content("<![CDATA[a]b]]>");
4073 assert_eq!(
4074 kinds(&tokens),
4075 vec![
4076 &TokenKind::Character('a'),
4077 &TokenKind::Character(']'),
4078 &TokenKind::Character('b'),
4079 &TokenKind::Eof,
4080 ]
4081 );
4082 }
4083
4084 #[test]
4085 fn cdata_section_three_consecutive_brackets_then_close_keeps_only_the_first_as_content() {
4086 let tokens = tokenize_in_foreign_content("<![CDATA[]]]>");
4091 assert_eq!(
4092 tokens,
4093 vec![
4094 Token {
4095 kind: TokenKind::Character(']'),
4096 position: pos(1, 10, 9),
4097 },
4098 Token {
4099 kind: TokenKind::Eof,
4100 position: pos(1, 14, 13),
4101 },
4102 ]
4103 );
4104 }
4105
4106 #[test]
4107 fn cdata_section_four_consecutive_brackets_then_close_keeps_first_two_as_content() {
4108 let tokens = tokenize_in_foreign_content("<![CDATA[]]]]>");
4109 assert_eq!(
4110 kinds(&tokens),
4111 vec![
4112 &TokenKind::Character(']'),
4113 &TokenKind::Character(']'),
4114 &TokenKind::Eof,
4115 ]
4116 );
4117 }
4118
4119 #[test]
4120 fn eof_inside_cdata_section_emits_eof_after_any_withheld_brackets_flush() {
4121 let tokens = tokenize_in_foreign_content("<![CDATA[a]");
4122 assert_eq!(
4123 kinds(&tokens),
4124 vec![
4125 &TokenKind::Character('a'),
4126 &TokenKind::Character(']'),
4127 &TokenKind::Eof,
4128 ]
4129 );
4130 }
4131
4132 #[test]
4140 fn tokenizer_level_parse_errors_fire_with_the_right_kind() {
4141 let cases: &[(&str, ParseErrorKind)] = &[
4142 ("<![CDATA[x]]>", ParseErrorKind::CdataInHtmlContent),
4143 ("<!x>", ParseErrorKind::IncorrectlyOpenedComment),
4144 ("<!-->", ParseErrorKind::AbruptClosingOfEmptyComment),
4145 ("<!--<!--x-->", ParseErrorKind::NestedComment),
4146 ("<!--x--!>", ParseErrorKind::IncorrectlyClosedComment),
4147 ("<!--", ParseErrorKind::EofInComment),
4148 ("<1>", ParseErrorKind::InvalidFirstCharacterOfTagName),
4149 ("<", ParseErrorKind::EofBeforeTagName),
4150 ("</>", ParseErrorKind::MissingEndTagName),
4151 ("<p x=", ParseErrorKind::EofInTag),
4152 (r#"<p id="a" id="b">"#, ParseErrorKind::DuplicateAttribute),
4153 ("\0", ParseErrorKind::UnexpectedNullCharacter),
4154 (
4155 r#"<p ">"#,
4156 ParseErrorKind::UnexpectedCharacterInAttributeName,
4157 ),
4158 ("<p x=>", ParseErrorKind::MissingAttributeValue),
4159 (
4160 r#"<p x=a"b>"#,
4161 ParseErrorKind::UnexpectedCharacterInUnquotedAttributeValue,
4162 ),
4163 (
4164 r#"<p x="a"y="b">"#,
4165 ParseErrorKind::MissingWhitespaceBetweenAttributes,
4166 ),
4167 ("<p/ x>", ParseErrorKind::UnexpectedSolidusInTag),
4168 (
4169 "<p =>",
4170 ParseErrorKind::UnexpectedEqualsSignBeforeAttributeName,
4171 ),
4172 ("&zzz;", ParseErrorKind::UnknownNamedCharacterReference),
4173 (
4174 "&#;",
4175 ParseErrorKind::AbsenceOfDigitsInNumericCharacterReference,
4176 ),
4177 (
4178 "A ",
4179 ParseErrorKind::MissingSemicolonAfterCharacterReference,
4180 ),
4181 ("�", ParseErrorKind::NullCharacterReference),
4182 (
4183 "�",
4184 ParseErrorKind::CharacterReferenceOutsideUnicodeRange,
4185 ),
4186 ("�", ParseErrorKind::SurrogateCharacterReference),
4187 ("", ParseErrorKind::NoncharacterCharacterReference),
4188 ("", ParseErrorKind::ControlCharacterReference),
4189 (
4190 "<!DOCTYPEhtml>",
4191 ParseErrorKind::MissingWhitespaceBeforeDoctypeName,
4192 ),
4193 ("<!DOCTYPE >", ParseErrorKind::MissingDoctypeName),
4194 (
4195 "<!DOCTYPE html foo>",
4196 ParseErrorKind::InvalidCharacterSequenceAfterDoctypeName,
4197 ),
4198 (
4199 r#"<!DOCTYPE html PUBLIC "a""b">"#,
4200 ParseErrorKind::MissingWhitespaceBetweenDoctypePublicAndSystemIdentifiers,
4201 ),
4202 (
4203 r#"<!DOCTYPE html SYSTEM "a"b>"#,
4204 ParseErrorKind::UnexpectedCharacterAfterDoctypeSystemIdentifier,
4205 ),
4206 ("<!DOCTYPE", ParseErrorKind::EofInDoctype),
4207 ("<?", ParseErrorKind::EofInProcessingInstruction),
4208 (
4209 "<? >",
4210 ParseErrorKind::InvalidFirstCharacterOfProcessingInstructionTarget,
4211 ),
4212 ("<?a$>", ParseErrorKind::InvalidProcessingInstructionTarget),
4213 (
4214 "<?xml?>",
4215 ParseErrorKind::DisallowedProcessingInstructionTarget,
4216 ),
4217 (
4218 r#"<!DOCTYPE html PUBLIC "ab>"#,
4219 ParseErrorKind::AbruptDoctypePublicIdentifier,
4220 ),
4221 (
4222 r#"<!DOCTYPE html SYSTEM "ab>"#,
4223 ParseErrorKind::AbruptDoctypeSystemIdentifier,
4224 ),
4225 (
4226 "<!DOCTYPE html PUBLIC>",
4227 ParseErrorKind::MissingDoctypePublicIdentifier,
4228 ),
4229 (
4230 "<!DOCTYPE html SYSTEM>",
4231 ParseErrorKind::MissingDoctypeSystemIdentifier,
4232 ),
4233 (
4234 "<!DOCTYPE html PUBLIC x>",
4235 ParseErrorKind::MissingQuoteBeforeDoctypePublicIdentifier,
4236 ),
4237 (
4238 "<!DOCTYPE html SYSTEM x>",
4239 ParseErrorKind::MissingQuoteBeforeDoctypeSystemIdentifier,
4240 ),
4241 (
4242 r#"<!DOCTYPE html PUBLIC"x">"#,
4243 ParseErrorKind::MissingWhitespaceAfterDoctypePublicKeyword,
4244 ),
4245 (
4246 r#"<!DOCTYPE html SYSTEM"x">"#,
4247 ParseErrorKind::MissingWhitespaceAfterDoctypeSystemKeyword,
4248 ),
4249 ("<p></p a=1>", ParseErrorKind::EndTagWithAttributes),
4250 ("<p></p/>", ParseErrorKind::EndTagWithTrailingSolidus),
4251 ("\u{FFFE}", ParseErrorKind::NoncharacterInInputStream),
4252 ("\u{1}", ParseErrorKind::ControlCharacterInInputStream),
4253 ];
4254 for (input, expected_kind) in cases {
4255 let errors = errors_for(input);
4256 assert!(
4257 errors.contains(expected_kind),
4258 "input {input:?}: expected {expected_kind:?} among {errors:?}"
4259 );
4260 }
4261 }
4262
4263 #[test]
4264 fn eof_in_cdata_fires_in_foreign_content() {
4265 let mut tokenizer = Tokenizer::new("<![CDATA[abc");
4266 tokenizer.set_in_foreign_content(true);
4267 for _ in tokenizer.by_ref() {}
4268 let kinds: Vec<_> = tokenizer
4269 .take_errors()
4270 .into_iter()
4271 .map(|error| error.kind)
4272 .collect();
4273 assert!(kinds.contains(&ParseErrorKind::EofInCdata));
4274 }
4275
4276 #[test]
4277 fn every_parse_error_kind_has_a_non_empty_display() {
4278 for kind in [
4284 ParseErrorKind::DuplicateAttribute,
4285 ParseErrorKind::EofInDoctype,
4286 ParseErrorKind::NoncharacterInInputStream,
4287 ParseErrorKind::ControlCharacterInInputStream,
4288 ] {
4289 assert!(!kind.to_string().is_empty());
4290 }
4291 }
4292
4293 #[test]
4294 fn eof_in_script_html_comment_like_text_fires_mid_wrapper() {
4295 let mut tokenizer = Tokenizer::new("<script><!--x");
4296 while let Some(token) = tokenizer.next() {
4297 if matches!(&token.kind, TokenKind::StartTag(_)) {
4298 tokenizer.switch_to(ExternalState::ScriptData);
4299 }
4300 }
4301 let kinds: Vec<_> = tokenizer
4302 .take_errors()
4303 .into_iter()
4304 .map(|error| error.kind)
4305 .collect();
4306 assert!(kinds.contains(&ParseErrorKind::EofInScriptHtmlCommentLikeText));
4307 }
4308}