Skip to main content

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(&current),
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(&current),
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(&current),
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}