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