imsc
Version:
Renders IMSC 1.1 documents to HTML5 fragments
735 lines (450 loc) • 21.7 kB
JavaScript
/*
* 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 imscISD
*/
;
(function (imscISD, imscNames, imscStyles, imscUtils) { // wrapper for non-node envs
/**
* Creates a canonical representation of an IMSC1 document returned by <pre>imscDoc.fromXML()</pre>
* at a given absolute offset in seconds. This offset does not have to be one of the values returned
* by <pre>getMediaTimeEvents()</pre>.
*
* @param {Object} tt IMSC1 document
* @param {number} offset Absolute offset (in seconds)
* @param {?module:imscUtils.ErrorHandler} errorHandler Error callback
* @returns {Object} Opaque in-memory representation of an ISD
*/
imscISD.generateISD = function (tt, offset, errorHandler) {
/* TODO check for tt and offset validity */
/* create the ISD object from the IMSC1 doc */
var isd = new ISD(tt);
/* context */
var context = {
/*rubyfs: []*/ /* font size of the nearest textContainer or container */
};
/* process regions */
for (var r in tt.head.layout.regions) {
/* post-order traversal of the body tree per [construct intermediate document] */
var c = isdProcessContentElement(tt, offset, tt.head.layout.regions[r], tt.body, null, '', tt.head.layout.regions[r], errorHandler, context);
if (c !== null) {
/* add the region to the ISD */
isd.contents.push(c.element);
}
}
return isd;
};
function isdProcessContentElement(doc, offset, region, body, parent, inherited_region_id, elem, errorHandler, context) {
/* prune if temporally inactive */
if (offset < elem.begin || offset >= elem.end) {
return null;
}
/*
* set the associated region as specified by the regionID attribute, or the
* inherited associated region otherwise
*/
var associated_region_id = 'regionID' in elem && elem.regionID !== '' ? elem.regionID : inherited_region_id;
/* prune the element if either:
* - the element is not terminal and the associated region is neither the default
* region nor the parent region (this allows children to be associated with a
* region later on)
* - the element is terminal and the associated region is not the parent region
*/
/* TODO: improve detection of terminal elements since <region> has no contents */
if (parent !== null /* are we in the region element */ &&
associated_region_id !== region.id &&
(
(!('contents' in elem)) ||
('contents' in elem && elem.contents.length === 0) ||
associated_region_id !== ''
)
)
return null;
/* create an ISD element, including applying specified styles */
var isd_element = new ISDContentElement(elem);
/* apply set (animation) styling */
for (var i in elem.sets) {
if (offset < elem.sets[i].begin || offset >= elem.sets[i].end)
continue;
isd_element.styleAttrs[elem.sets[i].qname] = elem.sets[i].value;
}
/*
* keep track of specified styling attributes so that we
* can compute them later
*/
var spec_attr = {};
for (var qname in isd_element.styleAttrs) {
spec_attr[qname] = true;
/* special rule for tts:writingMode (section 7.29.1 of XSL)
* direction is set consistently with writingMode only
* if writingMode sets inline-direction to LTR or RTL
*/
if (qname === imscStyles.byName.writingMode.qname &&
!(imscStyles.byName.direction.qname in isd_element.styleAttrs)) {
var wm = isd_element.styleAttrs[qname];
if (wm === "lrtb" || wm === "lr") {
isd_element.styleAttrs[imscStyles.byName.direction.qname] = "ltr";
} else if (wm === "rltb" || wm === "rl") {
isd_element.styleAttrs[imscStyles.byName.direction.qname] = "rtl";
}
}
}
/* inherited styling */
if (parent !== null) {
for (var j in imscStyles.all) {
var sa = imscStyles.all[j];
/* textDecoration has special inheritance rules */
if (sa.qname === imscStyles.byName.textDecoration.qname) {
/* handle both textDecoration inheritance and specification */
var ps = parent.styleAttrs[sa.qname];
var es = isd_element.styleAttrs[sa.qname];
var outs = [];
if (es === undefined) {
outs = ps;
} else if (es.indexOf("none") === -1) {
if ((es.indexOf("noUnderline") === -1 &&
ps.indexOf("underline") !== -1) ||
es.indexOf("underline") !== -1) {
outs.push("underline");
}
if ((es.indexOf("noLineThrough") === -1 &&
ps.indexOf("lineThrough") !== -1) ||
es.indexOf("lineThrough") !== -1) {
outs.push("lineThrough");
}
if ((es.indexOf("noOverline") === -1 &&
ps.indexOf("overline") !== -1) ||
es.indexOf("overline") !== -1) {
outs.push("overline");
}
} else {
outs.push("none");
}
isd_element.styleAttrs[sa.qname] = outs;
} else if (sa.qname === imscStyles.byName.fontSize.qname &&
!(sa.qname in isd_element.styleAttrs) &&
isd_element.kind === 'span' &&
isd_element.styleAttrs[imscStyles.byName.ruby.qname] === "textContainer") {
/* special inheritance rule for ruby text container font size */
var ruby_fs = parent.styleAttrs[imscStyles.byName.fontSize.qname];
isd_element.styleAttrs[sa.qname] = new imscUtils.ComputedLength(
0.5 * ruby_fs.rw,
0.5 * ruby_fs.rh);
} else if (sa.qname === imscStyles.byName.fontSize.qname &&
!(sa.qname in isd_element.styleAttrs) &&
isd_element.kind === 'span' &&
isd_element.styleAttrs[imscStyles.byName.ruby.qname] === "text") {
/* special inheritance rule for ruby text font size */
var parent_fs = parent.styleAttrs[imscStyles.byName.fontSize.qname];
if (parent.styleAttrs[imscStyles.byName.ruby.qname] === "textContainer") {
isd_element.styleAttrs[sa.qname] = parent_fs;
} else {
isd_element.styleAttrs[sa.qname] = new imscUtils.ComputedLength(
0.5 * parent_fs.rw,
0.5 * parent_fs.rh);
}
} else if (sa.inherit &&
(sa.qname in parent.styleAttrs) &&
!(sa.qname in isd_element.styleAttrs)) {
isd_element.styleAttrs[sa.qname] = parent.styleAttrs[sa.qname];
}
}
}
/* initial value styling */
for (var k in imscStyles.all) {
var ivs = imscStyles.all[k];
/* skip if value is already specified */
if (ivs.qname in isd_element.styleAttrs) continue;
/* skip tts:position if tts:origin is specified */
if (ivs.qname === imscStyles.byName.position.qname &&
imscStyles.byName.origin.qname in isd_element.styleAttrs)
continue;
/* skip tts:origin if tts:position is specified */
if (ivs.qname === imscStyles.byName.origin.qname &&
imscStyles.byName.position.qname in isd_element.styleAttrs)
continue;
/* determine initial value */
var iv = doc.head.styling.initials[ivs.qname] || ivs.initial;
/* apply initial value to elements other than region only if non-inherited */
if (isd_element.kind === 'region' || (ivs.inherit === false && iv !== null)) {
isd_element.styleAttrs[ivs.qname] = ivs.parse(iv);
/* keep track of the style as specified */
spec_attr[ivs.qname] = true;
}
}
/* compute styles (only for non-inherited styles) */
/* TODO: get rid of spec_attr */
for (var z in imscStyles.all) {
var cs = imscStyles.all[z];
if (!(cs.qname in spec_attr)) continue;
if (cs.compute !== null) {
var cstyle = cs.compute(
/*doc, parent, element, attr, context*/
doc,
parent,
isd_element,
isd_element.styleAttrs[cs.qname],
context
);
if (cstyle !== null) {
isd_element.styleAttrs[cs.qname] = cstyle;
} else {
reportError(errorHandler, "Style '" + cs.qname + "' on element '" + isd_element.kind + "' cannot be computed");
}
}
}
/* tts:fontSize special ineritance for ruby */
/* var isrubycontainer = false;
if (isd_element.kind === "span") {
var rtemp = isd_element.styleAttrs[imscStyles.byName.ruby.qname];
if (rtemp === "container" || rtemp === "textContainer") {
isrubycontainer = true;
context.rubyfs.unshift(isd_element.styleAttrs[imscStyles.byName.fontSize.qname]);
}
} */
/* prune if tts:display is none */
if (isd_element.styleAttrs[imscStyles.byName.display.qname] === "none")
return null;
/* process contents of the element */
var contents;
if (parent === null) {
/* we are processing the region */
if (body === null) {
/* if there is no body, still process the region but with empty content */
contents = [];
} else {
/*use the body element as contents */
contents = [body];
}
} else if ('contents' in elem) {
contents = elem.contents;
}
for (var x in contents) {
var c = isdProcessContentElement(doc, offset, region, body, isd_element, associated_region_id, contents[x], errorHandler, context);
/*
* keep child element only if they are non-null and their region match
* the region of this element
*/
if (c !== null) {
isd_element.contents.push(c.element);
}
}
/* compute used value of lineHeight="normal" */
/* if (isd_element.styleAttrs[imscStyles.byName.lineHeight.qname] === "normal" ) {
isd_element.styleAttrs[imscStyles.byName.lineHeight.qname] =
isd_element.styleAttrs[imscStyles.byName.fontSize.qname] * 1.2;
}
*/
/* tts:fontSize special ineritance for ruby */
/*if (isrubycontainer) {
context.rubyfs.shift();
}*/
/* remove styles that are not applicable */
for (var qnameb in isd_element.styleAttrs) {
var na = false;
if (qnameb === imscStyles.byName.rubyAlign.qname) {
/* special applicability rule */
na = (isd_element.kind !== 'span') ||
(isd_element.styleAttrs[imscStyles.byName.ruby.qname] !== 'container');
} else if (qnameb === imscStyles.byName.rubyPosition.qname) {
/* special applicability rule */
na = (isd_element.kind !== 'span') ||
! (isd_element.styleAttrs[imscStyles.byName.ruby.qname] === 'textContainer' ||
isd_element.styleAttrs[imscStyles.byName.ruby.qname] === 'text');
} else {
var da = imscStyles.byQName[qnameb];
na = da.applies.indexOf(isd_element.kind) === -1;
}
if (na) {
delete isd_element.styleAttrs[qnameb];
}
}
/* collapse white space if space is "default" */
if (isd_element.kind === 'span' && isd_element.text && isd_element.space === "default") {
var trimmedspan = isd_element.text.replace(/\s+/g, ' ');
isd_element.text = trimmedspan;
}
/* trim whitespace around explicit line breaks */
if (isd_element.kind === 'p') {
var elist = [];
constructSpanList(isd_element, elist);
var l = 0;
var state = "after_br";
var br_pos = 0;
while (true) {
if (state === "after_br") {
if (l >= elist.length || elist[l].kind === "br") {
state = "before_br";
br_pos = l;
l--;
} else {
if (elist[l].space !== "preserve") {
elist[l].text = elist[l].text.replace(/^\s+/g, '');
}
if (elist[l].text.length > 0) {
state = "looking_br";
l++;
} else {
elist.splice(l, 1);
}
}
} else if (state === "before_br") {
if (l < 0 || elist[l].kind === "br") {
state = "after_br";
l = br_pos + 1;
if (l >= elist.length) break;
} else {
if (elist[l].space !== "preserve") {
elist[l].text = elist[l].text.replace(/\s+$/g, '');
}
if (elist[l].text.length > 0) {
state = "after_br";
l = br_pos + 1;
if (l >= elist.length) break;
} else {
elist.splice(l, 1);
l--;
}
}
} else {
if (l >= elist.length || elist[l].kind === "br") {
state = "before_br";
br_pos = l;
l--;
} else {
l++;
}
}
}
pruneEmptySpans(isd_element);
}
/* keep element if:
* * contains a background image
* * <br/>
* * if there are children
* * if it is an image
* * if <span> and has text
* * if region and showBackground = always
*/
if ((isd_element.kind === 'div' && imscStyles.byName.backgroundImage.qname in isd_element.styleAttrs) ||
isd_element.kind === 'br' ||
isd_element.kind === 'image' ||
('contents' in isd_element && isd_element.contents.length > 0) ||
(isd_element.kind === 'span' && isd_element.text !== null) ||
(isd_element.kind === 'region' &&
isd_element.styleAttrs[imscStyles.byName.showBackground.qname] === 'always')) {
return {
region_id: associated_region_id,
element: isd_element
};
}
return null;
}
function constructSpanList(element, elist) {
if ('contents' in element) {
for (var i in element.contents) {
constructSpanList(element.contents[i], elist);
}
} else if (element.kind === 'span' || element.kind === 'br') {
elist.push(element);
}
}
function pruneEmptySpans(element) {
if (element.kind === 'br') {
return false;
} else if ('text' in element) {
return element.text.length === 0;
} else if ('contents' in element) {
var i = element.contents.length;
while (i--) {
if (pruneEmptySpans(element.contents[i])) {
element.contents.splice(i, 1);
}
}
return element.contents.length === 0;
}
}
function ISD(tt) {
this.contents = [];
this.aspectRatio = tt.aspectRatio;
}
function ISDContentElement(ttelem) {
/* assume the element is a region if it does not have a kind */
this.kind = ttelem.kind || 'region';
/* copy id */
if (ttelem.id) {
this.id = ttelem.id;
}
/* deep copy of style attributes */
this.styleAttrs = {};
for (var sname in ttelem.styleAttrs) {
this.styleAttrs[sname] =
ttelem.styleAttrs[sname];
}
/* copy src and type if image */
if ('src' in ttelem) {
this.src = ttelem.src;
}
if ('type' in ttelem) {
this.type = ttelem.type;
}
/* TODO: clean this!
* TODO: ISDElement and document element should be better tied together */
if ('text' in ttelem) {
this.text = ttelem.text;
} else if (this.kind === 'region' || 'contents' in ttelem) {
this.contents = [];
}
if ('space' in ttelem) {
this.space = ttelem.space;
}
}
/*
* ERROR HANDLING UTILITY FUNCTIONS
*
*/
function reportInfo(errorHandler, msg) {
if (errorHandler && errorHandler.info && errorHandler.info(msg))
throw msg;
}
function reportWarning(errorHandler, msg) {
if (errorHandler && errorHandler.warn && errorHandler.warn(msg))
throw msg;
}
function reportError(errorHandler, msg) {
if (errorHandler && errorHandler.error && errorHandler.error(msg))
throw msg;
}
function reportFatal(errorHandler, msg) {
if (errorHandler && errorHandler.fatal)
errorHandler.fatal(msg);
throw msg;
}
})(typeof exports === 'undefined' ? this.imscISD = {} : exports,
typeof imscNames === 'undefined' ? require("./names") : imscNames,
typeof imscStyles === 'undefined' ? require("./styles") : imscStyles,
typeof imscUtils === 'undefined' ? require("./utils") : imscUtils
);