├── .gitignore ├── .gitmodules ├── Gruntfile.js ├── LICENSE ├── README.md ├── package.json ├── properties.json ├── script ├── png-diff.sh └── refpngcompare.py └── src ├── main └── js │ ├── doc.js │ ├── html.js │ ├── isd.js │ ├── main.js │ ├── names.js │ ├── styles.js │ └── utils.js └── test ├── js ├── ParseColor.js ├── ParsePositionTest.js └── ParseTextShadowTest.js ├── resources └── unit-tests │ ├── fontFamily.ttml │ ├── lengthExpressions.ttml │ ├── metadataHandler.ttml │ └── timeExpressions.ttml └── webapp ├── css └── gen-renders.css ├── gen-renders.html ├── js ├── gen-renders.js ├── reffiles.js └── unit-tests.js └── unit_tests.html /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | nbproject/private 5 | ga.js 6 | .vscode/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/test/resources/imsc-tests"] 2 | path = src/test/resources/imsc-tests 3 | url = https://github.com/w3c/imsc-tests.git 4 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 3 | grunt.initConfig({ 4 | 5 | properties: grunt.file.readJSON('properties.json'), 6 | 7 | pkg: grunt.file.readJSON('package.json'), 8 | 9 | clean: ['<%= properties.webappBuildDir %>', '<%= properties.umdBuildDir %>'], 10 | 11 | sync: { 12 | all: { 13 | files: 14 | [ 15 | // copy tests 16 | { expand: true, cwd: '<%= properties.webappTestDir %>', src: '**', dest: '<%= properties.webappBuildDir %>' }, 17 | 18 | // copy tests resources 19 | { expand: true, cwd: '<%= properties.unitTestsResourcesDir %>', src: 'imsc-tests/imsc1/ttml/**', dest: '<%= properties.webappBuildDir %>' }, 20 | { expand: true, cwd: '<%= properties.unitTestsResourcesDir %>', src: 'imsc-tests/imsc1/tests.json', dest: '<%= properties.webappBuildDir %>' }, 21 | { expand: true, cwd: '<%= properties.unitTestsResourcesDir %>', src: 'imsc-tests/imsc1_1/ttml/**', dest: '<%= properties.webappBuildDir %>' }, 22 | { expand: true, cwd: '<%= properties.unitTestsResourcesDir %>', src: 'imsc-tests/imsc1_1/tests.json', dest: '<%= properties.webappBuildDir %>' }, 23 | { expand: true, cwd: '<%= properties.unitTestsResourcesDir %>', src: 'unit-tests/**', dest: '<%= properties.webappBuildDir %>' } 24 | ] 25 | }, 26 | 27 | release: { 28 | src: '<%= properties.umdBuildDir %>/<%= properties.umdMinName %>', 29 | dest: '<%= properties.webappBuildDir %>/libs/imsc.js' 30 | }, 31 | 32 | debug: { 33 | src: '<%= properties.umdBuildDir %>/<%= properties.umdDebugName %>', 34 | dest: '<%= properties.webappBuildDir %>/libs/imsc.js' 35 | } 36 | }, 37 | 38 | npmcopy: { 39 | default: { 40 | files: { 41 | '<%= properties.webappBuildDir %>/libs/': [ 42 | 'sax:main', 43 | 'qunit-assert-close:main', 44 | 'qunitjs:main', 45 | 'filesaver.js-npm:main', 46 | 'jszip/dist/jszip.js' 47 | ] 48 | } 49 | } 50 | }, 51 | 52 | browserify: [ 53 | { 54 | src: "<%= pkg.main %>", 55 | dest: "<%= properties.umdBuildDir %>/<%= properties.umdDebugName %>", 56 | options: { 57 | exclude: ["sax"], 58 | browserifyOptions: { 59 | standalone: 'imsc' 60 | } 61 | } 62 | }, 63 | { 64 | src: "<%= pkg.main %>", 65 | dest: "<%= properties.umdBuildDir %>/<%= properties.umdAllDebugName %>", 66 | options: { 67 | browserifyOptions: { 68 | standalone: 'imsc' 69 | } 70 | } 71 | } 72 | ], 73 | 74 | jshint: { 75 | 'default': { 76 | src: "src/main/js", 77 | options: { 78 | "-W032": true 79 | } 80 | } 81 | }, 82 | 83 | exec: { 84 | minify: 85 | { 86 | cmd: [ 87 | "npx google-closure-compiler --js=<%= properties.umdBuildDir %>/<%= properties.umdAllDebugName %> --js_output_file=<%= properties.umdBuildDir %>/<%= properties.umdAllMinName %>", 88 | "npx google-closure-compiler --js=<%= properties.umdBuildDir %>/<%= properties.umdDebugName %> --js_output_file=<%= properties.umdBuildDir %>/<%= properties.umdMinName %>" 89 | ].join("&&") 90 | } 91 | } 92 | } 93 | 94 | ); 95 | 96 | grunt.loadNpmTasks('grunt-contrib-clean'); 97 | 98 | grunt.loadNpmTasks('grunt-npmcopy'); 99 | 100 | grunt.loadNpmTasks('grunt-browserify'); 101 | 102 | grunt.loadNpmTasks('grunt-contrib-jshint'); 103 | 104 | grunt.loadNpmTasks('grunt-sync'); 105 | 106 | grunt.loadNpmTasks('grunt-exec'); 107 | 108 | grunt.registerTask('build:release', ['jshint', 'browserify', 'exec:minify', 'sync:all', 'sync:release', 'npmcopy']); 109 | 110 | grunt.registerTask('build:debug', ['jshint', 'browserify', 'exec:minify', 'sync:all', 'sync:debug', 'npmcopy']); 111 | 112 | grunt.registerTask('build', ['build:debug']); 113 | 114 | grunt.registerTask('clean', ['clean']); 115 | 116 | }; 117 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Pierre-Anthony Lemieux (pal@sandflow.com) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 14 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 16 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 17 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 18 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 19 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 20 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 21 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 22 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 23 | POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | _ _ _____ 2 | (_) | | / ____| 3 | _ _ __ ___ ___ ___ | | | (___ 4 | | | | '_ ` _ \ / __| / __| _ | | \___ \ 5 | | | | | | | | | \__ \ | (__ | |__| | ____) | 6 | |_| |_| |_| |_| |___/ \___| \____/ |_____/ 7 | 8 | 9 | 10 | INTRODUCTION 11 | ============ 12 | 13 | imscJS is a JavaScript library for rendering [IMSC 1.0.1](https://www.w3.org/TR/ttml-imsc1.0.1/) and [IMSC 1.1](https://www.w3.org/TR/ttml-imsc1.1/) documents to HTML5. IMSC is a profile of [TTML 2](https://www.w3.org/TR/ttml2/) designed for subtitle and caption delivery worldwide. 14 | 15 | A sample web app that uses imscJS is available at https://www.sandflow.com/imsc1_1/index.html. 16 | 17 | Documentation is available on [MDN](https://developer.mozilla.org/en-US/docs/Related/IMSC). 18 | 19 | 20 | 21 | KNOWN ISSUES AND LIMITATIONS 22 | ============================ 23 | 24 | imscJS is primarily developed on Firefox. Latest versions of Chrome, Safari, and Microsoft Edge are intended to be supported nevertheless, albeit with potentially reduced capabilities. In particular, advanced ruby layout is currently only supported by Firefox. 25 | 26 | imscJS is intended to reflect the most recent published versions of [IMSC 1.0.1](https://www.w3.org/TR/ttml-imsc1.0.1/) and [IMSC 1.1](https://www.w3.org/TR/ttml-imsc1.1/). These publications are routinely clarified by proposed resolutions to issues captured in their respective bug trackers. 27 | 28 | imscJS bugs are tracked at https://github.com/sandflow/imscJS/issues. 29 | 30 | 31 | 32 | RUNTIME DEPENDENCIES 33 | ==================== 34 | 35 | (required) [sax-js 1.2.1](https://www.npmjs.com/package/sax) 36 | 37 | Rendering to HTML5 requires a browser environment, but parsing an IMSC document and transforming it into ISDs does not. 38 | 39 | 40 | 41 | DEVELOPMENT DEPENDENCIES 42 | ======================== 43 | 44 | (required) node.js (see [package.json](package.json) for a complete list of dependencies) 45 | 46 | (recommended) git 47 | 48 | 49 | 50 | QUICK START 51 | =========== 52 | 53 | * run the `build` target defined in [Gruntfile.js](Gruntfile.js) using [grunt](http://gruntjs.com/). 54 | 55 | * the resulting `imsc.js` and `sax.js` files at `build/public_html/libs` are, respectively, the imscJS library and its sax-js dependency. For example, both libraries can be included in a web page as follows: 56 | 57 | ```html 58 | 59 | 60 | ``` 61 | 62 | See BUILD ARTIFACTS for a full list of build artifacts, and TESTS AND SAMPLES for a list of samples and tests available. 63 | 64 | 65 | 66 | ARCHITECTURE 67 | ============ 68 | 69 | API 70 | --- 71 | 72 | imscJS renders an IMSC document in three distinct steps: 73 | 74 | * `fromXML(xmlstring, errorHandler, metadataHandler)` parses the document and returns a TT object. The latter contains opaque representation of the document and exposes the 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. 75 | 76 | * `generateISD(tt, offset, errorHandler)` creates a canonical representation of the document (provided as a TT object generated by `fromXML()`) at a point in time (`offset` parameter). This point in time does not have to be one of the values returned by `getMediaTimeEvents()`. For example, given an ISOBMFF sample covering the interval `[a, b[`, `generateISD(tt, offset, errorHandler)` would be called first with `offset = a`, then in turn with offset set to each value of `getMediaTimeEvents()` that fall in the interval `]a, b[`. 77 | 78 | * `renderHTML(isd, element, imgResolver, eheight, ewidth, displayForcedOnlyMode, errorHandler, previousISDState, enableRollUp)` renders an `isd` object returned by `generateISD()` into a newly-created `div` element that is appended to the `element`. The `element` must be attached to the DOM. The height and width of the child `div` element are equal to `eheight` and `ewidth` if not null, or `clientWidth` and `clientHeight` of the parent `element` otherwise. Images URIs specified in `smpte:background` attributes are mapped to image resource URLs by the `imgResolver` function. The latter takes the value of the `smpte:background` attribute URI and an `img` DOM element as input and is expected to set the `src` attribute of the `img` DOM element to the absolute URI of the image. `displayForcedOnlyMode` sets the (boolean) value of the IMSC displayForcedOnlyMode parameter. `enableRollUp` enables roll-up as specified in CEA 708. `previousISDState` maintains states across calls, e.g. for roll-up processing. 79 | 80 | In each step, the caller can provide an `errorHandler` to be notified of events during processing. The `errorHandler` may define four methods: `info`, `warn`, `error` and `fatal`. Each is called with a string argument describing the event, and will cause processing to terminate if it returns `true`. 81 | 82 | Inline documentation provides additional information. 83 | 84 | 85 | MODULES 86 | ------- 87 | 88 | imscJS consists of the following modules, which can be used in a node 89 | environment using the `require` functionality, or standalone, in which case each module hosts its 90 | definitions under a global name (the token between parantheses): 91 | 92 | * `doc.js` (`imscDoc`): parses an IMSC document into an in-memory TT object 93 | * `isd.js` (`imscISD`): generates an ISD object from a TT object 94 | * `html.js` (`imscHTML`): generates an HTML fragment from an ISD object 95 | * `names.js` (`imscNames`): common constants 96 | * `styles.js` (`imscStyles`): defines TTML styling attributes processing 97 | * `utils.js` (`imscUtils`): common utility functions 98 | 99 | 100 | 101 | BUILD 102 | ===== 103 | 104 | imscJS is built using the `build:release` or `build:debug` Grunt tasks -- the `build` task is an alias of `build:debug`. 105 | 106 | The `dist` directory contains the following build artifacts: 107 | * `imsc.all.debug.js`: Non-minified build that includes the sax-js dependency. 108 | * `imsc.all.min.js`: Minified build that includes the sax-js dependency. 109 | * `imsc.debug.js`: Non-minified build that does not include the sax-js dependency. 110 | * `imsc.min.js`: Minified build that does not include the sax-js dependency. 111 | 112 | The `build/public_html/libs/imsc.js` files is identical to: 113 | * `imsc.debug.js`, if the `build:debug` task is executed. 114 | * `imsc.min.js`, if the `build:release` task is executed. 115 | 116 | 117 | 118 | RELEASES 119 | ======== 120 | 121 | imscJS is released as an NPM package under [imsc](https://www.npmjs.com/package/imsc). The `dev` distribution tag indicates pre-releases. 122 | 123 | Builds/dist are available on the [unpkg](https://unpkg.com/) CDN under the [`dist`](https://unpkg.com/browse/imsc/dist/) directory. 124 | 125 | To access the latest builds, please consult the [release page](https://github.com/sandflow/imscJS/releases). 126 | 127 | 128 | 129 | TESTS AND SAMPLES 130 | ================= 131 | 132 | 133 | W3C Test Suite 134 | -------------- 135 | 136 | imscJS imports the [IMSC test suite](https://github.com/w3c/imsc-tests) as a submodule at `src/test/resources/imsc-tests`. The gen-renders.html web app can be used to generate PNG renderings as as well intermediary files from these tests. 137 | 138 | 139 | Unit tests 140 | ---------- 141 | 142 | Unit tests run using [QUnit](https://qunitjs.com/) are split between: 143 | 144 | * [src/test/webapp/js/unit-tests.js](src/test/webapp/js/unit-tests.js) 145 | * [src/test/js](src/test/js) 146 | 147 | 148 | NOTABLE DIRECTORIES AND FILES 149 | ============================= 150 | 151 | * [package.json](package.json): NPM package definition 152 | 153 | * [Gruntfile.js](Gruntfile.js): Grunt build script 154 | 155 | * [properties.json](properties.json): General project properties 156 | 157 | * [LICENSE](LICENSE): License under which imscJS is made available 158 | 159 | * [src/main/js](src/main/js): JavaScript modules 160 | 161 | * [src/test](src/test): Test files 162 | 163 | * [dist](dist): Built libraries 164 | 165 | * [build/public_html](build/public_html): Test web applications 166 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "imsc", 3 | "description": "Renders IMSC documents to HTML5 fragments", 4 | "version": "1.1.5", 5 | "license": "BSD-2-Clause", 6 | "homepage": "https://github.com/sandflow/imscJS", 7 | "bugs": "https://github.com/sandflow/imscJS/issues", 8 | "repository": "github:sandflow/imscJS", 9 | "files": [ 10 | "src/main/js", 11 | "README.txt", 12 | "dist" 13 | ], 14 | "keywords": [ 15 | "imsc1", 16 | "imsc1.0.1", 17 | "imsc", 18 | "imsc1.1", 19 | "ebut-tt-d", 20 | "smpte-tt", 21 | "sdp-us", 22 | "cff-tt", 23 | "ttml", 24 | "imsc" 25 | ], 26 | "author": "Pierre-Anthony Lemieux ", 27 | "main": "src/main/js/main.js", 28 | "unpkg": "dist/imsc.min.js", 29 | "scripts": { 30 | "prepublishOnly": "grunt build:release" 31 | }, 32 | "dependencies": { 33 | "sax": "1.2.1" 34 | }, 35 | "devDependencies": { 36 | "browserify": "^16.2.3", 37 | "filesaver.js-npm": "latest", 38 | "google-closure-compiler": "^20180910.1.0", 39 | "grunt": "latest", 40 | "grunt-browserify": "latest", 41 | "grunt-contrib-clean": "latest", 42 | "grunt-contrib-jshint": "latest", 43 | "grunt-exec": "^3.0.0", 44 | "grunt-npmcopy": "latest", 45 | "grunt-sync": "latest", 46 | "jszip": "latest", 47 | "node-qunit": "^1.0.0", 48 | "qunit-assert-close": "latest", 49 | "qunitjs": "latest" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "webappSrcDir" : "src/main/webapp", 3 | "webappTestDir" : "src/test/webapp", 4 | "webappBuildDir" : "build/public_html", 5 | "unitTestsResourcesDir" : "src/test/resources", 6 | "umdBuildDir" : "dist", 7 | "umdAllDebugName" : "imsc.all.debug.js", 8 | "umdDebugName" : "imsc.debug.js", 9 | "umdAllMinName" : "imsc.all.min.js", 10 | "umdMinName" : "imsc.min.js" 11 | } 12 | -------------------------------------------------------------------------------- /script/png-diff.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | #compare $2 $1 png:- | montage -geometry +4+4 $2 - $1 png:- | imdisplay -title "$1" - 3 | compare $2 $1 png:- | montage -geometry +4+4 $2 - $1 show: -------------------------------------------------------------------------------- /script/refpngcompare.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import subprocess 3 | import os 4 | import fnmatch 5 | import re 6 | import sys 7 | 8 | parser = argparse.ArgumentParser() 9 | 10 | parser.add_argument("ref_dir", help = "Path of the Reference directory") 11 | 12 | parser.add_argument("render_dir", help = "Path of the Generated directory") 13 | 14 | parser.add_argument("-d", help = "Specifies whether a diff of images is output") 15 | 16 | parser.add_argument("-t", type=float, help = "Specifies the difference threshold", default=0.00001) 17 | 18 | args = parser.parse_args() 19 | 20 | for dir, dirs, files in os.walk(args.ref_dir): 21 | for file in files: 22 | reffile = os.path.join(dir, file) 23 | destdir = os.path.join(args.render_dir, os.path.relpath(dir, args.ref_dir)) 24 | genfile = os.path.join(destdir, file) 25 | 26 | if not os.path.exists(genfile): 27 | print("File " + genfile + " does not exist") 28 | continue 29 | 30 | if fnmatch.fnmatch(file, '*.png'): 31 | try: 32 | subprocess.check_output(["magick", "compare", '-metric', 'mse', reffile, genfile, 'null:'], stderr=subprocess.STDOUT, universal_newlines=True) 33 | except subprocess.CalledProcessError as err: 34 | m = re.search('([^\(]+)\(([^\)]+)', err.output) 35 | r = float(m.group(2)) 36 | if (r > args.t) : 37 | print(reffile + ": " + str(r)) 38 | if args.d is not None: 39 | diffdir = args.d 40 | #diffdir = os.path.join(args.d, os.path.relpath(dir, args.ref_dir)) 41 | if not os.path.exists(diffdir): 42 | os.makedirs(diffdir) 43 | difffile = os.path.join(args.d, os.path.relpath(dir, args.ref_dir) + "-" + file) 44 | p1 = subprocess.Popen(["magick", "compare", reffile, genfile, "png:-"], stdout=subprocess.PIPE) 45 | p2 = subprocess.Popen(["magick", "montage", "-mode", "concatenate", reffile, "-", genfile, difffile], stdin=p1.stdout) 46 | p1.stdout.close() 47 | p2.communicate() 48 | 49 | 50 | # elif not fnmatch.fnmatch(file, 'manifest.json'): 51 | # try: 52 | # subprocess.check_output(["diff", '-w', reffile, genfile], stderr=subprocess.STDOUT, universal_newlines=True) 53 | # except subprocess.CalledProcessError as err: 54 | # print(err.output) 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/main/js/doc.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016, Pierre-Anthony Lemieux 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * * Redistributions in binary form must reproduce the above copyright notice, 11 | * this list of conditions and the following disclaimer in the documentation 12 | * and/or other materials provided with the distribution. 13 | * 14 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 18 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 19 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 20 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 21 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 22 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 23 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 24 | * POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | 27 | /** 28 | * @module imscDoc 29 | */ 30 | 31 | ; 32 | (function (imscDoc, sax, imscNames, imscStyles, imscUtils) { 33 | 34 | 35 | /** 36 | * Allows a client to provide callbacks to handle children of the element 37 | * @typedef {Object} MetadataHandler 38 | * @property {?OpenTagCallBack} onOpenTag 39 | * @property {?CloseTagCallBack} onCloseTag 40 | * @property {?TextCallBack} onText 41 | */ 42 | 43 | /** 44 | * Called when the opening tag of an element node is encountered. 45 | * @callback OpenTagCallBack 46 | * @param {string} ns Namespace URI of the element 47 | * @param {string} name Local name of the element 48 | * @param {Object[]} attributes List of attributes, each consisting of a 49 | * `uri`, `name` and `value` 50 | */ 51 | 52 | /** 53 | * Called when the closing tag of an element node is encountered. 54 | * @callback CloseTagCallBack 55 | */ 56 | 57 | /** 58 | * Called when a text node is encountered. 59 | * @callback TextCallBack 60 | * @param {string} contents Contents of the text node 61 | */ 62 | 63 | /** 64 | * Parses an IMSC1 document into an opaque in-memory representation that exposes 65 | * a single method
getMediaTimeEvents()
that returns a list of time 66 | * offsets (in seconds) of the ISD, i.e. the points in time where the visual 67 | * representation of the document change. `metadataHandler` allows the caller to 68 | * be called back when nodes are present in elements. 69 | * 70 | * @param {string} xmlstring XML document 71 | * @param {?module:imscUtils.ErrorHandler} errorHandler Error callback 72 | * @param {?MetadataHandler} metadataHandler Callback for elements 73 | * @returns {Object} Opaque in-memory representation of an IMSC1 document 74 | */ 75 | 76 | imscDoc.fromXML = function (xmlstring, errorHandler, metadataHandler) { 77 | var p = sax.parser(true, {xmlns: true}); 78 | var estack = []; 79 | var xmllangstack = []; 80 | var xmlspacestack = []; 81 | var metadata_depth = 0; 82 | var doc = null; 83 | 84 | p.onclosetag = function (node) { 85 | 86 | 87 | if (estack[0] instanceof Region) { 88 | 89 | /* merge referenced styles */ 90 | 91 | if (doc.head !== null && doc.head.styling !== null) { 92 | mergeReferencedStyles(doc.head.styling, estack[0].styleRefs, estack[0].styleAttrs, errorHandler); 93 | } 94 | 95 | delete estack[0].styleRefs; 96 | 97 | } else if (estack[0] instanceof Styling) { 98 | 99 | /* flatten chained referential styling */ 100 | 101 | for (var sid in estack[0].styles) { 102 | 103 | if (! estack[0].styles.hasOwnProperty(sid)) continue; 104 | 105 | mergeChainedStyles(estack[0], estack[0].styles[sid], errorHandler); 106 | 107 | } 108 | 109 | } else if (estack[0] instanceof P || estack[0] instanceof Span) { 110 | 111 | /* merge anonymous spans */ 112 | 113 | if (estack[0].contents.length > 1) { 114 | 115 | var cs = [estack[0].contents[0]]; 116 | 117 | var c; 118 | 119 | for (c = 1; c < estack[0].contents.length; c++) { 120 | 121 | if (estack[0].contents[c] instanceof AnonymousSpan && 122 | cs[cs.length - 1] instanceof AnonymousSpan) { 123 | 124 | cs[cs.length - 1].text += estack[0].contents[c].text; 125 | 126 | } else { 127 | 128 | cs.push(estack[0].contents[c]); 129 | 130 | } 131 | 132 | } 133 | 134 | estack[0].contents = cs; 135 | 136 | } 137 | 138 | // remove redundant nested anonymous spans (9.3.3(1)(c)) 139 | 140 | if (estack[0] instanceof Span && 141 | estack[0].contents.length === 1 && 142 | estack[0].contents[0] instanceof AnonymousSpan) { 143 | 144 | estack[0].text = estack[0].contents[0].text; 145 | delete estack[0].contents; 146 | 147 | } 148 | 149 | } else if (estack[0] instanceof ForeignElement) { 150 | 151 | if (estack[0].node.uri === imscNames.ns_tt && 152 | estack[0].node.local === 'metadata') { 153 | 154 | /* leave the metadata element */ 155 | 156 | metadata_depth--; 157 | 158 | } else if (metadata_depth > 0 && 159 | metadataHandler && 160 | 'onCloseTag' in metadataHandler) { 161 | 162 | /* end of child of metadata element */ 163 | 164 | metadataHandler.onCloseTag(); 165 | 166 | } 167 | 168 | } 169 | 170 | // TODO: delete stylerefs? 171 | 172 | // maintain the xml:space stack 173 | 174 | xmlspacestack.shift(); 175 | 176 | // maintain the xml:lang stack 177 | 178 | xmllangstack.shift(); 179 | 180 | // prepare for the next element 181 | 182 | estack.shift(); 183 | }; 184 | 185 | p.ontext = function (str) { 186 | 187 | if (estack[0] === undefined) { 188 | 189 | /* ignoring text outside of elements */ 190 | 191 | } else if (estack[0] instanceof Span || estack[0] instanceof P) { 192 | 193 | /* ignore children text nodes in ruby container spans */ 194 | 195 | if (estack[0] instanceof Span) { 196 | 197 | var ruby = estack[0].styleAttrs[imscStyles.byName.ruby.qname]; 198 | 199 | if (ruby === 'container' || ruby === 'textContainer' || ruby === 'baseContainer') { 200 | 201 | return; 202 | 203 | } 204 | 205 | } 206 | 207 | /* create an anonymous span */ 208 | 209 | var s = new AnonymousSpan(); 210 | 211 | s.initFromText(doc, estack[0], str, xmllangstack[0], xmlspacestack[0], errorHandler); 212 | 213 | estack[0].contents.push(s); 214 | 215 | } else if (estack[0] instanceof ForeignElement && 216 | metadata_depth > 0 && 217 | metadataHandler && 218 | 'onText' in metadataHandler) { 219 | 220 | /* text node within a child of metadata element */ 221 | 222 | metadataHandler.onText(str); 223 | 224 | } 225 | 226 | }; 227 | 228 | 229 | p.onopentag = function (node) { 230 | 231 | // maintain the xml:space stack 232 | 233 | var xmlspace = node.attributes["xml:space"]; 234 | 235 | if (xmlspace) { 236 | 237 | xmlspacestack.unshift(xmlspace.value); 238 | 239 | } else { 240 | 241 | if (xmlspacestack.length === 0) { 242 | 243 | xmlspacestack.unshift("default"); 244 | 245 | } else { 246 | 247 | xmlspacestack.unshift(xmlspacestack[0]); 248 | 249 | } 250 | 251 | } 252 | 253 | /* maintain the xml:lang stack */ 254 | 255 | 256 | var xmllang = node.attributes["xml:lang"]; 257 | 258 | if (xmllang) { 259 | 260 | xmllangstack.unshift(xmllang.value); 261 | 262 | } else { 263 | 264 | if (xmllangstack.length === 0) { 265 | 266 | xmllangstack.unshift(""); 267 | 268 | } else { 269 | 270 | xmllangstack.unshift(xmllangstack[0]); 271 | 272 | } 273 | 274 | } 275 | 276 | 277 | /* process the element */ 278 | 279 | if (node.uri === imscNames.ns_tt) { 280 | 281 | if (node.local === 'tt') { 282 | 283 | if (doc !== null) { 284 | 285 | reportFatal(errorHandler, "Two elements at (" + this.line + "," + this.column + ")"); 286 | 287 | } 288 | 289 | doc = new TT(); 290 | 291 | doc.initFromNode(node, xmllangstack[0], errorHandler); 292 | 293 | estack.unshift(doc); 294 | 295 | } else if (node.local === 'head') { 296 | 297 | if (!(estack[0] instanceof TT)) { 298 | reportFatal(errorHandler, "Parent of element is not at (" + this.line + "," + this.column + ")"); 299 | } 300 | 301 | estack.unshift(doc.head); 302 | 303 | } else if (node.local === 'styling') { 304 | 305 | if (!(estack[0] instanceof Head)) { 306 | reportFatal(errorHandler, "Parent of element is not at (" + this.line + "," + this.column + ")"); 307 | } 308 | 309 | estack.unshift(doc.head.styling); 310 | 311 | } else if (node.local === 'style') { 312 | 313 | var s; 314 | 315 | if (estack[0] instanceof Styling) { 316 | 317 | s = new Style(); 318 | 319 | s.initFromNode(node, errorHandler); 320 | 321 | /* ignore