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(¤t),
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(¤t),
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(¤t),
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}