2d0f381d9ff10545398abff5c453214da9026b98
[WebKit-https.git] / Source / WebCore / html / track / WebVTTParser.cpp
1 /*
2  * Copyright (C) 2011 Google Inc.  All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are
6  * met:
7  *
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
13  * distribution.
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.
17  *
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.
29  */
30
31 #include "config.h"
32
33 #if ENABLE(VIDEO_TRACK)
34
35 #include "WebVTTParser.h"
36
37 #include "HTMLElement.h"
38 #include "ProcessingInstruction.h"
39 #include "SegmentedString.h"
40 #include "Text.h"
41 #include "WebVTTElement.h"
42 #include <wtf/text/WTFString.h>
43
44 namespace WebCore {
45
46 const double secondsPerHour = 3600;
47 const double secondsPerMinute = 60;
48 const double secondsPerMillisecond = 0.001;
49 const double malformedTime = -1;
50 const unsigned bomLength = 3;
51 const unsigned fileIdentifierLength = 6;
52
53 String WebVTTParser::collectDigits(const String& input, unsigned* position)
54 {
55     StringBuilder digits;
56     while (*position < input.length() && isASCIIDigit(input[*position]))
57         digits.append(input[(*position)++]);
58     return digits.toString();
59 }
60
61 String WebVTTParser::collectWord(const String& input, unsigned* position)
62 {
63     StringBuilder string;
64     while (*position < input.length() && !isASpace(input[*position]))
65         string.append(input[(*position)++]);
66     return string.toString();
67 }
68
69 WebVTTParser::WebVTTParser(WebVTTParserClient* client, ScriptExecutionContext* context)
70     : m_scriptExecutionContext(context)
71     , m_state(Initial)
72     , m_currentStartTime(0)
73     , m_currentEndTime(0)
74     , m_tokenizer(WebVTTTokenizer::create())
75     , m_client(client)
76 {
77 }
78
79 void WebVTTParser::getNewCues(Vector<RefPtr<TextTrackCue> >& outputCues)
80 {
81     outputCues = m_cuelist;
82     m_cuelist.clear();
83 }
84
85 void WebVTTParser::parseBytes(const char* data, unsigned length)
86 {
87     // 4.8.10.13.3 WHATWG WebVTT Parser algorithm.
88     // 1-3 - Initial setup.
89     unsigned position = 0;
90
91     while (position < length) {
92         String line = collectNextLine(data, length, &position);
93         
94         switch (m_state) {
95         case Initial:
96             // Buffer up at least 9 bytes before proceeding with checking for the file identifier.
97             m_identifierData.append(data, length);
98             if (m_identifierData.size() < bomLength + fileIdentifierLength)
99                 return;
100
101             // 4-12 - Collect the first line and check for "WEBVTT".
102             if (!hasRequiredFileIdentifier()) {
103                 if (m_client)
104                     m_client->fileFailedToParse();
105                 return;
106             }
107
108             m_state = Header;
109             m_identifierData.clear();
110             break;
111         
112         case Header:
113             // 13-18 - Allow a header (comment area) under the WEBVTT line.
114             if (line.isEmpty())
115                 m_state = Id;
116             break;
117         
118         case Id:
119             // 19-29 - Allow any number of line terminators, then initialize new cue values.
120             if (line.isEmpty())
121                 break;
122             resetCueValues();
123             
124             // 30-39 - Check if this line contains an optional identifier or timing data.
125             m_state = collectCueId(line);
126             break;
127         
128         case TimingsAndSettings:
129             // 40 - Collect cue timings and settings.
130             m_state = collectTimingsAndSettings(line);
131             break;
132         
133         case CueText:
134             // 41-53 - Collect the cue text, create a cue, and add it to the output.
135             m_state = collectCueText(line, length, position);
136             break;
137
138         case BadCue:
139             // 54-62 - Collect and discard the remaining cue.
140             m_state = ignoreBadCue(line);
141             break;
142         }
143     }
144 }
145
146 bool WebVTTParser::hasRequiredFileIdentifier()
147 {
148     // A WebVTT file identifier consists of an optional BOM character,
149     // the string "WEBVTT" followed by an optional space or tab character,
150     // and any number of characters that are not line terminators ...
151     unsigned position = 0;
152     if (m_identifierData.size() >= bomLength && m_identifierData[0] == '\xEF' && m_identifierData[1] == '\xBB' && m_identifierData[2] == '\xBF')
153         position += bomLength;
154     String line = collectNextLine(m_identifierData.data(), m_identifierData.size(), &position);
155
156     if (line.length() < fileIdentifierLength)
157         return false;
158     if (line.substring(0, fileIdentifierLength) != "WEBVTT")
159         return false;
160     if (line.length() > fileIdentifierLength && line[fileIdentifierLength] != ' ' && line[fileIdentifierLength] != '\t')
161         return false;
162
163     return true;
164 }
165
166 WebVTTParser::ParseState WebVTTParser::collectCueId(const String& line)
167 {
168     if (line.contains("-->"))
169         return collectTimingsAndSettings(line);
170     m_currentId = line;
171     return TimingsAndSettings;
172 }
173
174 WebVTTParser::ParseState WebVTTParser::collectTimingsAndSettings(const String& line)
175 {
176     // 4.8.10.13.3 Collect WebVTT cue timings and settings.
177     // 1-3 - Let input be the string being parsed and position be a pointer into input
178     unsigned position = 0;
179     skipWhiteSpace(line, &position);
180
181     // 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.
182     m_currentStartTime = collectTimeStamp(line, &position);
183     if (m_currentStartTime == malformedTime)
184         return BadCue;
185     if (position >= line.length())
186         return BadCue;
187     char nextChar = line[position++];
188     if (nextChar != ' ' && nextChar != '\t')
189         return BadCue;
190     skipWhiteSpace(line, &position);
191
192     // 6-9 - If the next three characters are not "-->", abort and return failure.
193     if (line.find("-->", position) == notFound)
194         return BadCue;
195     position += 3;
196     if (position >= line.length())
197         return BadCue;
198     nextChar = line[position++];
199     if (nextChar != ' ' && nextChar != '\t')
200         return BadCue;
201     skipWhiteSpace(line, &position);
202
203     // 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.
204     m_currentEndTime = collectTimeStamp(line, &position);
205     if (m_currentEndTime == malformedTime)
206         return BadCue;
207     skipWhiteSpace(line, &position);
208
209     // 12 - Parse the WebVTT settings for the cue (conducted in TextTrackCue).
210     m_currentSettings = line.substring(position, line.length()-1);
211     return CueText;
212 }
213
214 WebVTTParser::ParseState WebVTTParser::collectCueText(const String& line, unsigned length, unsigned position)
215 {
216     if (line.isEmpty()) {
217         createNewCue();
218         return Id;
219     }
220     if (!m_currentContent.isEmpty())
221         m_currentContent.append("\n");
222     m_currentContent.append(line);
223
224     if (position >= length)
225         createNewCue();
226                 
227     return CueText;
228 }
229
230 WebVTTParser::ParseState WebVTTParser::ignoreBadCue(const String& line)
231 {
232     if (!line.isEmpty())
233         return BadCue;
234     return Id;
235 }
236
237 PassRefPtr<DocumentFragment>  WebVTTParser::createDocumentFragmentFromCueText(const String& text)
238 {
239     // Cue text processing based on
240     // 4.8.10.13.4 WebVTT cue text parsing rules and
241     // 4.8.10.13.5 WebVTT cue text DOM construction rules.
242
243     if (!text.length())
244         return 0;
245
246     ASSERT(m_scriptExecutionContext->isDocument());
247     Document* document = static_cast<Document*>(m_scriptExecutionContext);
248     
249     RefPtr<DocumentFragment> fragment = DocumentFragment::create(document);
250     m_currentNode = fragment;
251     m_tokenizer->reset();
252     m_token.clear();
253     
254     m_languageStack.clear();
255     SegmentedString content(text);
256     while (m_tokenizer->nextToken(content, m_token))
257         constructTreeFromToken(document);
258     
259     return fragment.release();
260 }
261
262 void WebVTTParser::createNewCue()
263 {
264     if (!m_currentContent.length())
265         return;
266
267     RefPtr<TextTrackCue> cue = TextTrackCue::create(m_scriptExecutionContext, m_currentStartTime, m_currentEndTime, m_currentContent.toString());
268     cue->setId(m_currentId);
269     cue->setCueSettings(m_currentSettings);
270
271     m_cuelist.append(cue);
272     if (m_client)
273         m_client->newCuesParsed();
274 }
275
276 void WebVTTParser::resetCueValues()
277 {
278     m_currentId = emptyString();
279     m_currentSettings = emptyString();
280     m_currentStartTime = 0;
281     m_currentEndTime = 0;
282     m_currentContent.clear();
283 }
284
285 double WebVTTParser::collectTimeStamp(const String& line, unsigned* position)
286 {
287     // 4.8.10.13.3 Collect a WebVTT timestamp.
288     // 1-4 - Initial checks, let most significant units be minutes.
289     enum Mode { minutes, hours };
290     Mode mode = minutes;
291     if (*position >= line.length() || !isASCIIDigit(line[*position]))
292         return malformedTime;
293
294     // 5-6 - Collect a sequence of characters that are 0-9.
295     String digits1 = collectDigits(line, position);
296     int value1 = digits1.toInt();
297
298     // 7 - If not 2 characters or value is greater than 59, interpret as hours.
299     if (digits1.length() != 2 || value1 > 59)
300         mode = hours;
301
302     // 8-12 - Collect the next sequence of 0-9 after ':' (must be 2 chars).
303     if (*position >= line.length() || line[(*position)++] != ':')
304         return malformedTime;
305     if (*position >= line.length() || !isASCIIDigit(line[(*position)]))
306         return malformedTime;
307     String digits2 = collectDigits(line, position);
308     int value2 = digits2.toInt();
309     if (digits2.length() != 2) 
310         return malformedTime;
311
312     // 13 - Detect whether this timestamp includes hours.
313     int value3;
314     if (mode == hours || (*position < line.length() && line[*position] == ':')) {
315         if (*position >= line.length() || line[(*position)++] != ':')
316             return malformedTime;
317         if (*position >= line.length() || !isASCIIDigit(line[*position]))
318             return malformedTime;
319         String digits3 = collectDigits(line, position);
320         if (digits3.length() != 2)
321             return malformedTime;
322         value3 = digits3.toInt();
323     } else {
324         value3 = value2;
325         value2 = value1;
326         value1 = 0;
327     }
328
329     // 14-19 - Collect next sequence of 0-9 after '.' (must be 3 chars).
330     if (*position >= line.length() || line[(*position)++] != '.')
331         return malformedTime;
332     if (*position >= line.length() || !isASCIIDigit(line[*position]))
333         return malformedTime;
334     String digits4 = collectDigits(line, position);
335     if (digits4.length() != 3)
336         return malformedTime;
337     int value4 = digits4.toInt();
338     if (value2 > 59 || value3 > 59)
339         return malformedTime;
340
341     // 20-21 - Calculate result.
342     return (value1 * secondsPerHour) + (value2 * secondsPerMinute) + value3 + (value4 * secondsPerMillisecond);
343 }
344
345 static WebVTTNodeType tokenToNodeType(WebVTTToken& token)
346 {
347     switch (token.name().size()) {
348     case 1:
349         if (token.name()[0] == 'c')
350             return WebVTTNodeTypeClass;
351         if (token.name()[0] == 'v')
352             return WebVTTNodeTypeVoice;
353         if (token.name()[0] == 'b')
354             return WebVTTNodeTypeBold;
355         if (token.name()[0] == 'i')
356             return WebVTTNodeTypeItalic;
357         if (token.name()[0] == 'u')
358             return WebVTTNodeTypeUnderline;
359         break;
360     case 2:
361         if (token.name()[0] == 'r' && token.name()[1] == 't')
362             return WebVTTNodeTypeRubyText;
363         break;
364     case 4:
365         if (token.name()[0] == 'r' && token.name()[1] == 'u' && token.name()[2] == 'b' && token.name()[3] == 'y')
366             return WebVTTNodeTypeRuby;
367         if (token.name()[0] == 'l' && token.name()[1] == 'a' && token.name()[2] == 'n' && token.name()[3] == 'g')
368             return WebVTTNodeTypeLanguage;
369         break;
370     }
371     return WebVTTNodeTypeNone;
372 }
373
374 void WebVTTParser::constructTreeFromToken(Document* document)
375 {
376     QualifiedName tagName(nullAtom, AtomicString(m_token.name()), xhtmlNamespaceURI);
377
378     // http://dev.w3.org/html5/webvtt/#webvtt-cue-text-dom-construction-rules
379
380     switch (m_token.type()) {
381     case WebVTTTokenTypes::Character: {
382         String content(m_token.characters()); // FIXME: This should be 8bit if possible.
383         RefPtr<Text> child = Text::create(document, content);
384         m_currentNode->parserAppendChild(child);
385         break;
386     }
387     case WebVTTTokenTypes::StartTag: {
388         RefPtr<WebVTTElement> child;
389         WebVTTNodeType nodeType = tokenToNodeType(m_token);
390         if (nodeType != WebVTTNodeTypeNone)
391             child = WebVTTElement::create(nodeType, document);
392         if (child) {
393             if (m_token.classes().size() > 0)
394                 child->setAttribute(classAttr, AtomicString(m_token.classes()));
395
396             if (child->webVTTNodeType() == WebVTTNodeTypeVoice)
397                 child->setAttribute(WebVTTElement::voiceAttributeName(), AtomicString(m_token.annotation()));
398             else if (child->webVTTNodeType() == WebVTTNodeTypeLanguage) {
399                 m_languageStack.append(AtomicString(m_token.annotation()));
400                 child->setAttribute(WebVTTElement::langAttributeName(), m_languageStack.last());
401             }
402             if (!m_languageStack.isEmpty())
403                 child->setLanguage(m_languageStack.last());
404             m_currentNode->parserAppendChild(child);
405             m_currentNode = child;
406         }
407         break;
408     }
409     case WebVTTTokenTypes::EndTag: {
410         WebVTTNodeType nodeType = tokenToNodeType(m_token);
411         if (nodeType != WebVTTNodeTypeNone) {
412             if (nodeType == WebVTTNodeTypeLanguage && m_currentNode->isWebVTTElement() && toWebVTTElement(m_currentNode.get())->webVTTNodeType() == WebVTTNodeTypeLanguage)
413                 m_languageStack.removeLast();
414             if (m_currentNode->parentNode())
415                 m_currentNode = m_currentNode->parentNode();
416         }
417         break;
418     }
419     case WebVTTTokenTypes::TimestampTag: {
420         unsigned position = 0;
421         String charactersString(StringImpl::create8BitIfPossible(m_token.characters()));
422         double time = collectTimeStamp(charactersString, &position);
423         if (time != malformedTime)
424             m_currentNode->parserAppendChild(ProcessingInstruction::create(document, "timestamp", charactersString));
425         break;
426     }
427     default:
428         break;
429     }
430     m_token.clear();
431 }
432
433 void WebVTTParser::skipWhiteSpace(const String& line, unsigned* position)
434 {
435     while (*position < line.length() && isASpace(line[*position]))
436         (*position)++;
437 }
438
439 void WebVTTParser::skipLineTerminator(const char* data, unsigned length, unsigned* position)
440 {
441     if (*position >= length)
442         return;
443     if (data[*position] == '\r')
444         (*position)++;
445     if (*position >= length)
446         return;
447     if (data[*position] == '\n')
448         (*position)++;
449 }
450
451 String WebVTTParser::collectNextLine(const char* data, unsigned length, unsigned* position)
452 {
453     unsigned oldPosition = *position;
454     while (*position < length && data[*position] != '\r' && data[*position] != '\n')
455         (*position)++;
456     String line = String::fromUTF8(data + oldPosition, *position - oldPosition);
457     skipLineTerminator(data, length, position);
458     return line;
459 }
460
461 }
462
463 #endif