summaryrefslogtreecommitdiff
path: root/src/old_tokenizer.rs
blob: 9f9b79de3a32f016d0fc9cf757f2ed43f148a821 (plain)
enum TokenizerState {
	Data,
	CharacterReferenceInData,
	RcData,
	CharacterReferenceInRcData,
	RawText,
	ScriptData,
	PlainText,
	TagOpen,
	EndTagOpen,
	TagName,
	RcDataLessThan,
	RcDataEndTagOpen,
	RcDataEndTagName,
	RawTextLessThanSign,
	RawTextEndTagOpen,
	RawTextEndTagName,
	ScriptDataLessThanSign,
	ScriptDataEndTagOpen,
	ScriptDataEndTagName,
	ScriptDataEscapeStart,
	ScriptDataEscapeStartDash,
	ScriptDataEscaped,
	ScriptDataEscapedDash,
	ScriptDataEscapedDashDash,
	ScriptDataEscapedLessThanSign,
	ScriptDataEscapedEndTagOpen,
	ScriptDataEscapedEndTagName,
	ScriptDataDoubleEscaped,
	ScriptDataDoubleEscapedDash,
	ScriptDataDoubleEscapedDashDash,
	ScriptDataDoubleEscapedLessThanSign,
	ScriptDataDoubleEscapeEnd,
	BeforeAttributeName,
	AttributeName,
	AfterAttributeName,
	BeforeAttributeValue,
	AttributeValueDoubleQuoted,
	AttributeValueSingleQuoted,
	AttributeValueUnquoted,
	CharacterReferenceInAttributeValue,
	AfterAttributeValue,
	SelfClosingStartTag,
	BogusComment,
	MarkupDeclarationOpen,
	CommentStart,
	CommentStartDash,
	Comment,
	CommentEndDash,
	CommentEnd,
	CommendEndBang,
	DocType,
	BeforeDocTypeName,
	DocTypeName,
	AfterDocTypeName,
	AfterDocTypePublicKeyword,
	DocTypePublicIdentifierDoubleQuoted,
	DocTypePublicIdentifierSingleQuoted,
	AfterDocTypePublicIdentifier,
	BetweenDocTypePublicAndSystemIdentifiers,
	AfterDocTypeSystemKeyword,
	BeforeDocTypeSystemIdentifier,
	DocTypeSystemIdentifierDoubleQuoted,
	DocTypeSystemIdentifierSingleQuoted,
	AfterDocTypeSystemIdentifier,
	BogusDocType,
	CdataSection,
}

struct TagBuilder {
	name: String,
	self_closing: bool,
	end_tag: bool,
	attributes: Vec<(Option<String>, Option<String>)>,
}

impl TagBuilder {
	fn is_appropriate_end_tag(&self, last_start_tag: &Option<String>) -> bool {
		last_start_tag
			.as_ref()
			.is_some_and(|name| name == &self.name)
	}
}

impl From<TagBuilder> for Token {
	fn from(value: TagBuilder) -> Self {
		let attributes = value
			.attributes
			.into_iter()
			.map(|(key, value)| (key.map(Into::into), value.map(Into::into)))
			.collect();
		if value.end_tag {
			Token::EndTag {
				tag_name: value.name.into(),
				self_closing: value.self_closing,
				attributes,
			}
		} else {
			Token::StartTag {
				tag_name: value.name.into(),
				self_closing: value.self_closing,
				attributes,
			}
		}
	}
}

