blob: be67a4aad3400c8f5ad593f2aaf20cc294551e57 [file] [log] [blame]
/*
* Copyright (C) 2011, 2013 Google Inc. All rights reserved.
* Copyright (C) 2013 Cable Television Labs, Inc.
* Copyright (C) 2011-2020 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "WebVTTParser.h"
#if ENABLE(VIDEO)
#include "Document.h"
#include "HTMLParserIdioms.h"
#include "ISOVTTCue.h"
#include "ProcessingInstruction.h"
#include "StyleRule.h"
#include "StyleRuleImport.h"
#include "StyleSheetContents.h"
#include "Text.h"
#include "VTTScanner.h"
#include "WebVTTElement.h"
#include "WebVTTTokenizer.h"
namespace WebCore {
const double secondsPerHour = 3600;
const double secondsPerMinute = 60;
const double secondsPerMillisecond = 0.001;
const char* fileIdentifier = "WEBVTT";
const unsigned fileIdentifierLength = 6;
const unsigned regionIdentifierLength = 6;
const unsigned styleIdentifierLength = 5;
bool WebVTTParser::parseFloatPercentageValue(VTTScanner& valueScanner, float& percentage)
{
float number;
if (!valueScanner.scanFloat(number))
return false;
// '%' must be present and at the end of the setting value.
if (!valueScanner.scan('%'))
return false;
if (number < 0 || number > 100)
return false;
percentage = number;
return true;
}
bool WebVTTParser::parseFloatPercentageValuePair(VTTScanner& valueScanner, char delimiter, FloatPoint& valuePair)
{
float firstCoord;
if (!parseFloatPercentageValue(valueScanner, firstCoord))
return false;
if (!valueScanner.scan(delimiter))
return false;
float secondCoord;
if (!parseFloatPercentageValue(valueScanner, secondCoord))
return false;
valuePair = FloatPoint(firstCoord, secondCoord);
return true;
}
WebVTTParser::WebVTTParser(WebVTTParserClient& client, Document& document)
: m_document(document)
, m_decoder(TextResourceDecoder::create("text/plain", UTF8Encoding()))
, m_client(client)
{
}
Vector<Ref<WebVTTCueData>> WebVTTParser::takeCues()
{
return WTFMove(m_cuelist);
}
Vector<Ref<VTTRegion>> WebVTTParser::takeRegions()
{
return WTFMove(m_regionList);
}
Vector<String> WebVTTParser::takeStyleSheets()
{
return WTFMove(m_styleSheets);
}
void WebVTTParser::parseFileHeader(String&& data)
{
m_state = Initial;
m_lineReader.reset();
m_lineReader.append(WTFMove(data));
parse();
}
void WebVTTParser::parseBytes(const uint8_t* data, unsigned length)
{
m_lineReader.append(m_decoder->decode(data, length));
parse();
}
void WebVTTParser::parseCueData(const ISOWebVTTCue& data)
{
auto cue = WebVTTCueData::create();
MediaTime startTime = data.presentationTime();
cue->setStartTime(startTime);
cue->setEndTime(startTime + data.duration());
cue->setContent(data.cueText());
cue->setId(data.id());
cue->setSettings(data.settings());
MediaTime originalStartTime;
if (WebVTTParser::collectTimeStamp(data.originalStartTime(), originalStartTime))
cue->setOriginalStartTime(originalStartTime);
m_cuelist.append(WTFMove(cue));
m_client.newCuesParsed();
}
void WebVTTParser::flush()
{
m_lineReader.append(m_decoder->flush());
m_lineReader.appendEndOfStream();
parse();
flushPendingCue();
}
void WebVTTParser::parse()
{
// WebVTT parser algorithm. (5.1 WebVTT file parsing.)
// Steps 1 - 3 - Initial setup.
while (auto line = m_lineReader.nextLine()) {
switch (m_state) {
case Initial:
// Steps 4 - 9 - Check for a valid WebVTT signature.
if (!hasRequiredFileIdentifier(*line)) {
m_client.fileFailedToParse();
return;
}
m_state = Header;
break;
case Header:
// Steps 11 - 14 - Collect WebVTT block
m_state = collectWebVTTBlock(*line);
break;
case Region:
m_state = collectRegionSettings(*line);
break;
case Style:
m_state = collectStyleSheet(*line);
break;
case Id:
// Steps 17 - 20 - Allow any number of line terminators, then initialize new cue values.
if (line->isEmpty())
break;
// Step 21 - Cue creation (start a new cue).
resetCueValues();
// Steps 22 - 25 - Check if this line contains an optional identifier or timing data.
m_state = collectCueId(*line);
break;
case TimingsAndSettings:
// Steps 26 - 27 - Discard current cue if the line is empty.
if (line->isEmpty()) {
m_state = Id;
break;
}
// Steps 28 - 29 - Collect cue timings and settings.
m_state = collectTimingsAndSettings(*line);
break;
case CueText:
// Steps 31 - 41 - Collect the cue text, create a cue, and add it to the output.
m_state = collectCueText(*line);
break;
case BadCue:
// Steps 42 - 48 - Discard lines until an empty line or a potential timing line is seen.
m_state = ignoreBadCue(*line);
break;
case Finished:
ASSERT_NOT_REACHED();
break;
}
}
}
void WebVTTParser::fileFinished()
{
ASSERT(m_state != Finished);
constexpr uint8_t endLines[] = { '\n', '\n' };
parseBytes(endLines, 2);
m_state = Finished;
}
void WebVTTParser::flushPendingCue()
{
ASSERT(m_lineReader.isAtEndOfStream());
// If we're in the CueText state when we run out of data, we emit the pending cue.
if (m_state == CueText)
createNewCue();
}
bool WebVTTParser::hasRequiredFileIdentifier(const String& line)
{
// A WebVTT file identifier consists of an optional BOM character,
// the string "WEBVTT" followed by an optional space or tab character,
// and any number of characters that are not line terminators ...
if (!line.startsWith(fileIdentifier))
return false;
if (line.length() > fileIdentifierLength && !isHTMLSpace(line[fileIdentifierLength]))
return false;
return true;
}
WebVTTParser::ParseState WebVTTParser::collectRegionSettings(const String& line)
{
// End of region block
if (checkAndStoreRegion(line))
return checkAndRecoverCue(line);
m_currentRegion->setRegionSettings(line);
return Region;
}
WebVTTParser::ParseState WebVTTParser::collectWebVTTBlock(const String& line)
{
// collect a WebVTT block parsing. (WebVTT parser algorithm step 14)
if (checkAndCreateRegion(line))
return Region;
if (checkStyleSheet(line))
return Style;
// Handle cue block.
ParseState state = checkAndRecoverCue(line);
if (state != Header) {
if (!m_regionList.isEmpty())
m_client.newRegionsParsed();
if (!m_styleSheets.isEmpty())
m_client.newStyleSheetsParsed();
if (!m_previousLine.isEmpty() && !m_previousLine.contains("-->"))
m_currentId = m_previousLine;
return state;
}
// store previous line for cue id.
// length is more than 1 line clear m_previousLine and ignore line.
if (m_previousLine.isEmpty())
m_previousLine = line;
else
m_previousLine = emptyString();
return state;
}
WebVTTParser::ParseState WebVTTParser::checkAndRecoverCue(const String& line)
{
// parse cue timings and settings
if (line.contains("-->")) {
ParseState state = recoverCue(line);
if (state != BadCue)
return state;
}
return Header;
}
WebVTTParser::ParseState WebVTTParser::collectStyleSheet(const String& line)
{
// End of style block
if (checkAndStoreStyleSheet(line))
return checkAndRecoverCue(line);
m_currentSourceStyleSheet.append(line);
return Style;
}
bool WebVTTParser::checkAndCreateRegion(const String& line)
{
if (m_previousLine.contains("-->"))
return false;
// line starts with the substring "REGION" and remaining characters
// zero or more U+0020 SPACE characters or U+0009 CHARACTER TABULATION
// (tab) characters expected other than these charecters it is invalid.
if (line.startsWith("REGION") && line.substring(regionIdentifierLength).isAllSpecialCharacters<isASpace>()) {
m_currentRegion = VTTRegion::create(m_document);
return true;
}
return false;
}
bool WebVTTParser::checkAndStoreRegion(const String& line)
{
if (!line.isEmpty() && !line.contains("-->"))
return false;
if (!m_currentRegion->id().isEmpty()) {
m_regionList.removeFirstMatching([this] (auto& region) {
return region->id() == m_currentRegion->id();
});
m_regionList.append(m_currentRegion.releaseNonNull());
}
m_currentRegion = nullptr;
return true;
}
bool WebVTTParser::checkStyleSheet(const String& line)
{
if (m_previousLine.contains("-->"))
return false;
// line starts with the substring "STYLE" and remaining characters
// zero or more U+0020 SPACE characters or U+0009 CHARACTER TABULATION
// (tab) characters expected other than these charecters it is invalid.
if (line.startsWith("STYLE") && line.substring(styleIdentifierLength).isAllSpecialCharacters<isASpace>())
return true;
return false;
}
bool WebVTTParser::checkAndStoreStyleSheet(const String& line)
{
if (!line.isEmpty() && !line.contains("-->"))
return false;
auto styleSheetText = WTFMove(m_currentSourceStyleSheet);
// WebVTTMode disallows non-data URLs.
auto contents = StyleSheetContents::create(CSSParserContext(WebVTTMode));
if (!contents->parseString(styleSheetText))
return true;
auto& namespaceRules = contents->namespaceRules();
if (namespaceRules.size())
return true;
auto& importRules = contents->importRules();
if (importRules.size())
return true;
auto& childRules = contents->childRules();
if (!childRules.size())
return true;
StringBuilder sanitizedStyleSheetBuilder;
for (const auto& rule : childRules) {
if (!rule->isStyleRule())
return true;
const auto& styleRule = downcast<StyleRule>(*rule);
const auto& selectorList = styleRule.selectorList();
if (selectorList.listSize() != 1)
return true;
auto selector = selectorList.selectorAt(0);
auto selectorText = selector->selectorText();
bool isCue = selectorText == "::cue" || selectorText.startsWith("::cue(");
if (!isCue)
return true;
if (styleRule.properties().isEmpty())
continue;
sanitizedStyleSheetBuilder.append(selectorText, " { ", styleRule.properties().asText(), " }\n");
}
// It would be more stylish to parse the stylesheet only once instead of serializing a sanitized version.
if (!sanitizedStyleSheetBuilder.isEmpty())
m_styleSheets.append(sanitizedStyleSheetBuilder.toString());
return true;
}
WebVTTParser::ParseState WebVTTParser::collectCueId(const String& line)
{
if (line.contains("-->"))
return collectTimingsAndSettings(line);
m_currentId = line;
return TimingsAndSettings;
}
WebVTTParser::ParseState WebVTTParser::collectTimingsAndSettings(const String& line)
{
if (line.isEmpty())
return BadCue;
VTTScanner input(line);
// Collect WebVTT cue timings and settings. (5.3 WebVTT cue timings and settings parsing.)
// Steps 1 - 3 - Let input be the string being parsed and position be a pointer into input
input.skipWhile<isHTMLSpace<UChar>>();
// 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.
if (!collectTimeStamp(input, m_currentStartTime))
return BadCue;
input.skipWhile<isHTMLSpace<UChar>>();
// Steps 6 - 9 - If the next three characters are not "-->", abort and return failure.
if (!input.scan("-->"))
return BadCue;
input.skipWhile<isHTMLSpace<UChar>>();
// 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.
if (!collectTimeStamp(input, m_currentEndTime))
return BadCue;
input.skipWhile<isHTMLSpace<UChar>>();
// Step 12 - Parse the WebVTT settings for the cue (conducted in TextTrackCue).
m_currentSettings = input.restOfInputAsString();
return CueText;
}
WebVTTParser::ParseState WebVTTParser::collectCueText(const String& line)
{
// Step 34.
if (line.isEmpty()) {
createNewCue();
return Id;
}
// Step 35.
if (line.contains("-->")) {
// Step 39-40.
createNewCue();
// Step 41 - New iteration of the cue loop.
return recoverCue(line);
}
if (!m_currentContent.isEmpty())
m_currentContent.append('\n');
m_currentContent.append(line);
return CueText;
}
WebVTTParser::ParseState WebVTTParser::recoverCue(const String& line)
{
// Step 17 and 21.
resetCueValues();
// Step 22.
return collectTimingsAndSettings(line);
}
WebVTTParser::ParseState WebVTTParser::ignoreBadCue(const String& line)
{
if (line.isEmpty())
return Id;
if (line.contains("-->"))
return recoverCue(line);
return BadCue;
}
// A helper class for the construction of a "cue fragment" from the cue text.
class WebVTTTreeBuilder {
public:
WebVTTTreeBuilder(Document& document)
: m_document(document) { }
Ref<DocumentFragment> buildFromString(const String& cueText);
private:
void constructTreeFromToken(Document&);
WebVTTToken m_token;
RefPtr<ContainerNode> m_currentNode;
Vector<AtomString> m_languageStack;
Document& m_document;
};
Ref<DocumentFragment> WebVTTTreeBuilder::buildFromString(const String& cueText)
{
// Cue text processing based on
// 5.4 WebVTT cue text parsing rules, and
// 5.5 WebVTT cue text DOM construction rules.
auto fragment = DocumentFragment::create(m_document);
if (cueText.isEmpty()) {
fragment->parserAppendChild(Text::create(m_document, emptyString()));
return fragment;
}
m_currentNode = fragment.ptr();
WebVTTTokenizer tokenizer(cueText);
m_languageStack.clear();
while (tokenizer.nextToken(m_token))
constructTreeFromToken(m_document);
return fragment;
}
Ref<DocumentFragment> WebVTTParser::createDocumentFragmentFromCueText(Document& document, const String& cueText)
{
WebVTTTreeBuilder treeBuilder(document);
return treeBuilder.buildFromString(cueText);
}
void WebVTTParser::createNewCue()
{
auto cue = WebVTTCueData::create();
cue->setStartTime(m_currentStartTime);
cue->setEndTime(m_currentEndTime);
cue->setContent(m_currentContent.toString());
cue->setId(m_currentId);
cue->setSettings(m_currentSettings);
m_cuelist.append(WTFMove(cue));
m_client.newCuesParsed();
}
void WebVTTParser::resetCueValues()
{
m_currentId = emptyString();
m_currentSettings = emptyString();
m_currentStartTime = MediaTime::zeroTime();
m_currentEndTime = MediaTime::zeroTime();
m_currentContent.clear();
}
bool WebVTTParser::collectTimeStamp(const String& line, MediaTime& timeStamp)
{
if (line.isEmpty())
return false;
VTTScanner input(line);
return collectTimeStamp(input, timeStamp);
}
bool WebVTTParser::collectTimeStamp(VTTScanner& input, MediaTime& timeStamp)
{
// Collect a WebVTT timestamp (5.3 WebVTT cue timings and settings parsing.)
// Steps 1 - 4 - Initial checks, let most significant units be minutes.
enum Mode { minutes, hours };
Mode mode = minutes;
// Steps 5 - 7 - Collect a sequence of characters that are 0-9.
// If not 2 characters or value is greater than 59, interpret as hours.
int value1;
unsigned value1Digits = input.scanDigits(value1);
if (!value1Digits)
return false;
if (value1Digits != 2 || value1 > 59)
mode = hours;
// Steps 8 - 11 - Collect the next sequence of 0-9 after ':' (must be 2 chars).
int value2;
if (!input.scan(':') || input.scanDigits(value2) != 2)
return false;
// Step 12 - Detect whether this timestamp includes hours.
int value3;
if (mode == hours || input.match(':')) {
if (!input.scan(':') || input.scanDigits(value3) != 2)
return false;
} else {
value3 = value2;
value2 = value1;
value1 = 0;
}
// Steps 13 - 17 - Collect next sequence of 0-9 after '.' (must be 3 chars).
int value4;
if (!input.scan('.') || input.scanDigits(value4) != 3)
return false;
if (value2 > 59 || value3 > 59)
return false;
// Steps 18 - 19 - Calculate result.
timeStamp = MediaTime::createWithDouble((value1 * secondsPerHour) + (value2 * secondsPerMinute) + value3 + (value4 * secondsPerMillisecond));
return true;
}
static WebVTTNodeType tokenToNodeType(WebVTTToken& token)
{
switch (token.name().length()) {
case 1:
if (token.name()[0] == 'c')
return WebVTTNodeTypeClass;
if (token.name()[0] == 'v')
return WebVTTNodeTypeVoice;
if (token.name()[0] == 'b')
return WebVTTNodeTypeBold;
if (token.name()[0] == 'i')
return WebVTTNodeTypeItalic;
if (token.name()[0] == 'u')
return WebVTTNodeTypeUnderline;
break;
case 2:
if (token.name()[0] == 'r' && token.name()[1] == 't')
return WebVTTNodeTypeRubyText;
break;
case 4:
if (token.name()[0] == 'r' && token.name()[1] == 'u' && token.name()[2] == 'b' && token.name()[3] == 'y')
return WebVTTNodeTypeRuby;
if (token.name()[0] == 'l' && token.name()[1] == 'a' && token.name()[2] == 'n' && token.name()[3] == 'g')
return WebVTTNodeTypeLanguage;
break;
}
return WebVTTNodeTypeNone;
}
void WebVTTTreeBuilder::constructTreeFromToken(Document& document)
{
// http://dev.w3.org/html5/webvtt/#webvtt-cue-text-dom-construction-rules
switch (m_token.type()) {
case WebVTTTokenTypes::Character: {
m_currentNode->parserAppendChild(Text::create(document, m_token.characters()));
break;
}
case WebVTTTokenTypes::StartTag: {
WebVTTNodeType nodeType = tokenToNodeType(m_token);
if (nodeType == WebVTTNodeTypeNone)
break;
WebVTTNodeType currentType = is<WebVTTElement>(*m_currentNode) ? downcast<WebVTTElement>(*m_currentNode).webVTTNodeType() : WebVTTNodeTypeNone;
// <rt> is only allowed if the current node is <ruby>.
if (nodeType == WebVTTNodeTypeRubyText && currentType != WebVTTNodeTypeRuby)
break;
auto child = WebVTTElement::create(nodeType, document);
if (!m_token.classes().isEmpty())
child->setAttributeWithoutSynchronization(classAttr, m_token.classes());
if (nodeType == WebVTTNodeTypeVoice)
child->setAttributeWithoutSynchronization(WebVTTElement::voiceAttributeName(), m_token.annotation());
else if (nodeType == WebVTTNodeTypeLanguage) {
m_languageStack.append(m_token.annotation());
child->setAttributeWithoutSynchronization(WebVTTElement::langAttributeName(), m_languageStack.last());
}
if (!m_languageStack.isEmpty())
child->setLanguage(m_languageStack.last());
m_currentNode->parserAppendChild(child);
m_currentNode = WTFMove(child);
break;
}
case WebVTTTokenTypes::EndTag: {
WebVTTNodeType nodeType = tokenToNodeType(m_token);
if (nodeType == WebVTTNodeTypeNone)
break;
// The only non-VTTElement would be the DocumentFragment root. (Text
// nodes and PIs will never appear as m_currentNode.)
if (!is<WebVTTElement>(*m_currentNode))
break;
WebVTTNodeType currentType = downcast<WebVTTElement>(*m_currentNode).webVTTNodeType();
bool matchesCurrent = nodeType == currentType;
if (!matchesCurrent) {
// </ruby> auto-closes <rt>
if (currentType == WebVTTNodeTypeRubyText && nodeType == WebVTTNodeTypeRuby) {
if (m_currentNode->parentNode())
m_currentNode = m_currentNode->parentNode();
} else
break;
}
if (nodeType == WebVTTNodeTypeLanguage)
m_languageStack.removeLast();
if (m_currentNode->parentNode())
m_currentNode = m_currentNode->parentNode();
break;
}
case WebVTTTokenTypes::TimestampTag: {
String charactersString = m_token.characters();
MediaTime parsedTimeStamp;
if (WebVTTParser::collectTimeStamp(charactersString, parsedTimeStamp))
m_currentNode->parserAppendChild(ProcessingInstruction::create(document, "timestamp", charactersString));
break;
}
default:
break;
}
}
}
#endif