1use alloc::vec::Vec;
14
15use crate::error::{ErrorKind, ParseError};
16use crate::scan;
17use crate::token::{AttrValue, Token};
18
19pub struct Tokenizer<'a> {
22 src: &'a str,
23 bytes: &'a [u8],
24 pos: usize,
25 raw_text_end: Option<&'a str>,
28 attrs: Vec<(&'a str, AttrValue<'a>)>,
30 pub(crate) errors: Vec<ParseError>,
31}
32
33impl<'a> Tokenizer<'a> {
34 pub fn new(src: &'a str) -> Self {
36 Tokenizer {
37 src,
38 bytes: src.as_bytes(),
39 pos: 0,
40 raw_text_end: None,
41 attrs: Vec::new(),
42 errors: Vec::new(),
43 }
44 }
45
46 #[inline]
49 fn peek(&self) -> Option<u8> {
50 self.bytes.get(self.pos).copied()
51 }
52
53 #[inline]
54 fn peek_at(&self, off: usize) -> Option<u8> {
55 self.bytes.get(self.pos + off).copied()
56 }
57
58 #[inline]
59 fn slice(&self, start: usize, end: usize) -> &'a str {
60 self.src.get(start..end).unwrap_or("")
61 }
62
63 #[inline]
64 fn rest(&self) -> &'a [u8] {
65 self.bytes.get(self.pos..).unwrap_or(&[])
66 }
67
68 fn starts_with(&self, s: &[u8]) -> bool {
69 self.rest().starts_with(s)
70 }
71
72 fn starts_with_ci(&self, s: &[u8]) -> bool {
73 self.rest()
74 .get(..s.len())
75 .is_some_and(|p| p.eq_ignore_ascii_case(s))
76 }
77
78 #[inline]
80 fn skip_to(&mut self, needle: u8) {
81 match scan::find_byte(needle, self.rest()) {
82 Some(rel) => self.pos += rel,
83 None => self.pos = self.bytes.len(),
84 }
85 }
86
87 fn skip_whitespace(&mut self) {
88 while let Some(b) = self.peek() {
89 if b.is_ascii_whitespace() {
90 self.pos += 1;
91 } else {
92 break;
93 }
94 }
95 }
96
97 fn error(&mut self, pos: usize, kind: ErrorKind) {
98 self.errors.push(ParseError::new(pos, kind));
99 }
100
101 #[inline]
103 pub(crate) fn offset(&self) -> usize {
104 self.pos
105 }
106
107 #[inline]
112 pub(crate) fn attrs(&self) -> &[(&'a str, AttrValue<'a>)] {
113 &self.attrs
114 }
115
116 pub(crate) fn next_token(&mut self) -> Option<(Token<'a>, usize)> {
120 if self.pos >= self.bytes.len() {
121 return None;
122 }
123 let start = self.pos;
124 if let Some(end_name) = self.raw_text_end.take() {
125 return Some((self.read_raw_text(end_name), start));
126 }
127 let tok = match self.peek() {
128 Some(b'<') => self.read_markup(),
129 _ => self.read_text(),
130 };
131 Some((tok, start))
132 }
133
134 fn read_text(&mut self) -> Token<'a> {
135 let start = self.pos;
136 self.skip_to(b'<');
138 Token::Text(self.slice(start, self.pos))
139 }
140
141 fn read_markup(&mut self) -> Token<'a> {
142 match self.peek_at(1) {
143 Some(b'/') => self.read_end_tag(),
144 Some(b'!') => self.read_bang(),
145 Some(b'?') => self.read_bogus_comment(),
146 Some(c) if c.is_ascii_alphabetic() => self.read_start_tag(),
147 _ => {
148 let s = self.slice(self.pos, self.pos + 1);
150 self.pos += 1;
151 Token::Text(s)
152 }
153 }
154 }
155
156 fn read_tag_name(&mut self) -> &'a str {
157 let start = self.pos;
158 while let Some(b) = self.peek() {
159 if b.is_ascii_alphanumeric() || matches!(b, b'-' | b':' | b'_' | b'.') {
160 self.pos += 1;
161 } else {
162 break;
163 }
164 }
165 self.slice(start, self.pos)
166 }
167
168 fn read_start_tag(&mut self) -> Token<'a> {
169 let open = self.pos;
170 self.pos += 1; let name = self.read_tag_name();
172 self.attrs.clear();
173 let mut self_closing = false;
174
175 loop {
176 self.skip_whitespace();
177 match self.peek() {
178 None => {
179 self.error(open, ErrorKind::UnterminatedTag);
180 break;
181 }
182 Some(b'>') => {
183 self.pos += 1;
184 break;
185 }
186 Some(b'/') => {
187 if self.peek_at(1) == Some(b'>') {
188 self_closing = true;
189 self.pos += 2;
190 break;
191 }
192 self.pos += 1; }
194 Some(_) => match self.read_attribute() {
195 Some(attr) => self.attrs.push(attr),
196 None => self.pos += 1, },
198 }
199 }
200
201 if !self_closing && is_rawish_name(name) {
202 self.raw_text_end = Some(name);
203 }
204
205 Token::StartTag {
206 name,
207 attrs: Vec::new(),
208 self_closing,
209 }
210 }
211
212 fn read_attribute(&mut self) -> Option<(&'a str, AttrValue<'a>)> {
213 let nstart = self.pos;
214 while let Some(b) = self.peek() {
215 if b.is_ascii_whitespace() || matches!(b, b'=' | b'>' | b'/') {
216 break;
217 }
218 self.pos += 1;
219 }
220 let name = self.slice(nstart, self.pos);
221 if name.is_empty() {
222 return None;
223 }
224 self.skip_whitespace();
225 if self.peek() == Some(b'=') {
226 self.pos += 1;
227 self.skip_whitespace();
228 Some((name, self.read_attr_value()))
229 } else {
230 Some((name, AttrValue::Empty))
231 }
232 }
233
234 fn read_attr_value(&mut self) -> AttrValue<'a> {
235 match self.peek() {
236 Some(q @ (b'"' | b'\'')) => {
237 self.pos += 1; let start = self.pos;
239 self.skip_to(q);
240 let val = self.slice(start, self.pos);
241 if self.peek() == Some(q) {
242 self.pos += 1; }
244 AttrValue::Quoted(val)
245 }
246 _ => {
247 let start = self.pos;
248 while let Some(b) = self.peek() {
249 if b.is_ascii_whitespace() || b == b'>' {
250 break;
251 }
252 self.pos += 1;
253 }
254 AttrValue::Unquoted(self.slice(start, self.pos))
255 }
256 }
257 }
258
259 fn read_end_tag(&mut self) -> Token<'a> {
260 let open = self.pos;
261 self.pos += 2; let name = self.read_tag_name();
263 self.skip_to(b'>');
264 if self.peek() == Some(b'>') {
265 self.pos += 1;
266 } else {
267 self.error(open, ErrorKind::UnterminatedTag);
268 }
269 Token::EndTag { name }
270 }
271
272 fn read_bang(&mut self) -> Token<'a> {
273 if self.starts_with(b"<!--") {
274 self.read_comment()
275 } else if self.starts_with(b"<![CDATA[") {
276 self.read_cdata()
277 } else if self.starts_with_ci(b"<!doctype") {
278 self.read_doctype()
279 } else {
280 self.read_bogus_comment()
281 }
282 }
283
284 fn read_comment(&mut self) -> Token<'a> {
285 let open = self.pos;
286 self.pos += 4; let start = self.pos;
288 if let Some(rel) = scan::find(b"-->", self.rest()) {
289 self.pos += rel;
290 let s = self.slice(start, self.pos);
291 self.pos += 3;
292 return Token::Comment(s);
293 }
294 self.pos = self.bytes.len();
295 self.error(open, ErrorKind::UnterminatedComment);
296 Token::Comment(self.slice(start, self.pos))
297 }
298
299 fn read_cdata(&mut self) -> Token<'a> {
300 let open = self.pos;
301 self.pos += 9; let start = self.pos;
303 if let Some(rel) = scan::find(b"]]>", self.rest()) {
304 self.pos += rel;
305 let s = self.slice(start, self.pos);
306 self.pos += 3;
307 return Token::Cdata(s);
308 }
309 self.pos = self.bytes.len();
310 self.error(open, ErrorKind::UnterminatedCdata);
311 Token::Cdata(self.slice(start, self.pos))
312 }
313
314 fn read_doctype(&mut self) -> Token<'a> {
315 let open = self.pos;
316 self.pos += 9; self.skip_whitespace();
318 let start = self.pos;
319 self.skip_to(b'>');
320 let s = self.slice(start, self.pos);
321 if self.peek() == Some(b'>') {
322 self.pos += 1;
323 } else {
324 self.error(open, ErrorKind::UnterminatedTag);
325 }
326 Token::Doctype(s)
327 }
328
329 fn read_bogus_comment(&mut self) -> Token<'a> {
332 let open = self.pos;
333 self.pos += 1; if matches!(self.peek(), Some(b'!') | Some(b'?')) {
335 self.pos += 1;
336 }
337 let start = self.pos;
338 self.skip_to(b'>');
339 let s = self.slice(start, self.pos);
340 if self.peek() == Some(b'>') {
341 self.pos += 1;
342 }
343 self.error(open, ErrorKind::BogusComment);
344 Token::Comment(s)
345 }
346
347 fn read_raw_text(&mut self, end_name: &str) -> Token<'a> {
349 let start = self.pos;
350 loop {
351 match scan::find_byte(b'<', self.rest()) {
352 None => {
353 self.pos = self.bytes.len();
354 break;
355 }
356 Some(rel) => {
357 let lt = self.pos + rel;
358 if self.is_close_tag_at(lt, end_name) {
359 self.pos = lt;
360 break;
361 }
362 self.pos = lt + 1;
363 }
364 }
365 }
366 Token::Text(self.slice(start, self.pos))
367 }
368
369 fn is_close_tag_at(&self, lt: usize, name: &str) -> bool {
370 if self.bytes.get(lt) != Some(&b'<') || self.bytes.get(lt + 1) != Some(&b'/') {
371 return false;
372 }
373 let after = lt + 2;
374 let nm = name.as_bytes();
375 match self.bytes.get(after..after + nm.len()) {
376 Some(slice) if slice.eq_ignore_ascii_case(nm) => {
377 match self.bytes.get(after + nm.len()) {
378 None => true,
379 Some(b) => b.is_ascii_whitespace() || matches!(b, b'>' | b'/'),
380 }
381 }
382 _ => false,
383 }
384 }
385}
386
387impl<'a> Iterator for Tokenizer<'a> {
388 type Item = Token<'a>;
389
390 fn next(&mut self) -> Option<Token<'a>> {
391 let (mut tok, _) = self.next_token()?;
392 if let Token::StartTag { attrs, .. } = &mut tok {
393 *attrs = core::mem::take(&mut self.attrs);
395 }
396 Some(tok)
397 }
398}
399
400fn is_rawish_name(name: &str) -> bool {
403 ["script", "style", "title", "textarea"]
404 .iter()
405 .any(|t| name.eq_ignore_ascii_case(t))
406}