fn consume_character_reference(chars: &mut Peekable<Chars<'_>>) -> Option<char> {
	match chars.next() {
		Some('\t' | '\u{000a}' | '\u{000c}' | ' ' | '<' | '&') | None => None,
		Some('#') => match chars.peek() {
			Some('x' | 'X') => {
				chars.next();
				let mut digits = chars
					.take_while(|c| c.is_ascii_hexdigit())
					.collect::<Box<str>>();
				if digits.is_empty() {
					panic!("Parse error: no digits found after the hexadecimal specifier");
					return None;
				}
				if chars.next_if(|c| *c == ';').is_none() {
					panic!("Parse error: expected a semicolon after escaped digits");
				}

				let number = u32::from_str_radix(&digits, 16).expect("only hexadecimal digits");
				if let Some(c) = match number {
					0x00 => Some('\u{fffd}'),
					0x0d => Some('\r'),
					0x80 => Some('\u{20ac}'),
					0x81 => Some('\u{0081}'),
					0x82 => Some('\u{201a}'),
					0x83 => Some('\u{0192}'),
					0x84 => Some('\u{201e}'),
					0x85 => Some('\u{2026}'),
					0x86 => Some('\u{2020}'),
					0x87 => Some('\u{2021}'),
					0x88 => Some('\u{02c6}'),
					0x89 => Some('\u{2030}'),
					0x8a => Some('\u{0160}'),
					0x8b => Some('\u{2039}'),
					0x8c => Some('\u{0152}'),
					0x8d => Some('\u{008d}'),
					0x8e => Some('\u{017d}'),
					0x8f => Some('\u{008f}'),
					0x90 => Some('\u{0090}'),
					0x91 => Some('\u{2018}'),
					0x92 => Some('\u{2019}'),
					0x93 => Some('\u{201c}'),
					0x94 => Some('\u{201d}'),
					0x95 => Some('\u{2022}'),
					0x96 => Some('\u{2013}'),
					0x97 => Some('\u{2014}'),
					0x98 => Some('\u{02dc}'),
					0x99 => Some('\u{2122}'),
					0x9a => Some('\u{0161}'),
					0x9b => Some('\u{203a}'),
					0x9c => Some('\u{0152}'),
					0x9d => Some('\u{009d}'),
					0x9e => Some('\u{017e}'),
					0x9f => Some('\u{0178}'),
					_ => None,
				} {
					panic!("Parse error: invalid character");
					return Some(c);
				}

				if (0xd800 < number && number < 0xdfff) || number > 0x10ffff {
					panic!("Parse error: invalid character reference");
					return Some(char::REPLACEMENT_CHARACTER);
				}

				match char::from_u32(number) {
					Some(char) => Some(char),
					None => panic!("Parse error: invalid character reference"),
				}
			}
			_ => {
				let mut digits = chars
					.take_while(|c| c.is_ascii_digit())
					.collect::<Box<str>>();
				if digits.is_empty() {
					panic!("Parse error: no digits found after the ampersand");
					return None;
				}
				if chars.next_if(|c| *c == ';').is_none() {
					panic!("Parse error: expected a semicolon after escaped digits");
				}

				let number = u32::from_str_radix(&digits, 10).expect("only decimal digits");
				if let Some(c) = match number {
					0x00 => Some('\u{fffd}'),
					0x0d => Some('\r'),
					0x80 => Some('\u{20ac}'),
					0x81 => Some('\u{0081}'),
					0x82 => Some('\u{201a}'),
					0x83 => Some('\u{0192}'),
					0x84 => Some('\u{201e}'),
					0x85 => Some('\u{2026}'),
					0x86 => Some('\u{2020}'),
					0x87 => Some('\u{2021}'),
					0x88 => Some('\u{02c6}'),
					0x89 => Some('\u{2030}'),
					0x8a => Some('\u{0160}'),
					0x8b => Some('\u{2039}'),
					0x8c => Some('\u{0152}'),
					0x8d => Some('\u{008d}'),
					0x8e => Some('\u{017d}'),
					0x8f => Some('\u{008f}'),
					0x90 => Some('\u{0090}'),
					0x91 => Some('\u{2018}'),
					0x92 => Some('\u{2019}'),
					0x93 => Some('\u{201c}'),
					0x94 => Some('\u{201d}'),
					0x95 => Some('\u{2022}'),
					0x96 => Some('\u{2013}'),
					0x97 => Some('\u{2014}'),
					0x98 => Some('\u{02dc}'),
					0x99 => Some('\u{2122}'),
					0x9a => Some('\u{0161}'),
					0x9b => Some('\u{203a}'),
					0x9c => Some('\u{0152}'),
					0x9d => Some('\u{009d}'),
					0x9e => Some('\u{017e}'),
					0x9f => Some('\u{0178}'),
					_ => None,
				} {
					panic!("Parse error: invalid character");
					return Some(c);
				}

				if (0xd800 < number && number < 0xdfff) || number > 0x10ffff {
					panic!("Parse error: invalid character reference");
					return Some(char::REPLACEMENT_CHARACTER);
				}

				match char::from_u32(number) {
					Some(char) => Some(char),
					None => panic!("Parse error: invalid character reference"),
				}
			}
		},
		_ => todo!("Parse named character reference"),
	}
}

