2 * Copyright (C) 2011 Google Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
14 * * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 #include "core/html/track/vtt/VTTParser.h"
34 #include "RuntimeEnabledFeatures.h"
35 #include "core/dom/Document.h"
36 #include "core/dom/ProcessingInstruction.h"
37 #include "core/dom/Text.h"
38 #include "core/html/track/vtt/VTTElement.h"
39 #include "core/html/track/vtt/VTTScanner.h"
40 #include "platform/text/SegmentedString.h"
41 #include "wtf/text/WTFString.h"
45 using namespace HTMLNames;
47 const double secondsPerHour = 3600;
48 const double secondsPerMinute = 60;
49 const double secondsPerMillisecond = 0.001;
50 const unsigned fileIdentifierLength = 6;
52 bool VTTParser::parseFloatPercentageValue(VTTScanner& valueScanner, float& percentage)
55 if (!valueScanner.scanFloat(number))
57 // '%' must be present and at the end of the setting value.
58 if (!valueScanner.scan('%'))
60 if (number < 0 || number > 100)
66 bool VTTParser::parseFloatPercentageValuePair(VTTScanner& valueScanner, char delimiter, FloatPoint& valuePair)
69 if (!parseFloatPercentageValue(valueScanner, firstCoord))
72 if (!valueScanner.scan(delimiter))
76 if (!parseFloatPercentageValue(valueScanner, secondCoord))
79 valuePair = FloatPoint(firstCoord, secondCoord);
83 VTTParser::VTTParser(VTTParserClient* client, Document& document)
84 : m_document(&document)
86 , m_decoder(TextResourceDecoder::create("text/plain", UTF8Encoding()))
87 , m_currentStartTime(0)
93 void VTTParser::getNewCues(WillBeHeapVector<RefPtrWillBeMember<VTTCue> >& outputCues)
95 outputCues = m_cueList;
99 void VTTParser::getNewRegions(WillBeHeapVector<RefPtrWillBeMember<VTTRegion> >& outputRegions)
101 outputRegions = m_regionList;
102 m_regionList.clear();
105 void VTTParser::parseBytes(const char* data, unsigned length)
107 String textData = m_decoder->decode(data, length);
108 m_lineReader.append(textData);
112 void VTTParser::flush()
114 String textData = m_decoder->flush();
115 m_lineReader.append(textData);
116 m_lineReader.setEndOfStream();
121 void VTTParser::parse()
123 // WebVTT parser algorithm. (5.1 WebVTT file parsing.)
124 // Steps 1 - 3 - Initial setup.
127 while (m_lineReader.getLine(line)) {
130 // Steps 4 - 9 - Check for a valid WebVTT signature.
131 if (!hasRequiredFileIdentifier(line)) {
133 m_client->fileFailedToParse();
141 // Steps 10 - 14 - Allow a header (comment area) under the WEBVTT line.
142 collectMetadataHeader(line);
144 if (line.isEmpty()) {
145 if (m_client && m_regionList.size())
146 m_client->newRegionsParsed();
152 // Step 15 - Break out of header loop if the line could be a timestamp line.
153 if (line.contains("-->"))
154 m_state = recoverCue(line);
156 // Step 16 - Line is not the empty string and does not contain "-->".
160 // Steps 17 - 20 - Allow any number of line terminators, then initialize new cue values.
164 // Step 21 - Cue creation (start a new cue).
167 // Steps 22 - 25 - Check if this line contains an optional identifier or timing data.
168 m_state = collectCueId(line);
171 case TimingsAndSettings:
172 // Steps 26 - 27 - Discard current cue if the line is empty.
173 if (line.isEmpty()) {
178 // Steps 28 - 29 - Collect cue timings and settings.
179 m_state = collectTimingsAndSettings(line);
183 // Steps 31 - 41 - Collect the cue text, create a cue, and add it to the output.
184 m_state = collectCueText(line);
188 // Steps 42 - 48 - Discard lines until an empty line or a potential timing line is seen.
189 m_state = ignoreBadCue(line);
195 void VTTParser::flushPendingCue()
197 ASSERT(m_lineReader.isAtEndOfStream());
198 // If we're in the CueText state when we run out of data, we emit the pending cue.
199 if (m_state == CueText)
203 bool VTTParser::hasRequiredFileIdentifier(const String& line)
205 // A WebVTT file identifier consists of an optional BOM character,
206 // the string "WEBVTT" followed by an optional space or tab character,
207 // and any number of characters that are not line terminators ...
208 if (!line.startsWith("WEBVTT", fileIdentifierLength))
210 if (line.length() > fileIdentifierLength && !isASpace(line[fileIdentifierLength]))
216 void VTTParser::collectMetadataHeader(const String& line)
218 // WebVTT header parsing (WebVTT parser algorithm step 12)
219 DEFINE_STATIC_LOCAL(const AtomicString, regionHeaderName, ("Region", AtomicString::ConstructFromLiteral));
221 // The only currently supported header is the "Region" header.
222 if (!RuntimeEnabledFeatures::webVTTRegionsEnabled())
225 // Step 12.4 If line contains the character ":" (A U+003A COLON), then set metadata's
226 // name to the substring of line before the first ":" character and
227 // metadata's value to the substring after this character.
228 size_t colonPosition = line.find(':');
229 if (colonPosition == kNotFound)
232 String headerName = line.substring(0, colonPosition);
234 // Steps 12.5 If metadata's name equals "Region":
235 if (headerName == regionHeaderName) {
236 String headerValue = line.substring(colonPosition + 1);
237 // Steps 12.5.1 - 12.5.11 Region creation: Let region be a new text track region [...]
238 createNewRegion(headerValue);
242 VTTParser::ParseState VTTParser::collectCueId(const String& line)
244 if (line.contains("-->"))
245 return collectTimingsAndSettings(line);
246 m_currentId = AtomicString(line);
247 return TimingsAndSettings;
250 VTTParser::ParseState VTTParser::collectTimingsAndSettings(const String& line)
252 VTTScanner input(line);
254 // Collect WebVTT cue timings and settings. (5.3 WebVTT cue timings and settings parsing.)
255 // Steps 1 - 3 - Let input be the string being parsed and position be a pointer into input.
256 input.skipWhile<isASpace>();
258 // 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.
259 if (!collectTimeStamp(input, m_currentStartTime))
261 input.skipWhile<isASpace>();
263 // Steps 6 - 9 - If the next three characters are not "-->", abort and return failure.
264 if (!input.scan("-->"))
266 input.skipWhile<isASpace>();
268 // 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.
269 if (!collectTimeStamp(input, m_currentEndTime))
271 input.skipWhile<isASpace>();
273 // Step 12 - Parse the WebVTT settings for the cue (conducted in TextTrackCue).
274 m_currentSettings = input.restOfInputAsString();
278 VTTParser::ParseState VTTParser::collectCueText(const String& line)
281 if (line.isEmpty()) {
286 if (line.contains("-->")) {
290 // Step 41 - New iteration of the cue loop.
291 return recoverCue(line);
293 if (!m_currentContent.isEmpty())
294 m_currentContent.append("\n");
295 m_currentContent.append(line);
300 VTTParser::ParseState VTTParser::recoverCue(const String& line)
306 return collectTimingsAndSettings(line);
309 VTTParser::ParseState VTTParser::ignoreBadCue(const String& line)
313 if (line.contains("-->"))
314 return recoverCue(line);
318 // A helper class for the construction of a "cue fragment" from the cue text.
319 class VTTTreeBuilder {
321 VTTTreeBuilder(Document& document)
322 : m_document(document) { }
324 PassRefPtr<DocumentFragment> buildFromString(const String& cueText);
327 void constructTreeFromToken(Document&);
330 RefPtr<ContainerNode> m_currentNode;
331 Vector<AtomicString> m_languageStack;
332 Document& m_document;
335 PassRefPtr<DocumentFragment> VTTTreeBuilder::buildFromString(const String& cueText)
337 // Cue text processing based on
338 // 5.4 WebVTT cue text parsing rules, and
339 // 5.5 WebVTT cue text DOM construction rules
341 RefPtr<DocumentFragment> fragment = DocumentFragment::create(m_document);
343 if (cueText.isEmpty()) {
344 fragment->parserAppendChild(Text::create(m_document, ""));
348 m_currentNode = fragment;
350 VTTTokenizer tokenizer(cueText);
351 m_languageStack.clear();
353 while (tokenizer.nextToken(m_token))
354 constructTreeFromToken(m_document);
356 return fragment.release();
359 PassRefPtr<DocumentFragment> VTTParser::createDocumentFragmentFromCueText(Document& document, const String& cueText)
361 VTTTreeBuilder treeBuilder(document);
362 return treeBuilder.buildFromString(cueText);
365 void VTTParser::createNewCue()
367 RefPtrWillBeRawPtr<VTTCue> cue = VTTCue::create(*m_document, m_currentStartTime, m_currentEndTime, m_currentContent.toString());
368 cue->setId(m_currentId);
369 cue->parseSettings(m_currentSettings);
371 m_cueList.append(cue);
373 m_client->newCuesParsed();
376 void VTTParser::resetCueValues()
378 m_currentId = emptyAtom;
379 m_currentSettings = emptyString();
380 m_currentStartTime = 0;
381 m_currentEndTime = 0;
382 m_currentContent.clear();
385 void VTTParser::createNewRegion(const String& headerValue)
387 if (headerValue.isEmpty())
390 // Steps 12.5.1 - 12.5.9 - Construct and initialize a WebVTT Region object.
391 RefPtrWillBeRawPtr<VTTRegion> region = VTTRegion::create();
392 region->setRegionSettings(headerValue);
394 // Step 12.5.10 If the text track list of regions regions contains a region
395 // with the same region identifier value as region, remove that region.
396 for (size_t i = 0; i < m_regionList.size(); ++i) {
397 if (m_regionList[i]->id() == region->id()) {
398 m_regionList.remove(i);
404 m_regionList.append(region);
407 bool VTTParser::collectTimeStamp(const String& line, double& timeStamp)
409 VTTScanner input(line);
410 return collectTimeStamp(input, timeStamp);
413 bool VTTParser::collectTimeStamp(VTTScanner& input, double& timeStamp)
415 // Collect a WebVTT timestamp (5.3 WebVTT cue timings and settings parsing.)
416 // Steps 1 - 4 - Initial checks, let most significant units be minutes.
417 enum Mode { Minutes, Hours };
420 // Steps 5 - 7 - Collect a sequence of characters that are 0-9.
421 // If not 2 characters or value is greater than 59, interpret as hours.
423 unsigned value1Digits = input.scanDigits(value1);
426 if (value1Digits != 2 || value1 > 59)
429 // Steps 8 - 11 - Collect the next sequence of 0-9 after ':' (must be 2 chars).
431 if (!input.scan(':') || input.scanDigits(value2) != 2)
434 // Step 12 - Detect whether this timestamp includes hours.
436 if (mode == Hours || input.match(':')) {
437 if (!input.scan(':') || input.scanDigits(value3) != 2)
445 // Steps 13 - 17 - Collect next sequence of 0-9 after '.' (must be 3 chars).
447 if (!input.scan('.') || input.scanDigits(value4) != 3)
449 if (value2 > 59 || value3 > 59)
452 // Steps 18 - 19 - Calculate result.
453 timeStamp = (value1 * secondsPerHour) + (value2 * secondsPerMinute) + value3 + (value4 * secondsPerMillisecond);
457 static VTTNodeType tokenToNodeType(VTTToken& token)
459 switch (token.name().length()) {
461 if (token.name()[0] == 'c')
462 return VTTNodeTypeClass;
463 if (token.name()[0] == 'v')
464 return VTTNodeTypeVoice;
465 if (token.name()[0] == 'b')
466 return VTTNodeTypeBold;
467 if (token.name()[0] == 'i')
468 return VTTNodeTypeItalic;
469 if (token.name()[0] == 'u')
470 return VTTNodeTypeUnderline;
473 if (token.name()[0] == 'r' && token.name()[1] == 't')
474 return VTTNodeTypeRubyText;
477 if (token.name()[0] == 'r' && token.name()[1] == 'u' && token.name()[2] == 'b' && token.name()[3] == 'y')
478 return VTTNodeTypeRuby;
479 if (token.name()[0] == 'l' && token.name()[1] == 'a' && token.name()[2] == 'n' && token.name()[3] == 'g')
480 return VTTNodeTypeLanguage;
483 return VTTNodeTypeNone;
486 void VTTTreeBuilder::constructTreeFromToken(Document& document)
488 // http://dev.w3.org/html5/webvtt/#webvtt-cue-text-dom-construction-rules
490 switch (m_token.type()) {
491 case VTTTokenTypes::Character: {
492 RefPtr<Text> child = Text::create(document, m_token.characters());
493 m_currentNode->parserAppendChild(child);
496 case VTTTokenTypes::StartTag: {
497 VTTNodeType nodeType = tokenToNodeType(m_token);
498 if (nodeType == VTTNodeTypeNone)
501 VTTNodeType currentType = m_currentNode->isVTTElement() ? toVTTElement(m_currentNode.get())->webVTTNodeType() : VTTNodeTypeNone;
502 // <rt> is only allowed if the current node is <ruby>.
503 if (nodeType == VTTNodeTypeRubyText && currentType != VTTNodeTypeRuby)
506 RefPtrWillBeRawPtr<VTTElement> child = VTTElement::create(nodeType, &document);
507 if (!m_token.classes().isEmpty())
508 child->setAttribute(classAttr, m_token.classes());
510 if (nodeType == VTTNodeTypeVoice) {
511 child->setAttribute(VTTElement::voiceAttributeName(), m_token.annotation());
512 } else if (nodeType == VTTNodeTypeLanguage) {
513 m_languageStack.append(m_token.annotation());
514 child->setAttribute(VTTElement::langAttributeName(), m_languageStack.last());
516 if (!m_languageStack.isEmpty())
517 child->setLanguage(m_languageStack.last());
518 m_currentNode->parserAppendChild(child);
519 m_currentNode = child;
522 case VTTTokenTypes::EndTag: {
523 VTTNodeType nodeType = tokenToNodeType(m_token);
524 if (nodeType == VTTNodeTypeNone)
527 // The only non-VTTElement would be the DocumentFragment root. (Text
528 // nodes and PIs will never appear as m_currentNode.)
529 if (!m_currentNode->isVTTElement())
532 VTTNodeType currentType = toVTTElement(m_currentNode.get())->webVTTNodeType();
533 bool matchesCurrent = nodeType == currentType;
534 if (!matchesCurrent) {
535 // </ruby> auto-closes <rt>.
536 if (currentType == VTTNodeTypeRubyText && nodeType == VTTNodeTypeRuby) {
537 if (m_currentNode->parentNode())
538 m_currentNode = m_currentNode->parentNode();
543 if (nodeType == VTTNodeTypeLanguage)
544 m_languageStack.removeLast();
545 if (m_currentNode->parentNode())
546 m_currentNode = m_currentNode->parentNode();
549 case VTTTokenTypes::TimestampTag: {
550 String charactersString = m_token.characters();
551 double parsedTimeStamp;
552 if (VTTParser::collectTimeStamp(charactersString, parsedTimeStamp))
553 m_currentNode->parserAppendChild(ProcessingInstruction::create(document, "timestamp", charactersString));
561 void VTTParser::trace(Visitor* visitor)
563 visitor->trace(m_cueList);
564 visitor->trace(m_regionList);