diff --git a/Gruntfile.cjs b/Gruntfile.cjs index 1d4a45fd..e568c534 100644 --- a/Gruntfile.cjs +++ b/Gruntfile.cjs @@ -39,7 +39,6 @@ module.exports = function (grunt) { default: { files: { "<%= properties.webappBuildDir %>/libs/": [ - "sax:main", "filesaver.js-npm:main", "jszip/dist/jszip.js", ], @@ -80,6 +79,4 @@ module.exports = function (grunt) { grunt.registerTask("build", ["build:debug"]); - grunt.registerTask("clean", ["clean"]); - }; diff --git a/package-lock.json b/package-lock.json index 2a8b64e9..e689368f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,13 @@ "version": "1.1.5", "license": "BSD-2-Clause", "dependencies": { - "sax": "^1.6.1" + "tslib": "^2.6.2" }, "devDependencies": { "@eslint/js": "^9.39.5", "@rollup/plugin-commonjs": "^29.0.3", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-terser": "^1.0.0", - "@types/sax": "^1.2.7", "eslint": "^9.39.5", "filesaver.js-npm": "latest", "globals": "^17.11.0", @@ -28,7 +27,7 @@ "jszip": "latest", "rollup": "^4.14.3", "rollup-plugin-polyfill-node": "^0.13.0", - "tslib": "^2.6.2", + "sax": "^1.6.1", "typescript": "^5.4.5" } }, @@ -849,16 +848,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/node": { - "version": "26.4.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.0.tgz", - "integrity": "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~8.3.0" - } - }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -866,16 +855,6 @@ "dev": true, "license": "MIT" }, - "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, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -2991,6 +2970,7 @@ "version": "1.6.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -3159,7 +3139,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/type-check": { @@ -3213,13 +3192,6 @@ "node": "*" } }, - "node_modules/undici-types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", - "dev": true, - "license": "MIT" - }, "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 4e50e98b..42785526 100644 --- a/package.json +++ b/package.json @@ -25,9 +25,26 @@ "imsc" ], "author": "Pierre-Anthony Lemieux ", - "main": "dist/main/js/main.js", - "types": "dist/main/js/main.d.ts", + "main": "dist/main/main.js", + "types": "dist/main/main.d.ts", "unpkg": "dist/imsc.min.js", + "sideEffects": false, + "exports": { + ".": { + "types": "./dist/main/main.d.ts", + "default": "./dist/main/main.js" + }, + "./*.js": { + "types": "./dist/main/*.d.ts", + "default": "./dist/main/*.js" + }, + "./*": { + "types": "./dist/main/*.d.ts", + "default": "./dist/main/*.js" + }, + "./dist/*": "./dist/*", + "./package.json": "./package.json" + }, "scripts": { "prepublishOnly": "grunt build:release", "dev": "npx http-server build/public_html", @@ -37,14 +54,13 @@ "test": "node --test ./src/test/js/*Test.js" }, "dependencies": { - "sax": "^1.6.1" + "tslib": "^2.6.2" }, "devDependencies": { "@eslint/js": "^9.39.5", "@rollup/plugin-commonjs": "^29.0.3", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-terser": "^1.0.0", - "@types/sax": "^1.2.7", "eslint": "^9.39.5", "filesaver.js-npm": "latest", "globals": "^17.11.0", @@ -56,7 +72,7 @@ "jszip": "latest", "rollup": "^4.14.3", "rollup-plugin-polyfill-node": "^0.13.0", - "tslib": "^2.6.2", + "sax": "^1.6.1", "typescript": "^5.4.5" } } diff --git a/properties.json b/properties.json index ab796296..39e4f924 100644 --- a/properties.json +++ b/properties.json @@ -4,8 +4,6 @@ "webappBuildDir" : "build/public_html", "unitTestsResourcesDir" : "src/test/resources", "umdBuildDir" : "dist", - "umdAllDebugName" : "imsc.all.debug.js", "umdDebugName" : "imsc.debug.js", - "umdAllMinName" : "imsc.all.min.js", "umdMinName" : "imsc.min.js" } diff --git a/rollup.config.js b/rollup.config.js index c3339f46..8adc92ea 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -4,10 +4,8 @@ import terser from "@rollup/plugin-terser"; import nodePolyfill from "rollup-plugin-polyfill-node"; export default [ - // debug { input: "dist/main/main.js", - external: ["sax"], plugins: [ commonjs({ sourceMap: true, @@ -23,44 +21,11 @@ export default [ format: "umd", name: "imsc", sourcemap: true, - globals: { - sax: "sax", - }, }, { file: "dist/imsc.min.js", format: "umd", name: "imsc", sourcemap: true, - globals: { - sax: "sax", - }, - plugins: [terser()], - }], - }, - - // all - { - input: "dist/main/main.js", - plugins: [ - commonjs({ - sourceMap: true, - }), - nodeResolve({ - preferBuiltins: true, - browser: true, - }), - nodePolyfill(), - ], - output: [{ - file: "dist/imsc.all.debug.js", - format: "umd", - name: "imsc", - sourcemap: true, - }, { - file: "dist/imsc.all.min.js", - format: "umd", - name: "imsc", - sourcemap: true, plugins: [terser()], }], }, diff --git a/src/main/js/doc.js b/src/main/js/doc.js index f6420062..3cd709f4 100644 --- a/src/main/js/doc.js +++ b/src/main/js/doc.js @@ -24,7 +24,6 @@ * 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 { byName, byQName } from "./styles.js"; @@ -36,10 +35,8 @@ import { ComputedLength, hasOwnProperty, parseLength } from "./utils.js"; /** * @typedef {import("./error").ErrorHandler} ErrorHandler - */ - -/** - * @typedef {sax.Tag | sax.QualifiedTag} Node + * @typedef {import("./parser").Node} Node + * @typedef {import("./parser").Parser} Parser */ /** @@ -80,18 +77,22 @@ import { ComputedLength, hasOwnProperty, parseLength } from "./utils.js"; * @param {string} xmlstring XML document * @param {ErrorHandler} errorHandler Error callback * @param {?MetadataHandler} metadataHandler Callback for elements + * @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 fromParser(xmlstring, errorHandler, metadataHandler, parser) { + if (!parser) { + reportFatal(errorHandler, "No parser provided"); + } + const estack = []; const xmllangstack = []; const xmlspacestack = []; let metadata_depth = 0; let doc = null; - p.onclosetag = function () { + parser.onclosetag = function () { if (estack[0] instanceof Region) { @@ -191,7 +192,7 @@ export function fromXML(xmlstring, errorHandler, metadataHandler) { estack.shift(); }; - p.ontext = function (str) { + parser.ontext = function (str) { if (estack[0] === undefined) { @@ -234,7 +235,7 @@ export function fromXML(xmlstring, errorHandler, metadataHandler) { }; - p.onopentag = function (node) { + parser.onopentag = function (node) { // maintain the xml:space stack @@ -604,7 +605,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 diff --git a/src/main/js/dom_to_parser.js b/src/main/js/dom_to_parser.js new file mode 100644 index 00000000..7755bc0d --- /dev/null +++ b/src/main/js/dom_to_parser.js @@ -0,0 +1,115 @@ +/* + * Copyright (c) Sandflow Consulting LLC + * 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. + */ + +/** + * Implements an XML parser from the web browser's DOMParser + * + * @module parser + */ + +/** + * @typedef {import("./parser.js").Node} Node + * @typedef {import("./parser.js").Parser} Parser + * @typedef {import("./parser.js").Attribute} Attribute + */ + +export class XMLParser { + /** + * @param {Element} element + * @returns {Node} + */ + 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.documentElement); + + 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 createSAXParserFromDOMParser() { + return new XMLParser(); +} + diff --git a/src/main/js/isd.js b/src/main/js/isd.js index f3e677ad..11b6fe27 100644 --- a/src/main/js/isd.js +++ b/src/main/js/isd.js @@ -38,7 +38,7 @@ import { ComputedLength, hasOwnProperty } from "./utils.js"; */ /** - * Creates a canonical representation of an IMSC1 document returned by
imscDoc.fromXML()
+ * Creates a canonical representation of an IMSC1 document returned by
fromXML()
* at a given absolute offset in seconds. This offset does not have to be one of the values returned * by
getMediaTimeEvents()
. * diff --git a/src/main/js/main.js b/src/main/js/main.js index 0a093b7d..ca832e9c 100644 --- a/src/main/js/main.js +++ b/src/main/js/main.js @@ -24,6 +24,30 @@ * POSSIBILITY OF SUCH DAMAGE. */ -export { fromXML } from "./doc.js"; +import { fromParser } from "./doc.js"; +import { createSAXParserFromDOMParser } from "./dom_to_parser.js"; + export { renderHTML } from "./html.js"; export { generateISD } from "./isd.js"; +export { createSAXParserFromDOMParser }; + +/** + * @typedef {import("./error.js").ErrorHandler} ErrorHandler + * @typedef {import("./doc.js").MetadataHandler} MetadataHandler + * @typedef {import("./doc.js").TT} TT + * @typedef {import("./parser.js").Parser} Parser + */ + +/** + * Parses an IMSC1 document into an opaque in-memory representation, using the + * DOMParser-backed parser unless a parser is provided. + * + * @param {string} xmlstring XML document + * @param {ErrorHandler} errorHandler Error callback + * @param {?MetadataHandler} metadataHandler Callback for elements + * @param {?Parser} parser XML parser + * @returns {?TT} Opaque in-memory representation of an IMSC1 document + */ +export function fromXML(xmlstring, errorHandler, metadataHandler, parser = createSAXParserFromDOMParser()) { + return fromParser(xmlstring, errorHandler, metadataHandler, parser); +} diff --git a/src/main/js/parser.js b/src/main/js/parser.js new file mode 100644 index 00000000..4d6d6d29 --- /dev/null +++ b/src/main/js/parser.js @@ -0,0 +1,61 @@ +/* + * Copyright (c) Sandflow Consulting LLC + * 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. + */ + +/** + * Defines the contract for the parser expected by doc.js + * + * @module parser + */ + +/** + * @typedef {Object} Attribute + * @property {string} name + * @property {?string} prefix + * @property {?string} local + * @property {?string} uri + * @property {?string} value + */ + +/** + * @typedef {Object} Node + * @property {string} name + * @property {?string} prefix + * @property {?string} local + * @property {?string} uri + * @property {?string} value + * @property {Object.} attributes + */ + +/** + * @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 {}; diff --git a/src/test/js/RequiredParserTest.js b/src/test/js/RequiredParserTest.js new file mode 100644 index 00000000..54e63ca8 --- /dev/null +++ b/src/test/js/RequiredParserTest.js @@ -0,0 +1,22 @@ +import { equal, throws } from "node:assert"; +import { test } from "node:test"; +import { fromParser } from "../../main/js/doc.js"; + +const XML = '

hello

'; + +test("fromParser requires a parser", () => { + + let fatal_msg = null; + + const errorHandler = { + info: function () { }, + warn: function () { }, + error: function () { }, + fatal: function (msg) { fatal_msg = msg; }, + }; + + throws(() => fromParser(XML, errorHandler)); + + equal(fatal_msg, "No parser provided"); + +}); diff --git a/src/test/js/utils/getIMSC1Document.js b/src/test/js/utils/getIMSC1Document.js index 51d5fe70..b1f94ec9 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 { fromParser } from "../../../main/js/doc.js"; +import { createSAXParser } from "./saxParser.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 fromParser(contents, errorHandler, metadataHandler, createSAXParser()); } diff --git a/src/test/js/utils/saxParser.js b/src/test/js/utils/saxParser.js new file mode 100644 index 00000000..7fafa638 --- /dev/null +++ b/src/test/js/utils/saxParser.js @@ -0,0 +1,16 @@ +import sax from "sax"; + +/** + * @typedef {import("../../../main/js/parser.js").Parser} Parser + */ + +/** + * Creates a sax-based parser for use in the unit tests, which run in node, + * where the DOMParser global required by the default DOM parser does not + * exist. The library itself is browser-only and does not use sax. + * + * @returns {Parser} + */ +export function createSAXParser() { + return sax.parser(true, { xmlns: true }); +} diff --git a/src/test/webapp/gen-renders.html b/src/test/webapp/gen-renders.html index 68726056..76ad260f 100644 --- a/src/test/webapp/gen-renders.html +++ b/src/test/webapp/gen-renders.html @@ -9,7 +9,6 @@ - @@ -20,9 +19,9 @@
- -
diff --git a/tsconfig.json b/tsconfig.json index 8a728db1..bdb4e028 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -36,6 +36,7 @@ "strictNullChecks": true, "strictPropertyInitialization": true, "target": "ES5", + "types": [], }, "include": [ "src/main"