diff --git a/package-lock.json b/package-lock.json index be702943..0a2292a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,14 @@ "version": "1.1.5", "license": "BSD-2-Clause", "dependencies": { - "sax": "1.2.1" + "sax": "^1.4.1" }, "devDependencies": { "@eslint/js": "^9.1.1", "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-terser": "^0.4.4", + "@types/sax": "^1.2.7", "eslint": "^9.1.1", "filesaver.js-npm": "latest", "globals": "^15.0.0", @@ -580,12 +581,30 @@ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, + "node_modules/@types/node": { + "version": "20.14.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.5.tgz", + "integrity": "sha512-aoRR+fJkZT2l0aGOJhuA8frnCSoNX6W7U2mpNq63+BxBIj5BQFt8rHy627kijCmm63ijdSdwvGgpUsU6MBsZZA==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "dev": true }, + "node_modules/@types/sax": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", + "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/abbrev": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", @@ -1222,6 +1241,20 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -2592,20 +2625,6 @@ "rollup": "^1.20.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" } }, - "node_modules/rollup/node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -2642,9 +2661,9 @@ "dev": true }, "node_modules/sax": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", - "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" }, "node_modules/serialize-javascript": { "version": "6.0.2", @@ -2865,6 +2884,12 @@ "node": "*" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", diff --git a/package.json b/package.json index 591b4d61..23cf7820 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,20 @@ "main": "dist/main/js/main.js", "types": "dist/main/js/main.d.ts", "unpkg": "dist/imsc.min.js", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./*.js": { + "types": "./dist/*.d.ts", + "import": "./dist/*.js" + }, + "./*": { + "types": "./dist/*.d.ts", + "import": "./dist/*.js" + } + }, "scripts": { "prepublishOnly": "grunt build:release", "dev": "npx http-server build/public_html", @@ -37,13 +51,14 @@ "test": "node --test ./src/test/js/*Test.js" }, "dependencies": { - "sax": "1.2.1" + "sax": "^1.4.1" }, "devDependencies": { "@eslint/js": "^9.1.1", "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-terser": "^0.4.4", + "@types/sax": "^1.2.7", "eslint": "^9.1.1", "filesaver.js-npm": "latest", "globals": "^15.0.0", diff --git a/src/main/js/doc.js b/src/main/js/doc.js index e73b6bdb..ed5d5645 100644 --- a/src/main/js/doc.js +++ b/src/main/js/doc.js @@ -24,9 +24,9 @@ * POSSIBILITY OF SUCH DAMAGE. */ -import sax from "sax"; import { reportError, reportFatal, reportWarning } from "./error.js"; import { ns_ebutts, ns_ittp, ns_itts, ns_tt, ns_ttp, ns_tts } from "./names.js"; +import { createDOMParser } from "./parser.js"; import { byName, byQName } from "./styles.js"; import { ComputedLength, hasOwnProperty, parseLength } from "./utils.js"; @@ -34,6 +34,12 @@ import { ComputedLength, hasOwnProperty, parseLength } from "./utils.js"; * @module imscDoc */ +/** + * @typedef {import("./error").ErrorHandler} ErrorHandler + * @typedef {import("./parser").Node} Node + * @typedef {import("./parser").Parser} Parser + */ + /** * Allows a client to provide callbacks to handle children of the element * @typedef {Object} MetadataHandler @@ -47,7 +53,7 @@ import { ComputedLength, hasOwnProperty, parseLength } from "./utils.js"; * @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 + * @param {Record} attributes List of attributes, each consisting of a * `uri`, `name` and `value` */ @@ -70,20 +76,20 @@ import { ComputedLength, hasOwnProperty, parseLength } from "./utils.js"; * be called back when nodes are present in elements. * * @param {string} xmlstring XML document - * @param {?module:imscUtils.ErrorHandler} errorHandler Error callback + * @param {ErrorHandler} errorHandler Error callback * @param {?MetadataHandler} metadataHandler Callback for elements - * @returns {Object} Opaque in-memory representation of an IMSC1 document + * @param {?Parser} parser XML parser + * @returns {?TT} Opaque in-memory representation of an IMSC1 document */ -export function fromXML(xmlstring, errorHandler, metadataHandler) { - const p = sax.parser(true, { xmlns: true }); +export function fromXML(xmlstring, errorHandler, metadataHandler, parser = createDOMParser()) { const estack = []; const xmllangstack = []; const xmlspacestack = []; let metadata_depth = 0; let doc = null; - p.onclosetag = function () { + parser.onclosetag = function () { if (estack[0] instanceof Region) { @@ -183,7 +189,7 @@ export function fromXML(xmlstring, errorHandler, metadataHandler) { estack.shift(); }; - p.ontext = function (str) { + parser.ontext = function (str) { if (estack[0] === undefined) { @@ -226,7 +232,7 @@ export function fromXML(xmlstring, errorHandler, metadataHandler) { }; - p.onopentag = function (node) { + parser.onopentag = function (node) { // maintain the xml:space stack @@ -596,7 +602,7 @@ export function fromXML(xmlstring, errorHandler, metadataHandler) { // parse the document - p.write(xmlstring).close(); + parser.write(xmlstring).close(); // all referential styling has been flatten, so delete styles @@ -807,19 +813,31 @@ function resolveTiming(doc, element, prev_sibling, parent) { } -class ForeignElement { +export class ForeignElement { constructor(node) { this.node = node; } } -class TT { +export class TT { constructor() { + /** + * @type {number[]} + */ this.events = []; this.head = new Head(); + + /** + * @type {?Body} + */ this.body = null; } + /** + * @param {Node} node + * @param {string} xmllang + * @param {ErrorHandler} errorHandler + */ initFromNode(node, xmllang, errorHandler) { /* compute cell resolution */ @@ -887,7 +905,9 @@ class TT { }; /* xml:lang */ - + /** + * @type {string} + */ this.lang = xmllang; } @@ -922,10 +942,10 @@ class TT { } - /* + /** * Retrieves the range of ISD times covered by the document * - * @returns {Array} Array of two elements: min_begin_time and max_begin_time + * @returns {[number, number]} Array of two elements: min_begin_time and max_begin_time * */ getMediaTimeRange() { @@ -933,10 +953,10 @@ class TT { return [this.events[0], this.events[this.events.length - 1]]; }; - /* + /** * Returns list of ISD begin times * - * @returns {Array} + * @returns {number[]} */ getMediaTimeEvents() { @@ -948,7 +968,7 @@ class TT { * Represents a TTML Head element */ -class Head { +export class Head { constructor() { this.styling = new Styling(); this.layout = new Layout(); @@ -959,9 +979,16 @@ class Head { * Represents a TTML Styling element */ -class Styling { +export class Styling { constructor() { + /** + * @type {Record} + */ this.styles = {}; + + /** + * @type {Record} + */ this.initials = {}; } } @@ -970,13 +997,28 @@ class Styling { * Represents a TTML Style element */ -class Style { +export class Style { constructor() { + /** + * @type {string} + */ this.id = null; + + /** + * @type {Record} + */ this.styleAttrs = null; + + /** + * @type {string[]} + */ this.styleRefs = null; } + /** + * @param {Node} node + * @param {ErrorHandler} errorHandler + */ initFromNode(node, errorHandler) { this.id = elementGetXMLID(node); this.styleAttrs = elementGetStyles(node, errorHandler); @@ -988,11 +1030,17 @@ class Style { * Represents a TTML initial element */ -class Initial { +export class Initial { constructor() { + /** + * @type {Record} + */ this.styleAttrs = null; } + /** + * @param {Node} node + */ initFromNode(node) { this.styleAttrs = {}; @@ -1018,13 +1066,16 @@ class Initial { * */ -class Layout { +export class Layout { constructor() { this.regions = {}; } } -class ContentElement { +export class ContentElement { + /** + * @param {string} kind + */ constructor(kind) { this.kind = kind; } @@ -1034,13 +1085,32 @@ class ContentElement { * Represents a TTML image element */ -class Image extends ContentElement { +export class Image extends ContentElement { + /** + * @param {string} src + * @param {string} type + */ constructor(src, type) { super("image"); + + /** + * @type {string} + */ this.src = src; + + /** + * @type {string} + */ this.type = type; } + /** + * @param {TT} doc + * @param {Node} parent + * @param {Node} node + * @param {string} xmllang + * @param {ErrorHandler} errorHandler + */ initFromNode(doc, parent, node, xmllang, errorHandler) { this.src = "src" in node.attributes ? node.attributes.src.value : null; @@ -1068,31 +1138,56 @@ class Image extends ContentElement { * */ -class IdentifiedElement { +export class IdentifiedElement { + /** + * @param {string} id + */ constructor(id) { this.id = id; } + /** + * @param {TT} doc + * @param {Node} parent + * @param {Node} node + */ initFromNode(doc, parent, node) { this.id = elementGetXMLID(node); } } -class LayoutElement { +export class LayoutElement { + /** + * @param {string} id + */ constructor(id) { this.regionID = id; } + /** + * @param {TT} doc + * @param {Node} parent + * @param {Node} node + */ initFromNode(doc, parent, node) { this.regionID = elementGetRegionID(node); } } -class StyledElement { +export class StyledElement { + /** + * @param {Record} styleAttrs + */ constructor(styleAttrs) { this.styleAttrs = styleAttrs; } + /** + * @param {TT} doc + * @param {Node} parent + * @param {Node} node + * @param {ErrorHandler} errorHandler + */ initFromNode(doc, parent, node, errorHandler) { this.styleAttrs = elementGetStyles(node, errorHandler); @@ -1104,7 +1199,10 @@ class StyledElement { } } -class AnimatedElement { +export class AnimatedElement { + /** + * @param {any[]} sets + */ constructor(sets) { this.sets = sets; } @@ -1114,7 +1212,10 @@ class AnimatedElement { } } -class ContainerElement { +export class ContainerElement { + /** + * @param {string} contents + */ constructor(contents) { this.contents = contents; } @@ -1124,13 +1225,25 @@ class ContainerElement { } } -class TimedElement { +export class TimedElement { + /** + * + * @param {number} explicit_begin + * @param {number} explicit_end + * @param {number} explicit_dur + */ constructor(explicit_begin, explicit_end, explicit_dur) { this.explicit_begin = explicit_begin; this.explicit_end = explicit_end; this.explicit_dur = explicit_dur; } + /** + * @param {TT} doc + * @param {Node} parent + * @param {Node} node + * @param {ErrorHandler} errorHandler + */ initFromNode(doc, parent, node, errorHandler) { const t = processTiming(doc, parent, node, errorHandler); this.explicit_begin = t.explicit_begin; @@ -1145,11 +1258,17 @@ class TimedElement { * Represents a TTML body element */ -class Body extends ContentElement { +export class Body extends ContentElement { constructor() { super("body"); } + /** + * @param {TT} doc + * @param {Node} node + * @param {string} xmllang + * @param {ErrorHandler} errorHandler + */ initFromNode(doc, node, xmllang, errorHandler) { StyledElement.prototype.initFromNode.call(this, doc, null, node, errorHandler); TimedElement.prototype.initFromNode.call(this, doc, null, node, errorHandler); @@ -1165,11 +1284,18 @@ class Body extends ContentElement { * Represents a TTML div element */ -class Div extends ContentElement { +export class Div extends ContentElement { constructor() { super("div"); } + /** + * @param {TT} doc + * @param {Node} parent + * @param {Node} node + * @param {string} xmllang + * @param {ErrorHandler} errorHandler + */ initFromNode(doc, parent, node, xmllang, errorHandler) { StyledElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); TimedElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); @@ -1185,11 +1311,18 @@ class Div extends ContentElement { * Represents a TTML p element */ -class P extends ContentElement { +export class P extends ContentElement { constructor() { super("p"); } + /** + * @param {TT} doc + * @param {Node} parent + * @param {Node} node + * @param {string} xmllang + * @param {ErrorHandler} errorHandler + */ initFromNode(doc, parent, node, xmllang, errorHandler) { StyledElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); TimedElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); @@ -1205,11 +1338,19 @@ class P extends ContentElement { * Represents a TTML span element */ -class Span extends ContentElement { +export class Span extends ContentElement { constructor() { super("span"); } + /** + * @param {TT} doc + * @param {Node} parent + * @param {Node} node + * @param {string} xmllang + * @param {string} xmlspace + * @param {ErrorHandler} errorHandler + */ initFromNode(doc, parent, node, xmllang, xmlspace, errorHandler) { StyledElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); TimedElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); @@ -1226,11 +1367,19 @@ class Span extends ContentElement { * Represents a TTML anonymous span element */ -class AnonymousSpan extends ContentElement { +export class AnonymousSpan extends ContentElement { constructor() { super("span"); } + /** + * @param {TT} doc + * @param {Node} parent + * @param {string} text + * @param {string} xmlspace + * @param {string} xmllang + * @param {ErrorHandler} errorHandler + */ initFromText(doc, parent, text, xmllang, xmlspace, errorHandler) { TimedElement.prototype.initFromNode.call(this, doc, parent, null, errorHandler); @@ -1244,11 +1393,18 @@ class AnonymousSpan extends ContentElement { * Represents a TTML br element */ -class Br extends ContentElement { +export class Br extends ContentElement { constructor() { super("br"); } + /** + * @param {TT} doc + * @param {Node} parent + * @param {Node} node + * @param {string} xmllang + * @param {ErrorHandler} errorHandler + */ initFromNode(doc, parent, node, xmllang, errorHandler) { LayoutElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); TimedElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); @@ -1262,9 +1418,13 @@ class Br extends ContentElement { * */ -class Region { +export class Region { constructor() { } + /** + * @param {string} xmllang + * @returns {Region} + */ createDefaultRegion(xmllang) { const r = new Region(); @@ -1280,6 +1440,12 @@ class Region { return r; } + /** + * @param {TT} doc + * @param {Node} node + * @param {string} xmllang + * @param {ErrorHandler} errorHandler + */ initFromNode(doc, node, xmllang, errorHandler) { IdentifiedElement.prototype.initFromNode.call(this, doc, null, node, errorHandler); TimedElement.prototype.initFromNode.call(this, doc, null, node, errorHandler); @@ -1304,10 +1470,16 @@ class Region { * */ -class Set { +export class Set { constructor() { } + /** + * @param {TT} doc + * @param {Node} parent + * @param {Node} node + * @param {ErrorHandler} errorHandler + */ initFromNode(doc, parent, node, errorHandler) { TimedElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); @@ -1341,14 +1513,27 @@ class Set { * */ +/** + * @param {Node} node + * @returns {string | null} + */ function elementGetXMLID(node) { return node && "xml:id" in node.attributes ? node.attributes["xml:id"].value || null : null; } +/** + * @param {Node} node + * @returns {string} + */ function elementGetRegionID(node) { return node && "region" in node.attributes ? node.attributes.region.value : ""; } +/** + * @param {Node} node + * @param {ErrorHandler} errorHandler + * @returns {string} + */ function elementGetTimeContainer(node, errorHandler) { const tc = node && "timeContainer" in node.attributes ? node.attributes.timeContainer.value : null; @@ -1371,12 +1556,21 @@ function elementGetTimeContainer(node, errorHandler) { } +/** + * @param {Node} node + * @returns {string[]} + */ function elementGetStyleRefs(node) { return node && "style" in node.attributes ? node.attributes.style.value.split(" ") : []; } +/** + * @param {Node} node + * @param {ErrorHandler} errorHandler + * @returns {Record} + */ function elementGetStyles(node, errorHandler) { const s = {}; @@ -1418,6 +1612,12 @@ function elementGetStyles(node, errorHandler) { return s; } +/** + * @param {Node} node + * @param {string} ns + * @param {string} name + * @returns {string | null} + */ function findAttribute(node, ns, name) { for (const i in node.attributes) { @@ -1431,6 +1631,11 @@ function findAttribute(node, ns, name) { return null; } +/** + * @param {Node} node + * @param {ErrorHandler} errorHandler + * @returns {number | null} + */ function extractAspectRatio(node, errorHandler) { let ar = findAttribute(node, ns_ittp, "aspectRatio"); @@ -1475,9 +1680,12 @@ function extractAspectRatio(node, errorHandler) { } -/* +/** * Returns the cellResolution attribute from a node * + * @param {Node} node + * @param {ErrorHandler} errorHandler + * @returns {{w: number, h: number}} */ function extractCellResolution(node, errorHandler) { @@ -1512,6 +1720,11 @@ function extractCellResolution(node, errorHandler) { } +/** + * @param {Node} node + * @param {ErrorHandler} errorHandler + * @returns {{ effectiveFrameRate: number, tickRate: number }} + */ function extractFrameAndTickRate(node, errorHandler) { // subFrameRate is ignored per IMSC1 specification @@ -1604,6 +1817,11 @@ function extractFrameAndTickRate(node, errorHandler) { } +/** + * @param {Node} node + * @param {ErrorHandler} errorHandler + * @returns {{w: number, h: number} | null} + */ function extractExtent(node, errorHandler) { const attr = findAttribute(node, ns_tts, "extent"); @@ -1635,6 +1853,12 @@ function extractExtent(node, errorHandler) { } +/** + * @param {number} tickRate + * @param {number} effectiveFrameRate + * @param {string} str + * @returns {number | null} + */ function parseTimeExpression(tickRate, effectiveFrameRate, str) { const CLOCK_TIME_FRACTION_RE = /^(\d{2,}):(\d\d):(\d\d(?:\.\d+)?)$/; @@ -1700,6 +1924,13 @@ function parseTimeExpression(tickRate, effectiveFrameRate, str) { return r; } +/** + * @param {TT} doc + * @param {Node} parent + * @param {Node} node + * @param {ErrorHandler} errorHandler + * @returns {{explicit_begin: number, explicit_end: number, explicit_dur: number}} + */ function processTiming(doc, parent, node, errorHandler) { /* determine explicit begin */ @@ -1758,6 +1989,11 @@ function processTiming(doc, parent, node, errorHandler) { } +/** + * @param {Styling} styling + * @param {Style} style + * @param {ErrorHandler} errorHandler + */ function mergeChainedStyles(styling, style, errorHandler) { while (style.styleRefs.length > 0) { @@ -1777,6 +2013,12 @@ function mergeChainedStyles(styling, style, errorHandler) { } +/** + * @param {Styling} styling + * @param {string[]} stylerefs + * @param {Record} styleattrs + * @param {ErrorHandler} errorHandler + */ function mergeReferencedStyles(styling, stylerefs, styleattrs, errorHandler) { for (let i = stylerefs.length - 1; i >= 0; i--) { @@ -1794,6 +2036,10 @@ function mergeReferencedStyles(styling, stylerefs, styleattrs, errorHandler) { } +/** + * @param {Record} from_styles + * @param {Record} into_styles + */ function mergeStylesIfNotPresent(from_styles, into_styles) { for (const sname in from_styles) { @@ -1811,13 +2057,15 @@ function mergeStylesIfNotPresent(from_styles, into_styles) { /* TODO: validate style format at parsing */ -/* +/** * Binary search utility function * * @typedef {Object} BinarySearchResult * @property {boolean} found Was an exact match found? * @property {number} index Position of the exact match or insert position * + * @param {number[]} arr + * @param {number} searchval * @returns {BinarySearchResult} */ diff --git a/src/main/js/error.js b/src/main/js/error.js index 84c27f69..fc6b8e3d 100644 --- a/src/main/js/error.js +++ b/src/main/js/error.js @@ -3,6 +3,14 @@ * */ +/** + * @module imscError + */ + +/** + * @param {ErrorHandler} errorHandler + * @param {string} msg + */ export function reportWarning(errorHandler, msg) { if (errorHandler && errorHandler.warn && errorHandler.warn(msg)) @@ -10,6 +18,10 @@ export function reportWarning(errorHandler, msg) { } +/** + * @param {ErrorHandler} errorHandler + * @param {string} msg + */ export function reportError(errorHandler, msg) { if (errorHandler && errorHandler.error && errorHandler.error(msg)) @@ -17,6 +29,10 @@ export function reportError(errorHandler, msg) { } +/** + * @param {ErrorHandler} errorHandler + * @param {string} msg + */ export function reportFatal(errorHandler, msg) { if (errorHandler && errorHandler.fatal) @@ -25,3 +41,20 @@ export function reportFatal(errorHandler, msg) { throw msg; } + +/** + * Generic interface for handling events. The interface exposes four + * methods: + * -
info
: unusual event that does not result in an inconsistent state + * -
warn
: unexpected event that should not result in an inconsistent state + * -
error
: unexpected event that may result in an inconsistent state + * -
fatal
: unexpected event that results in an inconsistent state + * and termination of processing + * Each method takes a single
string
describing the event as argument, + * and returns a single
boolean
, which terminates processing if
true
. + * + * @typedef {Object} ErrorHandler + * @property {(msg: string) => boolean} warn + * @property {(msg: string) => boolean} error + * @property {(msg: string) => boolean} fatal + */ diff --git a/src/main/js/html.js b/src/main/js/html.js index bd0a557a..9a62ac70 100755 --- a/src/main/js/html.js +++ b/src/main/js/html.js @@ -27,6 +27,11 @@ import { reportError } from "./error.js"; import { byName } from "./styles.js"; +/** + * @typedef {import("./isd").ISD} ISD + * @typedef {import("./error").ErrorHandler} ErrorHandler + */ + /** * @module imscHTML */ @@ -36,7 +41,7 @@ const browserIsFirefox = /firefox/i.test(navigator.userAgent); /** * Function that maps
smpte:background
URIs to URLs resolving to image resource * @callback IMGResolver - * @param {string}
smpte:background
URI + * @param {string} value
smpte:background
URI * @return {string} PNG resource URL */ @@ -56,8 +61,8 @@ const browserIsFirefox = /firefox/i.test(navigator.userAgent); * is called for the next ISD, otherwise previousISDState should be set to * null. * - * @param {Object} isd ISD to be rendered - * @param {Object} element Element into which the ISD is rendered + * @param {ISD} isd ISD to be rendered + * @param {HTMLElement} element Element into which the ISD is rendered * @param {?IMGResolver} imgResolver Resolve
smpte:background
URIs into URLs. * @param {?number} eheight Height (in pixel) of the child
div
or null * to use clientHeight of the parent element @@ -65,10 +70,10 @@ const browserIsFirefox = /firefox/i.test(navigator.userAgent); * to use clientWidth of the parent element * @param {?boolean} displayForcedOnlyMode Value of the IMSC1 displayForcedOnlyMode parameter, * or false if null - * @param {?module:imscUtils.ErrorHandler} errorHandler Error callback - * @param {Object} previousISDState State saved during processing of the previous ISD, or null if initial call + * @param {?ErrorHandler} errorHandler Error callback + * @param {ISD} previousISDState State saved during processing of the previous ISD, or null if initial call * @param {?boolean} enableRollUp Enables roll-up animations (see CEA 708) - * @return {Object} ISD state to be provided when this funtion is called for the next ISD + * @return {ISD} ISD state to be provided when this function is called for the next ISD */ export function renderHTML(isd, diff --git a/src/main/js/isd.js b/src/main/js/isd.js index b3880e06..f3e677ad 100644 --- a/src/main/js/isd.js +++ b/src/main/js/isd.js @@ -32,15 +32,20 @@ import { ComputedLength, hasOwnProperty } from "./utils.js"; * @module imscISD */ +/** + * @typedef {import("./doc").TT} TT + * @typedef {import("./error").ErrorHandler} ErrorHandler + */ + /** * Creates a canonical representation of an IMSC1 document returned by
imscDoc.fromXML()
* at a given absolute offset in seconds. This offset does not have to be one of the values returned * by
getMediaTimeEvents()
. * - * @param {Object} tt IMSC1 document + * @param {TT} 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 + * @param {ErrorHandler} errorHandler Error callback + * @returns {ISD} Opaque in-memory representation of an ISD */ export function generateISD(tt, offset, errorHandler) { @@ -720,15 +725,26 @@ function pruneEmptySpans(element) { } } -class ISD { +export class ISD { constructor(tt) { + /** + * @type {ISDContentElement[]} + */ this.contents = []; + + /** + * @type {string} + */ this.aspectRatio = tt.aspectRatio; + + /** + * @type {string} + */ this.lang = tt.lang; } } -class ISDContentElement { +export class ISDContentElement { constructor(ttelem) { /* assume the element is a region if it does not have a kind */ diff --git a/src/main/js/main.js b/src/main/js/main.js index 0a093b7d..7a6d3bfd 100644 --- a/src/main/js/main.js +++ b/src/main/js/main.js @@ -27,3 +27,4 @@ export { fromXML } from "./doc.js"; export { renderHTML } from "./html.js"; export { generateISD } from "./isd.js"; +export { createDOMParser, createSAXParser } from "./parser.js"; diff --git a/src/main/js/parser.js b/src/main/js/parser.js new file mode 100644 index 00000000..a953e885 --- /dev/null +++ b/src/main/js/parser.js @@ -0,0 +1,98 @@ +import sax from "sax"; + +/** + * @typedef {sax.Tag | sax.QualifiedTag} Node + */ + +/** + * @typedef {Object} Parser + * @property {(xml: string) => Parser} write + * @property {() => Parser} close + * @property {(node: Node) => void} onopentag + * @property {(text: string) => void} ontext + * @property {() => void} onclosetag + */ + +export class XMLParser { + /** + * @param {Element} element + * @returns {SAX} + */ + static toNode(element) { + const attrs = element.attributes; + const node = XMLParser.toNS(element); + node.attributes = {}; + + for (let i = 0, len = attrs.length; i < len; i++) { + const attr = attrs[i]; + node.attributes[attr.name] = XMLParser.toNS(attr); + } + + return node; + } + + static toNS(node) { + return { + name: node.nodeName, + prefix: node.prefix, + local: node.localName, + uri: node.namespaceURI, + value: node.value, + }; + } + + onopentag = (node) => { console.log(node); } + ontext = (str) => { console.log(str); } + onclosetag = () => { } + + write(xmlstring) { + const parser = new DOMParser(); + const doc = parser.parseFromString(xmlstring, "application/xml"); + const errorNode = doc.querySelector("parsererror"); + + if (errorNode) { + throw new Error("XML parsing error: " + errorNode.textContent); + } + + this.process(doc.firstChild); + + return this; + } + + process(element) { + const node = XMLParser.toNode(element); + this.onopentag(node); + + const children = element.childNodes; + + for (let i = 0, len = children.length; i < len; i++) { + const child = children[i]; + + if (child.nodeType === Node.TEXT_NODE) { + this.ontext(child.textContent); + } else if (child.nodeType === Node.ELEMENT_NODE) { + this.process(child); + } + } + + this.onclosetag(); + } + + close() { + return this; + } +} + +/** + * @returns {Parser} + */ +export function createDOMParser() { + return new XMLParser(); +} + +/** + * @returns {Parser} + */ +export function createSAXParser() { + return sax.parser(true, { xmlns: true }); +} diff --git a/src/main/js/styles.js b/src/main/js/styles.js index df523e96..68423ed2 100644 --- a/src/main/js/styles.js +++ b/src/main/js/styles.js @@ -31,7 +31,22 @@ import { ComputedLength, parseColor, parseLength, parsePosition, parseTextShadow * @module imscStyles */ -class StylingAttributeDefinition { +/** + * @typedef {import("./doc").TT} TT + * @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: TT, parent: Node, element: Node, attr: string) => any} computeFunc + */ constructor(ns, name, initialValue, appliesTo, isInherit, isAnimatable, parseFunc, computeFunc) { this.name = name; this.ns = ns; @@ -1193,12 +1208,18 @@ export const all = [ /* TODO: allow null parse function */ +/** + * @type {Record} + */ export const byQName = {}; for (const i in all) { byQName[all[i].qname] = all[i]; } +/** + * @type {Record} + */ export const byName = {}; for (const j in all) { diff --git a/src/main/js/utils.js b/src/main/js/utils.js index 5a682f5a..036d74e5 100644 --- a/src/main/js/utils.js +++ b/src/main/js/utils.js @@ -28,23 +28,6 @@ * @module imscUtils */ -/* Documents the error handler interface */ - -/** - * @classdesc Generic interface for handling events. The interface exposes four - * methods: - * *
info
: unusual event that does not result in an inconsistent state - * *
warn
: unexpected event that should not result in an inconsistent state - * *
error
: unexpected event that may result in an inconsistent state - * *
fatal
: unexpected event that results in an inconsistent state - * and termination of processing - * Each method takes a single
string
describing the event as argument, - * and returns a single
boolean
, which terminates processing if
true
. - * - * @name ErrorHandler - * @class - */ - /* * Parses a TTML color expression * @@ -75,6 +58,10 @@ const NAMED_COLOR = { cyan: [0, 255, 255, 255], }; +/** + * @param {string} str + * @returns {[number, number, number, number]} + */ export function parseColor(str) { let m; @@ -115,6 +102,10 @@ export function parseColor(str) { const LENGTH_RE = /^((?:\+|-)?\d*(?:\.\d+)?)(px|em|c|%|rh|rw)$/; +/** + * @param {string} str + * @returns {{ value: number, unit: string } | null} + */ export function parseLength(str) { let m; @@ -129,6 +120,10 @@ export function parseLength(str) { return r; }; +/** + * @param {string} str + * @returns {string | any[] | null} + */ export function parseTextShadow(str) { const shadows = str.match(/([^(,)]|\([^)]+\))+/g); @@ -202,6 +197,15 @@ export function parseTextShadow(str) { return r; }; +/** + * @typedef {{ value: number, unit: string }} Offset + * @typedef {{ edge: string, offset: Offset }} Position + */ + +/** + * @param {string} str + * @returns {{h: Position, v: Position} | null} + */ export function parsePosition(str) { /* see https://www.w3.org/TR/ttml2/#style-value-position */ @@ -327,15 +331,27 @@ export function parsePosition(str) { }; export class ComputedLength { + /** + * @param {number} rw + * @param {number} rh + */ constructor(rw, rh) { this.rw = rw; this.rh = rh; } + /** + * @param {number} width + * @param {number} height + * @returns {number} + */ toUsedLength(width, height) { return width * this.rw + height * this.rh; }; + /** + * @returns {boolean} + */ isZero() { return this.rw === 0 && this.rh === 0; }; @@ -406,6 +422,11 @@ export function toComputedLength(lengthVal, lengthUnit, emLength, percentLength, } +/** + * @param {Object} obj + * @param {string} prop + * @returns {boolean} + */ export function hasOwnProperty(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } diff --git a/src/test/js/utils/getIMSC1Document.js b/src/test/js/utils/getIMSC1Document.js index 51d5fe70..871a0cb3 100644 --- a/src/test/js/utils/getIMSC1Document.js +++ b/src/test/js/utils/getIMSC1Document.js @@ -1,5 +1,6 @@ import fs from "node:fs/promises"; import { fromXML } from "../../../main/js/doc.js"; +import { createSAXParser } from "../../../main/js/parser.js"; const errorHandler = { info: function (msg) { @@ -18,5 +19,5 @@ const errorHandler = { export async function getIMSC1Document(url, metadataHandler) { const contents = await fs.readFile(url, "utf8"); - return fromXML(contents, errorHandler, metadataHandler); + return fromXML(contents, errorHandler, metadataHandler, createSAXParser()); } diff --git a/src/test/webapp/gen-renders.html b/src/test/webapp/gen-renders.html index 68726056..6a162b56 100644 --- a/src/test/webapp/gen-renders.html +++ b/src/test/webapp/gen-renders.html @@ -20,10 +20,14 @@
- - + + + +
diff --git a/src/test/webapp/js/gen-renders.js b/src/test/webapp/js/gen-renders.js index e25a803c..110b5c76 100644 --- a/src/test/webapp/js/gen-renders.js +++ b/src/test/webapp/js/gen-renders.js @@ -46,7 +46,7 @@ var errorHandler = { /* */ -function generateRenders(reffiles_root) { +function generateRenders(reffiles_root, use_sax) { var zip = new JSZip(); @@ -61,7 +61,7 @@ function generateRenders(reffiles_root) { for (var i in finfos) { - p.push(asyncProcessRefFile(reffiles_root, renders_dir, pngs_dir, finfos[i])); + p.push(asyncProcessRefFile(reffiles_root, renders_dir, pngs_dir, finfos[i], use_sax)); } @@ -85,7 +85,7 @@ function generateRenders(reffiles_root) { } -function asyncProcessRefFile(reffiles_root, renders_dir, pngs_dir, finfo) { +function asyncProcessRefFile(reffiles_root, renders_dir, pngs_dir, finfo, use_sax) { var test_name = finfo.name || getTestName(finfo.path, finfo.params || {}); @@ -94,7 +94,8 @@ function asyncProcessRefFile(reffiles_root, renders_dir, pngs_dir, finfo) { return asyncLoadFile(getReferenceFilePath(reffiles_root, finfo.path)) .then(function (contents) { - var doc = imsc.fromXML(contents.replace(/\r\n/g, '\n'), errorHandler); + var parser = use_sax ? imsc.createSAXParser() : imsc.createDOMParser(); + var doc = imsc.fromXML(contents.replace(/\r\n/g, '\n'), errorHandler, parser); test_renders_dir.file("doc.json", JSON.stringify(