diff --git a/src/main/js/doc.js b/src/main/js/doc.js index aaf0b153..b6c96ad9 100644 --- a/src/main/js/doc.js +++ b/src/main/js/doc.js @@ -30,23 +30,55 @@ ; (function (imscDoc, sax, imscNames, imscStyles, imscUtils) { - + + + /** + * Allows a client to provide callbacks to handle children of the element + * @typedef {Object} MetadataHandler + * @property {?OpenTagCallBack} onOpenTag + * @property {?CloseTagCallBack} onCloseTag + * @property {?TextCallBack} onText + */ + + /** + * Called when the opening tag of an element node is encountered. + * @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 + * `uri`, `name` and `value` + */ + + /** + * Called when the closing tag of an element node is encountered. + * @callback CloseTagCallBack + */ + + /** + * Called when a text node is encountered. + * @callback TextCallBack + * @param {string} contents Contents of the text node + */ + /** * Parses an IMSC1 document into an opaque in-memory representation that exposes * a single method
getMediaTimeEvents()
that returns a list of time * offsets (in seconds) of the ISD, i.e. the points in time where the visual - * representation of the document change. + * representation of the document change. `metadataHandler` allows the caller to + * be called back when nodes are present in elements. * * @param {string} xmlstring XML document * @param {?module:imscUtils.ErrorHandler} errorHandler Error callback + * @param {?MetadataHandler} metadataHandler Callback for elements * @returns {Object} Opaque in-memory representation of an IMSC1 document */ - imscDoc.fromXML = function (xmlstring, errorHandler) { + imscDoc.fromXML = function (xmlstring, errorHandler, metadataHandler) { var p = sax.parser(true, {xmlns: true}); var estack = []; var xmllangstack = []; var xmlspacestack = []; + var metadata_depth = 0; var doc = null; p.onclosetag = function (node) { @@ -58,7 +90,7 @@ for (var sid in estack[0].styles) { mergeChainedStyles(estack[0], estack[0].styles[sid], errorHandler); - + } } else if (estack[0] instanceof P || estack[0] instanceof Span) { @@ -74,7 +106,7 @@ for (c = 1; c < estack[0].contents.length; c++) { if (estack[0].contents[c] instanceof Span && estack[0].contents[c].anon && - cs[cs.length - 1] instanceof Span && cs[cs.length - 1].anon) { + cs[cs.length - 1] instanceof Span && cs[cs.length - 1].anon) { cs[cs.length - 1].text += estack[0].contents[c].text; @@ -93,16 +125,35 @@ // remove redundant nested anonymous spans (9.3.3(1)(c)) if (estack[0] instanceof Span && - estack[0].contents.length === 1 && - estack[0].contents[0] instanceof Span && - estack[0].contents[0].anon && - estack[0].text === null) { + estack[0].contents.length === 1 && + estack[0].contents[0] instanceof Span && + estack[0].contents[0].anon && + estack[0].text === null) { estack[0].text = estack[0].contents[0].text; estack[0].contents = []; } + } else if (estack[0] instanceof ForeignElement) { + + if (estack[0].node.uri === imscNames.ns_tt && + estack[0].node.local === 'metadata') { + + /* leave the metadata element */ + + metadata_depth--; + + } else if (metadata_depth > 0 && + metadataHandler && + 'onCloseTag' in metadataHandler) { + + /* end of child of metadata element */ + + metadataHandler.onCloseTag(); + + } + } // TODO: delete stylerefs? @@ -122,19 +173,29 @@ p.ontext = function (str) { - if (estack[0] === undefined || - !(estack[0] instanceof Span || estack[0] instanceof P)) { + if (estack[0] === undefined) { - reportError("Ignoring text outside of

or at (" + this.line + "," + this.column + ")"); + /* ignoring text outside of elements */ - return; - } + } else if (estack[0] instanceof Span || estack[0] instanceof P) { + + /* create an anonymous span */ + + var s = Span.createAnonymousSpan(doc, estack[0], xmlspacestack[0], str, errorHandler); - // create an anonymous span + estack[0].contents.push(s); - var s = Span.createAnonymousSpan(doc, estack[0], xmlspacestack[0], str, errorHandler); + } else if (estack[0] instanceof ForeignElement && + metadata_depth > 0 && + metadataHandler && + 'onText' in metadataHandler) { + + /* text node within a child of metadata element */ + + metadataHandler.onText(str); + + } - estack[0].contents.push(s); }; @@ -418,11 +479,11 @@ } else if (node.local === 'set') { if (!(estack[0] instanceof Span || - estack[0] instanceof P || - estack[0] instanceof Div || - estack[0] instanceof Body || - estack[0] instanceof Region || - estack[0] instanceof Br)) { + estack[0] instanceof P || + estack[0] instanceof Div || + estack[0] instanceof Body || + estack[0] instanceof Region || + estack[0] instanceof Br)) { reportFatal(errorHandler, "Parent of element is not a content element or a region at " + this.line + "," + this.column + ")"); @@ -440,16 +501,52 @@ } else { - // ignore other elements in the TTML namespace, e.g. metadata + /* element in the TT namespace, but not a content element */ - estack.unshift(node); + estack.unshift(new ForeignElement(node)); } } else { - // ignore elements not in the TTML namespace + /* ignore elements not in the TTML namespace unless in metadata element */ + + estack.unshift(new ForeignElement(node)); + + } + + /* handle metadata callbacks */ + + if (estack[0] instanceof ForeignElement) { + + if (node.uri === imscNames.ns_tt && + node.local === 'metadata') { + + /* enter the metadata element */ + + metadata_depth++; + + } else if ( + metadata_depth > 0 && + metadataHandler && + 'onOpenTag' in metadataHandler + ) { - estack.unshift(node); + /* start of child of metadata element */ + + var attrs = []; + + for (var a in node.attributes) { + attrs[node.attributes[a].uri + " " + node.attributes[a].local] = + { + uri: node.attributes[a].uri, + local: node.attributes[a].local, + value: node.attributes[a].value + }; + } + + metadataHandler.onOpenTag(node.uri, node.local, attrs); + + } } @@ -499,6 +596,10 @@ return doc; }; + function ForeignElement(node) { + this.node = node; + } + function TT() { this.events = []; this.head = null; @@ -672,7 +773,7 @@ this.styleAttrs = elementGetStyles(node, errorHandler); if (doc.head !== null && doc.head.styling !== null) { - mergeReferencedStyles(doc.head.styling, elementGetStyleRefs(node), this.styleAttrs, errorHandler); + mergeReferencedStyles(doc.head.styling, elementGetStyleRefs(node), this.styleAttrs, errorHandler); } this.contents = []; @@ -798,7 +899,7 @@ this.end = t.end; this.styleAttrs = elementGetStyles(node, errorHandler); - + this.sets = []; /* immediately merge referenced styles */ @@ -909,7 +1010,7 @@ s[qname] = val; /* TODO: consider refactoring errorHandler into parse and compute routines */ - + if (sa === imscStyles.byName.zIndex) { reportWarning(errorHandler, "zIndex attribute present but not used by IMSC1 since regions do not overlap"); } @@ -933,7 +1034,7 @@ for (var i in node.attributes) { if (node.attributes[i].uri === ns && - node.attributes[i].local === name) { + node.attributes[i].local === name) { return node.attributes[i].value; } @@ -1184,8 +1285,8 @@ } else if ((m = CLOCK_TIME_FRACTION_RE.exec(str)) !== null) { r = parseInt(m[1]) * 3600 + - parseInt(m[2]) * 60 + - parseFloat(m[3]); + parseInt(m[2]) * 60 + + parseFloat(m[3]); } else if ((m = CLOCK_TIME_FRAMES_RE.exec(str)) !== null) { @@ -1194,9 +1295,9 @@ if (effectiveFrameRate !== null) { r = parseInt(m[1]) * 3600 + - parseInt(m[2]) * 60 + - parseInt(m[3]) + - (m[4] === null ? 0 : parseInt(m[4]) / effectiveFrameRate); + parseInt(m[2]) * 60 + + parseInt(m[3]) + + (m[4] === null ? 0 : parseInt(m[4]) / effectiveFrameRate); } } @@ -1449,7 +1550,7 @@ })(typeof exports === 'undefined' ? this.imscDoc = {} : exports, - typeof sax === 'undefined' ? require("sax") : sax, - typeof imscNames === 'undefined' ? require("./names") : imscNames, - typeof imscStyles === 'undefined' ? require("./styles") : imscStyles, - typeof imscUtils === 'undefined' ? require("./utils") : imscUtils); + typeof sax === 'undefined' ? require("sax") : sax, + typeof imscNames === 'undefined' ? require("./names") : imscNames, + typeof imscStyles === 'undefined' ? require("./styles") : imscStyles, + typeof imscUtils === 'undefined' ? require("./utils") : imscUtils); diff --git a/src/test/resources/unit-tests/metadataHandler.ttml b/src/test/resources/unit-tests/metadataHandler.ttml new file mode 100644 index 00000000..622adafa --- /dev/null +++ b/src/test/resources/unit-tests/metadataHandler.ttml @@ -0,0 +1,24 @@ + + + + + Metadata Handler Test + + urn:ebu:distribution:2014-01 + http://www.w3.org/ns/ttml/profile/imsc1/text + + iVBORw0KGgoAAAANSUhEUgAAAaAAAAAkCAIAAABAAnl0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAIBklEQVR4nO1d65GrPAz1nfkKSAu4FWglqeXWQFohrZhW7o8zaBS/kB+QhE/nx86uA5YsnT1+CHb/3G43o1AoFFfEf592QKFQKI6CCpxCobgsVOAUCsVloQL3SxiGwRizruuht5TiBBM/hJ+Lxs85XAQVuJ/BOI7TNBljlmV5vV4H3XKCVxfGz0Xj5xwuRaXAjeOIb7ygoN05J58QqCuOoh7+PyAu8sZULjK3lOIEE5fBB6ORT1MK105fjcANwzBNk7XWU/1xHB+PhzFmnmehPOEWa63X7pxblsU5d8lZpSOQi2maEK4jZoUTTCjaoWmKokbgrLWQJOdc2I74VnRLd6Gfx+OBWUU1jlAXWMWZ0Bx9FeoFzhOyYRhI9Spmj2VZaJGM/jEdoU/VOMOCoNH4WmiOvg3FApcSstSyTg5ODpwmQOCmaVK6ABqH74fm6KsgFTjUkg0TMt5I7VC39sIzWGI3jOMY5Q05kLIVeiL0TXJZx4c2hO3hj5QOnpeObh9qopon8rTmTVTctbtT2R1U46hTDlekqdHDipHWpanuSkAkcFRLNix8tH+kdnx9PB6QucbC8+v1ggkgLNeSS8YY51xYkeAlcPOeeBQxUkShnqPdUo0l31WIYRhQhHHOPZ/PqJ/c1v1+hxUUbbzL8COndSbyIfUlbh9kojqAYbhCi8iXZyLqMPXj1cRS6aBoUAs8D6+JmpNQa3fI0XuL0pRC9/T1SlOR0RAigeOJ4Y3cM6+9y1Hrsiywa60dhoGvXHBCxy+GRS86dDt95X56zIYAhd1aa0mPotfYoKCcAepcfDh8LNQJL1XzKw2r6Idu0OIiE4dMBKLe9jXRHkCTTatzjv+2cxPmnR4IO/LrBSF8ciJa7o8u5aJPXUiolYKE7fI0pXBE+rqkqdSoB5HAQS+5017+SNRI17o84YEOU0rKfaDTuujGAb55Q4DQcNGkINJAaP0IPeLX8N7orrxY0KfoEz9623wSPs7RVHDmeeZzD7kU3iKJwAkmGgMosUhx82pWQB0n4TZ1S9wouj1Prd1OMmwvSlMKjemLClOmZ3maGjkjErjX6wWT9/vdBCtzSj8C3fcBHAicMYZPs+u6zvNsNsmARnhrXQ6UaBGLYRicc9gq8lDyiNPo6MAFhiisy7JgpGQabNudjbEm5Vnk8s2Fj9QtxdF1XZ/PJzlALlVH4AQT7QGUWAQ/6TeTFl9yNfHAucEjIDwPklArg122F6Upher04UqbLgY2pqmRM2VVVJtY91L+Tnu8kBvCJGbeBYKDO4yLvYUh7Qc97V7X1TvRN9u6Bu3rui7Lgq52F0TkTChq9Cn4RLZMp+eqdiNwgoleAdy16JGe2ruM0SPe7i0Sau2iiO11KEofpQnpozk7WgxsSVM7ZwoEjozt7k9PA7mUp++uY5TIzCj4NZ45osIu4XgWMU3xkIIomKDIVhcGn5CaoiBXB7DIYkfAQywZipIioZYQQrbXofp3BJqVmSxbRt3OmQKBIyHjjZigzGFsiy5kxnGkkfdKOfWQCZZl5WPyx1uO5U95vOnXMCHzevPar4H2AH4EfDnz9+9fzEbyN6Ik1MrgCLbXITUQnsru6WvkzI7ARYPr1XSonW+tu6w7UupGR358Mjkz65n5aheOHVHTnIFGbyb8yKL4HLQE8HzgFMxthT9+AN/4LNQuvoHtuzjHqzrO7AhcWJ82QU3aM790/bME3qTB8w3Oof1+v0NeT0BGdyR6RMsBCqPX4eUFrjGAHwEO8vl5EzJo09XDdnwD278E1ZzZETi31Xf5ZjhcKPLGXsdGtPk1QS3ZGDPPM2fVyccxjcXicKWG3kLhO61ocybaA/gpQObM9hTrdPCrhN/AdgmiO62+qObMjsBR+RkzCSq19Cm2pWgnDepFXL4k9NbA3ePID1lSTxLw+kAL+DEc9ex9E/3x19ErgN8AyA1NRZmnTyTUSuGrzmFTA+le8Y8are5hv8hAD56YYE3Bl299J2T+4Dg/0eMn9HxOa/y14ZWgaZqiT9bQNdHKdNErcjxt3uKXtxcNAc4fuixqNNExgB3hMYfvGzjGcYySfxcSaqVQx/aDmJAaSHQh0t1oNWekr2rZ2IMgaO8yMGvt/X53rPRLi3PeOX3PS7c29iZZKfiTNaEhLI/pGjoZoWsQByF9vRFx+Y4KXwb87YiD9rYdTfQKYBd4KabvQy5hunVbIaj0JEFCLYmHebafwIToQGghctCJSiNnCgRu6foH4EIT3rmpe3/QmTdibMS5Lrue1+tlt2Njr2fafWMWRaBD6/I9CM1L5l3saPdqSqK6bG/s0pl3d6XoZaJXALuAS9W0vZQavdJuTyqEH/HDmRQk1Ep5WMT2o5kQpo+cmee5b3UxY9SUcEYqcKl9qHChkULqdrf9yXLPolewt+w1t2jiU53TbMzbn8+ne38HGFdykaXDF+KQ21D0cAzmpdCHlG/54dAsZ4MHFYsikEJHE10CWDGosJHrDh+mCbQMVJzeX0dP+Ry1LqFWiAq2Z9KUQnv6woVIRc+ZW1o48+d2u2U+BrBYC+Wmy7kJP6QEJIsX/q9quKhH/wlOWORKjSj0J/XHW/g18tWW51vKAZOIan44UZ8rIhBFXxONASy1KPTEbe9LRrlk3nctpjBHRkatKIRsrzPRmL5M7jqmKRyakDMigVMoFIpfhP5fVIVCcVmowCkUistCBU6hUFwWKnAKheKyUIFTKBSXhQqcQqG4LFTgFArFZfEPuuTdBr3uWzgAAAAASUVORK5CYII= + + + + + + + + + + Metadata at the body + +

+ + \ No newline at end of file diff --git a/src/test/webapp/js/unit-tests.js b/src/test/webapp/js/unit-tests.js index 8f076381..453a7e3e 100644 --- a/src/test/webapp/js/unit-tests.js +++ b/src/test/webapp/js/unit-tests.js @@ -39,104 +39,170 @@ var errorHandler = { } }; -function getIMSC1Document(url) { +function getIMSC1Document(url, metadataHandler) { return new asyncLoadFile(url).then(function (contents) { - return imsc.fromXML(contents, errorHandler); + return imsc.fromXML(contents, errorHandler, metadataHandler); }); } QUnit.test( - "Parse Color Expressions", - function (assert) { + "Parse Color Expressions", + function (assert) { - return getIMSC1Document("unit-tests/colorExpressions.ttml").then( - function (doc) { + return getIMSC1Document("unit-tests/colorExpressions.ttml").then( + function (doc) { - assert.deepEqual( - doc.body.contents[0].contents[0].styleAttrs["http://www.w3.org/ns/ttml#styling color"], - [255, 255, 255, 255] - ); + assert.deepEqual( + doc.body.contents[0].contents[0].styleAttrs["http://www.w3.org/ns/ttml#styling color"], + [255, 255, 255, 255] + ); - assert.deepEqual( - doc.body.contents[0].contents[1].styleAttrs["http://www.w3.org/ns/ttml#styling color"], - [255, 255, 255, 127] - ); + assert.deepEqual( + doc.body.contents[0].contents[1].styleAttrs["http://www.w3.org/ns/ttml#styling color"], + [255, 255, 255, 127] + ); - assert.deepEqual( - doc.body.contents[0].contents[2].styleAttrs["http://www.w3.org/ns/ttml#styling color"], - [255, 128, 255, 255] - ); + assert.deepEqual( + doc.body.contents[0].contents[2].styleAttrs["http://www.w3.org/ns/ttml#styling color"], + [255, 128, 255, 255] + ); - assert.deepEqual( - doc.body.contents[0].contents[3].styleAttrs["http://www.w3.org/ns/ttml#styling color"], - [128, 255, 255, 63] - ); + assert.deepEqual( + doc.body.contents[0].contents[3].styleAttrs["http://www.w3.org/ns/ttml#styling color"], + [128, 255, 255, 63] + ); - assert.deepEqual( - doc.body.contents[0].contents[4].styleAttrs["http://www.w3.org/ns/ttml#styling color"], - [128, 128, 0, 255] - ); + assert.deepEqual( + doc.body.contents[0].contents[4].styleAttrs["http://www.w3.org/ns/ttml#styling color"], + [128, 128, 0, 255] + ); - } - ); + } + ); - } + } ); QUnit.test( - "Parse Time Expressions", - function (assert) { - - return getIMSC1Document("unit-tests/timeExpressions.ttml").then( - function (doc) { - - assert.close(doc.body.contents[0].contents[0].begin, 1.2, 1e-10); - assert.close(doc.body.contents[0].contents[1].begin, 72, 1e-10); - assert.close(doc.body.contents[0].contents[2].begin, 4320, 1e-10); - assert.close(doc.body.contents[0].contents[3].begin, 24 / 24000 * 1001, 1e-10); - assert.close(doc.body.contents[0].contents[4].begin, 2, 1e-10); - assert.close(doc.body.contents[0].contents[5].begin, 3723, 1e-10); - assert.close(doc.body.contents[0].contents[6].begin, 3723.235, 1e-10); - assert.close(doc.body.contents[0].contents[7].begin, 3723.235, 1e-10); - assert.close(doc.body.contents[0].contents[8].begin, 3600 + 2 * 60 + 3 + 20 / 24000 * 1001, 1e-10); - assert.close(doc.body.contents[0].contents[9].begin, 360000.1, 1e-10); - assert.close(doc.body.contents[0].contents[10].begin, 360000 + 100 / 24000 * 1001, 1e-10); - } - ); - - } + "Parse Time Expressions", + function (assert) { + + return getIMSC1Document("unit-tests/timeExpressions.ttml").then( + function (doc) { + + assert.close(doc.body.contents[0].contents[0].begin, 1.2, 1e-10); + assert.close(doc.body.contents[0].contents[1].begin, 72, 1e-10); + assert.close(doc.body.contents[0].contents[2].begin, 4320, 1e-10); + assert.close(doc.body.contents[0].contents[3].begin, 24 / 24000 * 1001, 1e-10); + assert.close(doc.body.contents[0].contents[4].begin, 2, 1e-10); + assert.close(doc.body.contents[0].contents[5].begin, 3723, 1e-10); + assert.close(doc.body.contents[0].contents[6].begin, 3723.235, 1e-10); + assert.close(doc.body.contents[0].contents[7].begin, 3723.235, 1e-10); + assert.close(doc.body.contents[0].contents[8].begin, 3600 + 2 * 60 + 3 + 20 / 24000 * 1001, 1e-10); + assert.close(doc.body.contents[0].contents[9].begin, 360000.1, 1e-10); + assert.close(doc.body.contents[0].contents[10].begin, 360000 + 100 / 24000 * 1001, 1e-10); + } + ); + + } ); QUnit.test( - "Parse Color Expressions", - function (assert) { + "Parse Color Expressions", + function (assert) { - return getIMSC1Document("unit-tests/lengthExpressions.ttml").then( - function (doc) { + return getIMSC1Document("unit-tests/lengthExpressions.ttml").then( + function (doc) { - assert.deepEqual( - doc.body.contents[0].contents[0].styleAttrs["http://www.w3.org/ns/ttml#styling fontSize"], - {"unit": "%", "value": 10.5} - ); + assert.deepEqual( + doc.body.contents[0].contents[0].styleAttrs["http://www.w3.org/ns/ttml#styling fontSize"], + {"unit": "%", "value": 10.5} + ); - assert.deepEqual( - doc.body.contents[0].contents[1].styleAttrs["http://www.w3.org/ns/ttml#styling fontSize"], - {"unit": "em", "value": 0.105} - ); + assert.deepEqual( + doc.body.contents[0].contents[1].styleAttrs["http://www.w3.org/ns/ttml#styling fontSize"], + {"unit": "em", "value": 0.105} + ); - assert.deepEqual( - doc.body.contents[0].contents[2].styleAttrs["http://www.w3.org/ns/ttml#styling fontSize"], - {"unit": "px", "value": 10.5} - ); + assert.deepEqual( + doc.body.contents[0].contents[2].styleAttrs["http://www.w3.org/ns/ttml#styling fontSize"], + {"unit": "px", "value": 10.5} + ); - assert.deepEqual( - doc.body.contents[0].contents[3].styleAttrs["http://www.w3.org/ns/ttml#styling fontSize"], - {"unit": "c", "value": 0.105} - ); + assert.deepEqual( + doc.body.contents[0].contents[3].styleAttrs["http://www.w3.org/ns/ttml#styling fontSize"], + {"unit": "c", "value": 0.105} + ); - } - ); + } + ); - } + } ); +QUnit.test( + "Metadata Callbacks", + function (assert) { + + var count = 0; + var cur_tag = 0; + var accumul_txt = ""; + + mh = { + onOpenTag: function (ns, name, attrs) { + + switch (count) { + case 0: + assert.equal(name, "title"); + assert.equal(ns, "http://www.w3.org/ns/ttml#metadata"); + cur_tag = 1; + break; + case 3: + assert.equal(name, "conformsToStandard"); + assert.equal(ns, "urn:ebu:metadata"); + cur_tag = 2; + break; + case 4: + assert.equal(name, "image"); + assert.equal(ns, "http://www.smpte-ra.org/schemas/2052-1/2010/smpte-tt"); + assert.equal(attrs["http://www.w3.org/XML/1998/namespace id"].value, "img_1"); + assert.equal(attrs[" imagetype"].value, "PNG"); + cur_tag = 3; + break; + } + + count++; + + }, + + onCloseTag: function () { + + switch (cur_tag) { + case 4: + var trimmed_text = accumul_txt.trim(); + assert.ok(trimmed_text.startswith("iVBORw0KGgoAAAANS")); + assert.equal(trimmed_text.length, 4146); + } + + cur_tag = 0; + accumul_txt = ""; + }, + + onText: function (contents) { + switch (cur_tag) { + case 1: + assert.equal(contents, "Metadata Handler Test"); + break; + case 2: + assert.equal(contents, "http://www.w3.org/ns/ttml/profile/imsc1/text"); + break; + case 4: + accumul_txt = accumul_txt + contents; + } + } + }; + + return getIMSC1Document("unit-tests/metadataHandler.ttml", mh); + + } +); \ No newline at end of file