Source code

Revision control

1
/*
2
* Copyright (C) 2011, 2013 Google Inc. All rights reserved.
3
* Copyright (C) 2013 Cable Television Labs, Inc.
4
* Copyright (C) 2011-2020 Apple Inc. All rights reserved.
5
*
6
* Redistribution and use in source and binary forms, with or without
7
* modification, are permitted provided that the following conditions are
8
* met:
9
*
10
* * Redistributions of source code must retain the above copyright
11
* notice, this list of conditions and the following disclaimer.
12
* * Redistributions in binary form must reproduce the above
13
* copyright notice, this list of conditions and the following disclaimer
14
* in the documentation and/or other materials provided with the
15
* distribution.
16
* * Neither the name of Google Inc. nor the names of its
17
* contributors may be used to endorse or promote products derived from
18
* this software without specific prior written permission.
19
*
20
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
*/
32
33
#include "config.h"
34
#include "WebVTTParser.h"
35
36
#if ENABLE(VIDEO)
37
38
#include "HTMLParserIdioms.h"
39
#include "ISOVTTCue.h"
40
#include "ProcessingInstruction.h"
41
#include "StyleRule.h"
42
#include "StyleRuleImport.h"
43
#include "StyleSheetContents.h"
44
#include "Text.h"
45
#include "VTTScanner.h"
46
#include "WebVTTElement.h"
47
#include "WebVTTTokenizer.h"
48
49
namespace WebCore {
50
51
const double secondsPerHour = 3600;
52
const double secondsPerMinute = 60;
53
const double secondsPerMillisecond = 0.001;
54
const char* fileIdentifier = "WEBVTT";
55
const unsigned fileIdentifierLength = 6;
56
const unsigned regionIdentifierLength = 6;
57
const unsigned styleIdentifierLength = 5;
58
59
bool WebVTTParser::parseFloatPercentageValue(VTTScanner& valueScanner, float& percentage)
60
{
61
float number;
62
if (!valueScanner.scanFloat(number))
63
return false;
64
// '%' must be present and at the end of the setting value.
65
if (!valueScanner.scan('%'))
66
return false;
67
68
if (number < 0 || number > 100)
69
return false;
70
71
percentage = number;
72
return true;
73
}
74
75
bool WebVTTParser::parseFloatPercentageValuePair(VTTScanner& valueScanner, char delimiter, FloatPoint& valuePair)
76
{
77
float firstCoord;
78
if (!parseFloatPercentageValue(valueScanner, firstCoord))
79
return false;
80
81
if (!valueScanner.scan(delimiter))
82
return false;
83
84
float secondCoord;
85
if (!parseFloatPercentageValue(valueScanner, secondCoord))
86
return false;
87
88
valuePair = FloatPoint(firstCoord, secondCoord);
89
return true;
90
}
91
92
WebVTTParser::WebVTTParser(WebVTTParserClient& client, Document& document)
93
: m_document(document)
94
, m_decoder(TextResourceDecoder::create("text/plain", UTF8Encoding()))
95
, m_client(client)
96
{
97
}
98
99
Vector<Ref<WebVTTCueData>> WebVTTParser::takeCues()
100
{
101
return WTFMove(m_cuelist);
102
}
103
104
Vector<Ref<VTTRegion>> WebVTTParser::takeRegions()
105
{
106
return WTFMove(m_regionList);
107
}
108
109
Vector<String> WebVTTParser::takeStyleSheets()
110
{
111
return WTFMove(m_styleSheets);
112
}
113
114
void WebVTTParser::parseFileHeader(String&& data)
115
{
116
m_state = Initial;
117
m_lineReader.reset();
118
m_lineReader.append(WTFMove(data));
119
parse();
120
}
121
122
void WebVTTParser::parseBytes(const char* data, unsigned length)
123
{
124
m_lineReader.append(m_decoder->decode(data, length));
125
parse();
126
}
127
128
void WebVTTParser::parseCueData(const ISOWebVTTCue& data)
129
{
130
auto cue = WebVTTCueData::create();
131
132
MediaTime startTime = data.presentationTime();
133
cue->setStartTime(startTime);
134
cue->setEndTime(startTime + data.duration());
135
136
cue->setContent(data.cueText());
137
cue->setId(data.id());
138
cue->setSettings(data.settings());
139
140
MediaTime originalStartTime;
141
if (WebVTTParser::collectTimeStamp(data.originalStartTime(), originalStartTime))
142
cue->setOriginalStartTime(originalStartTime);
143
144
m_cuelist.append(WTFMove(cue));
145
m_client.newCuesParsed();
146
}
147
148
void WebVTTParser::flush()
149
{
150
m_lineReader.append(m_decoder->flush());
151
m_lineReader.appendEndOfStream();
152
parse();
153
flushPendingCue();
154
}
155
156
void WebVTTParser::parse()
157
{
158
// WebVTT parser algorithm. (5.1 WebVTT file parsing.)
159
// Steps 1 - 3 - Initial setup.
160
while (auto line = m_lineReader.nextLine()) {
161
switch (m_state) {
162
case Initial:
163
// Steps 4 - 9 - Check for a valid WebVTT signature.
164
if (!hasRequiredFileIdentifier(*line)) {
165
m_client.fileFailedToParse();
166
return;
167
}
168
169
m_state = Header;
170
break;
171
172
case Header:
173
// Steps 11 - 14 - Collect WebVTT block
174
m_state = collectWebVTTBlock(*line);
175
break;
176
177
case Region:
178
m_state = collectRegionSettings(*line);
179
break;
180
181
case Style:
182
m_state = collectStyleSheet(*line);
183
break;
184
185
case Id:
186
// Steps 17 - 20 - Allow any number of line terminators, then initialize new cue values.
187
if (line->isEmpty())
188
break;
189
190
// Step 21 - Cue creation (start a new cue).
191
resetCueValues();
192
193
// Steps 22 - 25 - Check if this line contains an optional identifier or timing data.
194
m_state = collectCueId(*line);
195
break;
196
197
case TimingsAndSettings:
198
// Steps 26 - 27 - Discard current cue if the line is empty.
199
if (line->isEmpty()) {
200
m_state = Id;
201
break;
202
}
203
204
// Steps 28 - 29 - Collect cue timings and settings.
205
m_state = collectTimingsAndSettings(*line);
206
break;
207
208
case CueText:
209
// Steps 31 - 41 - Collect the cue text, create a cue, and add it to the output.
210
m_state = collectCueText(*line);
211
break;
212
213
case BadCue:
214
// Steps 42 - 48 - Discard lines until an empty line or a potential timing line is seen.
215
m_state = ignoreBadCue(*line);
216
break;
217
218
case Finished:
219
ASSERT_NOT_REACHED();
220
break;
221
}
222
}
223
}
224
225
void WebVTTParser::fileFinished()
226
{
227
ASSERT(m_state != Finished);
228
parseBytes("\n\n", 2);
229
m_state = Finished;
230
}
231
232
void WebVTTParser::flushPendingCue()
233
{
234
ASSERT(m_lineReader.isAtEndOfStream());
235
// If we're in the CueText state when we run out of data, we emit the pending cue.
236
if (m_state == CueText)
237
createNewCue();
238
}
239
240
bool WebVTTParser::hasRequiredFileIdentifier(const String& line)
241
{
242
// A WebVTT file identifier consists of an optional BOM character,
243
// the string "WEBVTT" followed by an optional space or tab character,
244
// and any number of characters that are not line terminators ...
245
if (!line.startsWith(fileIdentifier))
246
return false;
247
if (line.length() > fileIdentifierLength && !isHTMLSpace(line[fileIdentifierLength]))
248
return false;
249
return true;
250
}
251
252
WebVTTParser::ParseState WebVTTParser::collectRegionSettings(const String& line)
253
{
254
// End of region block
255
if (checkAndStoreRegion(line))
256
return checkAndRecoverCue(line);
257
258
m_currentRegion->setRegionSettings(line);
259
return Region;
260
}
261
262
WebVTTParser::ParseState WebVTTParser::collectWebVTTBlock(const String& line)
263
{
264
// collect a WebVTT block parsing. (WebVTT parser algorithm step 14)
265
266
if (checkAndCreateRegion(line))
267
return Region;
268
269
if (checkStyleSheet(line))
270
return Style;
271
272
// Handle cue block.
273
ParseState state = checkAndRecoverCue(line);
274
if (state != Header) {
275
if (!m_regionList.isEmpty())
276
m_client.newRegionsParsed();
277
if (!m_styleSheets.isEmpty())
278
m_client.newStyleSheetsParsed();
279
if (!m_previousLine.isEmpty() && !m_previousLine.contains("-->"))
280
m_currentId = m_previousLine;
281
282
return state;
283
}
284
285
// store previous line for cue id.
286
// length is more than 1 line clear m_previousLine and ignore line.
287
if (m_previousLine.isEmpty())
288
m_previousLine = line;
289
else
290
m_previousLine = emptyString();
291
292
return state;
293
}
294
295
WebVTTParser::ParseState WebVTTParser::checkAndRecoverCue(const String& line)
296
{
297
// parse cue timings and settings
298
if (line.contains("-->")) {
299
ParseState state = recoverCue(line);
300
if (state != BadCue)
301
return state;
302
}
303
return Header;
304
}
305
306
WebVTTParser::ParseState WebVTTParser::collectStyleSheet(const String& line)
307
{
308
// End of style block
309
if (checkAndStoreStyleSheet(line))
310
return checkAndRecoverCue(line);
311
312
m_currentSourceStyleSheet.append(line);
313
return Style;
314
}
315
316
bool WebVTTParser::checkAndCreateRegion(const String& line)
317
{
318
if (m_previousLine.contains("-->"))
319
return false;
320
// line starts with the substring "REGION" and remaining characters
321
// zero or more U+0020 SPACE characters or U+0009 CHARACTER TABULATION
322
// (tab) characters expected other than these charecters it is invalid.
323
if (line.startsWith("REGION") && line.substring(regionIdentifierLength).isAllSpecialCharacters<isASpace>()) {
324
m_currentRegion = VTTRegion::create(m_document);
325
return true;
326
}
327
return false;
328
}
329
330
bool WebVTTParser::checkAndStoreRegion(const String& line)
331
{
332
if (!line.isEmpty() && !line.contains("-->"))
333
return false;
334
335
if (!m_currentRegion->id().isEmpty()) {
336
m_regionList.removeFirstMatching([this] (auto& region) {
337
return region->id() == m_currentRegion->id();
338
});
339
m_regionList.append(m_currentRegion.releaseNonNull());
340
}
341
m_currentRegion = nullptr;
342
return true;
343
}
344
345
bool WebVTTParser::checkStyleSheet(const String& line)
346
{
347
if (m_previousLine.contains("-->"))
348
return false;
349
// line starts with the substring "STYLE" and remaining characters
350
// zero or more U+0020 SPACE characters or U+0009 CHARACTER TABULATION
351
// (tab) characters expected other than these charecters it is invalid.
352
if (line.startsWith("STYLE") && line.substring(styleIdentifierLength).isAllSpecialCharacters<isASpace>())
353
return true;
354
355
return false;
356
}
357
358
bool WebVTTParser::checkAndStoreStyleSheet(const String& line)
359
{
360
if (!line.isEmpty() && !line.contains("-->"))
361
return false;
362
363
auto styleSheetText = WTFMove(m_currentSourceStyleSheet);
364
365
// WebVTTMode disallows non-data URLs.
366
auto contents = StyleSheetContents::create(CSSParserContext(WebVTTMode));
367
if (!contents->parseString(styleSheetText))
368
return true;
369
370
auto& namespaceRules = contents->namespaceRules();
371
if (namespaceRules.size())
372
return true;
373
374
auto& importRules = contents->importRules();
375
if (importRules.size())
376
return true;
377
378
auto& childRules = contents->childRules();
379
if (!childRules.size())
380
return true;
381
382
StringBuilder sanitizedStyleSheetBuilder;
383
384
for (const auto& rule : childRules) {
385
if (!rule->isStyleRule())
386
return true;
387
const auto& styleRule = downcast<StyleRule>(*rule);
388
389
const auto& selectorList = styleRule.selectorList();
390
if (selectorList.listSize() != 1)
391
return true;
392
auto selector = selectorList.selectorAt(0);
393
auto selectorText = selector->selectorText();
394
395
bool isCue = selectorText == "::cue" || selectorText.startsWith("::cue(");
396
if (!isCue)
397
return true;
398
399
if (styleRule.properties().isEmpty())
400
continue;
401
402
sanitizedStyleSheetBuilder.append(selectorText, " { ", styleRule.properties().asText(), " }\n");
403
}
404
405
// It would be more stylish to parse the stylesheet only once instead of serializing a sanitized version.
406
if (!sanitizedStyleSheetBuilder.isEmpty())
407
m_styleSheets.append(sanitizedStyleSheetBuilder.toString());
408
409
return true;
410
}
411
412
WebVTTParser::ParseState WebVTTParser::collectCueId(const String& line)
413
{
414
if (line.contains("-->"))
415
return collectTimingsAndSettings(line);
416
m_currentId = line;
417
return TimingsAndSettings;
418
}
419
420
WebVTTParser::ParseState WebVTTParser::collectTimingsAndSettings(const String& line)
421
{
422
if (line.isEmpty())
423
return BadCue;
424
425
VTTScanner input(line);
426
427
// Collect WebVTT cue timings and settings. (5.3 WebVTT cue timings and settings parsing.)
428
// Steps 1 - 3 - Let input be the string being parsed and position be a pointer into input
429
input.skipWhile<isHTMLSpace<UChar>>();
430
431
// Steps 4 - 5 - Collect a WebVTT timestamp. If that fails, then abort and return failure. Otherwise, let cue's text track cue start time be the collected time.
432
if (!collectTimeStamp(input, m_currentStartTime))
433
return BadCue;
434
435
input.skipWhile<isHTMLSpace<UChar>>();
436
437
// Steps 6 - 9 - If the next three characters are not "-->", abort and return failure.
438
if (!input.scan("-->"))
439
return BadCue;
440
441
input.skipWhile<isHTMLSpace<UChar>>();
442
443
// Steps 10 - 11 - Collect a WebVTT timestamp. If that fails, then abort and return failure. Otherwise, let cue's text track cue end time be the collected time.
444
if (!collectTimeStamp(input, m_currentEndTime))
445
return BadCue;
446
447
input.skipWhile<isHTMLSpace<UChar>>();
448
449
// Step 12 - Parse the WebVTT settings for the cue (conducted in TextTrackCue).
450
m_currentSettings = input.restOfInputAsString();
451
return CueText;
452
}
453
454
WebVTTParser::ParseState WebVTTParser::collectCueText(const String& line)
455
{
456
// Step 34.
457
if (line.isEmpty()) {
458
createNewCue();
459
return Id;
460
}
461
// Step 35.
462
if (line.contains("-->")) {
463
// Step 39-40.
464
createNewCue();
465
466
// Step 41 - New iteration of the cue loop.
467
return recoverCue(line);
468
}
469
if (!m_currentContent.isEmpty())
470
m_currentContent.append('\n');
471
m_currentContent.append(line);
472
473
return CueText;
474
}
475
476
WebVTTParser::ParseState WebVTTParser::recoverCue(const String& line)
477
{
478
// Step 17 and 21.
479
resetCueValues();
480
481
// Step 22.
482
return collectTimingsAndSettings(line);
483
}
484
485
WebVTTParser::ParseState WebVTTParser::ignoreBadCue(const String& line)
486
{
487
if (line.isEmpty())
488
return Id;
489
if (line.contains("-->"))
490
return recoverCue(line);
491
return BadCue;
492
}
493
494
// A helper class for the construction of a "cue fragment" from the cue text.
495
class WebVTTTreeBuilder {
496
public:
497
WebVTTTreeBuilder(Document& document)
498
: m_document(document) { }
499
500
Ref<DocumentFragment> buildFromString(const String& cueText);
501
502
private:
503
void constructTreeFromToken(Document&);
504
505
WebVTTToken m_token;
506
RefPtr<ContainerNode> m_currentNode;
507
Vector<AtomString> m_languageStack;
508
Document& m_document;
509
};
510
511
Ref<DocumentFragment> WebVTTTreeBuilder::buildFromString(const String& cueText)
512
{
513
// Cue text processing based on
514
// 5.4 WebVTT cue text parsing rules, and
515
// 5.5 WebVTT cue text DOM construction rules.
516
auto fragment = DocumentFragment::create(m_document);
517
518
if (cueText.isEmpty()) {
519
fragment->parserAppendChild(Text::create(m_document, emptyString()));
520
return fragment;
521
}
522
523
m_currentNode = fragment.ptr();
524
525
WebVTTTokenizer tokenizer(cueText);
526
m_languageStack.clear();
527
528
while (tokenizer.nextToken(m_token))
529
constructTreeFromToken(m_document);
530
531
return fragment;
532
}
533
534
Ref<DocumentFragment> WebVTTParser::createDocumentFragmentFromCueText(Document& document, const String& cueText)
535
{
536
WebVTTTreeBuilder treeBuilder(document);
537
return treeBuilder.buildFromString(cueText);
538
}
539
540
void WebVTTParser::createNewCue()
541
{
542
auto cue = WebVTTCueData::create();
543
cue->setStartTime(m_currentStartTime);
544
cue->setEndTime(m_currentEndTime);
545
cue->setContent(m_currentContent.toString());
546
cue->setId(m_currentId);
547
cue->setSettings(m_currentSettings);
548
549
m_cuelist.append(WTFMove(cue));
550
m_client.newCuesParsed();
551
}
552
553
void WebVTTParser::resetCueValues()
554
{
555
m_currentId = emptyString();
556
m_currentSettings = emptyString();
557
m_currentStartTime = MediaTime::zeroTime();
558
m_currentEndTime = MediaTime::zeroTime();
559
m_currentContent.clear();
560
}
561
562
bool WebVTTParser::collectTimeStamp(const String& line, MediaTime& timeStamp)
563
{
564
if (line.isEmpty())
565
return false;
566
567
VTTScanner input(line);
568
return collectTimeStamp(input, timeStamp);
569
}
570
571
bool WebVTTParser::collectTimeStamp(VTTScanner& input, MediaTime& timeStamp)
572
{
573
// Collect a WebVTT timestamp (5.3 WebVTT cue timings and settings parsing.)
574
// Steps 1 - 4 - Initial checks, let most significant units be minutes.
575
enum Mode { minutes, hours };
576
Mode mode = minutes;
577
578
// Steps 5 - 7 - Collect a sequence of characters that are 0-9.
579
// If not 2 characters or value is greater than 59, interpret as hours.
580
int value1;
581
unsigned value1Digits = input.scanDigits(value1);
582
if (!value1Digits)
583
return false;
584
if (value1Digits != 2 || value1 > 59)
585
mode = hours;
586
587
// Steps 8 - 11 - Collect the next sequence of 0-9 after ':' (must be 2 chars).
588
int value2;
589
if (!input.scan(':') || input.scanDigits(value2) != 2)
590
return false;
591
592
// Step 12 - Detect whether this timestamp includes hours.
593
int value3;
594
if (mode == hours || input.match(':')) {
595
if (!input.scan(':') || input.scanDigits(value3) != 2)
596
return false;
597
} else {
598
value3 = value2;
599
value2 = value1;
600
value1 = 0;
601
}
602
603
// Steps 13 - 17 - Collect next sequence of 0-9 after '.' (must be 3 chars).
604
int value4;
605
if (!input.scan('.') || input.scanDigits(value4) != 3)
606
return false;
607
if (value2 > 59 || value3 > 59)
608
return false;
609
610
// Steps 18 - 19 - Calculate result.
611
timeStamp = MediaTime::createWithDouble((value1 * secondsPerHour) + (value2 * secondsPerMinute) + value3 + (value4 * secondsPerMillisecond));
612
return true;
613
}
614
615
static WebVTTNodeType tokenToNodeType(WebVTTToken& token)
616
{
617
switch (token.name().length()) {
618
case 1:
619
if (token.name()[0] == 'c')
620
return WebVTTNodeTypeClass;
621
if (token.name()[0] == 'v')
622
return WebVTTNodeTypeVoice;
623
if (token.name()[0] == 'b')
624
return WebVTTNodeTypeBold;
625
if (token.name()[0] == 'i')
626
return WebVTTNodeTypeItalic;
627
if (token.name()[0] == 'u')
628
return WebVTTNodeTypeUnderline;
629
break;
630
case 2:
631
if (token.name()[0] == 'r' && token.name()[1] == 't')
632
return WebVTTNodeTypeRubyText;
633
break;
634
case 4:
635
if (token.name()[0] == 'r' && token.name()[1] == 'u' && token.name()[2] == 'b' && token.name()[3] == 'y')
636
return WebVTTNodeTypeRuby;
637
if (token.name()[0] == 'l' && token.name()[1] == 'a' && token.name()[2] == 'n' && token.name()[3] == 'g')
638
return WebVTTNodeTypeLanguage;
639
break;
640
}
641
return WebVTTNodeTypeNone;
642
}
643
644
void WebVTTTreeBuilder::constructTreeFromToken(Document& document)
645
{
647
648
switch (m_token.type()) {
649
case WebVTTTokenTypes::Character: {
650
m_currentNode->parserAppendChild(Text::create(document, m_token.characters()));
651
break;
652
}
653
case WebVTTTokenTypes::StartTag: {
654
WebVTTNodeType nodeType = tokenToNodeType(m_token);
655
if (nodeType == WebVTTNodeTypeNone)
656
break;
657
658
WebVTTNodeType currentType = is<WebVTTElement>(*m_currentNode) ? downcast<WebVTTElement>(*m_currentNode).webVTTNodeType() : WebVTTNodeTypeNone;
659
// <rt> is only allowed if the current node is <ruby>.
660
if (nodeType == WebVTTNodeTypeRubyText && currentType != WebVTTNodeTypeRuby)
661
break;
662
663
auto child = WebVTTElement::create(nodeType, document);
664
if (!m_token.classes().isEmpty())
665
child->setAttributeWithoutSynchronization(classAttr, m_token.classes());
666
667
if (nodeType == WebVTTNodeTypeVoice)
668
child->setAttributeWithoutSynchronization(WebVTTElement::voiceAttributeName(), m_token.annotation());
669
else if (nodeType == WebVTTNodeTypeLanguage) {
670
m_languageStack.append(m_token.annotation());
671
child->setAttributeWithoutSynchronization(WebVTTElement::langAttributeName(), m_languageStack.last());
672
}
673
if (!m_languageStack.isEmpty())
674
child->setLanguage(m_languageStack.last());
675
m_currentNode->parserAppendChild(child);
676
m_currentNode = WTFMove(child);
677
break;
678
}
679
case WebVTTTokenTypes::EndTag: {
680
WebVTTNodeType nodeType = tokenToNodeType(m_token);
681
if (nodeType == WebVTTNodeTypeNone)
682
break;
683
684
// The only non-VTTElement would be the DocumentFragment root. (Text
685
// nodes and PIs will never appear as m_currentNode.)
686
if (!is<WebVTTElement>(*m_currentNode))
687
break;
688
689
WebVTTNodeType currentType = downcast<WebVTTElement>(*m_currentNode).webVTTNodeType();
690
bool matchesCurrent = nodeType == currentType;
691
if (!matchesCurrent) {
692
// </ruby> auto-closes <rt>
693
if (currentType == WebVTTNodeTypeRubyText && nodeType == WebVTTNodeTypeRuby) {
694
if (m_currentNode->parentNode())
695
m_currentNode = m_currentNode->parentNode();
696
} else
697
break;
698
}
699
if (nodeType == WebVTTNodeTypeLanguage)
700
m_languageStack.removeLast();
701
if (m_currentNode->parentNode())
702
m_currentNode = m_currentNode->parentNode();
703
break;
704
}
705
case WebVTTTokenTypes::TimestampTag: {
706
String charactersString = m_token.characters();
707
MediaTime parsedTimeStamp;
708
if (WebVTTParser::collectTimeStamp(charactersString, parsedTimeStamp))
709
m_currentNode->parserAppendChild(ProcessingInstruction::create(document, "timestamp", charactersString));
710
break;
711
}
712
default:
713
break;
714
}
715
}
716
717
}
718
719
#endif