servo_webvtt/cue/text.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::{Ref, RefCell};
6use std::fmt;
7use std::iter::Peekable;
8use std::rc::{Rc, Weak};
9use std::str::Chars;
10
11use crate::collectors::collect_webvtt_timestamp;
12
13/// <https://w3c.github.io/webvtt/#webvtt-internal-node-object>
14#[derive(Debug, Default, PartialEq)]
15pub enum WebVTTNodeObjectKind {
16 /// <https://w3c.github.io/webvtt/#list-of-webvtt-node-objects>
17 #[default]
18 List,
19 /// <https://w3c.github.io/webvtt/#webvtt-class-object>
20 Class,
21 /// <https://w3c.github.io/webvtt/#webvtt-italic-object>
22 Italic,
23 /// <https://w3c.github.io/webvtt/#webvtt-bold-object>
24 Bold,
25 /// <https://w3c.github.io/webvtt/#webvtt-underline-object>
26 Underline,
27 /// <https://w3c.github.io/webvtt/#webvtt-ruby-object>
28 Ruby,
29 /// <https://w3c.github.io/webvtt/#webvtt-ruby-text-object>
30 RubyText,
31 /// <https://w3c.github.io/webvtt/#webvtt-voice-object>
32 /// The string is the name of the voice
33 Voice(String),
34 /// <https://w3c.github.io/webvtt/#webvtt-language-object>
35 Language,
36 /// <https://w3c.github.io/webvtt/#webvtt-text-object>
37 Text(String),
38 /// <https://w3c.github.io/webvtt/#webvtt-timestamp-object>
39 /// The float is the timestamp value
40 Timestamp(WebVTTTimestamp),
41}
42
43/// <https://w3c.github.io/webvtt/#webvtt-timestamp>
44#[derive(Debug, Default, PartialEq)]
45pub struct WebVTTTimestamp(f64);
46
47impl fmt::Display for WebVTTTimestamp {
48 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
49 // Step 1. Optionally (required if hours is non-zero):
50 let time = if self.0 > 60. * 60. {
51 let hours = self.0.div_euclid(60. * 60.);
52 // Step 1.1. Two or more ASCII digits,
53 // representing the hours as a base ten integer.
54 if hours < 10. {
55 write!(f, "0")?;
56 }
57 // Step 1.2. A U+003A COLON character (:)
58 write!(f, "{hours}:")?;
59 self.0.rem_euclid(60. * 60.)
60 } else {
61 // In https://w3c.github.io/webvtt/#dom-construction-rules it
62 // states that it should also include all optional parts. Currently
63 // this is the only place where we serialize a timestamp, hence
64 // for now we always write the hours
65 write!(f, "00:")?;
66 self.0
67 };
68 // Step 2. Two ASCII digits, representing the minutes as
69 // a base ten integer in the range 0 ≤ minutes ≤ 59.
70 let minutes = time.div_euclid(60.);
71 debug_assert!(minutes < 60.);
72 if minutes < 10. {
73 write!(f, "0")?;
74 }
75 // Step 3. A U+003A COLON character (:)
76 write!(f, "{minutes}:")?;
77 // Step 4. Two ASCII digits, representing the seconds as
78 // a base ten integer in the range 0 ≤ seconds ≤ 59.
79 // Step 5. A U+002E FULL STOP character (.).
80 // Step 6. Three ASCII digits, representing the thousandths
81 // of a second seconds-frac as a base ten integer.
82 let seconds = time.rem_euclid(60.);
83 if seconds < 10. {
84 write!(f, "0")?;
85 }
86 write!(f, "{seconds:.3}")?;
87 Ok(())
88 }
89}
90
91/// <https://w3c.github.io/webvtt/#webvtt-node-object>
92#[derive(Debug, Default)]
93pub struct WebVTTNodeObject {
94 /// <https://w3c.github.io/webvtt/#webvtt-node-objects-applicable-classes>
95 pub applicable_classes: Vec<String>,
96 /// <https://w3c.github.io/webvtt/#webvtt-node-objects-applicable-language>
97 pub applicable_language: String,
98 /// <https://w3c.github.io/webvtt/#webvtt-internal-node-object>
99 pub kind: WebVTTNodeObjectKind,
100 children: RefCell<Vec<Rc<WebVTTNodeObject>>>,
101 parent: Weak<WebVTTNodeObject>,
102 index: usize,
103}
104
105impl WebVTTNodeObject {
106 pub fn children(&self) -> Ref<'_, Vec<Rc<WebVTTNodeObject>>> {
107 self.children.borrow()
108 }
109}
110
111impl PartialEq for WebVTTNodeObject {
112 fn eq(&self, other: &Self) -> bool {
113 // We compare all fields except parent
114 self.children.eq(&other.children) &&
115 self.applicable_classes.eq(&other.applicable_classes) &&
116 self.applicable_language.eq(&other.applicable_language) &&
117 self.kind.eq(&other.kind)
118 }
119}
120
121pub trait WebVTTNodeObjectIterable {
122 fn into_iter(self) -> WebVTTNodeObjectIterator;
123}
124
125impl WebVTTNodeObjectIterable for Rc<WebVTTNodeObject> {
126 fn into_iter(self) -> WebVTTNodeObjectIterator {
127 WebVTTNodeObjectIterator {
128 root: self.clone(),
129 current: Some(self),
130 current_child: 0,
131 }
132 }
133}
134
135#[derive(Debug, PartialEq)]
136pub enum WebVTTNodeObjectIteratorDirection {
137 NewChild(Rc<WebVTTNodeObject>),
138 BackToParent,
139}
140
141pub struct WebVTTNodeObjectIterator {
142 // To make sure that the weak pointers are kept alive
143 // while iterating through all objects
144 root: Rc<WebVTTNodeObject>,
145 current: Option<Rc<WebVTTNodeObject>>,
146 current_child: usize,
147}
148
149impl Iterator for WebVTTNodeObjectIterator {
150 type Item = WebVTTNodeObjectIteratorDirection;
151
152 fn next(&mut self) -> Option<WebVTTNodeObjectIteratorDirection> {
153 let current = self.current.clone()?;
154 if let Some(current_child) = current.children.borrow().get(self.current_child) {
155 if matches!(current_child.kind, WebVTTNodeObjectKind::Text(_)) {
156 self.current_child += 1;
157 } else {
158 self.current_child = 0;
159 self.current = Some(current_child.clone());
160 }
161 return Some(WebVTTNodeObjectIteratorDirection::NewChild(
162 current_child.clone(),
163 ));
164 }
165 self.current_child = current.index + 1;
166 self.current = current.parent.upgrade();
167 if self.current.as_ref().is_some_and(|current| {
168 current != &self.root || current.children.borrow().len() > self.current_child
169 }) {
170 return Some(WebVTTNodeObjectIteratorDirection::BackToParent);
171 }
172 None
173 }
174}
175
176/// <https://w3c.github.io/webvtt/#webvtt-cue-text-parsing-rules>
177pub fn webvtt_cue_text_parsing_rules(
178 input: &str,
179 language: Option<String>,
180) -> Rc<WebVTTNodeObject> {
181 // Step 1. Let input be the string being parsed.
182 // Step 2. Let position be a pointer into input,
183 // initially pointing at the start of the string.
184 let mut chars = input.chars().peekable();
185 // Step 3. Let result be a list of WebVTT Node Objects, initially empty.
186 let mut result = WebVTTNodeObject::default();
187 // Step 5. Let language stack be a stack of language tags, initially empty.
188 let mut language_stack = vec![];
189 // Step 6. If language is set, set result’s applicable language to language,
190 // and push language onto the language stack.
191 if let Some(language) = language {
192 result.applicable_language = language.clone();
193 language_stack.push(language);
194 }
195 // Step 4. Let current be the WebVTT Internal Node Object result.
196 let result = Rc::new(result);
197 let mut current = result.clone();
198 // Step 7. Loop: If position is past the end of input, return result and abort these steps.
199 // Step 10. Jump to the step labeled loop.
200 while chars.peek().is_some() {
201 // Step 8. Let token be the result of invoking the WebVTT cue text tokenizer.
202 let token = webvtt_cue_text_tokenizer(&mut chars);
203 // Step 9. Run the appropriate steps given the type of token:
204 match token {
205 // > If token is a string
206 CueTokenizerResult::String(str_) => {
207 // Step 1. Create a WebVTT Text Object whose value is the value of the string token token.
208 // Step 2. Append the newly created WebVTT Text Object to current.
209 let mut children = current.children.borrow_mut();
210
211 let text = Rc::new(WebVTTNodeObject {
212 kind: WebVTTNodeObjectKind::Text(str_),
213 index: children.len(),
214 parent: Rc::downgrade(¤t),
215 ..Default::default()
216 });
217 children.push(text);
218 },
219 // > If token is a start tag
220 CueTokenizerResult::StartTag(tag_name, applicable_classes, annotation) => {
221 // > How the start tag token token is processed depends on its tag name, as follows:
222 let kind = match tag_name.as_ref() {
223 // > If the tag name is "c"
224 "c" => {
225 // > Attach a WebVTT Class Object.
226 WebVTTNodeObjectKind::Class
227 },
228 // > If the tag name is "i"
229 "i" => {
230 // > Attach a WebVTT Italic Object.
231 WebVTTNodeObjectKind::Italic
232 },
233 // > If the tag name is "b"
234 "b" => {
235 // > Attach a WebVTT Bold Object.
236 WebVTTNodeObjectKind::Bold
237 },
238 // > If the tag name is "u"
239 "u" => {
240 // > Attach a WebVTT Underline Object.
241 WebVTTNodeObjectKind::Underline
242 },
243 // > If the tag name is "ruby"
244 "ruby" => {
245 // > Attach a WebVTT Ruby Object.
246 WebVTTNodeObjectKind::Ruby
247 },
248 // > If the tag name is "rt"
249 "rt" => {
250 // > If current is a WebVTT Ruby Object,
251 // > then attach a WebVTT Ruby Text Object.
252 if current.kind != WebVTTNodeObjectKind::Ruby {
253 continue;
254 }
255 WebVTTNodeObjectKind::RubyText
256 },
257 // > If the tag name is "v"
258 "v" => {
259 // > Attach a WebVTT Voice Object,
260 // > and set its value to the token’s annotation string,
261 // > or the empty string if there is no annotation string.
262 WebVTTNodeObjectKind::Voice(annotation)
263 },
264 // > If the tag name is "lang"
265 "lang" => {
266 // > Push the value of the token’s annotation string,
267 // > or the empty string if there is no annotation string,
268 // > onto the language stack; then attach a WebVTT Language Object.
269 language_stack.push(annotation);
270 WebVTTNodeObjectKind::Language
271 },
272 // > Otherwise
273 _ => {
274 // > Ignore the token.
275 continue;
276 },
277 };
278 // https://w3c.github.io/webvtt/#attach-a-webvtt-internal-node-object
279 // Step 1. Create a new WebVTT Internal Node Object of the specified concrete class.
280 current = {
281 let mut children = current.children.borrow_mut();
282 let new = WebVTTNodeObject {
283 kind,
284 // Step 2. Set the new object’s list of applicable classes
285 // to the list of classes in the token,
286 // excluding any classes that are the empty string.
287 applicable_classes,
288 // Step 3. Set the new object’s applicable language to the top entry on the language stack,
289 // if the stack is not empty.
290 applicable_language: language_stack
291 .last()
292 .map(|str_| str_.to_string())
293 .unwrap_or_default(),
294 index: children.len(),
295 parent: Rc::downgrade(¤t),
296 ..Default::default()
297 };
298 // Step 4. Append the newly created node object to current.
299 let new = Rc::new(new);
300 children.push(new.clone());
301 // Step 5. Let current be the newly created node object.
302 new
303 };
304 },
305 // > If token is an end tag
306 CueTokenizerResult::EndTag(tag_name) => {
307 // > If any of the following conditions is true,
308 // > then let current be the parent node of current.
309 let matches = match tag_name.as_ref() {
310 // > The tag name of the end tag token token is "c" and current is a WebVTT Class Object.
311 "c" => current.kind == WebVTTNodeObjectKind::Class,
312 // > The tag name of the end tag token token is "i" and current is a WebVTT Italic Object.
313 "i" => current.kind == WebVTTNodeObjectKind::Italic,
314 // > The tag name of the end tag token token is "b" and current is a WebVTT Bold Object.
315 "b" => current.kind == WebVTTNodeObjectKind::Bold,
316 // > The tag name of the end tag token token is "u" and current is a WebVTT Underline Object.
317 "u" => current.kind == WebVTTNodeObjectKind::Underline,
318 // > The tag name of the end tag token token is "ruby" and current is a WebVTT Ruby Object.
319 "ruby" => {
320 // > Otherwise, if the tag name of the end tag token token is "ruby"
321 // > and current is a WebVTT Ruby Text Object,
322 // > then let current be the parent node of the parent node of current.
323 if current.kind == WebVTTNodeObjectKind::RubyText {
324 current = current.parent.upgrade().expect("Must always have a parent");
325 true
326 } else {
327 current.kind == WebVTTNodeObjectKind::Ruby
328 }
329 },
330 // > The tag name of the end tag token token is "rt" and current is a WebVTT Ruby Text Object.
331 "rt" => current.kind == WebVTTNodeObjectKind::RubyText,
332 // > The tag name of the end tag token token is "v" and current is a WebVTT Voice Object.
333 "v" => matches!(current.kind, WebVTTNodeObjectKind::Voice(_)),
334 // > Otherwise, if the tag name of the end tag token token is "lang"
335 // > and current is a WebVTT Language Object,
336 // > then let current be the parent node of current,
337 // > and pop the top value from the language stack.
338 "lang" => {
339 let matches = current.kind == WebVTTNodeObjectKind::Language;
340 if matches {
341 language_stack.pop();
342 }
343 matches
344 },
345 // > Otherwise, ignore the token.
346 _ => false,
347 };
348 if matches && let Some(parent) = current.parent.upgrade() {
349 current = parent;
350 }
351 },
352 // > If token is a timestamp tag
353 CueTokenizerResult::TimestampTag(input) => {
354 // Step 1. Let input be the tag value.
355 //
356 // That's part of the match
357
358 // Step 2. Let position be a pointer into input, initially pointing at the start of the string.
359 let mut position = input.chars().peekable();
360 // Step 3. Collect a WebVTT timestamp.
361 // Step 4. If that algorithm does not fail, and if position now points at the end of input
362 // (i.e. there are no trailing characters after the timestamp),
363 // then create a WebVTT Timestamp Object whose value is the collected time,
364 // then append it to current.
365 // Otherwise, ignore the token.
366 if let Some(timestamp) = collect_webvtt_timestamp(position.by_ref()) &&
367 position.peek().is_none()
368 {
369 let mut children = current.children.borrow_mut();
370 let new = WebVTTNodeObject {
371 kind: WebVTTNodeObjectKind::Timestamp(WebVTTTimestamp(timestamp)),
372 parent: Rc::downgrade(¤t),
373 index: children.len(),
374 ..Default::default()
375 };
376 children.push(Rc::new(new));
377 }
378 },
379 }
380 }
381 result
382}
383
384/// <https://w3c.github.io/webvtt/#webvtt-cue-text-tokenizer>
385#[derive(PartialEq)]
386enum TokenizerState {
387 WebVTTData,
388 HTMLCharacterReferenceInData,
389 WebVTTTag,
390 WebVTTStartTag,
391 WebVTTStartTagClass,
392 WebVTTStartTagAnnotation,
393 HTMLCharacterReferenceInAnnotation,
394 WebVTTEndTag,
395 WebVTTTimestampTag,
396}
397
398/// <https://w3c.github.io/webvtt/#webvtt-cue-text-tokenizer>
399enum CueTokenizerResult {
400 String(String),
401 StartTag(String, Vec<String>, String),
402 EndTag(String),
403 TimestampTag(String),
404}
405
406/// <https://w3c.github.io/webvtt/#webvtt-cue-text-tokenizer>
407fn webvtt_cue_text_tokenizer(position: &mut Peekable<Chars<'_>>) -> CueTokenizerResult {
408 // Step 1. Let input and position be the same variables as
409 // those of the same name in the algorithm that invoked these steps.
410 //
411 // Passed in as arguments
412
413 // Step 2. Let tokenizer state be WebVTT data state.
414 let mut tokenizer_state = TokenizerState::WebVTTData;
415 // Step 3. Let result be the empty string.
416 let mut result = String::new();
417 let mut buffer = String::new();
418 // Step 4. Let classes be an empty list.
419 let mut classes = vec![];
420 loop {
421 // Step 5. Loop: If position is past the end of input,
422 // let c be an end-of-file marker. Otherwise, let c be the character in input pointed to by position.
423 //
424 // We codify the end-of-file marker as `None`
425 let c = position.peek().copied();
426 // Step 6. Jump to the state given by tokenizer state:
427 match tokenizer_state {
428 // https://w3c.github.io/webvtt/#webvtt-data-state
429 TokenizerState::WebVTTData => {
430 // > Jump to the entry that matches the value of c:
431 match c {
432 // > U+0026 AMPERSAND (&)
433 Some('\u{0026}') => {
434 // > Set tokenizer state to the HTML character reference in data state,
435 // > and jump to the step labeled next.
436 tokenizer_state = TokenizerState::HTMLCharacterReferenceInData;
437 },
438 // > U+003C LESS-THAN SIGN (<)
439 Some('\u{003C}') => {
440 // > If result is the empty string, then set tokenizer state
441 // > to the WebVTT tag state and jump to the step labeled next.
442 if result.is_empty() {
443 tokenizer_state = TokenizerState::WebVTTTag;
444 } else {
445 // > Otherwise, return a string token whose value is result and abort these steps.
446 return CueTokenizerResult::String(result);
447 }
448 },
449 // > End-of-file marker
450 None => {
451 // > Return a string token whose value is result and abort these steps.
452 return CueTokenizerResult::String(result);
453 },
454 // > Anything else
455 Some(c) => {
456 // > Append c to result and jump to the step labeled next.
457 result.push(c);
458 },
459 }
460 },
461 // https://w3c.github.io/webvtt/#html-character-reference-in-data-state
462 TokenizerState::HTMLCharacterReferenceInData => {
463 // > Attempt to consume an HTML character reference, with no additional allowed character.
464 // > If nothing is returned, append a U+0026 AMPERSAND character (&) to result.
465 // > Otherwise, append the data of the character tokens that were returned to result.
466 // TODO: The character reference case
467 result.push('\u{0026}');
468 // > Then, in any case, set tokenizer state to the WebVTT data state,
469 // > and jump to the step labeled next.
470 tokenizer_state = TokenizerState::WebVTTData;
471 },
472 // https://w3c.github.io/webvtt/#webvtt-tag-state
473 TokenizerState::WebVTTTag => {
474 // > Jump to the entry that matches the value of c:
475 match c {
476 // > U+0009 CHARACTER TABULATION (tab) character
477 // > U+000A LINE FEED (LF) character
478 // > U+000C FORM FEED (FF) character
479 // > U+0020 SPACE character
480 Some('\u{0009}' | '\u{000A}' | '\u{000C}' | '\u{0020}') => {
481 // > Set tokenizer state to the WebVTT start tag annotation state,
482 // > and jump to the step labeled next.
483 tokenizer_state = TokenizerState::WebVTTStartTagAnnotation;
484 },
485 // > U+002E FULL STOP character (.)
486 Some('\u{002E}') => {
487 // > Set tokenizer state to the WebVTT start tag class state,
488 // > and jump to the step labeled next.
489 tokenizer_state = TokenizerState::WebVTTStartTagClass;
490 },
491 // > U+002F SOLIDUS character (/)
492 Some('\u{002F}') => {
493 // > Set tokenizer state to the WebVTT end tag state,
494 // > and jump to the step labeled next.
495 tokenizer_state = TokenizerState::WebVTTEndTag;
496 },
497 // > ASCII digits
498 Some(c) if c.is_ascii_digit() => {
499 // > Set result to c, set tokenizer state to the WebVTT timestamp tag state,
500 // > and jump to the step labeled next.
501 result = String::new();
502 result.push(c);
503 tokenizer_state = TokenizerState::WebVTTTimestampTag;
504 },
505 // > U+003E GREATER-THAN SIGN character (>)
506 Some('\u{003E}') => {
507 // > Advance position to the next character in input,
508 // > then jump to the next "end-of-file marker" entry below.
509 position.next();
510 return CueTokenizerResult::StartTag(
511 Default::default(),
512 Default::default(),
513 Default::default(),
514 );
515 },
516 // > End-of-file marker
517 None => {
518 // > Return a start tag whose tag name is the empty string,
519 // > with no classes and no annotation, and abort these steps.
520 return CueTokenizerResult::StartTag(
521 Default::default(),
522 Default::default(),
523 Default::default(),
524 );
525 },
526 // > Anything else
527 Some(c) => {
528 // > Set result to c, set tokenizer state to the WebVTT start tag state,
529 // > and jump to the step labeled next.
530 result = c.into();
531 tokenizer_state = TokenizerState::WebVTTStartTag;
532 },
533 }
534 },
535 // https://w3c.github.io/webvtt/#webvtt-start-tag-state
536 TokenizerState::WebVTTStartTag => {
537 // > Jump to the entry that matches the value of c:
538 match c {
539 // > U+0009 CHARACTER TABULATION (tab) character
540 // > U+000C FORM FEED (FF) character
541 // > U+0020 SPACE character
542 Some('\u{0009}' | '\u{000C}' | '\u{0020}') => {
543 // > Set tokenizer state to the WebVTT start tag annotation state,
544 // > and jump to the step labeled next.
545 tokenizer_state = TokenizerState::WebVTTStartTagAnnotation;
546 },
547 // > U+000A LINE FEED (LF) character
548 Some('\u{000A}') => {
549 // > Set buffer to c, set tokenizer state to the WebVTT start tag annotation state,
550 // > and jump to the step labeled next.
551 buffer = '\u{000A}'.into();
552 tokenizer_state = TokenizerState::WebVTTStartTagAnnotation;
553 },
554 // > U+002E FULL STOP character (.)
555 Some('\u{002E}') => {
556 // > Set tokenizer state to the WebVTT start tag class state,
557 // > and jump to the step labeled next.
558 tokenizer_state = TokenizerState::WebVTTStartTagClass;
559 },
560 // > U+003E GREATER-THAN SIGN character (>)
561 Some('\u{003E}') => {
562 // > Advance position to the next character in input,
563 // > then jump to the next "end-of-file marker" entry below.
564 position.next();
565 return CueTokenizerResult::StartTag(
566 result,
567 Default::default(),
568 Default::default(),
569 );
570 },
571 // > End-of-file marker
572 None => {
573 // > Return a start tag whose tag name is result,
574 // > with no classes and no annotation, and abort these steps.
575 return CueTokenizerResult::StartTag(
576 result,
577 Default::default(),
578 Default::default(),
579 );
580 },
581 // > Anything else
582 Some(c) => {
583 // > Append c to result and jump to the step labeled next.
584 result.push(c);
585 },
586 }
587 },
588 // https://w3c.github.io/webvtt/#webvtt-start-tag-class-state
589 TokenizerState::WebVTTStartTagClass => {
590 // > Jump to the entry that matches the value of c:
591 match c {
592 // > U+0009 CHARACTER TABULATION (tab) character
593 // > U+000C FORM FEED (FF) character
594 // > U+0020 SPACE character
595 Some('\u{0009}' | '\u{000C}' | '\u{0020}') => {
596 // > Append to classes an entry whose value is buffer,
597 classes.push(buffer);
598 // > set buffer to the empty string,
599 buffer = String::new();
600 // > set tokenizer state to the WebVTT start tag annotation state,
601 // > and jump to the step labeled next.
602 tokenizer_state = TokenizerState::WebVTTStartTagAnnotation;
603 },
604 // > U+000A LINE FEED (LF) character
605 Some('\u{000A}') => {
606 // > Append to classes an entry whose value is buffer,
607 classes.push(buffer);
608 // > set buffer to c,
609 buffer = '\u{000A}'.into();
610 // > set tokenizer state to the WebVTT start tag annotation state,
611 // > and jump to the step labeled next.
612 tokenizer_state = TokenizerState::WebVTTStartTagAnnotation;
613 },
614 // > U+002E FULL STOP character (.)
615 Some('\u{002E}') => {
616 // > Append to classes an entry whose value is buffer,
617 classes.push(buffer);
618 // > set buffer to the empty string,
619 buffer = String::new();
620 // > and jump to the step labeled next.
621 },
622 // > U+003E GREATER-THAN SIGN character (>)
623 Some('\u{003E}') => {
624 // > Advance position to the next character in input,
625 // > then jump to the next "end-of-file marker" entry below.
626 position.next();
627 classes.push(buffer);
628 return CueTokenizerResult::StartTag(result, classes, String::new());
629 },
630 // > End-of-file marker
631 None => {
632 // > Append to classes an entry whose value is buffer,
633 classes.push(buffer);
634 // > then return a start tag whose tag name is result,
635 // > with the classes given in classes but no annotation,
636 // > and abort these steps.
637 return CueTokenizerResult::StartTag(result, classes, String::new());
638 },
639 // > Anything else
640 Some(c) => {
641 // > Append c to buffer and jump to the step labeled next.
642 buffer.push(c);
643 },
644 }
645 },
646 // https://w3c.github.io/webvtt/#webvtt-start-tag-annotation-state
647 TokenizerState::WebVTTStartTagAnnotation => {
648 // > Jump to the entry that matches the value of c:
649 match c {
650 // > U+0026 AMPERSAND (&)
651 Some('\u{0026}') => {
652 // > Set tokenizer state to the HTML character reference in annotation state,
653 // > and jump to the step labeled next.
654 tokenizer_state = TokenizerState::HTMLCharacterReferenceInAnnotation;
655 },
656 // > U+003E GREATER-THAN SIGN character (>)
657 Some('\u{003E}') => {
658 // > Advance position to the next character in input,
659 // > then jump to the next "end-of-file marker" entry below.
660 position.next();
661 return CueTokenizerResult::StartTag(
662 result,
663 classes,
664 buffer.trim().to_owned(),
665 );
666 },
667 // > End-of-file marker
668 None => {
669 // > Remove any leading or trailing ASCII whitespace characters from buffer,
670 // > and replace any sequence of one or more consecutive ASCII whitespace characters
671 // > in buffer with a single U+0020 SPACE character;
672 // > then, return a start tag whose tag name is result,
673 // > with the classes given in classes, and with buffer as the annotation,
674 // > and abort these steps.
675 // TODO: consecutive whitespace replacing
676 return CueTokenizerResult::StartTag(
677 result,
678 classes,
679 buffer.trim().to_owned(),
680 );
681 },
682 // > Anything else
683 Some(c) => {
684 // > Append c to buffer and jump to the step labeled next.
685 buffer.push(c);
686 },
687 }
688 },
689 TokenizerState::HTMLCharacterReferenceInAnnotation => {
690 // > Attempt to consume an HTML character reference,
691 // > with the additional allowed character being U+003E GREATER-THAN SIGN (>).
692 // > If nothing is returned, append a U+0026 AMPERSAND character (&) to buffer.
693 // > Otherwise, append the data of the character tokens that were returned to buffer.
694 // TODO: The character reference case
695 buffer.push('\u{0026}');
696 // > Then, in any case, set tokenizer state to the WebVTT start tag annotation state,
697 // > and jump to the step labeled next.
698 tokenizer_state = TokenizerState::WebVTTStartTagAnnotation;
699 },
700 // https://w3c.github.io/webvtt/#webvtt-end-tag-state
701 TokenizerState::WebVTTEndTag => {
702 // > Jump to the entry that matches the value of c:
703 match c {
704 // > U+003E GREATER-THAN SIGN character (>)
705 Some('\u{003E}') => {
706 // > Advance position to the next character in input,
707 // > then jump to the next "end-of-file marker" entry below.
708 position.next();
709 return CueTokenizerResult::EndTag(result);
710 },
711 // > End-of-file marker
712 None => {
713 // > Return an end tag whose tag name is result and abort these steps.
714 return CueTokenizerResult::EndTag(result);
715 },
716 // > Anything else
717 Some(c) => {
718 // > Append c to result and jump to the step labeled next.
719 result.push(c);
720 },
721 }
722 },
723 // https://w3c.github.io/webvtt/#webvtt-timestamp-tag-state
724 TokenizerState::WebVTTTimestampTag => {
725 // > Jump to the entry that matches the value of c:
726 match c {
727 // > U+003E GREATER-THAN SIGN character (>)
728 Some('\u{003E}') => {
729 // > Advance position to the next character in input,
730 // > then jump to the next "end-of-file marker" entry below.
731 position.next();
732 return CueTokenizerResult::TimestampTag(result);
733 },
734 // > End-of-file marker
735 None => {
736 // > Return a timestamp tag whose tag name is result and abort these steps.
737 return CueTokenizerResult::TimestampTag(result);
738 },
739 // > Anything else
740 Some(c) => {
741 // > Append c to result and jump to the step labeled next.
742 result.push(c);
743 },
744 }
745 },
746 }
747 // Step 7. Next: Advance position to the next character in input.
748 position.next();
749 // Step 8. Jump to the step labeled loop.
750 }
751}
752
753#[cfg(any(test, feature = "test-util"))]
754impl WebVTTNodeObjectIterator {
755 pub fn assert_and_return_next_child(&mut self) -> Rc<WebVTTNodeObject> {
756 let Some(iterator_direction) = self.next() else {
757 unreachable!("Must have a new item");
758 };
759 let WebVTTNodeObjectIteratorDirection::NewChild(node) = iterator_direction else {
760 unreachable!("Must be a new child");
761 };
762 node
763 }
764
765 pub fn next_is_parent(&mut self) -> bool {
766 let Some(iterator_direction) = self.next() else {
767 unreachable!("Must have a new item");
768 };
769 iterator_direction == WebVTTNodeObjectIteratorDirection::BackToParent
770 }
771}