diff --git a/package-lock.json b/package-lock.json index 38042b02..0a2292a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.1.5", "license": "BSD-2-Clause", "dependencies": { - "sax": "1.2.1" + "sax": "^1.4.1" }, "devDependencies": { "@eslint/js": "^9.1.1", @@ -1241,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", @@ -2611,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", @@ -2661,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", diff --git a/package.json b/package.json index da0c1afc..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,7 +51,7 @@ "test": "node --test ./src/test/js/*Test.js" }, "dependencies": { - "sax": "1.2.1" + "sax": "^1.4.1" }, "devDependencies": { "@eslint/js": "^9.1.1", diff --git a/src/main/js/doc.js b/src/main/js/doc.js index f6420062..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"; @@ -36,10 +36,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 +78,18 @@ 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 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) { @@ -191,7 +189,7 @@ export function fromXML(xmlstring, errorHandler, metadataHandler) { estack.shift(); }; - p.ontext = function (str) { + parser.ontext = function (str) { if (estack[0] === undefined) { @@ -234,7 +232,7 @@ export function fromXML(xmlstring, errorHandler, metadataHandler) { }; - p.onopentag = function (node) { + parser.onopentag = function (node) { // maintain the xml:space stack @@ -604,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 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/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(