imsc
Version:
Renders IMSC documents to HTML5 fragments
1,245 lines (980 loc) • 28.3 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.
*/
import { ns_ebutts, ns_itts, ns_smpte, ns_tts } from "./names.js";
import { ComputedLength, parseColor, parseLength, parsePosition, parseTextShadow, toComputedLength } from "./utils.js";
/**
* @module imscStyles
*/
/**
* @typedef {import("./doc").TTElement} TTElement
* @typedef {import("./doc").Node} Node
*/
export class StylingAttributeDefinition {
/**
* @param {string} ns
* @param {string} name
* @param {string} initialValue
* @param {string[]} appliesTo
* @param {boolean} isInherit
* @param {boolean} isAnimatable
* @param {(value: string) => any} parseFunc
* @param {(doc: TTElement, parent: Node, element: Node, attr: string) => any} computeFunc
*/
constructor(ns, name, initialValue, appliesTo, isInherit, isAnimatable, parseFunc, computeFunc) {
this.name = name;
this.ns = ns;
this.qname = ns + " " + name;
this.inherit = isInherit;
this.animatable = isAnimatable;
this.initial = initialValue;
this.applies = appliesTo;
this.parse = parseFunc;
this.compute = computeFunc;
}
}
export const all = [
new StylingAttributeDefinition(
ns_tts,
"backgroundColor",
"transparent",
["body", "div", "p", "region", "span"],
false,
true,
parseColor,
null,
),
new StylingAttributeDefinition(
ns_tts,
"color",
"white",
["span"],
true,
true,
parseColor,
null,
),
new StylingAttributeDefinition(
ns_tts,
"direction",
"ltr",
["p", "span"],
true,
true,
function (str) {
return str;
},
null,
),
new StylingAttributeDefinition(
ns_tts,
"display",
"auto",
["body", "div", "p", "region", "span"],
false,
true,
function (str) {
return str;
},
null,
),
new StylingAttributeDefinition(
ns_tts,
"displayAlign",
"before",
["region"],
false,
true,
function (str) {
return str;
},
null,
),
new StylingAttributeDefinition(
ns_tts,
"extent",
"auto",
["tt", "region"],
false,
true,
function (str) {
if (str === "auto") {
return str;
} else {
const s = str.split(" ");
if (s.length !== 2)
return null;
const w = parseLength(s[0]);
const h = parseLength(s[1]);
if (!h || !w)
return null;
return { "h": h, "w": w };
}
},
function (doc, parent, element, attr) {
let h;
let w;
if (attr === "auto") {
h = new ComputedLength(0, 1);
} else {
h = toComputedLength(
attr.h.value,
attr.h.unit,
null,
doc.dimensions.h,
null,
doc.pxLength.h,
);
if (h === null) {
return null;
}
}
if (attr === "auto") {
w = new ComputedLength(1, 0);
} else {
w = toComputedLength(
attr.w.value,
attr.w.unit,
null,
doc.dimensions.w,
null,
doc.pxLength.w,
);
if (w === null) {
return null;
}
}
return { "h": h, "w": w };
},
),
new StylingAttributeDefinition(
ns_tts,
"fontFamily",
"default",
["span", "p"],
true,
true,
function (str) {
const ffs = str.split(",");
const rslt = [];
for (let i = 0; i < ffs.length; i++) {
ffs[i] = ffs[i].trim();
if (ffs[i].charAt(0) !== "'" && ffs[i].charAt(0) !== '"') {
if (ffs[i] === "default") {
/* per IMSC1 */
rslt.push("monospaceSerif");
} else {
rslt.push(ffs[i]);
}
} else {
rslt.push(ffs[i]);
}
}
return rslt;
},
null,
),
new StylingAttributeDefinition(
ns_tts,
"shear",
"0%",
["p"],
true,
true,
parseLength,
function (doc, parent, element, attr) {
let fs;
if (attr.unit === "%") {
fs = Math.abs(attr.value) > 100 ? Math.sign(attr.value) * 100 : attr.value;
} else {
return null;
}
return fs;
},
),
new StylingAttributeDefinition(
ns_tts,
"fontSize",
"1c",
["span", "p"],
true,
true,
parseLength,
function (doc, parent, element, attr) {
const fs = toComputedLength(
attr.value,
attr.unit,
parent !== null ? parent.styleAttrs[byName.fontSize.qname] : doc.cellLength.h,
parent !== null ? parent.styleAttrs[byName.fontSize.qname] : doc.cellLength.h,
doc.cellLength.h,
doc.pxLength.h,
);
return fs;
},
),
new StylingAttributeDefinition(
ns_tts,
"fontStyle",
"normal",
["span", "p"],
true,
true,
function (str) {
/* TODO: handle font style */
return str;
},
null,
),
new StylingAttributeDefinition(
ns_tts,
"fontVariant",
"normal",
["span"],
true,
true,
function (str) {
if (str === "normal" || str === "sub" || str === "super") {
return str;
}
return null;
},
null,
),
new StylingAttributeDefinition(
ns_tts,
"fontWeight",
"normal",
["span", "p"],
true,
true,
function (str) {
/* TODO: handle font weight */
return str;
},
null,
),
new StylingAttributeDefinition(
ns_tts,
"lineHeight",
"normal",
["p"],
true,
true,
function (str) {
if (str === "normal") {
return str;
} else {
return parseLength(str);
}
},
function (doc, parent, element, attr) {
let lh;
if (attr === "normal") {
/* inherit normal per https://github.com/w3c/ttml1/issues/220 */
lh = attr;
} else {
lh = toComputedLength(
attr.value,
attr.unit,
element.styleAttrs[byName.fontSize.qname],
element.styleAttrs[byName.fontSize.qname],
doc.cellLength.h,
doc.pxLength.h,
);
if (lh === null) {
return null;
}
}
/* TODO: create a Length constructor */
return lh;
},
),
new StylingAttributeDefinition(
ns_tts,
"opacity",
1.0,
["region"],
false,
true,
parseFloat,
null,
),
new StylingAttributeDefinition(
ns_tts,
"origin",
"auto",
["region"],
false,
true,
function (str) {
if (str === "auto") {
return str;
} else {
const s = str.split(" ");
if (s.length !== 2)
return null;
const w = parseLength(s[0]);
const h = parseLength(s[1]);
if (!h || !w)
return null;
return { "h": h, "w": w };
}
},
function (doc, parent, element, attr) {
let h;
let w;
if (attr === "auto") {
h = new ComputedLength(0, 0);
} else {
h = toComputedLength(
attr.h.value,
attr.h.unit,
null,
doc.dimensions.h,
null,
doc.pxLength.h,
);
if (h === null) {
return null;
}
}
if (attr === "auto") {
w = new ComputedLength(0, 0);
} else {
w = toComputedLength(
attr.w.value,
attr.w.unit,
null,
doc.dimensions.w,
null,
doc.pxLength.w,
);
if (w === null) {
return null;
}
}
return { "h": h, "w": w };
},
),
new StylingAttributeDefinition(
ns_tts,
"overflow",
"hidden",
["region"],
false,
true,
function (str) {
return str;
},
null,
),
new StylingAttributeDefinition(
ns_tts,
"padding",
"0px",
["region"],
false,
true,
function (str) {
const s = str.split(" ");
if (s.length > 4)
return null;
const r = [];
for (let i = 0; i < s.length; i++) {
const l = parseLength(s[i]);
if (!l)
return null;
r.push(l);
}
return r;
},
function (doc, parent, element, attr) {
let padding;
/* TODO: make sure we are in region */
/*
* expand padding shortcuts to
* [before, end, after, start]
*
*/
if (attr.length === 1) {
padding = [attr[0], attr[0], attr[0], attr[0]];
} else if (attr.length === 2) {
padding = [attr[0], attr[1], attr[0], attr[1]];
} else if (attr.length === 3) {
padding = [attr[0], attr[1], attr[2], attr[1]];
} else if (attr.length === 4) {
padding = [attr[0], attr[1], attr[2], attr[3]];
} else {
return null;
}
/* TODO: take into account tts:direction */
/*
* transform [before, end, after, start] according to writingMode to
* [top,left,bottom,right]
*
*/
const dir = element.styleAttrs[byName.writingMode.qname];
if (dir === "lrtb" || dir === "lr") {
padding = [padding[0], padding[3], padding[2], padding[1]];
} else if (dir === "rltb" || dir === "rl") {
padding = [padding[0], padding[1], padding[2], padding[3]];
} else if (dir === "tblr") {
padding = [padding[3], padding[0], padding[1], padding[2]];
} else if (dir === "tbrl" || dir === "tb") {
padding = [padding[3], padding[2], padding[1], padding[0]];
} else {
return null;
}
const out = [];
for (let i = 0; i < padding.length; i++) {
if (padding[i].value === 0) {
out[i] = new ComputedLength(0, 0);
} else {
out[i] = toComputedLength(
padding[i].value,
padding[i].unit,
element.styleAttrs[byName.fontSize.qname],
i === 0 || i === 2 ? element.styleAttrs[byName.extent.qname].h : element.styleAttrs[byName.extent.qname].w,
i === 0 || i === 2 ? doc.cellLength.h : doc.cellLength.w,
i === 0 || i === 2 ? doc.pxLength.h : doc.pxLength.w,
);
if (out[i] === null) return null;
}
}
return out;
},
),
new StylingAttributeDefinition(
ns_tts,
"position",
"top left",
["region"],
false,
true,
function (str) {
return parsePosition(str);
},
function (doc, parent, element, attr) {
let h;
let w;
h = toComputedLength(
attr.v.offset.value,
attr.v.offset.unit,
null,
new ComputedLength(
- element.styleAttrs[byName.extent.qname].h.rw,
doc.dimensions.h.rh - element.styleAttrs[byName.extent.qname].h.rh,
),
null,
doc.pxLength.h,
);
if (h === null) return null;
if (attr.v.edge === "bottom") {
h = new ComputedLength(
- h.rw - element.styleAttrs[byName.extent.qname].h.rw,
doc.dimensions.h.rh - h.rh - element.styleAttrs[byName.extent.qname].h.rh,
);
}
w = toComputedLength(
attr.h.offset.value,
attr.h.offset.unit,
null,
new ComputedLength(
doc.dimensions.w.rw - element.styleAttrs[byName.extent.qname].w.rw,
- element.styleAttrs[byName.extent.qname].w.rh,
),
null,
doc.pxLength.w,
);
if (h === null) return null;
if (attr.h.edge === "right") {
w = new ComputedLength(
doc.dimensions.w.rw - w.rw - element.styleAttrs[byName.extent.qname].w.rw,
- w.rh - element.styleAttrs[byName.extent.qname].w.rh,
);
}
return { "h": h, "w": w };
},
),
new StylingAttributeDefinition(
ns_tts,
"ruby",
"none",
["span"],
false,
true,
function (str) {
return str;
},
null,
),
new StylingAttributeDefinition(
ns_tts,
"rubyAlign",
"center",
["span"],
true,
true,
function (str) {
if (!(str === "center" || str === "spaceAround")) {
return null;
}
return str;
},
null,
),
new StylingAttributeDefinition(
ns_tts,
"rubyPosition",
"outside",
["span"],
true,
true,
function (str) {
return str;
},
null,
),
new StylingAttributeDefinition(
ns_tts,
"rubyReserve",
"none",
["p"],
true,
true,
function (str) {
const s = str.split(" ");
const r = [null, null];
if (s.length === 0 || s.length > 2)
return null;
if (s[0] === "none" ||
s[0] === "both" ||
s[0] === "after" ||
s[0] === "before" ||
s[0] === "outside") {
r[0] = s[0];
} else {
return null;
}
if (s.length === 2 && s[0] !== "none") {
const l = parseLength(s[1]);
if (l) {
r[1] = l;
} else {
return null;
}
}
return r;
},
function (doc, parent, element, attr) {
if (attr[0] === "none") {
return attr;
}
let fs = null;
if (attr[1] === null) {
fs = new ComputedLength(
element.styleAttrs[byName.fontSize.qname].rw * 0.5,
element.styleAttrs[byName.fontSize.qname].rh * 0.5,
);
} else {
fs = toComputedLength(attr[1].value,
attr[1].unit,
element.styleAttrs[byName.fontSize.qname],
element.styleAttrs[byName.fontSize.qname],
doc.cellLength.h,
doc.pxLength.h,
);
}
if (fs === null) return null;
return [attr[0], fs];
},
),
new StylingAttributeDefinition(
ns_tts,
"showBackground",
"always",
["region"],
false,
true,
function (str) {
return str;
},
null,
),
new StylingAttributeDefinition(
ns_tts,
"textAlign",
"start",
["p"],
true,
true,
function (str) {
return str;
},
function (doc, parent, element, attr) {
/* Section 7.16.9 of XSL */
if (attr === "left") {
return "start";
} else if (attr === "right") {
return "end";
} else {
return attr;
}
},
),
new StylingAttributeDefinition(
ns_tts,
"textCombine",
"none",
["span"],
true,
true,
function (str) {
if (str === "none" || str === "all") {
return str;
}
return null;
},
null,
),
new StylingAttributeDefinition(
ns_tts,
"textDecoration",
"none",
["span"],
true,
true,
function (str) {
return str.split(" ");
},
null,
),
new StylingAttributeDefinition(
ns_tts,
"textEmphasis",
"none",
["span"],
true,
true,
function (str) {
const e = str.split(" ");
const rslt = { style: null, symbol: null, color: null, position: null };
for (let i = 0; i < e.length; i++) {
if (e[i] === "none" || e[i] === "auto") {
rslt.style = e[i];
} else if (e[i] === "filled" ||
e[i] === "open") {
rslt.style = e[i];
} else if (e[i] === "circle" ||
e[i] === "dot" ||
e[i] === "sesame") {
rslt.symbol = e[i];
} else if (e[i] === "current") {
rslt.color = e[i];
} else if (e[i] === "outside" || e[i] === "before" || e[i] === "after") {
rslt.position = e[i];
} else {
rslt.color = parseColor(e[i]);
if (rslt.color === null)
return null;
}
}
if (rslt.style == null && rslt.symbol == null) {
rslt.style = "auto";
} else {
rslt.symbol = rslt.symbol || "circle";
rslt.style = rslt.style || "filled";
}
rslt.position = rslt.position || "outside";
rslt.color = rslt.color || "current";
return rslt;
},
null,
),
new StylingAttributeDefinition(
ns_tts,
"textOutline",
"none",
["span"],
true,
true,
function (str) {
/*
* returns {c: <color>?, thichness: <length>} | "none"
*
*/
if (str === "none") {
return str;
} else {
const r = {};
const s = str.split(" ");
if (s.length === 0 || s.length > 2)
return null;
const c = parseColor(s[0]);
r.color = c;
if (c !== null)
s.shift();
if (s.length !== 1)
return null;
const l = parseLength(s[0]);
if (!l)
return null;
r.thickness = l;
return r;
}
},
function (doc, parent, element, attr) {
/*
* returns {color: <color>, thickness: <norm length>}
*
*/
if (attr === "none")
return attr;
const rslt = {};
if (attr.color === null) {
rslt.color = element.styleAttrs[byName.color.qname];
} else {
rslt.color = attr.color;
}
rslt.thickness = toComputedLength(
attr.thickness.value,
attr.thickness.unit,
element.styleAttrs[byName.fontSize.qname],
element.styleAttrs[byName.fontSize.qname],
doc.cellLength.h,
doc.pxLength.h,
);
if (rslt.thickness === null)
return null;
return rslt;
},
),
new StylingAttributeDefinition(
ns_tts,
"textShadow",
"none",
["span"],
true,
true,
parseTextShadow,
function (doc, parent, element, attr) {
/*
* returns [{x_off: <length>, y_off: <length>, b_radius: <length>, color: <color>}*] or "none"
*
*/
if (attr === "none")
return attr;
const r = [];
for (let i = 0; i < attr.length; i++) {
const shadow = {};
shadow.x_off = toComputedLength(
attr[i][0].value,
attr[i][0].unit,
null,
element.styleAttrs[byName.fontSize.qname],
null,
doc.pxLength.w,
);
if (shadow.x_off === null)
return null;
shadow.y_off = toComputedLength(
attr[i][1].value,
attr[i][1].unit,
null,
element.styleAttrs[byName.fontSize.qname],
null,
doc.pxLength.h,
);
if (shadow.y_off === null)
return null;
if (attr[i][2] === null) {
shadow.b_radius = 0;
} else {
shadow.b_radius = toComputedLength(
attr[i][2].value,
attr[i][2].unit,
null,
element.styleAttrs[byName.fontSize.qname],
null,
doc.pxLength.h,
);
if (shadow.b_radius === null)
return null;
}
if (attr[i][3] === null) {
shadow.color = element.styleAttrs[byName.color.qname];
} else {
shadow.color = attr[i][3];
}
r.push(shadow);
}
return r;
},
),
new StylingAttributeDefinition(
ns_tts,
"unicodeBidi",
"normal",
["span", "p"],
false,
true,
function (str) {
return str;
},
null,
),
new StylingAttributeDefinition(
ns_tts,
"visibility",
"visible",
["body", "div", "p", "region", "span"],
true,
true,
function (str) {
return str;
},
null,
),
new StylingAttributeDefinition(
ns_tts,
"wrapOption",
"wrap",
["span"],
true,
true,
function (str) {
return str;
},
null,
),
new StylingAttributeDefinition(
ns_tts,
"writingMode",
"lrtb",
["region"],
false,
true,
function (str) {
return str;
},
null,
),
new StylingAttributeDefinition(
ns_tts,
"zIndex",
"auto",
["region"],
false,
true,
function (str) {
let rslt;
if (str === "auto") {
rslt = str;
} else {
rslt = parseInt(str);
if (isNaN(rslt)) {
rslt = null;
}
}
return rslt;
},
null,
),
new StylingAttributeDefinition(
ns_ebutts,
"linePadding",
"0c",
["p"],
true,
false,
parseLength,
function (doc, parent, element, attr) {
return toComputedLength(attr.value, attr.unit, null, null, doc.cellLength.w, null);
},
),
new StylingAttributeDefinition(
ns_ebutts,
"multiRowAlign",
"auto",
["p"],
true,
false,
function (str) {
return str;
},
null,
),
new StylingAttributeDefinition(
ns_smpte,
"backgroundImage",
null,
["div"],
false,
false,
function (str) {
return str;
},
null,
),
new StylingAttributeDefinition(
ns_itts,
"forcedDisplay",
"false",
["body", "div", "p", "region", "span"],
true,
true,
function (str) {
return str === "true" ? true : false;
},
null,
),
new StylingAttributeDefinition(
ns_itts,
"fillLineGap",
"false",
["p"],
true,
true,
function (str) {
return str === "true" ? true : false;
},
null,
),
];
/* TODO: allow null parse function */
/**
* @type {Record<string, StylingAttributeDefinition>}
*/
export const byQName = {};
for (const i in all) {
byQName[all[i].qname] = all[i];
}
/**
* @type {Record<string, StylingAttributeDefinition>}
*/
export const byName = {};
for (const j in all) {
byName[all[j].name] = all[j];
}