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