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