imsc
Version:
Renders IMSC documents to HTML5 fragments
1,736 lines (1,047 loc) • 188 kB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.imsc = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
/*
* Copyright (c) 2016, Pierre-Anthony Lemieux <pal@sandflow.com>
* 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.
*
* 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 HOLDER 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.
*/
/**
* @module imscDoc
*/
;
(function (imscDoc, sax, imscNames, imscStyles, imscUtils) {
/**
* Allows a client to provide callbacks to handle children of the <metadata> element
* @typedef {Object} MetadataHandler
* @property {?OpenTagCallBack} onOpenTag
* @property {?CloseTagCallBack} onCloseTag
* @property {?TextCallBack} onText
*/
/**
* Called when the opening tag of an element node is encountered.
* @callback OpenTagCallBack
* @param {string} ns Namespace URI of the element
* @param {string} name Local name of the element
* @param {Object[]} attributes List of attributes, each consisting of a
* `uri`, `name` and `value`
*/
/**
* Called when the closing tag of an element node is encountered.
* @callback CloseTagCallBack
*/
/**
* Called when a text node is encountered.
* @callback TextCallBack
* @param {string} contents Contents of the text node
*/
/**
* Parses an IMSC1 document into an opaque in-memory representation that exposes
* a single method <pre>getMediaTimeEvents()</pre> that returns a list of time
* offsets (in seconds) of the ISD, i.e. the points in time where the visual
* representation of the document change. `metadataHandler` allows the caller to
* be called back when nodes are present in <metadata> elements.
*
* @param {string} xmlstring XML document
* @param {?module:imscUtils.ErrorHandler} errorHandler Error callback
* @param {?MetadataHandler} metadataHandler Callback for <Metadata> elements
* @returns {Object} Opaque in-memory representation of an IMSC1 document
*/
imscDoc.fromXML = function (xmlstring, errorHandler, metadataHandler) {
var p = sax.parser(true, {xmlns: true});
var estack = [];
var xmllangstack = [];
var xmlspacestack = [];
var metadata_depth = 0;
var doc = null;
p.onclosetag = function (node) {
if (estack[0] instanceof Region) {
/* merge referenced styles */
if (doc.head !== null && doc.head.styling !== null) {
mergeReferencedStyles(doc.head.styling, estack[0].styleRefs, estack[0].styleAttrs, errorHandler);
}
delete estack[0].styleRefs;
} else if (estack[0] instanceof Styling) {
/* flatten chained referential styling */
for (var sid in estack[0].styles) {
if (! estack[0].styles.hasOwnProperty(sid)) continue;
mergeChainedStyles(estack[0], estack[0].styles[sid], errorHandler);
}
} else if (estack[0] instanceof P || estack[0] instanceof Span) {
/* merge anonymous spans */
if (estack[0].contents.length > 1) {
var cs = [estack[0].contents[0]];
var c;
for (c = 1; c < estack[0].contents.length; c++) {
if (estack[0].contents[c] instanceof AnonymousSpan &&
cs[cs.length - 1] instanceof AnonymousSpan) {
cs[cs.length - 1].text += estack[0].contents[c].text;
} else {
cs.push(estack[0].contents[c]);
}
}
estack[0].contents = cs;
}
// remove redundant nested anonymous spans (9.3.3(1)(c))
if (estack[0] instanceof Span &&
estack[0].contents.length === 1 &&
estack[0].contents[0] instanceof AnonymousSpan) {
estack[0].text = estack[0].contents[0].text;
delete estack[0].contents;
}
} else if (estack[0] instanceof ForeignElement) {
if (estack[0].node.uri === imscNames.ns_tt &&
estack[0].node.local === 'metadata') {
/* leave the metadata element */
metadata_depth--;
} else if (metadata_depth > 0 &&
metadataHandler &&
'onCloseTag' in metadataHandler) {
/* end of child of metadata element */
metadataHandler.onCloseTag();
}
}
// TODO: delete stylerefs?
// maintain the xml:space stack
xmlspacestack.shift();
// maintain the xml:lang stack
xmllangstack.shift();
// prepare for the next element
estack.shift();
};
p.ontext = function (str) {
if (estack[0] === undefined) {
/* ignoring text outside of elements */
} else if (estack[0] instanceof Span || estack[0] instanceof P) {
/* ignore children text nodes in ruby container spans */
if (estack[0] instanceof Span) {
var ruby = estack[0].styleAttrs[imscStyles.byName.ruby.qname];
if (ruby === 'container' || ruby === 'textContainer' || ruby === 'baseContainer') {
return;
}
}
/* create an anonymous span */
var s = new AnonymousSpan();
s.initFromText(doc, estack[0], str, xmllangstack[0], xmlspacestack[0], errorHandler);
estack[0].contents.push(s);
} else if (estack[0] instanceof ForeignElement &&
metadata_depth > 0 &&
metadataHandler &&
'onText' in metadataHandler) {
/* text node within a child of metadata element */
metadataHandler.onText(str);
}
};
p.onopentag = function (node) {
// maintain the xml:space stack
var xmlspace = node.attributes["xml:space"];
if (xmlspace) {
xmlspacestack.unshift(xmlspace.value);
} else {
if (xmlspacestack.length === 0) {
xmlspacestack.unshift("default");
} else {
xmlspacestack.unshift(xmlspacestack[0]);
}
}
/* maintain the xml:lang stack */
var xmllang = node.attributes["xml:lang"];
if (xmllang) {
xmllangstack.unshift(xmllang.value);
} else {
if (xmllangstack.length === 0) {
xmllangstack.unshift("");
} else {
xmllangstack.unshift(xmllangstack[0]);
}
}
/* process the element */
if (node.uri === imscNames.ns_tt) {
if (node.local === 'tt') {
if (doc !== null) {
reportFatal(errorHandler, "Two <tt> elements at (" + this.line + "," + this.column + ")");
}
doc = new TT();
doc.initFromNode(node, xmllangstack[0], errorHandler);
estack.unshift(doc);
} else if (node.local === 'head') {
if (!(estack[0] instanceof TT)) {
reportFatal(errorHandler, "Parent of <head> element is not <tt> at (" + this.line + "," + this.column + ")");
}
estack.unshift(doc.head);
} else if (node.local === 'styling') {
if (!(estack[0] instanceof Head)) {
reportFatal(errorHandler, "Parent of <styling> element is not <head> at (" + this.line + "," + this.column + ")");
}
estack.unshift(doc.head.styling);
} else if (node.local === 'style') {
var s;
if (estack[0] instanceof Styling) {
s = new Style();
s.initFromNode(node, errorHandler);
/* ignore <style> element missing @id */
if (!s.id) {
reportError(errorHandler, "<style> element missing @id attribute");
} else {
doc.head.styling.styles[s.id] = s;
}
estack.unshift(s);
} else if (estack[0] instanceof Region) {
/* nested styles can be merged with specified styles
* immediately, with lower priority
* (see 8.4.4.2(3) at TTML1 )
*/
s = new Style();
s.initFromNode(node, errorHandler);
mergeStylesIfNotPresent(s.styleAttrs, estack[0].styleAttrs);
estack.unshift(s);
} else {
reportFatal(errorHandler, "Parent of <style> element is not <styling> or <region> at (" + this.line + "," + this.column + ")");
}
} else if (node.local === 'initial') {
var ini;
if (estack[0] instanceof Styling) {
ini = new Initial();
ini.initFromNode(node, errorHandler);
for (var qn in ini.styleAttrs) {
if (! ini.styleAttrs.hasOwnProperty(qn)) continue;
doc.head.styling.initials[qn] = ini.styleAttrs[qn];
}
estack.unshift(ini);
} else {
reportFatal(errorHandler, "Parent of <initial> element is not <styling> at (" + this.line + "," + this.column + ")");
}
} else if (node.local === 'layout') {
if (!(estack[0] instanceof Head)) {
reportFatal(errorHandler, "Parent of <layout> element is not <head> at " + this.line + "," + this.column + ")");
}
estack.unshift(doc.head.layout);
} else if (node.local === 'region') {
if (!(estack[0] instanceof Layout)) {
reportFatal(errorHandler, "Parent of <region> element is not <layout> at " + this.line + "," + this.column + ")");
}
var r = new Region();
r.initFromNode(doc, node, xmllangstack[0], errorHandler);
if (!r.id || r.id in doc.head.layout.regions) {
reportError(errorHandler, "Ignoring <region> with duplicate or missing @id at " + this.line + "," + this.column + ")");
} else {
doc.head.layout.regions[r.id] = r;
}
estack.unshift(r);
} else if (node.local === 'body') {
if (!(estack[0] instanceof TT)) {
reportFatal(errorHandler, "Parent of <body> element is not <tt> at " + this.line + "," + this.column + ")");
}
if (doc.body !== null) {
reportFatal(errorHandler, "Second <body> element at " + this.line + "," + this.column + ")");
}
var b = new Body();
b.initFromNode(doc, node, xmllangstack[0], errorHandler);
doc.body = b;
estack.unshift(b);
} else if (node.local === 'div') {
if (!(estack[0] instanceof Div || estack[0] instanceof Body)) {
reportFatal(errorHandler, "Parent of <div> element is not <body> or <div> at " + this.line + "," + this.column + ")");
}
var d = new Div();
d.initFromNode(doc, estack[0], node, xmllangstack[0], errorHandler);
/* transform smpte:backgroundImage to TTML2 image element */
var bi = d.styleAttrs[imscStyles.byName.backgroundImage.qname];
if (bi) {
d.contents.push(new Image(bi));
delete d.styleAttrs[imscStyles.byName.backgroundImage.qname];
}
estack[0].contents.push(d);
estack.unshift(d);
} else if (node.local === 'image') {
if (!(estack[0] instanceof Div)) {
reportFatal(errorHandler, "Parent of <image> element is not <div> at " + this.line + "," + this.column + ")");
}
var img = new Image();
img.initFromNode(doc, estack[0], node, xmllangstack[0], errorHandler);
estack[0].contents.push(img);
estack.unshift(img);
} else if (node.local === 'p') {
if (!(estack[0] instanceof Div)) {
reportFatal(errorHandler, "Parent of <p> element is not <div> at " + this.line + "," + this.column + ")");
}
var p = new P();
p.initFromNode(doc, estack[0], node, xmllangstack[0], errorHandler);
estack[0].contents.push(p);
estack.unshift(p);
} else if (node.local === 'span') {
if (!(estack[0] instanceof Span || estack[0] instanceof P)) {
reportFatal(errorHandler, "Parent of <span> element is not <span> or <p> at " + this.line + "," + this.column + ")");
}
var ns = new Span();
ns.initFromNode(doc, estack[0], node, xmllangstack[0], xmlspacestack[0], errorHandler);
estack[0].contents.push(ns);
estack.unshift(ns);
} else if (node.local === 'br') {
if (!(estack[0] instanceof Span || estack[0] instanceof P)) {
reportFatal(errorHandler, "Parent of <br> element is not <span> or <p> at " + this.line + "," + this.column + ")");
}
var nb = new Br();
nb.initFromNode(doc, estack[0], node, xmllangstack[0], errorHandler);
estack[0].contents.push(nb);
estack.unshift(nb);
} else if (node.local === 'set') {
if (!(estack[0] instanceof Span ||
estack[0] instanceof P ||
estack[0] instanceof Div ||
estack[0] instanceof Body ||
estack[0] instanceof Region ||
estack[0] instanceof Br)) {
reportFatal(errorHandler, "Parent of <set> element is not a content element or a region at " + this.line + "," + this.column + ")");
}
var st = new Set();
st.initFromNode(doc, estack[0], node, errorHandler);
estack[0].sets.push(st);
estack.unshift(st);
} else {
/* element in the TT namespace, but not a content element */
estack.unshift(new ForeignElement(node));
}
} else {
/* ignore elements not in the TTML namespace unless in metadata element */
estack.unshift(new ForeignElement(node));
}
/* handle metadata callbacks */
if (estack[0] instanceof ForeignElement) {
if (node.uri === imscNames.ns_tt &&
node.local === 'metadata') {
/* enter the metadata element */
metadata_depth++;
} else if (
metadata_depth > 0 &&
metadataHandler &&
'onOpenTag' in metadataHandler
) {
/* start of child of metadata element */
var attrs = [];
for (var a in node.attributes) {
attrs[node.attributes[a].uri + " " + node.attributes[a].local] =
{
uri: node.attributes[a].uri,
local: node.attributes[a].local,
value: node.attributes[a].value
};
}
metadataHandler.onOpenTag(node.uri, node.local, attrs);
}
}
};
// parse the document
p.write(xmlstring).close();
// all referential styling has been flatten, so delete styles
delete doc.head.styling.styles;
// create default region if no regions specified
var hasRegions = false;
/* AFAIK the only way to determine whether an object has members */
for (var i in doc.head.layout.regions) {
if (doc.head.layout.regions.hasOwnProperty(i)) {
hasRegions = true;
break;
}
}
if (!hasRegions) {
/* create default region */
var dr = Region.prototype.createDefaultRegion(doc.lang);
doc.head.layout.regions[dr.id] = dr;
}
/* resolve desired timing for regions */
for (var region_i in doc.head.layout.regions) {
if (! doc.head.layout.regions.hasOwnProperty(region_i)) continue;
resolveTiming(doc, doc.head.layout.regions[region_i], null, null);
}
/* resolve desired timing for content elements */
if (doc.body) {
resolveTiming(doc, doc.body, null, null);
}
/* remove undefined spans in ruby containers */
if (doc.body) {
cleanRubyContainers(doc.body);
}
return doc;
};
function cleanRubyContainers(element) {
if (! ('contents' in element)) return;
var rubyval = 'styleAttrs' in element ? element.styleAttrs[imscStyles.byName.ruby.qname] : null;
var isrubycontainer = (element.kind === 'span' && (rubyval === "container" || rubyval === "textContainer" || rubyval === "baseContainer"));
for (var i = element.contents.length - 1; i >= 0; i--) {
if (isrubycontainer && !('styleAttrs' in element.contents[i] && imscStyles.byName.ruby.qname in element.contents[i].styleAttrs)) {
/* prune undefined <span> in ruby containers */
delete element.contents[i];
} else {
cleanRubyContainers(element.contents[i]);
}
}
}
function resolveTiming(doc, element, prev_sibling, parent) {
/* are we in a seq container? */
var isinseq = parent && parent.timeContainer === "seq";
/* determine implicit begin */
var implicit_begin = 0; /* default */
if (parent) {
if (isinseq && prev_sibling) {
/*
* if seq time container, offset from the previous sibling end
*/
implicit_begin = prev_sibling.end;
} else {
implicit_begin = parent.begin;
}
}
/* compute desired begin */
element.begin = element.explicit_begin ? element.explicit_begin + implicit_begin : implicit_begin;
/* determine implicit end */
var implicit_end = element.begin;
var s = null;
if ("sets" in element) {
for (var set_i = 0; set_i < element.sets.length; set_i++) {
resolveTiming(doc, element.sets[set_i], s, element);
if (element.timeContainer === "seq") {
implicit_end = element.sets[set_i].end;
} else {
implicit_end = Math.max(implicit_end, element.sets[set_i].end);
}
s = element.sets[set_i];
}
}
if (!('contents' in element)) {
/* anonymous spans and regions and <set> and <br>s and spans with only children text nodes */
if (isinseq) {
/* in seq container, implicit duration is zero */
implicit_end = element.begin;
} else {
/* in par container, implicit duration is indefinite */
implicit_end = Number.POSITIVE_INFINITY;
}
} else if ("contents" in element) {
for (var content_i = 0; content_i < element.contents.length; content_i++) {
resolveTiming(doc, element.contents[content_i], s, element);
if (element.timeContainer === "seq") {
implicit_end = element.contents[content_i].end;
} else {
implicit_end = Math.max(implicit_end, element.contents[content_i].end);
}
s = element.contents[content_i];
}
}
/* determine desired end */
/* it is never made really clear in SMIL that the explicit end is offset by the implicit begin */
if (element.explicit_end !== null && element.explicit_dur !== null) {
element.end = Math.min(element.begin + element.explicit_dur, implicit_begin + element.explicit_end);
} else if (element.explicit_end === null && element.explicit_dur !== null) {
element.end = element.begin + element.explicit_dur;
} else if (element.explicit_end !== null && element.explicit_dur === null) {
element.end = implicit_begin + element.explicit_end;
} else {
element.end = implicit_end;
}
delete element.explicit_begin;
delete element.explicit_dur;
delete element.explicit_end;
doc._registerEvent(element);
}
function ForeignElement(node) {
this.node = node;
}
function TT() {
this.events = [];
this.head = new Head();
this.body = null;
}
TT.prototype.initFromNode = function (node, xmllang, errorHandler) {
/* compute cell resolution */
var cr = extractCellResolution(node, errorHandler);
this.cellLength = {
'h': new imscUtils.ComputedLength(0, 1/cr.h),
'w': new imscUtils.ComputedLength(1/cr.w, 0)
};
/* extract frame rate and tick rate */
var frtr = extractFrameAndTickRate(node, errorHandler);
this.effectiveFrameRate = frtr.effectiveFrameRate;
this.tickRate = frtr.tickRate;
/* extract aspect ratio */
this.aspectRatio = extractAspectRatio(node, errorHandler);
/* check timebase */
var attr = findAttribute(node, imscNames.ns_ttp, "timeBase");
if (attr !== null && attr !== "media") {
reportFatal(errorHandler, "Unsupported time base");
}
/* retrieve extent */
var e = extractExtent(node, errorHandler);
if (e === null) {
this.pxLength = {
'h': null,
'w': null
};
} else {
if (e.h.unit !== "px" || e.w.unit !== "px") {
reportFatal(errorHandler, "Extent on TT must be in px or absent");
}
this.pxLength = {
'h': new imscUtils.ComputedLength(0, 1 / e.h.value),
'w': new imscUtils.ComputedLength(1 / e.w.value, 0)
};
}
/** set root container dimensions to (1, 1) arbitrarily
* the root container is mapped to actual dimensions at rendering
**/
this.dimensions = {
'h': new imscUtils.ComputedLength(0, 1),
'w': new imscUtils.ComputedLength(1, 0)
};
/* xml:lang */
this.lang = xmllang;
};
/* register a temporal events */
TT.prototype._registerEvent = function (elem) {
/* skip if begin is not < then end */
if (elem.end <= elem.begin)
return;
/* index the begin time of the event */
var b_i = indexOf(this.events, elem.begin);
if (!b_i.found) {
this.events.splice(b_i.index, 0, elem.begin);
}
/* index the end time of the event */
if (elem.end !== Number.POSITIVE_INFINITY) {
var e_i = indexOf(this.events, elem.end);
if (!e_i.found) {
this.events.splice(e_i.index, 0, elem.end);
}
}
};
/*
* Retrieves the range of ISD times covered by the document
*
* @returns {Array} Array of two elements: min_begin_time and max_begin_time
*
*/
TT.prototype.getMediaTimeRange = function () {
return [this.events[0], this.events[this.events.length - 1]];
};
/*
* Returns list of ISD begin times
*
* @returns {Array}
*/
TT.prototype.getMediaTimeEvents = function () {
return this.events;
};
/*
* Represents a TTML Head element
*/
function Head() {
this.styling = new Styling();
this.layout = new Layout();
}
/*
* Represents a TTML Styling element
*/
function Styling() {
this.styles = {};
this.initials = {};
}
/*
* Represents a TTML Style element
*/
function Style() {
this.id = null;
this.styleAttrs = null;
this.styleRefs = null;
}
Style.prototype.initFromNode = function (node, errorHandler) {
this.id = elementGetXMLID(node);
this.styleAttrs = elementGetStyles(node, errorHandler);
this.styleRefs = elementGetStyleRefs(node);
};
/*
* Represents a TTML initial element
*/
function Initial() {
this.styleAttrs = null;
}
Initial.prototype.initFromNode = function (node, errorHandler) {
this.styleAttrs = {};
for (var i in node.attributes) {
if (node.attributes[i].uri === imscNames.ns_itts ||
node.attributes[i].uri === imscNames.ns_ebutts ||
node.attributes[i].uri === imscNames.ns_tts) {
var qname = node.attributes[i].uri + " " + node.attributes[i].local;
this.styleAttrs[qname] = node.attributes[i].value;
}
}
};
/*
* Represents a TTML Layout element
*
*/
function Layout() {
this.regions = {};
}
/*
* Represents a TTML image element
*/
function Image(src, type) {
ContentElement.call(this, 'image');
this.src = src;
this.type = type;
}
Image.prototype.initFromNode = function (doc, parent, node, xmllang, errorHandler) {
this.src = 'src' in node.attributes ? node.attributes.src.value : null;
if (! this.src) {
reportError(errorHandler, "Invalid image@src attribute");
}
this.type = 'type' in node.attributes ? node.attributes.type.value : null;
if (! this.type) {
reportError(errorHandler, "Invalid image@type attribute");
}
StyledElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler);
TimedElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler);
AnimatedElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler);
LayoutElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler);
this.lang = xmllang;
};
/*
* TTML element utility functions
*
*/
function ContentElement(kind) {
this.kind = kind;
}
function IdentifiedElement(id) {
this.id = id;
}
IdentifiedElement.prototype.initFromNode = function (doc, parent, node, errorHandler) {
this.id = elementGetXMLID(node);
};
function LayoutElement(id) {
this.regionID = id;
}
LayoutElement.prototype.initFromNode = function (doc, parent, node, errorHandler) {
this.regionID = elementGetRegionID(node);
};
function StyledElement(styleAttrs) {
this.styleAttrs = styleAttrs;
}
StyledElement.prototype.initFromNode = function (doc, parent, node, errorHandler) {
this.styleAttrs = elementGetStyles(node, errorHandler);
if (doc.head !== null && doc.head.styling !== null) {
mergeReferencedStyles(doc.head.styling, elementGetStyleRefs(node), this.styleAttrs, errorHandler);
}
};
function AnimatedElement(sets) {
this.sets = sets;
}
AnimatedElement.prototype.initFromNode = function (doc, parent, node, errorHandler) {
this.sets = [];
};
function ContainerElement(contents) {
this.contents = contents;
}
ContainerElement.prototype.initFromNode = function (doc, parent, node, errorHandler) {
this.contents = [];
};
function TimedElement(explicit_begin, explicit_end, explicit_dur) {
this.explicit_begin = explicit_begin;
this.explicit_end = explicit_end;
this.explicit_dur = explicit_dur;
}
TimedElement.prototype.initFromNode = function (doc, parent, node, errorHandler) {
var t = processTiming(doc, parent, node, errorHandler);
this.explicit_begin = t.explicit_begin;
this.explicit_end = t.explicit_end;
this.explicit_dur = t.explicit_dur;
this.timeContainer = elementGetTimeContainer(node, errorHandler);
};
/*
* Represents a TTML body element
*/
function Body() {
ContentElement.call(this, 'body');
}
Body.prototype.initFromNode = function (doc, node, xmllang, errorHandler) {
StyledElement.prototype.initFromNode.call(this, doc, null, node, errorHandler);
TimedElement.prototype.initFromNode.call(this, doc, null, node, errorHandler);
AnimatedElement.prototype.initFromNode.call(this, doc, null, node, errorHandler);
LayoutElement.prototype.initFromNode.call(this, doc, null, node, errorHandler);
ContainerElement.prototype.initFromNode.call(this, doc, null, node, errorHandler);
this.lang = xmllang;
};
/*
* Represents a TTML div element
*/
function Div() {
ContentElement.call(this, 'div');
}
Div.prototype.initFromNode = function (doc, parent, node, xmllang, errorHandler) {
StyledElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler);
TimedElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler);
AnimatedElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler);
LayoutElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler);
ContainerElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler);
this.lang = xmllang;
};
/*
* Represents a TTML p element
*/
function P() {
ContentElement.call(this, 'p');
}
P.prototype.initFromNode = function (doc, parent, node, xmllang, errorHandler) {
StyledElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler);
TimedElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler);
AnimatedElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler);
LayoutElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler);
ContainerElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler);
this.lang = xmllang;
};
/*
* Represents a TTML span element
*/
function Span() {
ContentElement.call(this, 'span');
}
Span.prototype.initFromNode = function (doc, parent, node, xmllang, xmlspace, errorHandler) {
StyledElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler);
TimedElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler);
AnimatedElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler);
LayoutElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler);
ContainerElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler);
this.space = xmlspace;
this.lang = xmllang;
};
/*
* Represents a TTML anonymous span element
*/
function AnonymousSpan() {
ContentElement.call(this, 'span');
}
AnonymousSpan.prototype.initFromText = function (doc, parent, text, xmllang, xmlspace, errorHandler) {
TimedElement.prototype.initFromNode.call(this, doc, parent, null, errorHandler);
this.text = text;
this.space = xmlspace;
this.lang = xmllang;
};
/*
* Represents a TTML br element
*/
function Br() {
ContentElement.call(this, 'br');
}
Br.prototype.initFromNode = function (doc, parent, node, xmllang, errorHandler) {
LayoutElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler);
TimedElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler);
this.lang = xmllang;
};
/*
* Represents a TTML Region element
*
*/
function Region() {
}
Region.prototype.createDefaultRegion = function (xmllang) {
var r = new Region();
IdentifiedElement.call(r, '');
StyledElement.call(r, {});
AnimatedElement.call(r, []);
TimedElement.call(r, 0, Number.POSITIVE_INFINITY, null);
this.lang = xmllang;
return r;
};
Region.prototype.initFromNode = function (doc, node, xmllang, errorHandler) {
IdentifiedElement.prototype.initFromNode.call(this, doc, null, node, errorHandler);
TimedElement.prototype.initFromNode.call(this, doc, null, node, errorHandler);
AnimatedElement.prototype.initFromNode.call(this, doc, null, node, errorHandler);
/* add specified styles */
this.styleAttrs = elementGetStyles(node, errorHandler);
/* remember referential styles for merging after nested styling is processed*/
this.styleRefs = elementGetStyleRefs(node);
/* xml:lang */
this.lang = xmllang;
};
/*
* Represents a TTML Set element
*
*/
function Set() {
}
Set.prototype.initFromNode = function (doc, parent, node, errorHandler) {
TimedElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler);
var styles = elementGetStyles(node, errorHandler);
this.qname = null;
this.value = null;
for (var qname in styles) {
if (! styles.hasOwnProperty(qname)) continue;
if (this.qname) {
reportError(errorHandler, "More than one style specified on set");
break;
}
this.qname = qname;
this.value = styles[qname];
}
};
/*
* Utility functions
*
*/
function elementGetXMLID(node) {
return node && 'xml:id' in node.attributes ? node.attributes['xml:id'].value || null : null;
}
function elementGetRegionID(node) {
return node && 'region' in node.attributes ? node.attributes.region.value : '';
}
function elementGetTimeContainer(node, errorHandler) {
var tc = node && 'timeContainer' in node.attributes ? node.attributes.timeContainer.value : null;
if ((!tc) || tc === "par") {
return "par";
} else if (tc === "seq") {
return "seq";
} else {
reportError(errorHandler, "Illegal value of timeContainer (assuming 'par')");
return "par";
}
}
function elementGetStyleRefs(node) {
return node && 'style' in node.attributes ? node.attributes.style.value.split(" ") : [];
}
function elementGetStyles(node, errorHandler) {
var s = {};
if (node !== null) {
for (var i in node.attributes) {
var qname = node.attributes[i].uri + " " + node.attributes[i].local;
var sa = imscStyles.byQName[qname];
if (sa !== undefined) {
var val = sa.parse(node.attributes[i].value);
if (val !== null) {
s[qname] = val;
/* TODO: consider refactoring errorHandler into parse and compute routines */
if (sa === imscStyles.byName.zIndex) {
reportWarning(errorHandler, "zIndex attribute present but not used by IMSC1 since regions do not overlap");
}
} else {
reportError(errorHandler, "Cannot parse styling attribute " + qname + " --> " + node.attributes[i].value);
}
}
}
}
return s;
}
function findAttribute(node, ns, name) {
for (var i in node.attributes) {
if (node.attributes[i].uri === ns &&
node.attributes[i].local === name) {
return node.attributes[i].value;
}
}
return null;
}
function extractAspectRatio(node, errorHandler) {
var ar = findAttribute(node, imscNames.ns_ittp, "aspectRatio");
if (ar === null) {
ar = findAttribute(node, imscNames.ns_ttp, "displayAspectRatio");
}
var rslt = null;
if (ar !== null) {
var ASPECT_RATIO_RE = /(\d+)\s+(\d+)/;
var m = ASPECT_RATIO_RE.exec(ar);
if (m !== null) {
var w = parseInt(m[1]);
var h = parseInt(m[2]);
if (w !== 0 && h !== 0) {
rslt = w / h;
} else {
reportError(errorHandler, "Illegal aspectRatio values (ignoring)");
}
} else {
reportError(errorHandler, "Malformed aspectRatio attribute (ignoring)");
}
}
return rslt;
}
/*
* Returns the cellResolution attribute from a node
*
*/
function extractCellResolution(node, errorHandler) {
var cr = findAttribute(node, imscNames.ns_ttp, "cellResolution");
// initial value
var h = 15;
var w = 32;
if (cr !== null) {
var CELL_RESOLUTION_RE = /(\d+) (\d+)/;
var m = CELL_RESOLUTION_RE.exec(cr);
if (m !== null) {
w = parseInt(m[1]);
h = parseInt(m[2]);
} else {
reportWarning(errorHandler, "Malformed cellResolution value (using initial value instead)");
}
}
return {'w': w, 'h': h};
}
function extractFrameAndTickRate(node, errorHandler) {
// subFrameRate is ignored per IMSC1 specification
// extract frame rate
var fps_attr = findAttribute(node, imscNames.ns_ttp, "frameRate");
// initial value
var fps = 30;
// match variable
var m;
if (fps_attr !== null) {
var FRAME_RATE_RE = /(\d+)/;
m = FRAME_RATE_RE.exec(fps_attr);
if (m !== null) {
fps = parseInt(m[1]);
} else {
reportWarning(errorHandler, "Malformed frame rate attribute (using initial value instead)");
}
}
// extract frame rate multiplier
var frm_attr = findAttribute(node, imscNames.ns_ttp, "frameRateMultiplier");
// initial value
var frm = 1;
if (frm_attr !== null) {
var FRAME_RATE_MULT_RE = /(\d+) (\d+)/;
m = FRAME_RATE_MULT_RE.exec(frm_attr);
if (m !== null) {
frm = parseInt(m[1]) / parseInt(m[2]);
} else {
reportWarning(errorHandler, "Malformed frame rate multiplier attribute (using initial value instead)");
}
}
var efps = frm * fps;
// extract tick rate
var tr = 1;
var trattr = findAttribute(node, imscNames.ns_ttp, "tickRate");
if (trattr === null) {
if (fps_attr !== null)
tr = efps;
} else {
var TICK_RATE_RE = /(\d+)/;
m = TICK_RATE_RE.exec(trattr);
if (m !== null) {
tr = parseInt(m[1]);
} else {
reportWarning(errorHandler, "Malformed tick rate attribute (using initial value instead)");
}
}
return {effectiveFrameRate: efps, tickRate: tr};
}
function extractExtent(node, errorHandler) {
var attr = findAttribute(node, imscNames.ns_tts, "extent");
if (attr === null)
return null;
var s = attr.split(" ");
if (s.length !== 2) {
reportWarning(errorHandler, "Malformed extent (ignoring)");
return null;
}
var w = imscUtils.parseLength(s[0]);
var h = imscUtils.parseLength(s[1]);
if (!h || !w) {
reportWarning(errorHandler, "Malformed extent values (ignoring)");
return null;
}
return {'h': h, 'w': w};
}
function parseTimeExpression(tickRate, effectiveFrameRate, str) {
var CLOCK_TIME_FRACTION_RE = /^(\d{2,}):(\d\d):(\d\d(?:\.\d+)?)$/;
var CLOCK_TIME_FRAMES_RE = /^(\d{2,}):(\d\d):(\d\d)\:(\d{2,})$/;
var OFFSET_FRAME_RE = /^(\d+(?:\.\d+)?)f$/;
var OFFSET_TICK_RE = /^(\d+(?:\.\d+)?)t$/;
var OFFSET_MS_RE = /^(\d+(?:\.\d+)?)ms$/;
var OFFSET_S_RE = /^(\d+(?:\.\d+)?)s$/;
var OFFSET_H_RE = /^(\d+(?:\.\d+)?)h$/;
var OFFSET_M_RE = /^(\d+(?:\.\d+)?)m$/;
var m;
var r = null;
if ((m = OFFSET_FRAME_RE.exec(str)) !== null) {
if (effectiveFrameRate !== null) {
r = parseFloat(m[1]) / effectiveFrameRate;
}
} else if ((m = OFFSET_TICK_RE.exec(str)) !== null) {
if (tickRate !== null) {
r = parseFloat(m[1]) / tickRate;
}
} else if ((m = OFFSET_MS_RE.exec(str)) !== null) {
r = parseFloat(m[1]) / 1000.0;
} else if ((m = OFFSET_S_RE.exec(str)) !== null) {
r = parseFloat(m[1]);
} else if ((m = OFFSET_H_RE.exec(str)) !== null) {
r = parseFloat(m[1]) * 3600.0;
} else if ((m = OFFSET_M_RE.exec(str)) !== null) {
r = parseFloat(m[1]) * 60.0;
} else if ((m = CLOCK_TIME_FRACTION_RE.exec(str)) !== null) {
r = parseInt(m[1]) * 3600 +
parseInt(m[2]) * 60 +
parseFloat(m[3]);
} else if ((m = CLOCK_TIME_FRAMES_RE.exec(str)) !== null) {
/* this assumes that HH:MM:SS is a clock-time-with-fraction */
if (effectiveFrameRate !== null) {
r = parseInt(m[1]) * 3600 +
parseInt(m[2]) * 60 +
parseInt(m[3]) +
(m[4] === null ? 0 : parseInt(m[4]) / effectiveFrameRate);
}
}
return r;
}
function processTiming(doc, parent, node, errorHandler) {
/* determine explicit begin */
var explicit_begin = null;
if (node && 'begin' in node.attributes) {
explicit_begin = parseTimeExpression(doc.tickRate, doc.effectiveFrameRate, node.attributes.begin.value);
if (explicit_begin === null) {
reportWarning(errorHandler, "Malformed begin value " + node.attributes.begin.value + " (using 0)");
}
}
/* determine explicit duration */
var explicit_dur = null;
if (node && 'dur' in node.attributes) {
explicit_dur = parseTimeExpression(doc.tickRate, doc.effectiveFrameRate, node.attributes.dur.value);
if (explicit_dur === null) {
reportWarning(errorHandler, "Malformed dur value " + node.attributes.dur.value + " (ignoring)");
}
}
/* determine explicit end */
var explicit_end = null;
if (node && 'end' in node.attributes) {
explicit_end = parseTimeExpression(doc.tickRate, doc.effectiveFrameRate, node.attributes.end.value);
if (explicit_end === null) {
reportWarning(errorHandler, "Malformed end value (ignoring)");
}
}
return {explicit_begin: explicit_begin,
explicit_end: explicit_end,
explicit_dur: explicit_dur};
}
function mergeChainedStyles(styling, style, errorHandler) {
while (style.styleRefs.length > 0) {
var sref = style.styleRefs.pop();
if (!(sref in styling.styles)) {
reportError(errorHandler, "Non-existant style id referenced");
continue;
}
mergeChain