#include "config.h"
#if ENABLE(VIDEO_TRACK)
#include "WebVTTParser.h"
#include "HTMLParserIdioms.h"
#include "ISOVTTCue.h"
#include "ProcessingInstruction.h"
#include "Text.h"
#include "VTTScanner.h"
#include "WebVTTElement.h"
namespace WebCore {
const double secondsPerHour = 3600;
const double secondsPerMinute = 60;
const double secondsPerMillisecond = 0.001;
const char* fileIdentifier = "WEBVTT";
const unsigned fileIdentifierLength = 6;
bool WebVTTParser::parseFloatPercentageValue(VTTScanner& valueScanner, float& percentage)
{
float number;
if (!valueScanner.scanFloat(number))
return false;
if (!valueScanner.scan('%'))
return false;
if (number < 0 || number > 100)
return false;
percentage = number;
return true;
}
#if ENABLE(WEBVTT_REGIONS)
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;
}
#endif
WebVTTParser::WebVTTParser(WebVTTParserClient* client, ScriptExecutionContext* context)
: m_scriptExecutionContext(context)
, m_state(Initial)
, m_decoder(TextResourceDecoder::create("text/plain", UTF8Encoding()))
, m_client(client)
{
}
void WebVTTParser::getNewCues(Vector<RefPtr<WebVTTCueData>>& outputCues)
{
outputCues = m_cuelist;
m_cuelist.clear();
}
#if ENABLE(WEBVTT_REGIONS)
void WebVTTParser::getNewRegions(Vector<RefPtr<VTTRegion>>& outputRegions)
{
outputRegions = m_regionList;
m_regionList.clear();
}
#endif
void WebVTTParser::parseFileHeader(const String& data)
{
m_state = Initial;
m_lineReader.reset();
m_lineReader.append(data);
parse();
}
void WebVTTParser::parseBytes(const char* data, unsigned length)
{
String textData = m_decoder->decode(data, length);
m_lineReader.append(textData);
parse();
}
void WebVTTParser::parseCueData(const ISOWebVTTCue& data)
{
RefPtr<WebVTTCueData> 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(cue);
if (m_client)
m_client->newCuesParsed();
}
void WebVTTParser::flush()
{
String textData = m_decoder->flush();
m_lineReader.append(textData);
m_lineReader.setEndOfStream();
parse();
flushPendingCue();
}
void WebVTTParser::parse()
{
String line;
while (m_lineReader.getLine(line)) {
if (line.isNull())
return;
switch (m_state) {
case Initial:
if (!hasRequiredFileIdentifier(line)) {
if (m_client)
m_client->fileFailedToParse();
return;
}
m_state = Header;
break;
case Header:
collectMetadataHeader(line);
if (line.isEmpty()) {
#if ENABLE(WEBVTT_REGIONS)
if (m_client && m_regionList.size())
m_client->newRegionsParsed();
#endif
m_state = Id;
break;
}
if (line.contains("-->"))
m_state = recoverCue(line);
break;
case Id:
if (line.isEmpty())
break;
resetCueValues();
m_state = collectCueId(line);
break;
case TimingsAndSettings:
if (line.isEmpty()) {
m_state = Id;
break;
}
m_state = collectTimingsAndSettings(line);
break;
case CueText:
m_state = collectCueText(line);
break;
case BadCue:
m_state = ignoreBadCue(line);
break;
case Finished:
ASSERT_NOT_REACHED();
break;
}
}
}
void WebVTTParser::fileFinished()
{
ASSERT(m_state != Finished);
parseBytes("\n\n", 2);
m_state = Finished;
}
void WebVTTParser::flushPendingCue()
{
ASSERT(m_lineReader.isAtEndOfStream());
if (m_state == CueText)
createNewCue();
}
bool WebVTTParser::hasRequiredFileIdentifier(const String& line)
{
if (line.isEmpty())
return false;
if (!line.startsWith(fileIdentifier, fileIdentifierLength))
return false;
if (line.length() > fileIdentifierLength && !isHTMLSpace(line[fileIdentifierLength]))
return false;
return true;
}
void WebVTTParser::collectMetadataHeader(const String& line)
{
#if ENABLE(WEBVTT_REGIONS)
DEPRECATED_DEFINE_STATIC_LOCAL(const AtomicString, regionHeaderName, ("Region", AtomicString::ConstructFromLiteral));
size_t colonPosition = line.find(':');
if (colonPosition == notFound)
return;
String headerName = line.substring(0, colonPosition);
if (headerName == regionHeaderName) {
String headerValue = line.substring(colonPosition + 1, line.length() - 1);
createNewRegion(headerValue);
}
#else
UNUSED_PARAM(line);
#endif
}
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);
input.skipWhile<isHTMLSpace<UChar>>();
if (!collectTimeStamp(input, m_currentStartTime))
return BadCue;
input.skipWhile<isHTMLSpace<UChar>>();
if (!input.scan("-->"))
return BadCue;
input.skipWhile<isHTMLSpace<UChar>>();
if (!collectTimeStamp(input, m_currentEndTime))
return BadCue;
input.skipWhile<isHTMLSpace<UChar>>();
m_currentSettings = input.restOfInputAsString();
return CueText;
}
WebVTTParser::ParseState WebVTTParser::collectCueText(const String& line)
{
if (line.isEmpty()) {
createNewCue();
return Id;
}
if (line.contains("-->")) {
createNewCue();
return recoverCue(line);
}
if (!m_currentContent.isEmpty())
m_currentContent.append('\n');
m_currentContent.append(line);
return CueText;
}
WebVTTParser::ParseState WebVTTParser::recoverCue(const String& line)
{
resetCueValues();
return collectTimingsAndSettings(line);
}
WebVTTParser::ParseState WebVTTParser::ignoreBadCue(const String& line)
{
if (line.isEmpty())
return Id;
if (line.contains("-->"))
return recoverCue(line);
return BadCue;
}
class WebVTTTreeBuilder {
public:
WebVTTTreeBuilder(Document& document)
: m_document(document) { }
PassRefPtr<DocumentFragment> buildFromString(const String& cueText);
private:
void constructTreeFromToken(Document&);
WebVTTToken m_token;
RefPtr<ContainerNode> m_currentNode;
Vector<AtomicString> m_languageStack;
Document& m_document;
};
PassRefPtr<DocumentFragment> WebVTTTreeBuilder::buildFromString(const String& cueText)
{
RefPtr<DocumentFragment> fragment = DocumentFragment::create(m_document);
if (cueText.isEmpty()) {
fragment->parserAppendChild(Text::create(m_document, emptyString()));
return fragment.release();
}
m_currentNode = fragment;
WebVTTTokenizer tokenizer(cueText);
m_languageStack.clear();
while (tokenizer.nextToken(m_token))
constructTreeFromToken(m_document);
return fragment.release();
}
PassRefPtr<DocumentFragment> WebVTTParser::createDocumentFragmentFromCueText(Document& document, const String& cueText)
{
WebVTTTreeBuilder treeBuilder(document);
return treeBuilder.buildFromString(cueText);
}
void WebVTTParser::createNewCue()
{
RefPtr<WebVTTCueData> 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(cue);
if (m_client)
m_client->newCuesParsed();
}
void WebVTTParser::resetCueValues()
{
m_currentId = emptyString();
m_currentSettings = emptyString();
m_currentStartTime = MediaTime::zeroTime();
m_currentEndTime = MediaTime::zeroTime();
m_currentContent.clear();
}
#if ENABLE(WEBVTT_REGIONS)
void WebVTTParser::createNewRegion(const String& headerValue)
{
if (headerValue.isEmpty())
return;
RefPtr<VTTRegion> region = VTTRegion::create(*m_scriptExecutionContext);
region->setRegionSettings(headerValue);
for (size_t i = 0; i < m_regionList.size(); ++i)
if (m_regionList[i]->id() == region->id()) {
m_regionList.remove(i);
break;
}
m_regionList.append(region);
}
#endif
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)
{
enum Mode { minutes, hours };
Mode mode = minutes;
int value1;
unsigned value1Digits = input.scanDigits(value1);
if (!value1Digits)
return false;
if (value1Digits != 2 || value1 > 59)
mode = hours;
int value2;
if (!input.scan(':') || input.scanDigits(value2) != 2)
return false;
int value3;
if (mode == hours || input.match(':')) {
if (!input.scan(':') || input.scanDigits(value3) != 2)
return false;
} else {
value3 = value2;
value2 = value1;
value1 = 0;
}
int value4;
if (!input.scan('.') || input.scanDigits(value4) != 3)
return false;
if (value2 > 59 || value3 > 59)
return false;
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)
{
switch (m_token.type()) {
case WebVTTTokenTypes::Character: {
RefPtr<Text> child = Text::create(document, m_token.characters());
m_currentNode->parserAppendChild(child);
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;
if (nodeType == WebVTTNodeTypeRubyText && currentType != WebVTTNodeTypeRuby)
break;
RefPtr<WebVTTElement> child = WebVTTElement::create(nodeType, document);
if (!m_token.classes().isEmpty())
child->setAttribute(classAttr, m_token.classes());
if (nodeType == WebVTTNodeTypeVoice)
child->setAttribute(WebVTTElement::voiceAttributeName(), m_token.annotation());
else if (nodeType == WebVTTNodeTypeLanguage) {
m_languageStack.append(m_token.annotation());
child->setAttribute(WebVTTElement::langAttributeName(), m_languageStack.last());
}
if (!m_languageStack.isEmpty())
child->setLanguage(m_languageStack.last());
m_currentNode->parserAppendChild(child);
m_currentNode = child;
break;
}
case WebVTTTokenTypes::EndTag: {
WebVTTNodeType nodeType = tokenToNodeType(m_token);
if (nodeType == WebVTTNodeTypeNone)
break;
if (!is<WebVTTElement>(*m_currentNode))
break;
WebVTTNodeType currentType = downcast<WebVTTElement>(*m_currentNode).webVTTNodeType();
bool matchesCurrent = nodeType == currentType;
if (!matchesCurrent) {
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