fn tokenize(source: &str) -> Box<[Token]> {
	let mut chars = source.chars().peekable();
	let mut tokens = Vec::new();
	let mut state = TokenizerState::Data;
	let mut tag_builder = TagBuilder {
		name: String::new(),
		self_closing: false,
		end_tag: false,
		attributes: Vec::new(),
	};
	let mut last_start_tag = None;
	let mut temp_buffer = String::new();
	loop {
		match state {
			TokenizerState::Data => match chars.next() {
				Some('&') => state = TokenizerState::CharacterReferenceInData,
				Some('<') => state = TokenizerState::TagOpen,
				Some('\0') => {
					panic!("Parse error: NUL character");
					tokens.push(Token::Character('\0'));
				}
				None => break,
				Some(c) => tokens.push(Token::Character(c)),
			},
			TokenizerState::CharacterReferenceInData => {
				tokens.push(Token::Character(
					consume_character_reference(&mut chars).unwrap_or('&'),
				));
				state = TokenizerState::Data;
			}
			TokenizerState::RcData => match chars.next() {
				Some('&') => state = TokenizerState::CharacterReferenceInRcData,
				Some('<') => state = TokenizerState::RcDataLessThan,
				Some('\0') => {
					panic!("Parse error: NUL character");
					tokens.push(Token::Character(char::REPLACEMENT_CHARACTER));
				}
				None => break,
				Some(c) => tokens.push(Token::Character(c)),
			},
			TokenizerState::CharacterReferenceInRcData => {
				tokens.push(Token::Character(
					consume_character_reference(&mut chars).unwrap_or('&'),
				));
				state = TokenizerState::RcData;
			}
			TokenizerState::RawText => match chars.next() {
				Some('<') => state = TokenizerState::RawTextLessThanSign,
				Some('\0') => {
					panic!("Parse error: NUL character");
					tokens.push(Token::Character(char::REPLACEMENT_CHARACTER));
				}
				None => break,
				Some(c) => tokens.push(Token::Character(c)),
			},
			TokenizerState::ScriptData => match chars.next() {
				Some('<') => state = TokenizerState::ScriptDataLessThanSign,
				Some('\0') => {
					panic!("Parse error: NUL character");
					tokens.push(Token::Character(char::REPLACEMENT_CHARACTER));
				}
				None => break,
				Some(c) => tokens.push(Token::Character(c)),
			},
			TokenizerState::PlainText => match chars.next() {
				Some('\0') => {
					panic!("Parse error: NUL character");
					tokens.push(Token::Character(char::REPLACEMENT_CHARACTER));
				}
				None => break,
				Some(c) => tokens.push(Token::Character(c)),
			},
			TokenizerState::TagOpen => match chars.next() {
				Some('!') => state = TokenizerState::MarkupDeclarationOpen,
				Some('/') => state = TokenizerState::EndTagOpen,
				Some(c) if c.is_ascii_uppercase() => {
					state = TokenizerState::TagName;
					tag_builder = TagBuilder {
						name: String::from(c.to_ascii_lowercase()),
						end_tag: false,
						self_closing: false,
						attributes: Vec::new(),
					};
				}
				Some(c) if c.is_ascii_lowercase() => {
					state = TokenizerState::TagName;
					tag_builder = TagBuilder {
						name: c.to_string(),
						end_tag: false,
						self_closing: false,
						attributes: Vec::new(),
					}
				}
				Some('?') => {
					panic!("Parse error: bogus comment");
					state = TokenizerState::BogusComment;
				}
				None => {
					panic!("Parse error: unfinished tag");
					tokens.push(Token::Character('<'));
					state = TokenizerState::Data;
				}
				_ => {
					chars.next_back();
					panic!("Parse error: less than sign without a tag");
					tokens.push(Token::Character('<'));
					state = TokenizerState::Data;
				}
			},
			TokenizerState::EndTagOpen => match chars.next() {
				Some(c) if c.is_ascii_uppercase() => {
					state = TokenizerState::TagName;
					tag_builder = TagBuilder {
						name: String::from(c.to_ascii_lowercase()),
						end_tag: true,
						self_closing: false,
						attributes: Vec::new(),
					}
				}
				Some(c) if c.is_ascii_lowercase() => {
					state = TokenizerState::TagName;
					tag_builder = TagBuilder {
						name: c.to_string(),
						end_tag: true,
						self_closing: false,
						attributes: Vec::new(),
					}
				}
				Some('>') => {
					panic!("Parse error: end tag with no name");
					state = TokenizerState::Data;
				}
				None => {
					panic!("Parse error: unfinished end tag");
					tokens.push(Token::Character('<'));
					tokens.push(Token::Character('/'));
					state = TokenizerState::Data;
				}
				_ => {
					chars.next_back();
					panic!("Parse error: less than sign without a tag");
					tokens.push(Token::Character('<'));
					state = TokenizerState::Data;
				}
			},
			TokenizerState::TagName => match chars.next() {
				Some('\t' | '\u{000a}' | '\u{000c}' | ' ') => {
					state = TokenizerState::BeforeAttributeName
				}
				Some('/') => state = TokenizerState::SelfClosingStartTag,
				Some('>') => {
					state = TokenizerState::Data;
					if !tag_builder.end_tag {
						last_start_tag = Some(tag_builder.name.clone());
					}
					tokens.push(tag_builder.into());
				}
				Some(c) if c.is_ascii_uppercase() => tag_builder.name.push(c.to_ascii_lowercase()),
				Some('\0') => {
					panic!("Parse error: NUL character in tag name");
					tag_builder.name.push(char::REPLACEMENT_CHARACTER);
				}
				None => {
					panic!("Parse error: unfinished tag");
					state = TokenizerState::Data;
				}
				Some(c) => tag_builder.name.push(c),
			},
			TokenizerState::RcDataLessThan => match chars.next() {
				Some('/') => {
					temp_buffer.clear();
					state = TokenizerState::RcDataEndTagOpen;
				}
				_ => {
					tokens.push(Token::Character('<'));
					chars.next_back();
					state = TokenizerState::RcData;
				}
			},
			TokenizerState::RcDataEndTagOpen => match chars.next() {
				Some(c) if c.is_ascii_uppercase() => {
					tag_builder = TagBuilder {
						end_tag: true,
						name: c.to_ascii_lowercase().to_string(),
						self_closing: false,
						attributes: Vec::new(),
					};
					temp_buffer = c.to_string();
					state = TokenizerState::RcDataEndTagName;
				}
				Some(c) if c.is_ascii_lowercase() => {
					tag_builder = TagBuilder {
						end_tag: true,
						name: c.to_string(),
						self_closing: false,
						attributes: Vec::new(),
					};
					temp_buffer = c.to_string();
					state = TokenizerState::RcDataEndTagName;
				}
				Some(c) => {
					tokens.push(Token::Character('<'));
					tokens.push(Token::Character('/'));
					chars.next_back();
					state = TokenizerState::RcData;
				}
				None => {
					tokens.push(Token::Character('<'));
					tokens.push(Token::Character('/'));
					state = TokenizerState::RcData;
				}
			},
			TokenizerState::RcDataEndTagName => match chars.next() {
				Some('\t' | '\u{000a}' | '\u{000c}' | ' ')
					if tag_builder.is_appropriate_end_tag(&last_start_tag) =>
				{
					state = TokenizerState::BeforeAttributeName
				}

				Some('/') if tag_builder.is_appropriate_end_tag(&last_start_tag) => {
					state = TokenizerState::SelfClosingStartTag
				}
				Some('>') if tag_builder.is_appropriate_end_tag(&last_start_tag) => {
					tokens.push(tag_builder.into());
					state = TokenizerState::Data;
				}
				Some(c) if c.is_ascii_uppercase() => {
					tag_builder.name.push(c.to_ascii_lowercase());
					temp_buffer.push(c);
				}
				Some(c) if c.is_ascii_lowercase() => {
					tag_builder.name.push(c);
					temp_buffer.push(c);
				}
				None => {
					tokens.push(Token::Character('<'));
					tokens.push(Token::Character('/'));
					for char in temp_buffer.chars() {
						tokens.push(Token::Character(char));
					}
					state = TokenizerState::RcData;
				}
				_ => {
					tokens.push(Token::Character('<'));
					tokens.push(Token::Character('/'));
					for char in temp_buffer.chars() {
						tokens.push(Token::Character(char));
					}
					chars.next_back();
					state = TokenizerState::RcData;
				}
			},
			TokenizerState::RawTextLessThanSign => match chars.next() {
				Some('/') => {
					temp_buffer = String::new();
					state = TokenizerState::RawTextEndTagOpen;
				}
				None => {
					tokens.push(Token::Character('<'));
					state = TokenizerState::RawText;
				}
				_ => {
					tokens.push(Token::Character('<'));
					chars.next_back();
					state = TokenizerState::RawText;
				}
			},
			TokenizerState::RawTextEndTagOpen => match chars.next() {
				Some(c) if c.is_ascii_uppercase() => {
					tag_builder = TagBuilder {
						name: c.to_ascii_lowercase().to_string(),
						self_closing: false,
						end_tag: true,
						attributes: Vec::new(),
					};
					temp_buffer.push(c);
					state = TokenizerState::RawTextEndTagName;
				}
				Some(c) if c.is_ascii_lowercase() => {
					tag_builder = TagBuilder {
						name: c.to_string(),
						self_closing: false,
						end_tag: true,
						attributes: Vec::new(),
					};
					temp_buffer.push(c);
					state = TokenizerState::RawTextEndTagName;
				}
				None => {
					tokens.push(Token::Character('<'));
					tokens.push(Token::Character('/'));
					state = TokenizerState::RawText;
				}
				Some(_) => {
					tokens.push(Token::Character('<'));
					tokens.push(Token::Character('/'));
					state = TokenizerState::RawText;
					chars.next_back();
				}
			},
			TokenizerState::RawTextEndTagName => match chars.next() {
				Some('\t' | '\u{000a}' | '\u{000c}' | ' ')
					if tag_builder.is_appropriate_end_tag(&last_start_tag) =>
				{
					state = TokenizerState::BeforeAttributeName
				}

				Some('/') if tag_builder.is_appropriate_end_tag(&last_start_tag) => {
					state = TokenizerState::SelfClosingStartTag
				}
				Some('>') if tag_builder.is_appropriate_end_tag(&last_start_tag) => {
					tokens.push(tag_builder.into());
					state = TokenizerState::Data;
				}
				Some(c) if c.is_ascii_uppercase() => {
					tag_builder.name.push(c.to_ascii_lowercase());
					temp_buffer.push(c);
				}
				Some(c) if c.is_ascii_lowercase() => {
					tag_builder.name.push(c);
					temp_buffer.push(c);
				}
				None => {
					tokens.push(Token::Character('<'));
					tokens.push(Token::Character('/'));
					for char in temp_buffer.chars() {
						tokens.push(Token::Character(char));
					}
					state = TokenizerState::RawText;
				}
				_ => {
					tokens.push(Token::Character('<'));
					tokens.push(Token::Character('/'));
					for char in temp_buffer.chars() {
						tokens.push(Token::Character(char));
					}
					chars.next_back();
					state = TokenizerState::RawText;
				}
			},
			TokenizerState::ScriptDataLessThanSign => match chars.next() {
				Some('/') => {
					temp_buffer = String::new();
					state = TokenizerState::ScriptDataEndTagOpen;
				}
				Some('!') => {
					state = TokenizerState::ScriptDataEscapeStart;
					tokens.push(Token::Character('<'));
					tokens.push(Token::Character('!'));
				}
				None => {
					tokens.push(Token::Character('<'));
					state = TokenizerState::ScriptData;
				}
				_ => {
					tokens.push(Token::Character('<'));
					state = TokenizerState::ScriptData;
					chars.next_back();
				}
			},
			TokenizerState::ScriptDataEndTagOpen => match chars.next() {
				Some(c) if c.is_ascii_uppercase() => {
					tag_builder = TagBuilder {
						name: c.to_ascii_lowercase().to_string(),
						self_closing: false,
						end_tag: true,
						attributes: Vec::new(),
					};
					temp_buffer.push(c);
					state = TokenizerState::ScriptDataEndTagName;
				}
				Some(c) if c.is_ascii_lowercase() => {
					tag_builder = TagBuilder {
						name: c.to_string(),
						self_closing: false,
						end_tag: true,
						attributes: Vec::new(),
					};
					temp_buffer.push(c);
					state = TokenizerState::ScriptDataEndTagName;
				}
				None => {
					tokens.push(Token::Character('<'));
					tokens.push(Token::Character('/'));
					state = TokenizerState::ScriptData;
				}
				_ => {
					tokens.push(Token::Character('<'));
					tokens.push(Token::Character('/'));
					chars.next_back();
					state = TokenizerState::ScriptData;
				}
			},
			TokenizerState::ScriptDataEndTagName => match chars.next() {
				Some('\t' | '\u{000a}' | '\u{000c}' | ' ')
					if tag_builder.is_appropriate_end_tag(&last_start_tag) =>
				{
					state = TokenizerState::BeforeAttributeName
				}

				Some('/') if tag_builder.is_appropriate_end_tag(&last_start_tag) => {
					state = TokenizerState::SelfClosingStartTag
				}
				Some('>') if tag_builder.is_appropriate_end_tag(&last_start_tag) => {
					tokens.push(tag_builder.into());
					state = TokenizerState::Data;
				}
				Some(c) if c.is_ascii_uppercase() => {
					tag_builder.name.push(c.to_ascii_lowercase());
					temp_buffer.push(c);
				}
				Some(c) if c.is_ascii_lowercase() => {
					tag_builder.name.push(c);
					temp_buffer.push(c);
				}
				None => {
					tokens.push(Token::Character('<'));
					tokens.push(Token::Character('/'));
					for char in temp_buffer.chars() {
						tokens.push(Token::Character(char));
					}
					state = TokenizerState::ScriptData;
				}
				_ => {
					tokens.push(Token::Character('<'));
					tokens.push(Token::Character('/'));
					for char in temp_buffer.chars() {
						tokens.push(Token::Character(char));
					}
					chars.next_back();
					state = TokenizerState::ScriptData;
				}
			},
			TokenizerState::ScriptDataEscapeStart => match chars.next() {
				Some('-') => {
					state = TokenizerState::ScriptDataEscapeStartDash;
					tokens.push(Token::Character('-'));
				}
				None => state = TokenizerState::ScriptData,
				_ => {
					chars.next_back();
					state = TokenizerState::ScriptData;
				}
			},
			TokenizerState::ScriptDataEscapeStartDash => match chars.next() {
				Some('-') => {
					state = TokenizerState::ScriptDataEscapedDashDash;
					tokens.push(Token::Character('-'));
				}
				None => state = TokenizerState::ScriptData,
				_ => {
					chars.next_back();
					state = TokenizerState::ScriptData;
				}
			},
			TokenizerState::ScriptDataEscaped => match chars.next() {
				Some('-') => {
					state = TokenizerState::ScriptDataEscapedDash;
					tokens.push(Token::Character('-'));
				}
				Some('<') => state = TokenizerState::ScriptDataEscapedLessThanSign,
				Some('\0') => {
					panic!("Parse error: unexpected NUL character");
					tokens.push(Token::Character(char::REPLACEMENT_CHARACTER));
				}
				None => {
					panic!("Parse error: unexpected end of file")
				}
				Some(c) => tokens.push(Token::Character(c)),
			},
		}
	}

	tokens.into_boxed_slice()
}