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