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