bf7d6035686ab77e539b4b50e850beb07f2e7a05
[WebKit-https.git] / Source / WebCore / html / track / WebVTTParser.cpp
1 /*
2  * Copyright (C) 2011, 2013 Google Inc.  All rights reserved.
3  * Copyright (C) 2013 Cable Television Labs, Inc.
4  * Copyright (C) 2014 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
35 #if ENABLE(VIDEO_TRACK)
36
37 #include "WebVTTParser.h"
38
39 #include "ProcessingInstruction.h"
40 #include "Text.h"
41 #include "VTTScanner.h"
42 #include "WebVTTElement.h"
43
44 namespace WebCore {
45
46 const double secondsPerHour = 3600;
47 const double secondsPerMinute = 60;
48 const double secondsPerMillisecond = 0.001;
49 const char* fileIdentifier = "WEBVTT";
50 const unsigned fileIdentifierLength = 6;
51
52 #if ENABLE(WEBVTT_REGIONS)
53 bool WebVTTParser::parseFloatPercentageValue(VTTScanner& valueScanner, float& percentage)
54 {
55     float number;
56     if (!valueScanner.scanFloat(number))
57         return false;
58     // '%' must be present and at the end of the setting value.
59     if (!valueScanner.scan('%'))
60         return false;
61
62     if (number < 0 || number > 100)
63         return false;
64
65     percentage = number;
66     return true;
67 }
68
69 bool WebVTTParser::parseFloatPercentageValuePair(VTTScanner& valueScanner, char delimiter, FloatPoint& valuePair)
70 {
71     float firstCoord;
72     if (!parseFloatPercentageValue(valueScanner, firstCoord))
73         return false;
74
75     if (!valueScanner.scan(delimiter))
76         return false;
77
78     float secondCoord;
79     if (!parseFloatPercentageValue(valueScanner, secondCoord))
80         return false;
81
82     valuePair = FloatPoint(firstCoord, secondCoord);
83     return true;
84 }
85 #endif
86
87 WebVTTParser::WebVTTParser(WebVTTParserClient* client, ScriptExecutionContext* context)
88     : m_scriptExecutionContext(context)
89     , m_state(Initial)
90     , m_decoder(TextResourceDecoder::create("text/plain", UTF8Encoding()))
91     , m_currentStartTime(0)
92     , m_currentEndTime(0)
93     , m_client(client)
94 {
95 }
96
97 void WebVTTParser::getNewCues(Vector<RefPtr<WebVTTCueData>>& outputCues)
98 {
99     outputCues = m_cuelist;
100     m_cuelist.clear();
101 }
102
103 #if ENABLE(WEBVTT_REGIONS)
104 void WebVTTParser::getNewRegions(Vector<RefPtr<VTTRegion>>& outputRegions)
105 {
106     outputRegions = m_regionList;
107     m_regionList.clear();
108 }
109 #endif
110
111 void WebVTTParser::parseBytes(const char* data, unsigned length)
112 {
113     String textData = m_decoder->decode(data, length);
114     m_lineReader.append(textData);
115     parse();
116 }
117
118 void WebVTTParser::flush()
119 {
120     String textData = m_decoder->flush();
121     m_lineReader.append(textData);
122     m_lineReader.setEndOfStream();
123     parse();
124     flushPendingCue();
125 }
126
127 void WebVTTParser::parse()
128 {    
129     // WebVTT parser algorithm. (5.1 WebVTT file parsing.)
130     // Steps 1 - 3 - Initial setup.
131     String line;
132     while (m_lineReader.getLine(line)) {
133         if (line.isNull())
134             return;
135
136         switch (m_state) {
137         case Initial:
138             // Steps 4 - 9 - Check for a valid WebVTT signature.
139             if (!hasRequiredFileIdentifier(line)) {
140                 if (m_client)
141                     m_client->fileFailedToParse();
142                 return;
143             }
144
145             m_state = Header;
146             break;
147
148         case Header:
149             collectMetadataHeader(line);
150
151             if (line.isEmpty()) {
152 #if ENABLE(WEBVTT_REGIONS)
153                 // Steps 10-14 - Allow a header (comment area) under the WEBVTT line.
154                 if (m_client && m_regionList.size())
155                     m_client->newRegionsParsed();
156 #endif
157                 m_state = Id;
158                 break;
159             }
160             // Step 15 - Break out of header loop if the line could be a timestamp line.
161             if (line.contains("-->"))
162                 m_state = recoverCue(line);
163
164             // Step 16 - Line is not the empty string and does not contain "-->".
165             break;
166
167         case Id:
168             // Steps 17 - 20 - Allow any number of line terminators, then initialize new cue values.
169             if (line.isEmpty())
170                 break;
171
172             // Step 21 - Cue creation (start a new cue).
173             resetCueValues();
174
175             // Steps 22 - 25 - Check if this line contains an optional identifier or timing data.
176             m_state = collectCueId(line);
177             break;
178
179         case TimingsAndSettings:
180             // Steps 26 - 27 - Discard current cue if the line is empty.
181             if (line.isEmpty()) {
182                 m_state = Id;
183                 break;
184             }
185
186             // Steps 28 - 29 - Collect cue timings and settings.
187             m_state = collectTimingsAndSettings(line);
188             break;
189
190         case CueText:
191             // Steps 31 - 41 - Collect the cue text, create a cue, and add it to the output.
192             m_state = collectCueText(line);
193             break;
194
195         case BadCue:
196             // Steps 42 - 48 - Discard lines until an empty line or a potential timing line is seen.
197             m_state = ignoreBadCue(line);
198             break;
199
200         case Finished:
201             ASSERT_NOT_REACHED();
202             break;
203         }
204     }
205 }
206
207 void WebVTTParser::fileFinished()
208 {
209     ASSERT(m_state != Finished);
210     parseBytes("\n\n", 2);
211     m_state = Finished;
212 }
213
214 void WebVTTParser::flushPendingCue()
215 {
216     ASSERT(m_lineReader.isAtEndOfStream());
217     // If we're in the CueText state when we run out of data, we emit the pending cue.
218     if (m_state == CueText)
219         createNewCue();
220 }
221
222 bool WebVTTParser::hasRequiredFileIdentifier(const String& line)
223 {
224     // A WebVTT file identifier consists of an optional BOM character,
225     // the string "WEBVTT" followed by an optional space or tab character,
226     // and any number of characters that are not line terminators ...
227     if (line.isEmpty())
228         return false;
229
230     if (!line.startsWith(fileIdentifier, fileIdentifierLength))
231         return false;
232
233     if (line.length() > fileIdentifierLength && !isASpace(line[fileIdentifierLength]))
234         return false;
235     return true;
236 }
237
238 void WebVTTParser::collectMetadataHeader(const String& line)
239 {
240 #if ENABLE(WEBVTT_REGIONS)
241     // WebVTT header parsing (WebVTT parser algorithm step 12)
242     DEPRECATED_DEFINE_STATIC_LOCAL(const AtomicString, regionHeaderName, ("Region", AtomicString::ConstructFromLiteral));
243
244     // Step 12.4 If line contains the character ":" (A U+003A COLON), then set metadata's
245     // name to the substring of line before the first ":" character and
246     // metadata's value to the substring after this character.
247     size_t colonPosition = line.find(':');
248     if (colonPosition == notFound)
249         return;
250
251     String headerName = line.substring(0, colonPosition);
252
253     // Step 12.5 If metadata's name equals "Region":
254     if (headerName == regionHeaderName) {
255         String headerValue = line.substring(colonPosition + 1, line.length() - 1);
256         // Steps 12.5.1 - 12.5.11 Region creation: Let region be a new text track region [...]
257         createNewRegion(headerValue);
258     }
259 #else
260     UNUSED_PARAM(line);
261 #endif
262 }
263
264 WebVTTParser::ParseState WebVTTParser::collectCueId(const String& line)
265 {
266     if (line.contains("-->"))
267         return collectTimingsAndSettings(line);
268     m_currentId = line;
269     return TimingsAndSettings;
270 }
271
272 WebVTTParser::ParseState WebVTTParser::collectTimingsAndSettings(const String& line)
273 {
274     VTTScanner input(line);
275
276     // Collect WebVTT cue timings and settings. (5.3 WebVTT cue timings and settings parsing.)
277     // Steps 1 - 3 - Let input be the string being parsed and position be a pointer into input
278     input.skipWhile<isASpace>();
279
280     // 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.
281     if (!collectTimeStamp(input, m_currentStartTime))
282         return BadCue;
283     
284     input.skipWhile<isASpace>();
285
286     // Steps 6 - 9 - If the next three characters are not "-->", abort and return failure.
287     if (!input.scan("-->"))
288         return BadCue;
289     
290     input.skipWhile<isASpace>();
291
292     // 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.
293     if (!collectTimeStamp(input, m_currentEndTime))
294         return BadCue;
295
296     input.skipWhile<isASpace>();
297
298     // Step 12 - Parse the WebVTT settings for the cue (conducted in TextTrackCue).
299     m_currentSettings = input.restOfInputAsString();
300     return CueText;
301 }
302
303 WebVTTParser::ParseState WebVTTParser::collectCueText(const String& line)
304 {
305     // Step 34.
306     if (line.isEmpty()) {
307         createNewCue();
308         return Id;
309     }
310     // Step 35.
311     if (line.contains("-->")) {
312         // Step 39-40.
313         createNewCue();
314
315         // Step 41 - New iteration of the cue loop.
316         return recoverCue(line);
317     }
318     if (!m_currentContent.isEmpty())
319         m_currentContent.append("\n");
320     m_currentContent.append(line);
321
322     return CueText;
323 }
324
325 WebVTTParser::ParseState WebVTTParser::recoverCue(const String& line)
326 {
327     // Step 17 and 21.
328     resetCueValues();
329
330     // Step 22.
331     return collectTimingsAndSettings(line);
332 }
333
334 WebVTTParser::ParseState WebVTTParser::ignoreBadCue(const String& line)
335 {
336     if (line.isEmpty())
337         return Id;
338     if (line.contains("-->"))
339         return recoverCue(line);
340     return BadCue;
341 }
342
343 // A helper class for the construction of a "cue fragment" from the cue text.
344 class WebVTTTreeBuilder {
345 public:
346     WebVTTTreeBuilder(Document& document)
347         : m_document(document) { }
348
349     PassRefPtr<DocumentFragment> buildFromString(const String& cueText);
350
351 private:
352     void constructTreeFromToken(Document&);
353
354     WebVTTToken m_token;
355     RefPtr<ContainerNode> m_currentNode;
356     Vector<AtomicString> m_languageStack;
357     Document& m_document;
358 };
359
360 PassRefPtr<DocumentFragment> WebVTTTreeBuilder::buildFromString(const String& cueText)
361 {
362     // Cue text processing based on
363     // 5.4 WebVTT cue text parsing rules, and
364     // 5.5 WebVTT cue text DOM construction rules.
365     RefPtr<DocumentFragment> fragment = DocumentFragment::create(m_document);
366
367     if (cueText.isEmpty()) {
368         fragment->parserAppendChild(Text::create(m_document, emptyString()));
369         return fragment.release();
370     }
371
372     m_currentNode = fragment;
373
374     WebVTTTokenizer tokenizer(cueText);
375     m_languageStack.clear();
376
377     while (tokenizer.nextToken(m_token))
378         constructTreeFromToken(m_document);
379     
380     return fragment.release();
381 }
382
383 PassRefPtr<DocumentFragment> WebVTTParser::createDocumentFragmentFromCueText(Document& document, const String& cueText)
384 {
385     WebVTTTreeBuilder treeBuilder(document);
386     return treeBuilder.buildFromString(cueText);
387 }
388
389 void WebVTTParser::createNewCue()
390 {
391     RefPtr<WebVTTCueData> cue = WebVTTCueData::create();
392     cue->setStartTime(m_currentStartTime);
393     cue->setEndTime(m_currentEndTime);
394     cue->setContent(m_currentContent.toString());
395     cue->setId(m_currentId);
396     cue->setSettings(m_currentSettings);
397
398     m_cuelist.append(cue);
399     if (m_client)
400         m_client->newCuesParsed();
401 }
402
403 void WebVTTParser::resetCueValues()
404 {
405     m_currentId = emptyString();
406     m_currentSettings = emptyString();
407     m_currentStartTime = 0;
408     m_currentEndTime = 0;
409     m_currentContent.clear();
410 }
411
412 #if ENABLE(WEBVTT_REGIONS)
413 void WebVTTParser::createNewRegion(const String& headerValue)
414 {
415     if (headerValue.isEmpty())
416         return;
417
418     // Steps 12.5.1 - 12.5.9 - Construct and initialize a WebVTT Region object.
419     RefPtr<VTTRegion> region = VTTRegion::create(*m_scriptExecutionContext);
420     region->setRegionSettings(headerValue);
421
422     // Step 12.5.10 If the text track list of regions regions contains a region
423     // with the same region identifier value as region, remove that region.
424     for (size_t i = 0; i < m_regionList.size(); ++i)
425         if (m_regionList[i]->id() == region->id()) {
426             m_regionList.remove(i);
427             break;
428         }
429
430     // Step 12.5.11
431     m_regionList.append(region);
432 }
433 #endif
434
435 bool WebVTTParser::collectTimeStamp(const String& line, double& timeStamp)
436 {
437     VTTScanner input(line);
438     return collectTimeStamp(input, timeStamp);
439 }
440
441 bool WebVTTParser::collectTimeStamp(VTTScanner& input, double& timeStamp)
442 {
443     // Collect a WebVTT timestamp (5.3 WebVTT cue timings and settings parsing.)
444     // Steps 1 - 4 - Initial checks, let most significant units be minutes.
445     enum Mode { minutes, hours };
446     Mode mode = minutes;
447
448     // Steps 5 - 7 - Collect a sequence of characters that are 0-9.
449     // If not 2 characters or value is greater than 59, interpret as hours.
450     int value1;
451     unsigned value1Digits = input.scanDigits(value1);
452     if (!value1Digits)
453         return false;
454     if (value1Digits != 2 || value1 > 59)
455         mode = hours;
456
457     // Steps 8 - 11 - Collect the next sequence of 0-9 after ':' (must be 2 chars).
458     int value2;
459     if (!input.scan(':') || input.scanDigits(value2) != 2)
460         return false;
461
462     // Step 12 - Detect whether this timestamp includes hours.
463     int value3;
464     if (mode == hours || input.match(':')) {
465         if (!input.scan(':') || input.scanDigits(value3) != 2)
466             return false;
467     } else {
468         value3 = value2;
469         value2 = value1;
470         value1 = 0;
471     }
472
473     // Steps 13 - 17 - Collect next sequence of 0-9 after '.' (must be 3 chars).
474     int value4;
475     if (!input.scan('.') || input.scanDigits(value4) != 3)
476         return false;
477     if (value2 > 59 || value3 > 59)
478         return false;
479
480     // Steps 18 - 19 - Calculate result.
481     timeStamp = (value1 * secondsPerHour) + (value2 * secondsPerMinute) + value3 + (value4 * secondsPerMillisecond);
482     return true;
483 }
484
485 static WebVTTNodeType tokenToNodeType(WebVTTToken& token)
486 {
487     switch (token.name().length()) {
488     case 1:
489         if (token.name()[0] == 'c')
490             return WebVTTNodeTypeClass;
491         if (token.name()[0] == 'v')
492             return WebVTTNodeTypeVoice;
493         if (token.name()[0] == 'b')
494             return WebVTTNodeTypeBold;
495         if (token.name()[0] == 'i')
496             return WebVTTNodeTypeItalic;
497         if (token.name()[0] == 'u')
498             return WebVTTNodeTypeUnderline;
499         break;
500     case 2:
501         if (token.name()[0] == 'r' && token.name()[1] == 't')
502             return WebVTTNodeTypeRubyText;
503         break;
504     case 4:
505         if (token.name()[0] == 'r' && token.name()[1] == 'u' && token.name()[2] == 'b' && token.name()[3] == 'y')
506             return WebVTTNodeTypeRuby;
507         if (token.name()[0] == 'l' && token.name()[1] == 'a' && token.name()[2] == 'n' && token.name()[3] == 'g')
508             return WebVTTNodeTypeLanguage;
509         break;
510     }
511     return WebVTTNodeTypeNone;
512 }
513
514 void WebVTTTreeBuilder::constructTreeFromToken(Document& document)
515 {
516     // http://dev.w3.org/html5/webvtt/#webvtt-cue-text-dom-construction-rules
517
518     switch (m_token.type()) {
519     case WebVTTTokenTypes::Character: {
520         RefPtr<Text> child = Text::create(document, m_token.characters());
521         m_currentNode->parserAppendChild(child);
522         break;
523     }
524     case WebVTTTokenTypes::StartTag: {
525         WebVTTNodeType nodeType = tokenToNodeType(m_token);
526         if (nodeType == WebVTTNodeTypeNone)
527             break;
528
529         WebVTTNodeType currentType = m_currentNode->isWebVTTElement() ? toWebVTTElement(m_currentNode.get())->webVTTNodeType() : WebVTTNodeTypeNone;
530         // <rt> is only allowed if the current node is <ruby>.
531         if (nodeType == WebVTTNodeTypeRubyText && currentType != WebVTTNodeTypeRuby)
532             break;
533
534         RefPtr<WebVTTElement> child = WebVTTElement::create(nodeType, document);
535         if (!m_token.classes().isEmpty())
536             child->setAttribute(classAttr, m_token.classes());
537
538         if (nodeType == WebVTTNodeTypeVoice)
539             child->setAttribute(WebVTTElement::voiceAttributeName(), m_token.annotation());
540         else if (nodeType == WebVTTNodeTypeLanguage) {
541             m_languageStack.append(m_token.annotation());
542             child->setAttribute(WebVTTElement::langAttributeName(), m_languageStack.last());
543         }
544         if (!m_languageStack.isEmpty())
545             child->setLanguage(m_languageStack.last());
546         m_currentNode->parserAppendChild(child);
547         m_currentNode = child;
548         break;
549     }
550     case WebVTTTokenTypes::EndTag: {
551         WebVTTNodeType nodeType = tokenToNodeType(m_token);
552         if (nodeType == WebVTTNodeTypeNone)
553             break;
554         
555         // The only non-VTTElement would be the DocumentFragment root. (Text
556         // nodes and PIs will never appear as m_currentNode.)
557         if (!m_currentNode->isWebVTTElement())
558             break;
559
560         WebVTTNodeType currentType = toWebVTTElement(m_currentNode.get())->webVTTNodeType();
561         bool matchesCurrent = nodeType == currentType;
562         if (!matchesCurrent) {
563             // </ruby> auto-closes <rt>
564             if (currentType == WebVTTNodeTypeRubyText && nodeType == WebVTTNodeTypeRuby) {
565                 if (m_currentNode->parentNode())
566                     m_currentNode = m_currentNode->parentNode();
567             } else
568                 break;
569         }
570         if (nodeType == WebVTTNodeTypeLanguage)
571             m_languageStack.removeLast();
572         if (m_currentNode->parentNode())
573             m_currentNode = m_currentNode->parentNode();
574         break;
575     }
576     case WebVTTTokenTypes::TimestampTag: {
577         String charactersString = m_token.characters();
578         double parsedTimeStamp;
579         if (WebVTTParser::collectTimeStamp(charactersString, parsedTimeStamp))
580             m_currentNode->parserAppendChild(ProcessingInstruction::create(document, "timestamp", charactersString));
581         break;
582     }
583     default:
584         break;
585     }
586 }
587
588 }
589
590 #endif