From 0bd233e8228c74cb956daeb6c7b0a43e788daea8 Mon Sep 17 00:00:00 2001 From: Janne Koschinski <janne@kuschku.de> Date: Fri, 27 Nov 2020 03:54:59 +0100 Subject: [PATCH] Implement subtitle handling for TTML subtitles --- package-lock.json | 3291 +++++++++++++++++- package.json | 3 + src/assets/fonts/subtitles.css | 27 + src/global.d.ts | 57 + src/index.tsx | 1 + src/routes/player/Player.tsx | 96 +- src/routes/player/subtitles/TtmlHelper.ts | 92 + src/routes/player/subtitles/TtmlRenderer.tsx | 24 + src/routes/player/video/DashVideoElement.tsx | 34 +- src/routes/player/video/RawVideoElement.tsx | 35 +- src/routes/player/video/VideoElement.tsx | 7 +- src/util/ttml/doc.js | 1097 ++++++ src/util/ttml/html.js | 1008 ++++++ src/util/ttml/isd.js | 449 +++ src/util/ttml/ismc.ts | 149 + src/util/ttml/names.js | 33 + src/util/ttml/styles.js | 941 +++++ src/util/ttml/utils.js | 284 ++ 18 files changed, 7468 insertions(+), 160 deletions(-) create mode 100644 src/assets/fonts/subtitles.css create mode 100644 src/global.d.ts create mode 100644 src/routes/player/subtitles/TtmlHelper.ts create mode 100644 src/routes/player/subtitles/TtmlRenderer.tsx create mode 100644 src/util/ttml/doc.js create mode 100644 src/util/ttml/html.js create mode 100644 src/util/ttml/isd.js create mode 100644 src/util/ttml/ismc.ts create mode 100644 src/util/ttml/names.js create mode 100644 src/util/ttml/styles.js create mode 100644 src/util/ttml/utils.js diff --git a/package-lock.json b/package-lock.json index 833b9fc..f7985a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6671,6 +6671,11 @@ "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" }, + "i": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/i/-/i-0.3.6.tgz", + "integrity": "sha1-2WyScyB28HJxG2sQ/X1PZa2O4j0=" + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -8842,118 +8847,3212 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "requires": { + "inherits": "2.0.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + } + } + } + } + }, + "node-modules-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", + "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=" + }, + "node-notifier": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.3.tgz", + "integrity": "sha512-M4UBGcs4jeOK9CjTsYwkvH6/MzuUmGCyTW+kCY7uO+1ZVr0+FHGdPdIf5CCLqAaxnRrWidyoQlNkMIIVwbKB8Q==", + "requires": { + "growly": "^1.3.0", + "is-wsl": "^1.1.0", + "semver": "^5.5.0", + "shellwords": "^0.1.1", + "which": "^1.3.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "node-releases": { + "version": "1.1.61", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.61.tgz", + "integrity": "sha512-DD5vebQLg8jLCOzwupn954fbIiZht05DAZs0k2u8NStSe6h9XdsuIQL8hSRKYiU8WUQRznmSDrKGbv3ObOmC7g==" + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=" + }, + "normalize-url": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", + "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", + "requires": { + "object-assign": "^4.0.1", + "prepend-http": "^1.0.0", + "query-string": "^4.1.0", + "sort-keys": "^1.0.0" + } + }, + "npm": { + "version": "6.14.9", + "resolved": "https://registry.npmjs.org/npm/-/npm-6.14.9.tgz", + "integrity": "sha512-yHi1+i9LyAZF1gAmgyYtVk+HdABlLy94PMIDoK1TRKWvmFQAt5z3bodqVwKvzY0s6dLqQPVsRLiwhJfNtiHeCg==", + "requires": { + "JSONStream": "^1.3.5", + "abbrev": "~1.1.1", + "ansicolors": "~0.3.2", + "ansistyles": "~0.1.3", + "aproba": "^2.0.0", + "archy": "~1.0.0", + "bin-links": "^1.1.8", + "bluebird": "^3.5.5", + "byte-size": "^5.0.1", + "cacache": "^12.0.3", + "call-limit": "^1.1.1", + "chownr": "^1.1.4", + "ci-info": "^2.0.0", + "cli-columns": "^3.1.2", + "cli-table3": "^0.5.1", + "cmd-shim": "^3.0.3", + "columnify": "~1.5.4", + "config-chain": "^1.1.12", + "debuglog": "*", + "detect-indent": "~5.0.0", + "detect-newline": "^2.1.0", + "dezalgo": "~1.0.3", + "editor": "~1.0.0", + "figgy-pudding": "^3.5.1", + "find-npm-prefix": "^1.0.2", + "fs-vacuum": "~1.2.10", + "fs-write-stream-atomic": "~1.0.10", + "gentle-fs": "^2.3.1", + "glob": "^7.1.6", + "graceful-fs": "^4.2.4", + "has-unicode": "~2.0.1", + "hosted-git-info": "^2.8.8", + "iferr": "^1.0.2", + "imurmurhash": "*", + "infer-owner": "^1.0.4", + "inflight": "~1.0.6", + "inherits": "^2.0.4", + "ini": "^1.3.5", + "init-package-json": "^1.10.3", + "is-cidr": "^3.0.0", + "json-parse-better-errors": "^1.0.2", + "lazy-property": "~1.0.0", + "libcipm": "^4.0.8", + "libnpm": "^3.0.1", + "libnpmaccess": "^3.0.2", + "libnpmhook": "^5.0.3", + "libnpmorg": "^1.0.1", + "libnpmsearch": "^2.0.2", + "libnpmteam": "^1.0.2", + "libnpx": "^10.2.4", + "lock-verify": "^2.1.0", + "lockfile": "^1.0.4", + "lodash._baseindexof": "*", + "lodash._baseuniq": "~4.6.0", + "lodash._bindcallback": "*", + "lodash._cacheindexof": "*", + "lodash._createcache": "*", + "lodash._getnative": "*", + "lodash.clonedeep": "~4.5.0", + "lodash.restparam": "*", + "lodash.union": "~4.6.0", + "lodash.uniq": "~4.5.0", + "lodash.without": "~4.4.0", + "lru-cache": "^5.1.1", + "meant": "^1.0.2", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.5", + "move-concurrently": "^1.0.1", + "node-gyp": "^5.1.0", + "nopt": "^4.0.3", + "normalize-package-data": "^2.5.0", + "npm-audit-report": "^1.3.3", + "npm-cache-filename": "~1.0.2", + "npm-install-checks": "^3.0.2", + "npm-lifecycle": "^3.1.5", + "npm-package-arg": "^6.1.1", + "npm-packlist": "^1.4.8", + "npm-pick-manifest": "^3.0.2", + "npm-profile": "^4.0.4", + "npm-registry-fetch": "^4.0.7", + "npm-user-validate": "^1.0.1", + "npmlog": "~4.1.2", + "once": "~1.4.0", + "opener": "^1.5.1", + "osenv": "^0.1.5", + "pacote": "^9.5.12", + "path-is-inside": "~1.0.2", + "promise-inflight": "~1.0.1", + "qrcode-terminal": "^0.12.0", + "query-string": "^6.8.2", + "qw": "~1.0.1", + "read": "~1.0.7", + "read-cmd-shim": "^1.0.5", + "read-installed": "~4.0.3", + "read-package-json": "^2.1.1", + "read-package-tree": "^5.3.1", + "readable-stream": "^3.6.0", + "readdir-scoped-modules": "^1.1.0", + "request": "^2.88.0", + "retry": "^0.12.0", + "rimraf": "^2.7.1", + "safe-buffer": "^5.1.2", + "semver": "^5.7.1", + "sha": "^3.0.0", + "slide": "~1.1.6", + "sorted-object": "~2.0.1", + "sorted-union-stream": "~2.1.3", + "ssri": "^6.0.1", + "stringify-package": "^1.0.1", + "tar": "^4.4.13", + "text-table": "~0.2.0", + "tiny-relative-date": "^1.3.0", + "uid-number": "0.0.6", + "umask": "~1.1.0", + "unique-filename": "^1.1.1", + "unpipe": "~1.0.0", + "update-notifier": "^2.5.0", + "uuid": "^3.3.3", + "validate-npm-package-license": "^3.0.4", + "validate-npm-package-name": "~3.0.0", + "which": "^1.3.1", + "worker-farm": "^1.7.0", + "write-file-atomic": "^2.4.3" + }, + "dependencies": { + "JSONStream": { + "version": "1.3.5", + "bundled": true, + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } + }, + "abbrev": { + "version": "1.1.1", + "bundled": true + }, + "agent-base": { + "version": "4.3.0", + "bundled": true, + "requires": { + "es6-promisify": "^5.0.0" + } + }, + "agentkeepalive": { + "version": "3.5.2", + "bundled": true, + "requires": { + "humanize-ms": "^1.2.1" + } + }, + "ansi-align": { + "version": "2.0.0", + "bundled": true, + "requires": { + "string-width": "^2.0.0" + } + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true + }, + "ansi-styles": { + "version": "3.2.1", + "bundled": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "ansicolors": { + "version": "0.3.2", + "bundled": true + }, + "ansistyles": { + "version": "0.1.3", + "bundled": true + }, + "aproba": { + "version": "2.0.0", + "bundled": true + }, + "archy": { + "version": "1.0.0", + "bundled": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "asap": { + "version": "2.0.6", + "bundled": true + }, + "asn1": { + "version": "0.2.4", + "bundled": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "bundled": true + }, + "asynckit": { + "version": "0.4.0", + "bundled": true + }, + "aws-sign2": { + "version": "0.7.0", + "bundled": true + }, + "aws4": { + "version": "1.8.0", + "bundled": true + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "bundled": true, + "optional": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "bin-links": { + "version": "1.1.8", + "bundled": true, + "requires": { + "bluebird": "^3.5.3", + "cmd-shim": "^3.0.0", + "gentle-fs": "^2.3.0", + "graceful-fs": "^4.1.15", + "npm-normalize-package-bin": "^1.0.0", + "write-file-atomic": "^2.3.0" + } + }, + "bluebird": { + "version": "3.5.5", + "bundled": true + }, + "boxen": { + "version": "1.3.0", + "bundled": true, + "requires": { + "ansi-align": "^2.0.0", + "camelcase": "^4.0.0", + "chalk": "^2.0.1", + "cli-boxes": "^1.0.0", + "string-width": "^2.0.0", + "term-size": "^1.2.0", + "widest-line": "^2.0.0" + } + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "buffer-from": { + "version": "1.0.0", + "bundled": true + }, + "builtins": { + "version": "1.0.3", + "bundled": true + }, + "byline": { + "version": "5.0.0", + "bundled": true + }, + "byte-size": { + "version": "5.0.1", + "bundled": true + }, + "cacache": { + "version": "12.0.3", + "bundled": true, + "requires": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + } + }, + "call-limit": { + "version": "1.1.1", + "bundled": true + }, + "camelcase": { + "version": "4.1.0", + "bundled": true + }, + "capture-stack-trace": { + "version": "1.0.0", + "bundled": true + }, + "caseless": { + "version": "0.12.0", + "bundled": true + }, + "chalk": { + "version": "2.4.1", + "bundled": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chownr": { + "version": "1.1.4", + "bundled": true + }, + "ci-info": { + "version": "2.0.0", + "bundled": true + }, + "cidr-regex": { + "version": "2.0.10", + "bundled": true, + "requires": { + "ip-regex": "^2.1.0" + } + }, + "cli-boxes": { + "version": "1.0.0", + "bundled": true + }, + "cli-columns": { + "version": "3.1.2", + "bundled": true, + "requires": { + "string-width": "^2.0.0", + "strip-ansi": "^3.0.1" + } + }, + "cli-table3": { + "version": "0.5.1", + "bundled": true, + "requires": { + "colors": "^1.1.2", + "object-assign": "^4.1.0", + "string-width": "^2.1.1" + } + }, + "cliui": { + "version": "5.0.0", + "bundled": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "bundled": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "bundled": true + }, + "string-width": { + "version": "3.1.0", + "bundled": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "bundled": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "clone": { + "version": "1.0.4", + "bundled": true + }, + "cmd-shim": { + "version": "3.0.3", + "bundled": true, + "requires": { + "graceful-fs": "^4.1.2", + "mkdirp": "~0.5.0" + } + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true + }, + "color-convert": { + "version": "1.9.1", + "bundled": true, + "requires": { + "color-name": "^1.1.1" + } + }, + "color-name": { + "version": "1.1.3", + "bundled": true + }, + "colors": { + "version": "1.3.3", + "bundled": true, + "optional": true + }, + "columnify": { + "version": "1.5.4", + "bundled": true, + "requires": { + "strip-ansi": "^3.0.0", + "wcwidth": "^1.0.0" + } + }, + "combined-stream": { + "version": "1.0.6", + "bundled": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "concat-stream": { + "version": "1.6.2", + "bundled": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "config-chain": { + "version": "1.1.12", + "bundled": true, + "requires": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "configstore": { + "version": "3.1.5", + "bundled": true, + "requires": { + "dot-prop": "^4.2.1", + "graceful-fs": "^4.1.2", + "make-dir": "^1.0.0", + "unique-string": "^1.0.0", + "write-file-atomic": "^2.0.0", + "xdg-basedir": "^3.0.0" + } + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true + }, + "copy-concurrently": { + "version": "1.0.5", + "bundled": true, + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + }, + "dependencies": { + "aproba": { + "version": "1.2.0", + "bundled": true + }, + "iferr": { + "version": "0.1.5", + "bundled": true + } + } + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true + }, + "create-error-class": { + "version": "3.0.2", + "bundled": true, + "requires": { + "capture-stack-trace": "^1.0.0" + } + }, + "cross-spawn": { + "version": "5.1.0", + "bundled": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "lru-cache": { + "version": "4.1.5", + "bundled": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "yallist": { + "version": "2.1.2", + "bundled": true + } + } + }, + "crypto-random-string": { + "version": "1.0.0", + "bundled": true + }, + "cyclist": { + "version": "0.2.2", + "bundled": true + }, + "dashdash": { + "version": "1.14.1", + "bundled": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "debug": { + "version": "3.1.0", + "bundled": true, + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "bundled": true + } + } + }, + "debuglog": { + "version": "1.0.1", + "bundled": true + }, + "decamelize": { + "version": "1.2.0", + "bundled": true + }, + "decode-uri-component": { + "version": "0.2.0", + "bundled": true + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true + }, + "defaults": { + "version": "1.0.3", + "bundled": true, + "requires": { + "clone": "^1.0.2" + } + }, + "define-properties": { + "version": "1.1.3", + "bundled": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "delayed-stream": { + "version": "1.0.0", + "bundled": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true + }, + "detect-indent": { + "version": "5.0.0", + "bundled": true + }, + "detect-newline": { + "version": "2.1.0", + "bundled": true + }, + "dezalgo": { + "version": "1.0.3", + "bundled": true, + "requires": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "dot-prop": { + "version": "4.2.1", + "bundled": true, + "requires": { + "is-obj": "^1.0.0" + } + }, + "dotenv": { + "version": "5.0.1", + "bundled": true + }, + "duplexer3": { + "version": "0.1.4", + "bundled": true + }, + "duplexify": { + "version": "3.6.0", + "bundled": true, + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "bundled": true, + "optional": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "editor": { + "version": "1.0.0", + "bundled": true + }, + "emoji-regex": { + "version": "7.0.3", + "bundled": true + }, + "encoding": { + "version": "0.1.12", + "bundled": true, + "requires": { + "iconv-lite": "~0.4.13" + } + }, + "end-of-stream": { + "version": "1.4.1", + "bundled": true, + "requires": { + "once": "^1.4.0" + } + }, + "env-paths": { + "version": "2.2.0", + "bundled": true + }, + "err-code": { + "version": "1.1.2", + "bundled": true + }, + "errno": { + "version": "0.1.7", + "bundled": true, + "requires": { + "prr": "~1.0.1" + } + }, + "es-abstract": { + "version": "1.12.0", + "bundled": true, + "requires": { + "es-to-primitive": "^1.1.1", + "function-bind": "^1.1.1", + "has": "^1.0.1", + "is-callable": "^1.1.3", + "is-regex": "^1.0.4" + } + }, + "es-to-primitive": { + "version": "1.2.0", + "bundled": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "es6-promise": { + "version": "4.2.8", + "bundled": true + }, + "es6-promisify": { + "version": "5.0.0", + "bundled": true, + "requires": { + "es6-promise": "^4.0.3" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "bundled": true + }, + "execa": { + "version": "0.7.0", + "bundled": true, + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "dependencies": { + "get-stream": { + "version": "3.0.0", + "bundled": true + } + } + }, + "extend": { + "version": "3.0.2", + "bundled": true + }, + "extsprintf": { + "version": "1.3.0", + "bundled": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "bundled": true + }, + "figgy-pudding": { + "version": "3.5.1", + "bundled": true + }, + "find-npm-prefix": { + "version": "1.0.2", + "bundled": true + }, + "flush-write-stream": { + "version": "1.0.3", + "bundled": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.4" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "forever-agent": { + "version": "0.6.1", + "bundled": true + }, + "form-data": { + "version": "2.3.2", + "bundled": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "1.0.6", + "mime-types": "^2.1.12" + } + }, + "from2": { + "version": "2.3.0", + "bundled": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "fs-minipass": { + "version": "1.2.7", + "bundled": true, + "requires": { + "minipass": "^2.6.0" + }, + "dependencies": { + "minipass": { + "version": "2.9.0", + "bundled": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + } + } + }, + "fs-vacuum": { + "version": "1.2.10", + "bundled": true, + "requires": { + "graceful-fs": "^4.1.2", + "path-is-inside": "^1.0.1", + "rimraf": "^2.5.2" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "bundled": true, + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + }, + "dependencies": { + "iferr": { + "version": "0.1.5", + "bundled": true + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true + }, + "function-bind": { + "version": "1.1.1", + "bundled": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + }, + "dependencies": { + "aproba": { + "version": "1.2.0", + "bundled": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + } + } + }, + "genfun": { + "version": "5.0.0", + "bundled": true + }, + "gentle-fs": { + "version": "2.3.1", + "bundled": true, + "requires": { + "aproba": "^1.1.2", + "chownr": "^1.1.2", + "cmd-shim": "^3.0.3", + "fs-vacuum": "^1.2.10", + "graceful-fs": "^4.1.11", + "iferr": "^0.1.5", + "infer-owner": "^1.0.4", + "mkdirp": "^0.5.1", + "path-is-inside": "^1.0.2", + "read-cmd-shim": "^1.0.1", + "slide": "^1.1.6" + }, + "dependencies": { + "aproba": { + "version": "1.2.0", + "bundled": true + }, + "iferr": { + "version": "0.1.5", + "bundled": true + } + } + }, + "get-caller-file": { + "version": "2.0.5", + "bundled": true + }, + "get-stream": { + "version": "4.1.0", + "bundled": true, + "requires": { + "pump": "^3.0.0" + } + }, + "getpass": { + "version": "0.1.7", + "bundled": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.6", + "bundled": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "global-dirs": { + "version": "0.1.1", + "bundled": true, + "requires": { + "ini": "^1.3.4" + } + }, + "got": { + "version": "6.7.1", + "bundled": true, + "requires": { + "create-error-class": "^3.0.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "is-redirect": "^1.0.0", + "is-retry-allowed": "^1.0.0", + "is-stream": "^1.0.0", + "lowercase-keys": "^1.0.0", + "safe-buffer": "^5.0.1", + "timed-out": "^4.0.0", + "unzip-response": "^2.0.1", + "url-parse-lax": "^1.0.0" + }, + "dependencies": { + "get-stream": { + "version": "3.0.0", + "bundled": true + } + } + }, + "graceful-fs": { + "version": "4.2.4", + "bundled": true + }, + "har-schema": { + "version": "2.0.0", + "bundled": true + }, + "har-validator": { + "version": "5.1.5", + "bundled": true, + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "bundled": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "bundled": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "bundled": true + } + } + }, + "has": { + "version": "1.0.3", + "bundled": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "bundled": true + }, + "has-symbols": { + "version": "1.0.0", + "bundled": true + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true + }, + "hosted-git-info": { + "version": "2.8.8", + "bundled": true + }, + "http-cache-semantics": { + "version": "3.8.1", + "bundled": true + }, + "http-proxy-agent": { + "version": "2.1.0", + "bundled": true, + "requires": { + "agent-base": "4", + "debug": "3.1.0" + } + }, + "http-signature": { + "version": "1.2.0", + "bundled": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-proxy-agent": { + "version": "2.2.4", + "bundled": true, + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + } + }, + "humanize-ms": { + "version": "1.2.1", + "bundled": true, + "requires": { + "ms": "^2.0.0" + } + }, + "iconv-lite": { + "version": "0.4.23", + "bundled": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "iferr": { + "version": "1.0.2", + "bundled": true + }, + "ignore-walk": { + "version": "3.0.3", + "bundled": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "import-lazy": { + "version": "2.1.0", + "bundled": true + }, + "imurmurhash": { + "version": "0.1.4", + "bundled": true + }, + "infer-owner": { + "version": "1.0.4", + "bundled": true + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "bundled": true + }, + "ini": { + "version": "1.3.5", + "bundled": true + }, + "init-package-json": { + "version": "1.10.3", + "bundled": true, + "requires": { + "glob": "^7.1.1", + "npm-package-arg": "^4.0.0 || ^5.0.0 || ^6.0.0", + "promzard": "^0.3.0", + "read": "~1.0.1", + "read-package-json": "1 || 2", + "semver": "2.x || 3.x || 4 || 5", + "validate-npm-package-license": "^3.0.1", + "validate-npm-package-name": "^3.0.0" + } + }, + "ip": { + "version": "1.1.5", + "bundled": true + }, + "ip-regex": { + "version": "2.1.0", + "bundled": true + }, + "is-callable": { + "version": "1.1.4", + "bundled": true + }, + "is-ci": { + "version": "1.2.1", + "bundled": true, + "requires": { + "ci-info": "^1.5.0" + }, + "dependencies": { + "ci-info": { + "version": "1.6.0", + "bundled": true + } + } + }, + "is-cidr": { + "version": "3.0.0", + "bundled": true, + "requires": { + "cidr-regex": "^2.0.10" + } + }, + "is-date-object": { + "version": "1.0.1", + "bundled": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-installed-globally": { + "version": "0.1.0", + "bundled": true, + "requires": { + "global-dirs": "^0.1.0", + "is-path-inside": "^1.0.0" + } + }, + "is-npm": { + "version": "1.0.0", + "bundled": true + }, + "is-obj": { + "version": "1.0.1", + "bundled": true + }, + "is-path-inside": { + "version": "1.0.1", + "bundled": true, + "requires": { + "path-is-inside": "^1.0.1" + } + }, + "is-redirect": { + "version": "1.0.0", + "bundled": true + }, + "is-regex": { + "version": "1.0.4", + "bundled": true, + "requires": { + "has": "^1.0.1" + } + }, + "is-retry-allowed": { + "version": "1.2.0", + "bundled": true + }, + "is-stream": { + "version": "1.1.0", + "bundled": true + }, + "is-symbol": { + "version": "1.0.2", + "bundled": true, + "requires": { + "has-symbols": "^1.0.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "bundled": true + }, + "isarray": { + "version": "1.0.0", + "bundled": true + }, + "isexe": { + "version": "2.0.0", + "bundled": true + }, + "isstream": { + "version": "0.1.2", + "bundled": true + }, + "jsbn": { + "version": "0.1.1", + "bundled": true, + "optional": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "bundled": true + }, + "json-schema": { + "version": "0.2.3", + "bundled": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "bundled": true + }, + "jsonparse": { + "version": "1.3.1", + "bundled": true + }, + "jsprim": { + "version": "1.4.1", + "bundled": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "latest-version": { + "version": "3.1.0", + "bundled": true, + "requires": { + "package-json": "^4.0.0" + } + }, + "lazy-property": { + "version": "1.0.0", + "bundled": true + }, + "libcipm": { + "version": "4.0.8", + "bundled": true, + "requires": { + "bin-links": "^1.1.2", + "bluebird": "^3.5.1", + "figgy-pudding": "^3.5.1", + "find-npm-prefix": "^1.0.2", + "graceful-fs": "^4.1.11", + "ini": "^1.3.5", + "lock-verify": "^2.1.0", + "mkdirp": "^0.5.1", + "npm-lifecycle": "^3.0.0", + "npm-logical-tree": "^1.2.1", + "npm-package-arg": "^6.1.0", + "pacote": "^9.1.0", + "read-package-json": "^2.0.13", + "rimraf": "^2.6.2", + "worker-farm": "^1.6.0" + } + }, + "libnpm": { + "version": "3.0.1", + "bundled": true, + "requires": { + "bin-links": "^1.1.2", + "bluebird": "^3.5.3", + "find-npm-prefix": "^1.0.2", + "libnpmaccess": "^3.0.2", + "libnpmconfig": "^1.2.1", + "libnpmhook": "^5.0.3", + "libnpmorg": "^1.0.1", + "libnpmpublish": "^1.1.2", + "libnpmsearch": "^2.0.2", + "libnpmteam": "^1.0.2", + "lock-verify": "^2.0.2", + "npm-lifecycle": "^3.0.0", + "npm-logical-tree": "^1.2.1", + "npm-package-arg": "^6.1.0", + "npm-profile": "^4.0.2", + "npm-registry-fetch": "^4.0.0", + "npmlog": "^4.1.2", + "pacote": "^9.5.3", + "read-package-json": "^2.0.13", + "stringify-package": "^1.0.0" + } + }, + "libnpmaccess": { + "version": "3.0.2", + "bundled": true, + "requires": { + "aproba": "^2.0.0", + "get-stream": "^4.0.0", + "npm-package-arg": "^6.1.0", + "npm-registry-fetch": "^4.0.0" + } + }, + "libnpmconfig": { + "version": "1.2.1", + "bundled": true, + "requires": { + "figgy-pudding": "^3.5.1", + "find-up": "^3.0.0", + "ini": "^1.3.5" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "bundled": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "bundled": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.2.0", + "bundled": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "bundled": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "bundled": true + } + } + }, + "libnpmhook": { + "version": "5.0.3", + "bundled": true, + "requires": { + "aproba": "^2.0.0", + "figgy-pudding": "^3.4.1", + "get-stream": "^4.0.0", + "npm-registry-fetch": "^4.0.0" + } + }, + "libnpmorg": { + "version": "1.0.1", + "bundled": true, + "requires": { + "aproba": "^2.0.0", + "figgy-pudding": "^3.4.1", + "get-stream": "^4.0.0", + "npm-registry-fetch": "^4.0.0" + } + }, + "libnpmpublish": { + "version": "1.1.2", + "bundled": true, + "requires": { + "aproba": "^2.0.0", + "figgy-pudding": "^3.5.1", + "get-stream": "^4.0.0", + "lodash.clonedeep": "^4.5.0", + "normalize-package-data": "^2.4.0", + "npm-package-arg": "^6.1.0", + "npm-registry-fetch": "^4.0.0", + "semver": "^5.5.1", + "ssri": "^6.0.1" + } + }, + "libnpmsearch": { + "version": "2.0.2", + "bundled": true, + "requires": { + "figgy-pudding": "^3.5.1", + "get-stream": "^4.0.0", + "npm-registry-fetch": "^4.0.0" + } + }, + "libnpmteam": { + "version": "1.0.2", + "bundled": true, + "requires": { + "aproba": "^2.0.0", + "figgy-pudding": "^3.4.1", + "get-stream": "^4.0.0", + "npm-registry-fetch": "^4.0.0" + } + }, + "libnpx": { + "version": "10.2.4", + "bundled": true, + "requires": { + "dotenv": "^5.0.1", + "npm-package-arg": "^6.0.0", + "rimraf": "^2.6.2", + "safe-buffer": "^5.1.0", + "update-notifier": "^2.3.0", + "which": "^1.3.0", + "y18n": "^4.0.0", + "yargs": "^14.2.3" + } + }, + "lock-verify": { + "version": "2.1.0", + "bundled": true, + "requires": { + "npm-package-arg": "^6.1.0", + "semver": "^5.4.1" + } + }, + "lockfile": { + "version": "1.0.4", + "bundled": true, + "requires": { + "signal-exit": "^3.0.2" + } + }, + "lodash._baseindexof": { + "version": "3.1.0", + "bundled": true + }, + "lodash._baseuniq": { + "version": "4.6.0", + "bundled": true, + "requires": { + "lodash._createset": "~4.0.0", + "lodash._root": "~3.0.0" + } + }, + "lodash._bindcallback": { + "version": "3.0.1", + "bundled": true + }, + "lodash._cacheindexof": { + "version": "3.0.2", + "bundled": true + }, + "lodash._createcache": { + "version": "3.1.2", + "bundled": true, + "requires": { + "lodash._getnative": "^3.0.0" + } + }, + "lodash._createset": { + "version": "4.0.3", + "bundled": true + }, + "lodash._getnative": { + "version": "3.9.1", + "bundled": true + }, + "lodash._root": { + "version": "3.0.1", + "bundled": true + }, + "lodash.clonedeep": { + "version": "4.5.0", + "bundled": true + }, + "lodash.restparam": { + "version": "3.6.1", + "bundled": true + }, + "lodash.union": { + "version": "4.6.0", + "bundled": true + }, + "lodash.uniq": { + "version": "4.5.0", + "bundled": true + }, + "lodash.without": { + "version": "4.4.0", + "bundled": true + }, + "lowercase-keys": { + "version": "1.0.1", + "bundled": true + }, + "lru-cache": { + "version": "5.1.1", + "bundled": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "make-dir": { + "version": "1.3.0", + "bundled": true, + "requires": { + "pify": "^3.0.0" + } + }, + "make-fetch-happen": { + "version": "5.0.2", + "bundled": true, + "requires": { + "agentkeepalive": "^3.4.1", + "cacache": "^12.0.0", + "http-cache-semantics": "^3.8.1", + "http-proxy-agent": "^2.1.0", + "https-proxy-agent": "^2.2.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "node-fetch-npm": "^2.0.2", + "promise-retry": "^1.1.1", + "socks-proxy-agent": "^4.0.0", + "ssri": "^6.0.0" + } + }, + "meant": { + "version": "1.0.2", + "bundled": true + }, + "mime-db": { + "version": "1.35.0", + "bundled": true + }, + "mime-types": { + "version": "2.1.19", + "bundled": true, + "requires": { + "mime-db": "~1.35.0" + } + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "bundled": true + }, + "minizlib": { + "version": "1.3.3", + "bundled": true, + "requires": { + "minipass": "^2.9.0" + }, + "dependencies": { + "minipass": { + "version": "2.9.0", + "bundled": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + } + } + }, + "mississippi": { + "version": "3.0.0", + "bundled": true, + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, + "mkdirp": { + "version": "0.5.5", + "bundled": true, + "requires": { + "minimist": "^1.2.5" + }, + "dependencies": { + "minimist": { + "version": "1.2.5", + "bundled": true + } + } + }, + "move-concurrently": { + "version": "1.0.1", + "bundled": true, + "requires": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + }, + "dependencies": { + "aproba": { + "version": "1.2.0", + "bundled": true + } + } + }, + "ms": { + "version": "2.1.1", + "bundled": true + }, + "mute-stream": { + "version": "0.0.7", + "bundled": true + }, + "node-fetch-npm": { + "version": "2.0.2", + "bundled": true, + "requires": { + "encoding": "^0.1.11", + "json-parse-better-errors": "^1.0.0", + "safe-buffer": "^5.1.1" + } + }, + "node-gyp": { + "version": "5.1.0", + "bundled": true, + "requires": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.2", + "mkdirp": "^0.5.1", + "nopt": "^4.0.1", + "npmlog": "^4.1.2", + "request": "^2.88.0", + "rimraf": "^2.6.3", + "semver": "^5.7.1", + "tar": "^4.4.12", + "which": "^1.3.1" + } + }, + "nopt": { + "version": "4.0.3", + "bundled": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "bundled": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "resolve": { + "version": "1.10.0", + "bundled": true, + "requires": { + "path-parse": "^1.0.6" + } + } + } + }, + "npm-audit-report": { + "version": "1.3.3", + "bundled": true, + "requires": { + "cli-table3": "^0.5.0", + "console-control-strings": "^1.1.0" + } + }, + "npm-bundled": { + "version": "1.1.1", + "bundled": true, + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-cache-filename": { + "version": "1.0.2", + "bundled": true + }, + "npm-install-checks": { + "version": "3.0.2", + "bundled": true, + "requires": { + "semver": "^2.3.0 || 3.x || 4 || 5" + } + }, + "npm-lifecycle": { + "version": "3.1.5", + "bundled": true, + "requires": { + "byline": "^5.0.0", + "graceful-fs": "^4.1.15", + "node-gyp": "^5.0.2", + "resolve-from": "^4.0.0", + "slide": "^1.1.6", + "uid-number": "0.0.6", + "umask": "^1.1.0", + "which": "^1.3.1" + } + }, + "npm-logical-tree": { + "version": "1.2.1", + "bundled": true + }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "bundled": true + }, + "npm-package-arg": { + "version": "6.1.1", + "bundled": true, + "requires": { + "hosted-git-info": "^2.7.1", + "osenv": "^0.1.5", + "semver": "^5.6.0", + "validate-npm-package-name": "^3.0.0" + } + }, + "npm-packlist": { + "version": "1.4.8", + "bundled": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-pick-manifest": { + "version": "3.0.2", + "bundled": true, + "requires": { + "figgy-pudding": "^3.5.1", + "npm-package-arg": "^6.0.0", + "semver": "^5.4.1" + } + }, + "npm-profile": { + "version": "4.0.4", + "bundled": true, + "requires": { + "aproba": "^1.1.2 || 2", + "figgy-pudding": "^3.4.1", + "npm-registry-fetch": "^4.0.0" + } + }, + "npm-registry-fetch": { + "version": "4.0.7", + "bundled": true, + "requires": { + "JSONStream": "^1.3.4", + "bluebird": "^3.5.1", + "figgy-pudding": "^3.4.1", + "lru-cache": "^5.1.1", + "make-fetch-happen": "^5.0.0", + "npm-package-arg": "^6.1.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "bundled": true + } + } + }, + "npm-run-path": { + "version": "2.0.2", + "bundled": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "npm-user-validate": { + "version": "1.0.1", + "bundled": true + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true + }, + "oauth-sign": { + "version": "0.9.0", + "bundled": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true + }, + "object-keys": { + "version": "1.0.12", + "bundled": true + }, + "object.getownpropertydescriptors": { + "version": "2.0.3", + "bundled": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.5.1" + } + }, + "once": { + "version": "1.4.0", + "bundled": true, + "requires": { + "wrappy": "1" + } + }, + "opener": { + "version": "1.5.1", + "bundled": true + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "p-finally": { + "version": "1.0.0", + "bundled": true + }, + "package-json": { + "version": "4.0.1", + "bundled": true, + "requires": { + "got": "^6.7.1", + "registry-auth-token": "^3.0.1", + "registry-url": "^3.0.3", + "semver": "^5.1.0" + } + }, + "pacote": { + "version": "9.5.12", + "bundled": true, + "requires": { + "bluebird": "^3.5.3", + "cacache": "^12.0.2", + "chownr": "^1.1.2", + "figgy-pudding": "^3.5.1", + "get-stream": "^4.1.0", + "glob": "^7.1.3", + "infer-owner": "^1.0.4", + "lru-cache": "^5.1.1", + "make-fetch-happen": "^5.0.0", + "minimatch": "^3.0.4", + "minipass": "^2.3.5", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "normalize-package-data": "^2.4.0", + "npm-normalize-package-bin": "^1.0.0", + "npm-package-arg": "^6.1.0", + "npm-packlist": "^1.1.12", + "npm-pick-manifest": "^3.0.0", + "npm-registry-fetch": "^4.0.0", + "osenv": "^0.1.5", + "promise-inflight": "^1.0.1", + "promise-retry": "^1.1.1", + "protoduck": "^5.0.1", + "rimraf": "^2.6.2", + "safe-buffer": "^5.1.2", + "semver": "^5.6.0", + "ssri": "^6.0.1", + "tar": "^4.4.10", + "unique-filename": "^1.1.1", + "which": "^1.3.1" + }, + "dependencies": { + "minipass": { + "version": "2.9.0", + "bundled": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + } + } + }, + "parallel-transform": { + "version": "1.1.0", + "bundled": true, + "requires": { + "cyclist": "~0.2.2", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "path-exists": { + "version": "3.0.0", + "bundled": true + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true + }, + "path-is-inside": { + "version": "1.0.2", + "bundled": true + }, + "path-key": { + "version": "2.0.1", + "bundled": true + }, + "path-parse": { + "version": "1.0.6", + "bundled": true + }, + "performance-now": { + "version": "2.1.0", + "bundled": true + }, + "pify": { + "version": "3.0.0", + "bundled": true + }, + "prepend-http": { + "version": "1.0.4", + "bundled": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true + }, + "promise-inflight": { + "version": "1.0.1", + "bundled": true + }, + "promise-retry": { + "version": "1.1.1", + "bundled": true, + "requires": { + "err-code": "^1.0.0", + "retry": "^0.10.0" + }, + "dependencies": { + "retry": { + "version": "0.10.1", + "bundled": true + } + } + }, + "promzard": { + "version": "0.3.0", + "bundled": true, + "requires": { + "read": "1" + } + }, + "proto-list": { + "version": "1.2.4", + "bundled": true + }, + "protoduck": { + "version": "5.0.1", + "bundled": true, + "requires": { + "genfun": "^5.0.0" + } + }, + "prr": { + "version": "1.0.1", + "bundled": true + }, + "pseudomap": { + "version": "1.0.2", + "bundled": true + }, + "psl": { + "version": "1.1.29", + "bundled": true + }, + "pump": { + "version": "3.0.0", + "bundled": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "bundled": true, + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "2.0.1", + "bundled": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "punycode": { + "version": "1.4.1", + "bundled": true + }, + "qrcode-terminal": { + "version": "0.12.0", + "bundled": true + }, + "qs": { + "version": "6.5.2", + "bundled": true + }, + "query-string": { + "version": "6.8.2", + "bundled": true, + "requires": { + "decode-uri-component": "^0.2.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + } + }, + "qw": { + "version": "1.0.1", + "bundled": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "read": { + "version": "1.0.7", + "bundled": true, + "requires": { + "mute-stream": "~0.0.4" + } + }, + "read-cmd-shim": { + "version": "1.0.5", + "bundled": true, + "requires": { + "graceful-fs": "^4.1.2" + } + }, + "read-installed": { + "version": "4.0.3", + "bundled": true, + "requires": { + "debuglog": "^1.0.1", + "graceful-fs": "^4.1.2", + "read-package-json": "^2.0.0", + "readdir-scoped-modules": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "slide": "~1.1.3", + "util-extend": "^1.0.1" + } + }, + "read-package-json": { + "version": "2.1.1", + "bundled": true, + "requires": { + "glob": "^7.1.1", + "graceful-fs": "^4.1.2", + "json-parse-better-errors": "^1.0.1", + "normalize-package-data": "^2.0.0", + "npm-normalize-package-bin": "^1.0.0" + } + }, + "read-package-tree": { + "version": "5.3.1", + "bundled": true, + "requires": { + "read-package-json": "^2.0.0", + "readdir-scoped-modules": "^1.0.0", + "util-promisify": "^2.1.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "bundled": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdir-scoped-modules": { + "version": "1.1.0", + "bundled": true, + "requires": { + "debuglog": "^1.0.1", + "dezalgo": "^1.0.0", + "graceful-fs": "^4.1.2", + "once": "^1.3.0" + } + }, + "registry-auth-token": { + "version": "3.4.0", + "bundled": true, + "requires": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "registry-url": { + "version": "3.1.0", + "bundled": true, + "requires": { + "rc": "^1.0.1" + } + }, + "request": { + "version": "2.88.0", + "bundled": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "require-directory": { + "version": "2.1.1", + "bundled": true + }, + "require-main-filename": { + "version": "2.0.0", + "bundled": true + }, + "resolve-from": { + "version": "4.0.0", + "bundled": true + }, + "retry": { + "version": "0.12.0", + "bundled": true + }, + "rimraf": { + "version": "2.7.1", + "bundled": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-queue": { + "version": "1.0.3", + "bundled": true, + "requires": { + "aproba": "^1.1.1" + }, + "dependencies": { + "aproba": { + "version": "1.2.0", + "bundled": true + } + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true + }, + "semver": { + "version": "5.7.1", + "bundled": true + }, + "semver-diff": { + "version": "2.1.0", + "bundled": true, + "requires": { + "semver": "^5.0.3" + } + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true + }, + "sha": { + "version": "3.0.0", + "bundled": true, + "requires": { + "graceful-fs": "^4.1.2" + } + }, + "shebang-command": { + "version": "1.2.0", + "bundled": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "bundled": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true + }, + "slide": { + "version": "1.1.6", + "bundled": true + }, + "smart-buffer": { + "version": "4.1.0", + "bundled": true + }, + "socks": { + "version": "2.3.3", + "bundled": true, + "requires": { + "ip": "1.1.5", + "smart-buffer": "^4.1.0" + } + }, + "socks-proxy-agent": { + "version": "4.0.2", + "bundled": true, + "requires": { + "agent-base": "~4.2.1", + "socks": "~2.3.2" + }, + "dependencies": { + "agent-base": { + "version": "4.2.1", + "bundled": true, + "requires": { + "es6-promisify": "^5.0.0" + } + } + } + }, + "sorted-object": { + "version": "2.0.1", + "bundled": true + }, + "sorted-union-stream": { + "version": "2.1.3", + "bundled": true, + "requires": { + "from2": "^1.3.0", + "stream-iterate": "^1.1.0" + }, + "dependencies": { + "from2": { + "version": "1.3.0", + "bundled": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "~1.1.10" + } + }, + "isarray": { + "version": "0.0.1", + "bundled": true + }, + "readable-stream": { + "version": "1.1.14", + "bundled": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "bundled": true + } + } + }, + "spdx-correct": { + "version": "3.0.0", + "bundled": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.1.0", + "bundled": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "bundled": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.5", + "bundled": true + }, + "split-on-first": { + "version": "1.1.0", + "bundled": true + }, + "sshpk": { + "version": "1.14.2", + "bundled": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "ssri": { + "version": "6.0.1", + "bundled": true, + "requires": { + "figgy-pudding": "^3.5.1" + } + }, + "stream-each": { + "version": "1.2.2", + "bundled": true, + "requires": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "stream-iterate": { + "version": "1.2.0", + "bundled": true, + "requires": { + "readable-stream": "^2.1.5", + "stream-shift": "^1.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "stream-shift": { + "version": "1.0.0", + "bundled": true + }, + "strict-uri-encode": { + "version": "2.0.0", + "bundled": true + }, + "string-width": { + "version": "2.1.1", + "bundled": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "bundled": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "bundled": true + }, + "strip-ansi": { + "version": "4.0.0", + "bundled": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "string_decoder": { + "version": "1.3.0", + "bundled": true, + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.0", + "bundled": true + } + } + }, + "stringify-package": { + "version": "1.0.1", + "bundled": true + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-eof": { + "version": "1.0.0", + "bundled": true + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true + }, + "supports-color": { + "version": "5.4.0", + "bundled": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "tar": { + "version": "4.4.13", + "bundled": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.8.6", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + }, + "dependencies": { + "minipass": { + "version": "2.9.0", + "bundled": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + } + } + }, + "term-size": { + "version": "1.2.0", + "bundled": true, "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "execa": "^0.7.0" + } + }, + "text-table": { + "version": "0.2.0", + "bundled": true + }, + "through": { + "version": "2.3.8", + "bundled": true + }, + "through2": { + "version": "2.0.3", + "bundled": true, + "requires": { + "readable-stream": "^2.1.5", + "xtend": "~4.0.1" }, "dependencies": { + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, "string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "bundled": true, "requires": { "safe-buffer": "~5.1.0" } } } }, - "util": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", - "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "timed-out": { + "version": "4.0.1", + "bundled": true + }, + "tiny-relative-date": { + "version": "1.3.0", + "bundled": true + }, + "tough-cookie": { + "version": "2.4.3", + "bundled": true, "requires": { - "inherits": "2.0.3" + "psl": "^1.1.24", + "punycode": "^1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "bundled": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "bundled": true, + "optional": true + }, + "typedarray": { + "version": "0.0.6", + "bundled": true + }, + "uid-number": { + "version": "0.0.6", + "bundled": true + }, + "umask": { + "version": "1.1.0", + "bundled": true + }, + "unique-filename": { + "version": "1.1.1", + "bundled": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.0", + "bundled": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "unique-string": { + "version": "1.0.0", + "bundled": true, + "requires": { + "crypto-random-string": "^1.0.0" + } + }, + "unpipe": { + "version": "1.0.0", + "bundled": true + }, + "unzip-response": { + "version": "2.0.1", + "bundled": true + }, + "update-notifier": { + "version": "2.5.0", + "bundled": true, + "requires": { + "boxen": "^1.2.1", + "chalk": "^2.0.1", + "configstore": "^3.0.0", + "import-lazy": "^2.1.0", + "is-ci": "^1.0.10", + "is-installed-globally": "^0.1.0", + "is-npm": "^1.0.0", + "latest-version": "^3.0.0", + "semver-diff": "^2.0.0", + "xdg-basedir": "^3.0.0" + } + }, + "uri-js": { + "version": "4.4.0", + "bundled": true, + "requires": { + "punycode": "^2.1.0" }, "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "punycode": { + "version": "2.1.1", + "bundled": true + } + } + }, + "url-parse-lax": { + "version": "1.0.0", + "bundled": true, + "requires": { + "prepend-http": "^1.0.1" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true + }, + "util-extend": { + "version": "1.0.3", + "bundled": true + }, + "util-promisify": { + "version": "2.1.0", + "bundled": true, + "requires": { + "object.getownpropertydescriptors": "^2.0.3" + } + }, + "uuid": { + "version": "3.3.3", + "bundled": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "bundled": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "validate-npm-package-name": { + "version": "3.0.0", + "bundled": true, + "requires": { + "builtins": "^1.0.3" + } + }, + "verror": { + "version": "1.10.0", + "bundled": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "wcwidth": { + "version": "1.0.1", + "bundled": true, + "requires": { + "defaults": "^1.0.3" + } + }, + "which": { + "version": "1.3.1", + "bundled": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "bundled": true + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "requires": { + "string-width": "^1.0.2" + }, + "dependencies": { + "string-width": { + "version": "1.0.2", + "bundled": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + } + } + }, + "widest-line": { + "version": "2.0.1", + "bundled": true, + "requires": { + "string-width": "^2.1.1" + } + }, + "worker-farm": { + "version": "1.7.0", + "bundled": true, + "requires": { + "errno": "~0.1.7" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "bundled": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "bundled": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "bundled": true + }, + "string-width": { + "version": "3.1.0", + "bundled": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "bundled": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true + }, + "write-file-atomic": { + "version": "2.4.3", + "bundled": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "xdg-basedir": { + "version": "3.0.0", + "bundled": true + }, + "xtend": { + "version": "4.0.1", + "bundled": true + }, + "y18n": { + "version": "4.0.0", + "bundled": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true + }, + "yargs": { + "version": "14.2.3", + "bundled": true, + "requires": { + "cliui": "^5.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^15.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "bundled": true + }, + "find-up": { + "version": "3.0.0", + "bundled": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "bundled": true + }, + "locate-path": { + "version": "3.0.0", + "bundled": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "bundled": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "bundled": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "bundled": true + }, + "string-width": { + "version": "3.1.0", + "bundled": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "bundled": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "yargs-parser": { + "version": "15.0.1", + "bundled": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "bundled": true } } } } }, - "node-modules-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", - "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=" - }, - "node-notifier": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.3.tgz", - "integrity": "sha512-M4UBGcs4jeOK9CjTsYwkvH6/MzuUmGCyTW+kCY7uO+1ZVr0+FHGdPdIf5CCLqAaxnRrWidyoQlNkMIIVwbKB8Q==", - "requires": { - "growly": "^1.3.0", - "is-wsl": "^1.1.0", - "semver": "^5.5.0", - "shellwords": "^0.1.1", - "which": "^1.3.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - } - } - }, - "node-releases": { - "version": "1.1.61", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.61.tgz", - "integrity": "sha512-DD5vebQLg8jLCOzwupn954fbIiZht05DAZs0k2u8NStSe6h9XdsuIQL8hSRKYiU8WUQRznmSDrKGbv3ObOmC7g==" - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - } - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, - "normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=" - }, - "normalize-url": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", - "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", - "requires": { - "object-assign": "^4.0.1", - "prepend-http": "^1.0.0", - "query-string": "^4.1.0", - "sort-keys": "^1.0.0" - } - }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -12057,6 +15156,12 @@ } } }, + "simplytyped": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/simplytyped/-/simplytyped-3.3.0.tgz", + "integrity": "sha512-mz4RaNdKTZiaKXgi6P1k/cdsxV3gz+y1Wh2NXHWD40dExktLh4Xx/h6MFakmQWODZHj/2rKe59acacpL74ZhQA==", + "dev": true + }, "sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", diff --git a/package.json b/package.json index 6e2091d..29862aa 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "@types/react-router": "^5.1.8", "@types/react-router-dom": "^5.1.5", "dashjs": "^3.1.3", + "i": "^0.3.6", "immer": "^7.0.9", + "npm": "^6.14.9", "react": "^16.13.1", "react-dom": "^16.13.1", "react-jss": "^10.4.0", @@ -23,6 +25,7 @@ "react-scripts": "3.4.3", "react-sweet-state": "^2.3.1", "resize-observer-polyfill": "^1.5.1", + "sax": "1.2.4", "typescript": "^3.7.5" }, "scripts": { diff --git a/src/assets/fonts/subtitles.css b/src/assets/fonts/subtitles.css new file mode 100644 index 0000000..2922f84 --- /dev/null +++ b/src/assets/fonts/subtitles.css @@ -0,0 +1,27 @@ +.ttml-sansSerif { + font-family: sans-serif; +} + +.ttml-serif { + font-family: serif; +} + +.ttml-monospace { + font-family: monospace; +} + +.ttml-proportionalSansSerif { + font-family: sans-serif; +} + +.ttml-monospaceSansSerif { + font-family: monospace; +} + +.ttml-proportionalSerif { + font-family: serif; +} + +.ttml-monospaceSerif { + font-family: monospace; +} diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 0000000..3da6d5d --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,57 @@ +declare module "imsc" { + export interface TtmlDocument { + getMediaTimeEvents(): number[], + getMediaTimeRange(): number[] + } + + export type ISD = unknown; + export type ISDState = unknown; + + export interface MetadataHandler { + onOpenTag(ns: string, name: string, attributes: Attribute[]): void, + + onCloseTag(): void, + + onText(contents: string): void, + } + + export interface Attribute { + uri: string, + name: string, + value: string + } + + export interface ErrorHandler { + info(error: string): boolean + + warn(error: string): boolean + + error(error: string): boolean + + fatal(error: string): boolean + } + + export function fromXML( + xmlstring: string, + errorHandler?: ErrorHandler, + metadataHandler?: MetadataHandler + ): TtmlDocument; + + export function generateISD( + tt: TtmlDocument, + offset?: number, + errorHandler?: ErrorHandler, + ): ISD; + + export function renderHTML( + isd: ISD, + element: HTMLElement, + imgResolver?: (backgroundUri: string) => string, + height?: number, + width?: number, + displayForcedOnlyMode?: boolean, + errorHandler?: ErrorHandler, + previousISDState?: ISDState, + enableRollUp?: boolean + ): ISDState; +} diff --git a/src/index.tsx b/src/index.tsx index 751e757..e245541 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -7,6 +7,7 @@ import {App} from "./App"; import './assets/fonts/fira-sans.css'; import './assets/fonts/inter.css'; +import './assets/fonts/subtitles.css'; import './base/base.css'; import * as serviceWorker from './serviceWorker'; diff --git a/src/routes/player/Player.tsx b/src/routes/player/Player.tsx index 132716c..dc54617 100644 --- a/src/routes/player/Player.tsx +++ b/src/routes/player/Player.tsx @@ -13,6 +13,8 @@ import {VideoElement} from "./video/VideoElement"; import {VideoApi} from "./video/VideoApi"; import {useAudioTracks} from "../../util/media/useAudioTracks"; import {useDebugInfo} from "../../util/media/useDebugInfo"; +import {Subtitle} from "../../api/models/Subtitle"; +import {TtmlRenderer} from "./subtitles/TtmlRenderer"; interface Props { meta: ContentMeta, @@ -30,27 +32,62 @@ export function Player( const description = getLocalizedDescription(content, locale); const rating = getLocalizedRating(content, locale); - const [previewTrackElement, setPreviewTrackElement] = useState<HTMLTrackElement | null>(null); + const [subtitle, setSubtitle] = useState<Subtitle | null>(null); + const [videoElement, setVideoElement] = useState<VideoApi | null>(null); - useDebugInfo("content", content); - useDebugInfo("player", videoElement); + const [previewTrack, setPreviewTrack] = useState<HTMLTrackElement | null>(null); + const [subtitleTrack, setSubtitleTrack] = useState<HTMLTrackElement | null>(null); const position = usePosition(videoElement); const duration = useDuration(videoElement); const audioTracks = useAudioTracks(videoElement); + useDebugInfo("playerEl", videoElement); + useDebugInfo("previewTrackEl", previewTrack); + useDebugInfo("subtitleTrackEl", subtitleTrack); + useDebugInfo("content", content); + useDebugInfo("subtitle", subtitle); + useDebugInfo("position", position); + useDebugInfo("duration", duration); + useDebugInfo("audioTracks", audioTracks); + return ( <div> <Link to="/">Back</Link> <p>{name?.name}</p> <p>{instalment?.content && getLocalizedName(instalment?.content, locale)?.name}</p> - <VideoElement - media={media} - autoPlay={true} - ref={setVideoElement} - previewSrc={content.preview || undefined} - previewTrackRef={setPreviewTrackElement} - /> + <div className={classes.player}> + <div className={classes.playerCanvas}> + <VideoElement + className={classes.video} + media={media} + autoPlay={true} + ref={setVideoElement} + > + {content.preview && ( + <track + ref={setPreviewTrack} + kind="metadata" + label="previews" + src={content.preview} + /> + )} + {subtitle && ( + <track + ref={setSubtitleTrack} + kind="captions" + label={`${subtitle.language} (${subtitle.specifier})`} + src={subtitle.src} + /> + )} + </VideoElement> + <TtmlRenderer + className={classes.subtitleCanvas} + trackElement={subtitleTrack} + duration={duration} + /> + </div> + </div> <p style={{fontVariant: "tabular-nums"}}>{formatDuration(position)} / {formatDuration(duration)}</p> <button onClick={videoElement?.play}>Play</button> <button onClick={videoElement?.pause}>Pause</button> @@ -63,9 +100,18 @@ export function Player( </li> ))} </ul> + <ul> + {content.subtitles.map(track => ( + <li key={track.src}> + <strong>{track.language}</strong> + {track.specifier} + <button onClick={() => setSubtitle(track)}>Choose</button> + </li> + ))} + </ul> <SeekBar video={videoElement} - previewTrack={previewTrackElement} + previewTrack={previewTrack} duration={duration} position={position} /> @@ -75,10 +121,32 @@ export function Player( const useStyles = createUseStyles({ player: { - maxWidth: "40rem", + width: "40rem", + height: "30rem", + background: "#0f0", + display: "flex", + flexDirection: "column", + justifyContent: "center", + alignItems: "center", + }, + playerCanvas: { + position: "relative", + display: "flex", + }, + video: { + maxWidth: "100%", + maxHeight: "100%", margin: { left: "auto", right: "auto", - } - } + }, + }, + subtitleCanvas: { + position: "absolute", + left: 0, + right: 0, + top: 0, + bottom: 0, + background: "rgba(1, 0, 0, 0.2)", + }, }); diff --git a/src/routes/player/subtitles/TtmlHelper.ts b/src/routes/player/subtitles/TtmlHelper.ts new file mode 100644 index 0000000..9f2d8e6 --- /dev/null +++ b/src/routes/player/subtitles/TtmlHelper.ts @@ -0,0 +1,92 @@ +import {fromXML, generateISD, renderHTML, TtmlDocument} from "../../../util/ttml/ismc"; + +interface TtmlState { + target: HTMLElement | null, + callback: number | null + cache: any, +} + +export default class TtmlHelper { + static bindToTrack(track: HTMLTrackElement, target: HTMLElement, duration: number): () => void { + console.debug(`Loading TTML track: ${track.src}`); + + const state: TtmlState = { + target: target, + callback: null, + cache: null, + } + + track.track.mode = "hidden"; + + fetch(track.src) + .then(response => response.text()) + .then(text => fromXML(text)) + .then(document => { + if (document) { + const Cue = window.VTTCue || window.TextTrackCue; + const timeEvents = document.getMediaTimeEvents(); + for (let i = 0; i < timeEvents.length; i++) { + let start = timeEvents[i]; + let end = (i + 1 === timeEvents.length) ? duration : timeEvents[i + 1]; + + let cue = new Cue(start, end, ""); + const render = () => { + TtmlHelper.renderTrack(track, document, state, start); + }; + cue.addEventListener("enter", () => { + if (state.callback) { + window.cancelAnimationFrame(state.callback); + } + state.callback = window.requestAnimationFrame(() => { + window.addEventListener("resize", render); + render(); + }); + }); + cue.addEventListener("exit", () => { + if (state.callback) { + window.cancelAnimationFrame(state.callback); + } + state.callback = window.requestAnimationFrame(() => { + window.removeEventListener("resize", render); + TtmlHelper.clearUi(state.target); + }); + }); + track.track.addCue(cue); + } + } + }); + + return () => { + this.clearUi(state.target); + if (state.callback) { + window.cancelAnimationFrame(state.callback); + } + state.target = null; + } + } + + static clearUi(target: HTMLElement | null) { + if (target) { + for (let child of Array.from(target.children)) { + target.removeChild(child); + } + } + } + + static renderTrack(track: HTMLTrackElement, document: TtmlDocument, state: TtmlState, time: number) { + TtmlHelper.clearUi(state.target); + if (track && state.target) { + state.cache = renderHTML( + generateISD(document, time), + state.target, + undefined, + undefined, + undefined, + false, + undefined, + state.cache, + undefined + ); + } + } +} diff --git a/src/routes/player/subtitles/TtmlRenderer.tsx b/src/routes/player/subtitles/TtmlRenderer.tsx new file mode 100644 index 0000000..5f5b151 --- /dev/null +++ b/src/routes/player/subtitles/TtmlRenderer.tsx @@ -0,0 +1,24 @@ +import React, {useEffect, useState} from "react"; +import TtmlHelper from "./TtmlHelper"; + +interface Props { + trackElement: HTMLTrackElement | null, + duration: number, + className?: string, +} + +export function TtmlRenderer( + {trackElement, duration, className}: Props +) { + const [subtitleCanvas, setSubtitleCanvas] = useState<HTMLElement | null>(null); + + useEffect(() => { + if (subtitleCanvas && trackElement) { + return TtmlHelper.bindToTrack(trackElement, subtitleCanvas, duration) + } + }, [subtitleCanvas, trackElement, duration]); + + return ( + <div ref={setSubtitleCanvas} className={className}/> + ) +} diff --git a/src/routes/player/video/DashVideoElement.tsx b/src/routes/player/video/DashVideoElement.tsx index 43cd84b..84a0b3b 100644 --- a/src/routes/player/video/DashVideoElement.tsx +++ b/src/routes/player/video/DashVideoElement.tsx @@ -1,5 +1,4 @@ -import React, {useEffect, useImperativeHandle, useMemo, useState} from "react"; -import {createUseStyles} from "react-jss"; +import React, {PropsWithChildren, useEffect, useImperativeHandle, useMemo, useState} from "react"; import {Media} from "../../../api/models/Media"; import dashjs, {MediaInfo} from "dashjs"; import {VideoApi} from "./VideoApi"; @@ -7,20 +6,13 @@ import {VideoApi} from "./VideoApi"; interface Props { media: Media, autoPlay: boolean, - previewSrc?: string, - previewTrackRef: React.Ref<HTMLTrackElement> + className?: string, } -interface VideoQuality { - -} - -export const DashVideoElement = React.forwardRef<VideoApi, Props>(function ( - {media, autoPlay, previewSrc, previewTrackRef}, +export const DashVideoElement = React.forwardRef<VideoApi, PropsWithChildren<Props>>(function ( + {media, autoPlay, className, children}, ref ) { - const classes = useStyles(); - const player = useMemo(() => dashjs.MediaPlayer().create(), []); const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>(null); @@ -107,23 +99,11 @@ export const DashVideoElement = React.forwardRef<VideoApi, Props>(function ( return ( <video ref={setVideoElement} - className={classes.player} + className={className} crossOrigin="anonymous" - controls + controls={false} > - {previewSrc && ( - <track ref={previewTrackRef} kind="metadata" label="previews" src={previewSrc}/> - )} + {children} </video> ); }); - -const useStyles = createUseStyles({ - player: { - maxWidth: "40rem", - margin: { - left: "auto", - right: "auto", - } - } -}); diff --git a/src/routes/player/video/RawVideoElement.tsx b/src/routes/player/video/RawVideoElement.tsx index 13a5448..4dba70e 100644 --- a/src/routes/player/video/RawVideoElement.tsx +++ b/src/routes/player/video/RawVideoElement.tsx @@ -1,5 +1,4 @@ -import React, {useImperativeHandle, useState} from "react"; -import {createUseStyles} from "react-jss"; +import React, {PropsWithChildren, useImperativeHandle, useState} from "react"; import {Media} from "../../../api/models/Media"; import {VideoApi} from "./VideoApi"; import {MediaInfo} from "dashjs"; @@ -7,15 +6,13 @@ import {MediaInfo} from "dashjs"; interface Props { media: Media, autoPlay: boolean, - previewSrc?: string, - previewTrackRef: React.Ref<HTMLTrackElement> + className?: string, } -export const RawVideoElement = React.forwardRef<VideoApi, Props>(function ( - {media, autoPlay, previewSrc, previewTrackRef}, +export const RawVideoElement = React.forwardRef<VideoApi, PropsWithChildren<Props>>(function ( + {media, autoPlay, className, children}, ref ) { - const classes = useStyles(); const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>(null); useImperativeHandle(ref, () => ({ @@ -109,21 +106,15 @@ export const RawVideoElement = React.forwardRef<VideoApi, Props>(function ( }), [videoElement]); return ( - <video ref={setVideoElement} autoPlay={autoPlay} src={media.src} className={classes.player} - crossOrigin="anonymous" controls> - {previewSrc && ( - <track ref={previewTrackRef} kind="metadata" label="previews" src={previewSrc}/> - )} + <video + ref={setVideoElement} + autoPlay={autoPlay} + src={media.src} + className={className} + crossOrigin="anonymous" + controls={false} + > + {children} </video> ); }); - -const useStyles = createUseStyles({ - player: { - maxWidth: "40rem", - margin: { - left: "auto", - right: "auto", - } - } -}); diff --git a/src/routes/player/video/VideoElement.tsx b/src/routes/player/video/VideoElement.tsx index cb2eff0..99c9c48 100644 --- a/src/routes/player/video/VideoElement.tsx +++ b/src/routes/player/video/VideoElement.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, {PropsWithChildren} from "react"; import {Media} from "../../../api/models/Media"; import {DashVideoElement} from "./DashVideoElement"; import {RawVideoElement} from "./RawVideoElement"; @@ -7,11 +7,10 @@ import {VideoApi} from "./VideoApi"; interface Props { media: Media, autoPlay: boolean, - previewSrc?: string, - previewTrackRef: React.Ref<HTMLTrackElement> + className?: string, } -export const VideoElement = React.forwardRef<VideoApi, Props>(function ( +export const VideoElement = React.forwardRef<VideoApi, PropsWithChildren<Props>>(function ( props: Props, ref ) { switch (props.media.mime) { diff --git a/src/util/ttml/doc.js b/src/util/ttml/doc.js new file mode 100644 index 0000000..774e356 --- /dev/null +++ b/src/util/ttml/doc.js @@ -0,0 +1,1097 @@ +/* + * Copyright (c) 2016, Pierre-Anthony Lemieux <pal@sandflow.com> + * 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. + */ +import sax from 'sax'; +import * as imscNames from './names'; +import * as imscStyles from './styles'; +import * as imscUtils from './utils'; +import {reportError, reportFatal, reportWarning} from "./utils"; + +export function fromXML(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) { + if (estack[0] instanceof Styling) { + /* flatten chained referential styling */ + 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) { + /* merge anonymous spans */ + if (estack[0].contents.length > 1) { + var cs = [estack[0].contents[0]]; + var c; + for (c = 1; c < estack[0].contents.length; c++) { + if (estack[0].contents[c] instanceof AnonymousSpan && + cs[cs.length - 1] instanceof AnonymousSpan) { + cs[cs.length - 1].text += estack[0].contents[c].text; + } else { + cs.push(estack[0].contents[c]); + } + } + estack[0].contents = cs; + } + // 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 AnonymousSpan) { + estack[0].text = estack[0].contents[0].text; + delete 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? + // maintain the xml:space stack + xmlspacestack.shift(); + // maintain the xml:lang stack + xmllangstack.shift(); + // prepare for the next element + estack.shift(); + }; + p.ontext = function (str) { + if (estack[0] === undefined) { + /* ignoring text outside of elements */ + } else if (estack[0] instanceof Span || estack[0] instanceof P) { + /* ignore children text nodes in ruby container spans */ + if (estack[0] instanceof Span) { + var ruby = estack[0].styleAttrs[imscStyles.byName.ruby.qname]; + if (ruby === 'container' || ruby === 'textContainer' || ruby === 'baseContainer') { + return; + } + } + /* create an anonymous span */ + var s = new AnonymousSpan(); + s.initFromText(doc, estack[0], str, xmlspacestack[0], errorHandler); + estack[0].contents.push(s); + } else if (estack[0] instanceof ForeignElement && + metadata_depth > 0 && + metadataHandler && + 'onText' in metadataHandler) { + /* text node within a child of metadata element */ + metadataHandler.onText(str); + } + }; + p.onopentag = function (node) { + // maintain the xml:space stack + var xmlspace = node.attributes["xml:space"]; + if (xmlspace) { + xmlspacestack.unshift(xmlspace.value); + } else { + if (xmlspacestack.length === 0) { + xmlspacestack.unshift("default"); + } else { + xmlspacestack.unshift(xmlspacestack[0]); + } + } + /* maintain the xml:lang stack */ + var xmllang = node.attributes["xml:lang"]; + if (xmllang) { + xmllangstack.unshift(xmllang.value); + } else { + if (xmllangstack.length === 0) { + xmllangstack.unshift(""); + } else { + xmllangstack.unshift(xmllangstack[0]); + } + } + /* process the element */ + if (node.uri === imscNames.ns_tt) { + if (node.local === 'tt') { + if (doc !== null) { + reportFatal(errorHandler, "Two <tt> elements at (" + this.line + "," + this.column + ")"); + } + doc = new TT(); + doc.initFromNode(node, errorHandler); + estack.unshift(doc); + } else if (node.local === 'head') { + if (!(estack[0] instanceof TT)) { + reportFatal(errorHandler, "Parent of <head> element is not <tt> at (" + this.line + "," + this.column + ")"); + } + estack.unshift(doc.head); + } else if (node.local === 'styling') { + if (!(estack[0] instanceof Head)) { + reportFatal(errorHandler, "Parent of <styling> element is not <head> at (" + this.line + "," + this.column + ")"); + } + estack.unshift(doc.head.styling); + } else if (node.local === 'style') { + var s; + if (estack[0] instanceof Styling) { + s = new Style(); + s.initFromNode(node, errorHandler); + /* ignore <style> element missing @id */ + if (!s.id) { + reportError(errorHandler, "<style> element missing @id attribute"); + } else { + doc.head.styling.styles[s.id] = s; + } + estack.unshift(s); + } else if (estack[0] instanceof Region) { + /* nested styles can be merged with specified styles + * immediately, with lower priority + * (see 8.4.4.2(3) at TTML1 ) + */ + s = new Style(); + s.initFromNode(node, errorHandler); + mergeStylesIfNotPresent(s.styleAttrs, estack[0].styleAttrs); + estack.unshift(s); + } else { + reportFatal(errorHandler, "Parent of <style> element is not <styling> or <region> at (" + this.line + "," + this.column + ")"); + } + } else if (node.local === 'initial') { + var ini; + if (estack[0] instanceof Styling) { + ini = new Initial(); + ini.initFromNode(node, errorHandler); + for (var qn in ini.styleAttrs) { + doc.head.styling.initials[qn] = ini.styleAttrs[qn]; + } + estack.unshift(ini); + } else { + reportFatal(errorHandler, "Parent of <initial> element is not <styling> at (" + this.line + "," + this.column + ")"); + } + } else if (node.local === 'layout') { + if (!(estack[0] instanceof Head)) { + reportFatal(errorHandler, "Parent of <layout> element is not <head> at " + this.line + "," + this.column + ")"); + } + estack.unshift(doc.head.layout); + } else if (node.local === 'region') { + if (!(estack[0] instanceof Layout)) { + reportFatal(errorHandler, "Parent of <region> element is not <layout> at " + this.line + "," + this.column + ")"); + } + var r = new Region(); + r.initFromNode(doc, node, errorHandler); + if (!r.id || r.id in doc.head.layout.regions) { + reportError(errorHandler, "Ignoring <region> with duplicate or missing @id at " + this.line + "," + this.column + ")"); + } else { + doc.head.layout.regions[r.id] = r; + } + estack.unshift(r); + } else if (node.local === 'body') { + if (!(estack[0] instanceof TT)) { + reportFatal(errorHandler, "Parent of <body> element is not <tt> at " + this.line + "," + this.column + ")"); + } + if (doc.body !== null) { + reportFatal(errorHandler, "Second <body> element at " + this.line + "," + this.column + ")"); + } + var b = new Body(); + b.initFromNode(doc, node, errorHandler); + doc.body = b; + estack.unshift(b); + } else if (node.local === 'div') { + if (!(estack[0] instanceof Div || estack[0] instanceof Body)) { + reportFatal(errorHandler, "Parent of <div> element is not <body> or <div> at " + this.line + "," + this.column + ")"); + } + var d = new Div(); + d.initFromNode(doc, estack[0], node, errorHandler); + /* transform smpte:backgroundImage to TTML2 image element */ + var bi = d.styleAttrs[imscStyles.byName.backgroundImage.qname]; + if (bi) { + d.contents.push(new Image(bi)); + delete d.styleAttrs[imscStyles.byName.backgroundImage.qname]; + } + estack[0].contents.push(d); + estack.unshift(d); + } else if (node.local === 'image') { + if (!(estack[0] instanceof Div)) { + reportFatal(errorHandler, "Parent of <image> element is not <div> at " + this.line + "," + this.column + ")"); + } + var img = new Image(); + img.initFromNode(doc, estack[0], node, errorHandler); + estack[0].contents.push(img); + estack.unshift(img); + } else if (node.local === 'p') { + if (!(estack[0] instanceof Div)) { + reportFatal(errorHandler, "Parent of <p> element is not <div> at " + this.line + "," + this.column + ")"); + } + var p = new P(); + p.initFromNode(doc, estack[0], node, errorHandler); + estack[0].contents.push(p); + estack.unshift(p); + } else if (node.local === 'span') { + if (!(estack[0] instanceof Span || estack[0] instanceof P)) { + reportFatal(errorHandler, "Parent of <span> element is not <span> or <p> at " + this.line + "," + this.column + ")"); + } + var ns = new Span(); + ns.initFromNode(doc, estack[0], node, xmlspacestack[0], errorHandler); + estack[0].contents.push(ns); + estack.unshift(ns); + } else if (node.local === 'br') { + if (!(estack[0] instanceof Span || estack[0] instanceof P)) { + reportFatal(errorHandler, "Parent of <br> element is not <span> or <p> at " + this.line + "," + this.column + ")"); + } + var nb = new Br(); + nb.initFromNode(doc, estack[0], node, errorHandler); + estack[0].contents.push(nb); + estack.unshift(nb); + } 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)) { + reportFatal(errorHandler, "Parent of <set> element is not a content element or a region at " + this.line + "," + this.column + ")"); + } + var st = new Set(); + st.initFromNode(doc, estack[0], node, errorHandler); + estack[0].sets.push(st); + estack.unshift(st); + } else { + /* element in the TT namespace, but not a content element */ + estack.unshift(new ForeignElement(node)); + } + } else { + /* 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 + ) { + /* 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); + } + } + }; + // parse the document + p.write(xmlstring).close(); + // all referential styling has been flatten, so delete styles + delete doc.head.styling.styles; + // create default region if no regions specified + var hasRegions = false; + /* AFAIK the only way to determine whether an object has members */ + for (var i in doc.head.layout.regions) { + if (doc.head.layout.regions.hasOwnProperty(i)) { + hasRegions = true; + break; + } + } + if (!hasRegions) { + /* create default region */ + var dr = Region.prototype.createDefaultRegion(); + doc.head.layout.regions[dr.id] = dr; + } + /* resolve desired timing for regions */ + for (var region_i in doc.head.layout.regions) { + resolveTiming(doc, doc.head.layout.regions[region_i], null, null); + } + /* resolve desired timing for content elements */ + if (doc.body) { + resolveTiming(doc, doc.body, null, null); + } + /* remove undefined spans in ruby containers */ + if (doc.body) { + cleanRubyContainers(doc.body); + } + return doc; +}; + +function cleanRubyContainers(element) { + if (!('contents' in element)) return; + var rubyval = 'styleAttrs' in element ? element.styleAttrs[imscStyles.byName.ruby.qname] : null; + var isrubycontainer = (element.kind === 'span' && (rubyval === "container" || rubyval === "textContainer" || rubyval === "baseContainer")); + for (var i = element.contents.length - 1; i >= 0; i--) { + if (isrubycontainer && !('styleAttrs' in element.contents[i] && imscStyles.byName.ruby.qname in element.contents[i].styleAttrs)) { + /* prune undefined <span> in ruby containers */ + delete element.contents[i]; + } else { + cleanRubyContainers(element.contents[i]); + } + } +} + +function resolveTiming(doc, element, prev_sibling, parent) { + /* are we in a seq container? */ + var isinseq = parent && parent.timeContainer === "seq"; + /* determine implicit begin */ + var implicit_begin = 0; /* default */ + if (parent) { + if (isinseq && prev_sibling) { + /* + * if seq time container, offset from the previous sibling end + */ + implicit_begin = prev_sibling.end; + } else { + implicit_begin = parent.begin; + } + } + /* compute desired begin */ + element.begin = element.explicit_begin ? element.explicit_begin + implicit_begin : implicit_begin; + /* determine implicit end */ + var implicit_end = element.begin; + var s = null; + for (var set_i in element.sets) { + resolveTiming(doc, element.sets[set_i], s, element); + if (element.timeContainer === "seq") { + implicit_end = element.sets[set_i].end; + } else { + implicit_end = Math.max(implicit_end, element.sets[set_i].end); + } + s = element.sets[set_i]; + } + if (!('contents' in element)) { + /* anonymous spans and regions and <set> and <br>s and spans with only children text nodes */ + if (isinseq) { + /* in seq container, implicit duration is zero */ + implicit_end = element.begin; + } else { + /* in par container, implicit duration is indefinite */ + implicit_end = Number.POSITIVE_INFINITY; + } + } else { + for (var content_i in element.contents) { + resolveTiming(doc, element.contents[content_i], s, element); + if (element.timeContainer === "seq") { + implicit_end = element.contents[content_i].end; + } else { + implicit_end = Math.max(implicit_end, element.contents[content_i].end); + } + s = element.contents[content_i]; + } + } + /* determine desired end */ + /* it is never made really clear in SMIL that the explicit end is offset by the implicit begin */ + if (element.explicit_end !== null && element.explicit_dur !== null) { + element.end = Math.min(element.begin + element.explicit_dur, implicit_begin + element.explicit_end); + } else if (element.explicit_end === null && element.explicit_dur !== null) { + element.end = element.begin + element.explicit_dur; + } else if (element.explicit_end !== null && element.explicit_dur === null) { + element.end = implicit_begin + element.explicit_end; + } else { + element.end = implicit_end; + } + delete element.explicit_begin; + delete element.explicit_dur; + delete element.explicit_end; + doc._registerEvent(element); +} + +function ForeignElement(node) { + this.node = node; +} + +function TT() { + this.events = []; + this.head = new Head(); + this.body = null; +} + +TT.prototype.initFromNode = function (node, errorHandler) { + /* compute cell resolution */ + var cr = extractCellResolution(node, errorHandler); + this.cellLength = { + 'h': new imscUtils.ComputedLength(0, 1 / cr.h), + 'w': new imscUtils.ComputedLength(1 / cr.w, 0) + }; + /* extract frame rate and tick rate */ + var frtr = extractFrameAndTickRate(node, errorHandler); + this.effectiveFrameRate = frtr.effectiveFrameRate; + this.tickRate = frtr.tickRate; + /* extract aspect ratio */ + this.aspectRatio = extractAspectRatio(node, errorHandler); + /* check timebase */ + var attr = findAttribute(node, imscNames.ns_ttp, "timeBase"); + if (attr !== null && attr !== "media") { + reportFatal(errorHandler, "Unsupported time base"); + } + /* retrieve extent */ + var e = extractExtent(node, errorHandler); + if (e === null) { + this.pxLength = { + 'h': null, + 'w': null + }; + } else { + if (e.h.unit !== "px" || e.w.unit !== "px") { + reportFatal(errorHandler, "Extent on TT must be in px or absent"); + } + this.pxLength = { + 'h': new imscUtils.ComputedLength(0, 1 / e.h.value), + 'w': new imscUtils.ComputedLength(1 / e.w.value, 0) + }; + } + /** set root container dimensions to (1, 1) arbitrarily + * the root container is mapped to actual dimensions at rendering + **/ + this.dimensions = { + 'h': new imscUtils.ComputedLength(0, 1), + 'w': new imscUtils.ComputedLength(1, 0) + }; +}; +/* register a temporal events */ +TT.prototype._registerEvent = function (elem) { + /* skip if begin is not < then end */ + if (elem.end <= elem.begin) + return; + /* index the begin time of the event */ + var b_i = indexOf(this.events, elem.begin); + if (!b_i.found) { + this.events.splice(b_i.index, 0, elem.begin); + } + /* index the end time of the event */ + if (elem.end !== Number.POSITIVE_INFINITY) { + var e_i = indexOf(this.events, elem.end); + if (!e_i.found) { + this.events.splice(e_i.index, 0, elem.end); + } + } +}; +/* + * Retrieves the range of ISD times covered by the document + * + * @returns {Array} Array of two elements: min_begin_time and max_begin_time + * + */ +TT.prototype.getMediaTimeRange = function () { + return [this.events[0], this.events[this.events.length - 1]]; +}; +/* + * Returns list of ISD begin times + * + * @returns {Array} + */ +TT.prototype.getMediaTimeEvents = function () { + return this.events; +}; + +/* + * Represents a TTML Head element + */ +function Head() { + this.styling = new Styling(); + this.layout = new Layout(); +} + +/* + * Represents a TTML Styling element + */ +function Styling() { + this.styles = {}; + this.initials = {}; +} + +/* + * Represents a TTML Style element + */ +function Style() { + this.id = null; + this.styleAttrs = null; + this.styleRefs = null; +} + +Style.prototype.initFromNode = function (node, errorHandler) { + this.id = elementGetXMLID(node); + this.styleAttrs = elementGetStyles(node, errorHandler); + this.styleRefs = elementGetStyleRefs(node); +}; + +/* + * Represents a TTML initial element + */ +function Initial() { + this.styleAttrs = null; +} + +Initial.prototype.initFromNode = function (node, errorHandler) { + this.styleAttrs = {}; + for (var i in node.attributes) { + if (node.attributes[i].uri === imscNames.ns_itts || + node.attributes[i].uri === imscNames.ns_ebutts || + node.attributes[i].uri === imscNames.ns_tts) { + var qname = node.attributes[i].uri + " " + node.attributes[i].local; + this.styleAttrs[qname] = node.attributes[i].value; + } + } +}; + +/* + * Represents a TTML Layout element + * + */ +function Layout() { + this.regions = {}; +} + +/* + * Represents a TTML image element + */ +function Image(src, type) { + ContentElement.call(this, 'image'); + this.src = src; + this.type = type; +} + +Image.prototype.initFromNode = function (doc, parent, node, errorHandler) { + this.src = 'src' in node.attributes ? node.attributes.src.value : null; + if (!this.src) { + reportError(errorHandler, "Invalid image@src attribute"); + } + this.type = 'type' in node.attributes ? node.attributes.type.value : null; + if (!this.type) { + reportError(errorHandler, "Invalid image@type attribute"); + } + StyledElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); + TimedElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); + AnimatedElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); + LayoutElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); +}; + +/* + * TTML element utility functions + * + */ +function ContentElement(kind) { + this.kind = kind; +} + +function IdentifiedElement(id) { + this.id = id; +} + +IdentifiedElement.prototype.initFromNode = function (doc, parent, node, errorHandler) { + this.id = elementGetXMLID(node); +}; + +function LayoutElement(id) { + this.regionID = id; +} + +LayoutElement.prototype.initFromNode = function (doc, parent, node, errorHandler) { + this.regionID = elementGetRegionID(node); +}; + +function StyledElement(styleAttrs) { + this.styleAttrs = styleAttrs; +} + +StyledElement.prototype.initFromNode = function (doc, parent, node, errorHandler) { + this.styleAttrs = elementGetStyles(node, errorHandler); + if (doc.head !== null && doc.head.styling !== null) { + mergeReferencedStyles(doc.head.styling, elementGetStyleRefs(node), this.styleAttrs, errorHandler); + } +}; + +function AnimatedElement(sets) { + this.sets = sets; +} + +AnimatedElement.prototype.initFromNode = function (doc, parent, node, errorHandler) { + this.sets = []; +}; + +function ContainerElement(contents) { + this.contents = contents; +} + +ContainerElement.prototype.initFromNode = function (doc, parent, node, errorHandler) { + this.contents = []; +}; + +function TimedElement(explicit_begin, explicit_end, explicit_dur) { + this.explicit_begin = explicit_begin; + this.explicit_end = explicit_end; + this.explicit_dur = explicit_dur; +} + +TimedElement.prototype.initFromNode = function (doc, parent, node, errorHandler) { + var t = processTiming(doc, parent, node, errorHandler); + this.explicit_begin = t.explicit_begin; + this.explicit_end = t.explicit_end; + this.explicit_dur = t.explicit_dur; + this.timeContainer = elementGetTimeContainer(node, errorHandler); +}; + +/* + * Represents a TTML body element + */ +function Body() { + ContentElement.call(this, 'body'); +} + +Body.prototype.initFromNode = function (doc, node, errorHandler) { + StyledElement.prototype.initFromNode.call(this, doc, null, node, errorHandler); + TimedElement.prototype.initFromNode.call(this, doc, null, node, errorHandler); + AnimatedElement.prototype.initFromNode.call(this, doc, null, node, errorHandler); + LayoutElement.prototype.initFromNode.call(this, doc, null, node, errorHandler); + ContainerElement.prototype.initFromNode.call(this, doc, null, node, errorHandler); +}; + +/* + * Represents a TTML div element + */ +function Div() { + ContentElement.call(this, 'div'); +} + +Div.prototype.initFromNode = function (doc, parent, node, errorHandler) { + StyledElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); + TimedElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); + AnimatedElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); + LayoutElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); + ContainerElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); +}; + +/* + * Represents a TTML p element + */ +function P() { + ContentElement.call(this, 'p'); +} + +P.prototype.initFromNode = function (doc, parent, node, errorHandler) { + StyledElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); + TimedElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); + AnimatedElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); + LayoutElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); + ContainerElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); +}; + +/* + * Represents a TTML span element + */ +function Span() { + ContentElement.call(this, 'span'); +} + +Span.prototype.initFromNode = function (doc, parent, node, xmlspace, errorHandler) { + StyledElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); + TimedElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); + AnimatedElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); + LayoutElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); + ContainerElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); + this.space = xmlspace; +}; + +/* + * Represents a TTML anonymous span element + */ +function AnonymousSpan() { + ContentElement.call(this, 'span'); +} + +AnonymousSpan.prototype.initFromText = function (doc, parent, text, xmlspace, errorHandler) { + TimedElement.prototype.initFromNode.call(this, doc, parent, null, errorHandler); + this.text = text; + this.space = xmlspace; +}; + +/* + * Represents a TTML br element + */ +function Br() { + ContentElement.call(this, 'br'); +} + +Br.prototype.initFromNode = function (doc, parent, node, errorHandler) { + LayoutElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); + TimedElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); +}; + +/* + * Represents a TTML Region element + * + */ +function Region() { +} + +Region.prototype.createDefaultRegion = function () { + var r = new Region(); + IdentifiedElement.call(r, ''); + StyledElement.call(r, {}); + AnimatedElement.call(r, []); + TimedElement.call(r, 0, Number.POSITIVE_INFINITY, null); + return r; +}; +Region.prototype.initFromNode = function (doc, node, errorHandler) { + IdentifiedElement.prototype.initFromNode.call(this, doc, null, node, errorHandler); + StyledElement.prototype.initFromNode.call(this, doc, null, node, errorHandler); + TimedElement.prototype.initFromNode.call(this, doc, null, node, errorHandler); + AnimatedElement.prototype.initFromNode.call(this, doc, null, node, errorHandler); + /* immediately merge referenced styles */ + if (doc.head !== null && doc.head.styling !== null) { + mergeReferencedStyles(doc.head.styling, elementGetStyleRefs(node), this.styleAttrs, errorHandler); + } +}; + +/* + * Represents a TTML Set element + * + */ +function Set() { +} + +Set.prototype.initFromNode = function (doc, parent, node, errorHandler) { + TimedElement.prototype.initFromNode.call(this, doc, parent, node, errorHandler); + var styles = elementGetStyles(node, errorHandler); + this.qname = null; + this.value = null; + for (var qname in styles) { + if (this.qname) { + reportError(errorHandler, "More than one style specified on set"); + break; + } + this.qname = qname; + this.value = styles[qname]; + } +}; + +/* + * Utility functions + * + */ +function elementGetXMLID(node) { + return node && 'xml:id' in node.attributes ? node.attributes['xml:id'].value || null : null; +} + +function elementGetRegionID(node) { + return node && 'region' in node.attributes ? node.attributes.region.value : ''; +} + +function elementGetTimeContainer(node, errorHandler) { + var tc = node && 'timeContainer' in node.attributes ? node.attributes.timeContainer.value : null; + if ((!tc) || tc === "par") { + return "par"; + } else if (tc === "seq") { + return "seq"; + } else { + reportError(errorHandler, "Illegal value of timeContainer (assuming 'par')"); + return "par"; + } +} + +function elementGetStyleRefs(node) { + return node && 'style' in node.attributes ? node.attributes.style.value.split(" ") : []; +} + +function elementGetStyles(node, errorHandler) { + var s = {}; + if (node !== null) { + for (var i in node.attributes) { + var qname = node.attributes[i].uri + " " + node.attributes[i].local; + var sa = imscStyles.byQName[qname]; + if (sa !== undefined) { + var val = sa.parse(node.attributes[i].value); + if (val !== null) { + 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"); + } + } else { + reportError(errorHandler, "Cannot parse styling attribute " + qname + " --> " + node.attributes[i].value); + } + } + } + } + return s; +} + +function findAttribute(node, ns, name) { + for (var i in node.attributes) { + if (node.attributes[i].uri === ns && + node.attributes[i].local === name) { + return node.attributes[i].value; + } + } + return null; +} + +function extractAspectRatio(node, errorHandler) { + var ar = findAttribute(node, imscNames.ns_ittp, "aspectRatio"); + if (ar === null) { + ar = findAttribute(node, imscNames.ns_ttp, "displayAspectRatio"); + } + var rslt = null; + if (ar !== null) { + var ASPECT_RATIO_RE = /(\d+)\s+(\d+)/; + var m = ASPECT_RATIO_RE.exec(ar); + if (m !== null) { + var w = parseInt(m[1]); + var h = parseInt(m[2]); + if (w !== 0 && h !== 0) { + rslt = w / h; + } else { + reportError(errorHandler, "Illegal aspectRatio values (ignoring)"); + } + } else { + reportError(errorHandler, "Malformed aspectRatio attribute (ignoring)"); + } + } + return rslt; +} + +/* + * Returns the cellResolution attribute from a node + * + */ +function extractCellResolution(node, errorHandler) { + var cr = findAttribute(node, imscNames.ns_ttp, "cellResolution"); + // initial value + var h = 15; + var w = 32; + if (cr !== null) { + var CELL_RESOLUTION_RE = /(\d+) (\d+)/; + var m = CELL_RESOLUTION_RE.exec(cr); + if (m !== null) { + w = parseInt(m[1]); + h = parseInt(m[2]); + } else { + reportWarning(errorHandler, "Malformed cellResolution value (using initial value instead)"); + } + } + return {'w': w, 'h': h}; +} + +function extractFrameAndTickRate(node, errorHandler) { + // subFrameRate is ignored per IMSC1 specification + // extract frame rate + var fps_attr = findAttribute(node, imscNames.ns_ttp, "frameRate"); + // initial value + var fps = 30; + // match variable + var m; + if (fps_attr !== null) { + var FRAME_RATE_RE = /(\d+)/; + m = FRAME_RATE_RE.exec(fps_attr); + if (m !== null) { + fps = parseInt(m[1]); + } else { + reportWarning(errorHandler, "Malformed frame rate attribute (using initial value instead)"); + } + } + // extract frame rate multiplier + var frm_attr = findAttribute(node, imscNames.ns_ttp, "frameRateMultiplier"); + // initial value + var frm = 1; + if (frm_attr !== null) { + var FRAME_RATE_MULT_RE = /(\d+) (\d+)/; + m = FRAME_RATE_MULT_RE.exec(frm_attr); + if (m !== null) { + frm = parseInt(m[1]) / parseInt(m[2]); + } else { + reportWarning(errorHandler, "Malformed frame rate multiplier attribute (using initial value instead)"); + } + } + var efps = frm * fps; + // extract tick rate + var tr = 1; + var trattr = findAttribute(node, imscNames.ns_ttp, "tickRate"); + if (trattr === null) { + if (fps_attr !== null) + tr = efps; + } else { + var TICK_RATE_RE = /(\d+)/; + m = TICK_RATE_RE.exec(trattr); + if (m !== null) { + tr = parseInt(m[1]); + } else { + reportWarning(errorHandler, "Malformed tick rate attribute (using initial value instead)"); + } + } + return {effectiveFrameRate: efps, tickRate: tr}; +} + +function extractExtent(node, errorHandler) { + var attr = findAttribute(node, imscNames.ns_tts, "extent"); + if (attr === null) + return null; + var s = attr.split(" "); + if (s.length !== 2) { + reportWarning(errorHandler, "Malformed extent (ignoring)"); + return null; + } + var w = imscUtils.parseLength(s[0]); + var h = imscUtils.parseLength(s[1]); + if (!h || !w) { + reportWarning(errorHandler, "Malformed extent values (ignoring)"); + return null; + } + return {'h': h, 'w': w}; +} + +function parseTimeExpression(tickRate, effectiveFrameRate, str) { + var CLOCK_TIME_FRACTION_RE = /^(\d{2,}):(\d\d):(\d\d(?:\.\d+)?)$/; + var CLOCK_TIME_FRAMES_RE = /^(\d{2,}):(\d\d):(\d\d):(\d{2,})$/; + var OFFSET_FRAME_RE = /^(\d+(?:\.\d+)?)f$/; + var OFFSET_TICK_RE = /^(\d+(?:\.\d+)?)t$/; + var OFFSET_MS_RE = /^(\d+(?:\.\d+)?)ms$/; + var OFFSET_S_RE = /^(\d+(?:\.\d+)?)s$/; + var OFFSET_H_RE = /^(\d+(?:\.\d+)?)h$/; + var OFFSET_M_RE = /^(\d+(?:\.\d+)?)m$/; + var m; + var r = null; + if ((m = OFFSET_FRAME_RE.exec(str)) !== null) { + if (effectiveFrameRate !== null) { + r = parseFloat(m[1]) / effectiveFrameRate; + } + } else if ((m = OFFSET_TICK_RE.exec(str)) !== null) { + if (tickRate !== null) { + r = parseFloat(m[1]) / tickRate; + } + } else if ((m = OFFSET_MS_RE.exec(str)) !== null) { + r = parseFloat(m[1]) / 1000.0; + } else if ((m = OFFSET_S_RE.exec(str)) !== null) { + r = parseFloat(m[1]); + } else if ((m = OFFSET_H_RE.exec(str)) !== null) { + r = parseFloat(m[1]) * 3600.0; + } else if ((m = OFFSET_M_RE.exec(str)) !== null) { + r = parseFloat(m[1]) * 60.0; + } else if ((m = CLOCK_TIME_FRACTION_RE.exec(str)) !== null) { + r = parseInt(m[1]) * 3600 + + parseInt(m[2]) * 60 + + parseFloat(m[3]); + } else if ((m = CLOCK_TIME_FRAMES_RE.exec(str)) !== null) { + /* this assumes that HH:MM:SS is a clock-time-with-fraction */ + if (effectiveFrameRate !== null) { + r = parseInt(m[1]) * 3600 + + parseInt(m[2]) * 60 + + parseInt(m[3]) + + (m[4] === null ? 0 : parseInt(m[4]) / effectiveFrameRate); + } + } + return r; +} + +function processTiming(doc, parent, node, errorHandler) { + /* determine explicit begin */ + var explicit_begin = null; + if (node && 'begin' in node.attributes) { + explicit_begin = parseTimeExpression(doc.tickRate, doc.effectiveFrameRate, node.attributes.begin.value); + if (explicit_begin === null) { + reportWarning(errorHandler, "Malformed begin value " + node.attributes.begin.value + " (using 0)"); + } + } + /* determine explicit duration */ + var explicit_dur = null; + if (node && 'dur' in node.attributes) { + explicit_dur = parseTimeExpression(doc.tickRate, doc.effectiveFrameRate, node.attributes.dur.value); + if (explicit_dur === null) { + reportWarning(errorHandler, "Malformed dur value " + node.attributes.dur.value + " (ignoring)"); + } + } + /* determine explicit end */ + var explicit_end = null; + if (node && 'end' in node.attributes) { + explicit_end = parseTimeExpression(doc.tickRate, doc.effectiveFrameRate, node.attributes.end.value); + if (explicit_end === null) { + reportWarning(errorHandler, "Malformed end value (ignoring)"); + } + } + return { + explicit_begin: explicit_begin, + explicit_end: explicit_end, + explicit_dur: explicit_dur + }; +} + +function mergeChainedStyles(styling, style, errorHandler) { + while (style.styleRefs.length > 0) { + var sref = style.styleRefs.pop(); + if (!(sref in styling.styles)) { + reportError(errorHandler, "Non-existant style id referenced"); + continue; + } + mergeChainedStyles(styling, styling.styles[sref], errorHandler); + mergeStylesIfNotPresent(styling.styles[sref].styleAttrs, style.styleAttrs); + } +} + +function mergeReferencedStyles(styling, stylerefs, styleattrs, errorHandler) { + for (var i = stylerefs.length - 1; i >= 0; i--) { + var sref = stylerefs[i]; + if (!(sref in styling.styles)) { + reportError(errorHandler, "Non-existant style id referenced"); + continue; + } + mergeStylesIfNotPresent(styling.styles[sref].styleAttrs, styleattrs); + } +} + +function mergeStylesIfNotPresent(from_styles, into_styles) { + for (var sname in from_styles) { + if (sname in into_styles) + continue; + into_styles[sname] = from_styles[sname]; + } +} + +/* 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 + * + * @returns {BinarySearchResult} + */ +function indexOf(arr, searchval) { + var min = 0; + var max = arr.length - 1; + var cur; + while (min <= max) { + cur = Math.floor((min + max) / 2); + var curval = arr[cur]; + if (curval < searchval) { + min = cur + 1; + } else if (curval > searchval) { + max = cur - 1; + } else { + return {found: true, index: cur}; + } + } + return {found: false, index: min}; +} diff --git a/src/util/ttml/html.js b/src/util/ttml/html.js new file mode 100644 index 0000000..fa42a24 --- /dev/null +++ b/src/util/ttml/html.js @@ -0,0 +1,1008 @@ +/* + * Copyright (c) 2016, Pierre-Anthony Lemieux <pal@sandflow.com> + * 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. + */ +import * as imscStyles from './styles'; + +export function render(isd, + element, + imgResolver, + eheight, + ewidth, + displayForcedOnlyMode, + errorHandler, + previousISDState, + enableRollUp +) { + /* maintain aspect ratio if specified */ + let height = eheight || element.clientHeight; + let width = ewidth || element.clientWidth; + if (isd.aspectRatio !== null) { + let twidth = height * isd.aspectRatio; + if (twidth > width) { + height = Math.round(width / isd.aspectRatio); + } else { + width = twidth; + } + } + let rootcontainer = document.createElement("div"); + rootcontainer.style.position = "relative"; + rootcontainer.style.width = width + "px"; + rootcontainer.style.height = height + "px"; + rootcontainer.style.margin = "auto"; + rootcontainer.style.top = 0; + rootcontainer.style.bottom = 0; + rootcontainer.style.left = 0; + rootcontainer.style.right = 0; + rootcontainer.style.zIndex = 0; + let context = { + h: height, + w: width, + regionH: null, + regionW: null, + imgResolver: imgResolver, + displayForcedOnlyMode: displayForcedOnlyMode || false, + isd: isd, + errorHandler: errorHandler, + previousISDState: previousISDState, + enableRollUp: enableRollUp || false, + currentISDState: {}, + flg: null, /* current fillLineGap value if active, null otherwise */ + lp: null, /* current linePadding value if active, null otherwise */ + mra: null, /* current multiRowAlign value if active, null otherwise */ + ipd: null, /* inline progression direction (lr, rl, tb) */ + bpd: null, /* block progression direction (lr, rl, tb) */ + ruby: null, /* is ruby present in a <p> */ + textEmphasis: null, /* is textEmphasis present in a <p> */ + rubyReserve: null /* is rubyReserve applicable to a <p> */ + }; + element.appendChild(rootcontainer); + if (isd.contents) { + for (let content of isd.contents) { + processElement(context, rootcontainer, content); + } + } + return context.currentISDState; +} + +function processElement(context, dom_parent, isd_element) { + let e; + if (isd_element.kind === 'region') { + e = document.createElement("div"); + e.style.position = "absolute"; + } else if (isd_element.kind === 'body') { + e = document.createElement("div"); + } else if (isd_element.kind === 'div') { + e = document.createElement("div"); + } else if (isd_element.kind === 'image') { + e = document.createElement("img"); + if (context.imgResolver !== null && isd_element.src !== null) { + let uri = context.imgResolver(isd_element.src, e); + if (uri) + e.src = uri; + e.height = context.regionH; + e.width = context.regionW; + } + } else if (isd_element.kind === 'p') { + e = document.createElement("p"); + } else if (isd_element.kind === 'span') { + if (isd_element.styleAttrs[imscStyles.byName.ruby.qname] === "container") { + e = document.createElement("ruby"); + context.ruby = true; + } else if (isd_element.styleAttrs[imscStyles.byName.ruby.qname] === "base") { + e = document.createElement("rb"); + } else if (isd_element.styleAttrs[imscStyles.byName.ruby.qname] === "text") { + e = document.createElement("rt"); + } else if (isd_element.styleAttrs[imscStyles.byName.ruby.qname] === "baseContainer") { + e = document.createElement("rbc"); + } else if (isd_element.styleAttrs[imscStyles.byName.ruby.qname] === "textContainer") { + e = document.createElement("rtc"); + } else if (isd_element.styleAttrs[imscStyles.byName.ruby.qname] === "delimiter") { + /* ignore rp */ + return; + } else { + e = document.createElement("span"); + } + let te = isd_element.styleAttrs[imscStyles.byName.textEmphasis.qname]; + if (te && te.style !== "none") { + context.textEmphasis = true; + } + //e.textContent = isd_element.text; + } else if (isd_element.kind === 'br') { + e = document.createElement("br"); + } + if (!e) { + reportError(context.errorHandler, "Error processing ISD element kind: " + isd_element.kind); + return; + } + /* add to parent */ + dom_parent.appendChild(e); + /* override UA default margin */ + /* TODO: should apply to <p> only */ + e.style.margin = "0"; + /* tranform TTML styles to CSS styles */ + for (let i in STYLING_MAP_DEFS) { + let sm = STYLING_MAP_DEFS[i]; + let attr = isd_element.styleAttrs[sm.qname]; + if (attr !== undefined && sm.map !== null) { + sm.map(context, e, isd_element, attr); + } + } + let proc_e = e; + /* remember writing direction */ + if (isd_element.kind === "region") { + let wdir = isd_element.styleAttrs[imscStyles.byName.writingMode.qname]; + if (wdir === "lrtb" || wdir === "lr") { + context.ipd = "lr"; + context.bpd = "tb"; + } else if (wdir === "rltb" || wdir === "rl") { + context.ipd = "rl"; + context.bpd = "tb"; + } else if (wdir === "tblr") { + context.ipd = "tb"; + context.bpd = "lr"; + } else if (wdir === "tbrl" || wdir === "tb") { + context.ipd = "tb"; + context.bpd = "rl"; + } + } + /* do we have linePadding ? */ + let lp = isd_element.styleAttrs[imscStyles.byName.linePadding.qname]; + if (lp && (!lp.isZero())) { + let plength = lp.toUsedLength(context.w, context.h); + if (plength > 0) { + /* apply padding to the <p> so that line padding does not cause line wraps */ + let padmeasure = Math.ceil(plength) + "px"; + if (context.bpd === "tb") { + proc_e.style.paddingLeft = padmeasure; + proc_e.style.paddingRight = padmeasure; + } else { + proc_e.style.paddingTop = padmeasure; + proc_e.style.paddingBottom = padmeasure; + } + context.lp = lp; + } + } + // do we have multiRowAlign? + let mra = isd_element.styleAttrs[imscStyles.byName.multiRowAlign.qname]; + if (mra && mra !== "auto") { + /* create inline block to handle multirowAlign */ + let s = document.createElement("span"); + s.style.display = "inline-block"; + s.style.textAlign = mra; + e.appendChild(s); + proc_e = s; + context.mra = mra; + } + /* do we have rubyReserve? */ + let rr = isd_element.styleAttrs[imscStyles.byName.rubyReserve.qname]; + if (rr && rr[0] !== "none") { + context.rubyReserve = rr; + } + /* remember we are filling line gaps */ + if (isd_element.styleAttrs[imscStyles.byName.fillLineGap.qname]) { + context.flg = true; + } + if (isd_element.kind === "span" && isd_element.text) { + if (imscStyles.byName.textCombine.qname in isd_element.styleAttrs && + isd_element.styleAttrs[imscStyles.byName.textCombine.qname][0] === "all") { + /* ignore tate-chu-yoku since line break cannot happen within */ + e.textContent = isd_element.text; + } else { + // wrap characters in spans to find the line wrap locations + let cbuf = ''; + for (let j = 0; j < isd_element.text.length; j++) { + cbuf += isd_element.text.charAt(j); + let cc = isd_element.text.charCodeAt(j); + if (cc < 0xD800 || cc > 0xDBFF || j === isd_element.text.length) { + /* wrap the character(s) in a span unless it is a high surrogate */ + let span = document.createElement("span"); + span.textContent = cbuf; + e.appendChild(span); + cbuf = ''; + } + } + } + } + /* process the children of the ISD element */ + if (isd_element.contents) { + for (let content of isd_element.contents) { + processElement(context, proc_e, content); + } + } + /* list of lines */ + let linelist = []; + /* paragraph processing */ + /* TODO: linePadding only supported for horizontal scripts */ + if ((context.lp || context.mra || context.flg || context.ruby || context.textEmphasis || context.rubyReserve) && + isd_element.kind === "p") { + constructLineList(context, proc_e, linelist, null); + /* apply rubyReserve */ + if (context.rubyReserve) { + applyRubyReserve(linelist, context); + context.rubyReserve = null; + } + /* apply tts:rubyPosition="outside" */ + if (context.ruby || context.rubyReserve) { + applyRubyPosition(linelist, context); + context.ruby = null; + } + /* apply text emphasis "outside" position */ + if (context.textEmphasis) { + applyTextEmphasis(linelist, context); + context.textEmphasis = null; + } + /* insert line breaks for multirowalign */ + if (context.mra) { + applyMultiRowAlign(linelist); + context.mra = null; + } + /* add linepadding */ + if (context.lp) { + applyLinePadding(linelist, context.lp.toUsedLength(context.w, context.h), context); + context.lp = null; + } + /* fill line gaps linepadding */ + if (context.flg) { + let par_edges = rect2edges(proc_e.getBoundingClientRect(), context); + applyFillLineGap(linelist, par_edges.before, par_edges.after, context); + context.flg = null; + } + } + /* region processing */ + if (isd_element.kind === "region") { + /* build line list */ + constructLineList(context, proc_e, linelist); + /* perform roll up if needed */ + if ((context.bpd === "tb") && + context.enableRollUp && + isd_element.contents.length > 0 && + isd_element.styleAttrs[imscStyles.byName.displayAlign.qname] === 'after') { + /* horrible hack, perhaps default region id should be underscore everywhere? */ + let rid = isd_element.id === '' ? '_' : isd_element.id; + let rb = new RegionPBuffer(rid, linelist); + context.currentISDState[rb.id] = rb; + if (context.previousISDState && + rb.id in context.previousISDState && + context.previousISDState[rb.id].plist.length > 0 && + rb.plist.length > 1 && + rb.plist[rb.plist.length - 2].text === + context.previousISDState[rb.id].plist[context.previousISDState[rb.id].plist.length - 1].text) { + let body_elem = e.firstElementChild; + let h = rb.plist[rb.plist.length - 1].after - rb.plist[rb.plist.length - 1].before; + body_elem.style.bottom = "-" + h + "px"; + body_elem.style.transition = "transform 0.4s"; + body_elem.style.position = "relative"; + body_elem.style.transform = "translateY(-" + h + "px)"; + } + } + /* TODO: clean-up the spans ? */ + } +} + +function applyLinePadding(lineList, lp, context) { + if (lineList) { + for (let line of lineList) { + let l = line.elements.length; + let se = line.elements[line.start_elem]; + let ee = line.elements[line.end_elem]; + let pospadpxlen = Math.ceil(lp) + "px"; + let negpadpxlen = "-" + Math.ceil(lp) + "px"; + if (l !== 0) { + if (context.ipd === "lr") { + se.node.style.borderLeftColor = se.bgcolor || "#00000000"; + se.node.style.borderLeftStyle = "solid"; + se.node.style.borderLeftWidth = pospadpxlen; + se.node.style.marginLeft = negpadpxlen; + } else if (context.ipd === "rl") { + se.node.style.borderRightColor = se.bgcolor || "#00000000"; + se.node.style.borderRightStyle = "solid"; + se.node.style.borderRightWidth = pospadpxlen; + se.node.style.marginRight = negpadpxlen; + } else if (context.ipd === "tb") { + se.node.style.borderTopColor = se.bgcolor || "#00000000"; + se.node.style.borderTopStyle = "solid"; + se.node.style.borderTopWidth = pospadpxlen; + se.node.style.marginTop = negpadpxlen; + } + if (context.ipd === "lr") { + ee.node.style.borderRightColor = ee.bgcolor || "#00000000"; + ee.node.style.borderRightStyle = "solid"; + ee.node.style.borderRightWidth = pospadpxlen; + ee.node.style.marginRight = negpadpxlen; + } else if (context.ipd === "rl") { + ee.node.style.borderLeftColor = ee.bgcolor || "#00000000"; + ee.node.style.borderLeftStyle = "solid"; + ee.node.style.borderLeftWidth = pospadpxlen; + ee.node.style.marginLeft = negpadpxlen; + } else if (context.ipd === "tb") { + ee.node.style.borderBottomColor = ee.bgcolor || "#00000000"; + ee.node.style.borderBottomStyle = "solid"; + ee.node.style.borderBottomWidth = pospadpxlen; + ee.node.style.marginBottom = negpadpxlen; + } + } + } + } +} + +function applyMultiRowAlign(lineList) { + /* apply an explicit br to all but the last line */ + for (let i = 0; i < lineList.length - 1; i++) { + let l = lineList[i].elements.length; + if (l !== 0 && lineList[i].br === false) { + let br = document.createElement("br"); + let lastnode = lineList[i].elements[l - 1].node; + lastnode.parentElement.insertBefore(br, lastnode.nextSibling); + } + } +} + +function applyTextEmphasis(lineList, context) { + /* supports "outside" only */ + for (let i = 0; i < lineList.length; i++) { + for (let j = 0; j < lineList[i].te.length; j++) { + /* skip if position already set */ + if (lineList[i].te[j].style.textEmphasisPosition && + lineList[i].te[j].style.textEmphasisPosition !== "none") + continue; + let pos; + if (context.bpd === "tb") { + pos = (i === 0) ? "left over" : "left under"; + } else { + if (context.bpd === "rl") { + pos = (i === 0) ? "right under" : "left under"; + } else { + pos = (i === 0) ? "left under" : "right under"; + } + } + lineList[i].te[j].style.textEmphasisPosition = pos; + } + } +} + +function applyRubyPosition(lineList, context) { + for (let i = 0; i < lineList.length; i++) { + for (let j = 0; j < lineList[i].rbc.length; j++) { + /* skip if ruby-position already set */ + if (lineList[i].rbc[j].style.rubyPosition) + continue; + let pos; + if (context.bpd === "tb") { + pos = (i === 0) ? "over" : "under"; + } else { + if (context.bpd === "rl") { + pos = (i === 0) ? "over" : "under"; + } else { + pos = (i === 0) ? "under" : "over"; + } + } + lineList[i].rbc[j].style.rubyPosition = pos; + } + } +} + +function applyRubyReserve(lineList, context) { + for (let i = 0; i < lineList.length; i++) { + let ruby = document.createElement("ruby"); + let rb = document.createElement("rb"); + rb.textContent = "\u200B"; + ruby.appendChild(rb); + let rt1; + let rt2; + let fs = context.rubyReserve[1].toUsedLength(context.w, context.h) + "px"; + if (context.rubyReserve[0] === "both") { + rt1 = document.createElement("rtc"); + rt1.style.rubyPosition = "under"; + rt1.textContent = "\u200B"; + rt1.style.fontSize = fs; + rt2 = document.createElement("rtc"); + rt2.style.rubyPosition = "over"; + rt2.textContent = "\u200B"; + rt2.style.fontSize = fs; + ruby.appendChild(rt1); + ruby.appendChild(rt2); + } else { + rt1 = document.createElement("rtc"); + rt1.textContent = "\u200B"; + rt1.style.fontSize = fs; + if (context.rubyReserve[0] === "after" || (context.rubyReserve[0] === "outside" && i > 0)) { + rt1.style.rubyPosition = (context.bpd === "tb" || context.bpd === "rl") ? "under" : "over"; + } else { + rt1.style.rubyPosition = (context.bpd === "tb" || context.bpd === "rl") ? "over" : "under"; + } + ruby.appendChild(rt1); + } + lineList[i].elements[0].node.parentElement.insertBefore( + ruby, + lineList[i].elements[0].node + ); + } +} + +function applyFillLineGap(lineList, par_before, par_after, context) { + /* positive for BPD = lr and tb, negative for BPD = rl */ + let s = Math.sign(par_after - par_before); + for (let i = 0; i <= lineList.length; i++) { + /* compute frontier between lines */ + let frontier; + if (i === 0) { + frontier = par_before; + } else if (i === lineList.length) { + frontier = par_after; + } else { + frontier = (lineList[i].before + lineList[i - 1].after) / 2; + } + /* padding amount */ + let pad; + /* current element */ + let e; + /* before line */ + if (i > 0) { + for (let j = 0; j < lineList[i - 1].elements.length; j++) { + if (lineList[i - 1].elements[j].bgcolor === null) + continue; + e = lineList[i - 1].elements[j]; + if (s * (e.after - frontier) < 0) { + pad = Math.ceil(Math.abs(frontier - e.after)) + "px"; + e.node.style.backgroundColor = e.bgcolor; + if (context.bpd === "lr") { + e.node.style.paddingRight = pad; + } else if (context.bpd === "rl") { + e.node.style.paddingLeft = pad; + } else if (context.bpd === "tb") { + e.node.style.paddingBottom = pad; + } + } + } + } + /* after line */ + if (i < lineList.length) { + for (let k = 0; k < lineList[i].elements.length; k++) { + e = lineList[i].elements[k]; + if (e.bgcolor === null) + continue; + if (s * (e.before - frontier) > 0) { + pad = Math.ceil(Math.abs(e.before - frontier)) + "px"; + e.node.style.backgroundColor = e.bgcolor; + if (context.bpd === "lr") { + e.node.style.paddingLeft = pad; + } else if (context.bpd === "rl") { + e.node.style.paddingRight = pad; + } else if (context.bpd === "tb") { + e.node.style.paddingTop = pad; + } + } + } + } + } +} + +function RegionPBuffer(id, lineList) { + this.id = id; + this.plist = lineList; +} + +function rect2edges(rect, context) { + let edges = {before: null, after: null, start: null, end: null}; + if (context.bpd === "tb") { + edges.before = rect.top; + edges.after = rect.bottom; + if (context.ipd === "lr") { + edges.start = rect.left; + edges.end = rect.right; + } else { + edges.start = rect.right; + edges.end = rect.left; + } + } else if (context.bpd === "lr") { + edges.before = rect.left; + edges.after = rect.right; + edges.start = rect.top; + edges.end = rect.bottom; + } else if (context.bpd === "rl") { + edges.before = rect.right; + edges.after = rect.left; + edges.start = rect.top; + edges.end = rect.bottom; + } + return edges; +} + +function constructLineList(context, element, llist, bgcolor) { + if (element.localName === "rt" || element.localName === "rtc") { + /* skip ruby annotations */ + return; + } + let curbgcolor = element.style.backgroundColor || bgcolor; + if (element.childElementCount === 0) { + if (element.localName === 'span' || element.localName === 'rb') { + let r = element.getBoundingClientRect(); + /* skip if span is not displayed */ + if (r.height === 0 || r.width === 0) + return; + let edges = rect2edges(r, context); + if (llist.length === 0 || + (!isSameLine(edges.before, edges.after, llist[llist.length - 1].before, llist[llist.length - 1].after)) + ) { + llist.push({ + before: edges.before, + after: edges.after, + start: edges.start, + end: edges.end, + start_elem: 0, + end_elem: 0, + elements: [], + rbc: [], + te: [], + text: "", + br: false + }); + } else { + /* positive for BPD = lr and tb, negative for BPD = rl */ + let bpd_dir = Math.sign(edges.after - edges.before); + /* positive for IPD = lr and tb, negative for IPD = rl */ + let ipd_dir = Math.sign(edges.end - edges.start); + /* check if the line height has increased */ + if (bpd_dir * (edges.before - llist[llist.length - 1].before) < 0) { + llist[llist.length - 1].before = edges.before; + } + if (bpd_dir * (edges.after - llist[llist.length - 1].after) > 0) { + llist[llist.length - 1].after = edges.after; + } + if (ipd_dir * (edges.start - llist[llist.length - 1].start) < 0) { + llist[llist.length - 1].start = edges.start; + llist[llist.length - 1].start_elem = llist[llist.length - 1].elements.length; + } + if (ipd_dir * (edges.end - llist[llist.length - 1].end) > 0) { + llist[llist.length - 1].end = edges.end; + llist[llist.length - 1].end_elem = llist[llist.length - 1].elements.length; + } + } + llist[llist.length - 1].text += element.textContent; + llist[llist.length - 1].elements.push( + { + node: element, + bgcolor: curbgcolor, + before: edges.before, + after: edges.after + } + ); + } else if (element.localName === 'br' && llist.length !== 0) { + llist[llist.length - 1].br = true; + } + } else { + let child = element.firstChild; + while (child) { + if (child.nodeType === Node.ELEMENT_NODE) { + constructLineList(context, child, llist, curbgcolor); + if (child.localName === 'ruby' || child.localName === 'rtc') { + /* remember non-empty ruby and rtc elements so that tts:rubyPosition can be applied */ + if (llist.length > 0) { + llist[llist.length - 1].rbc.push(child); + } + } else if (child.localName === 'span' && + child.style.textEmphasisStyle && + child.style.textEmphasisStyle !== "none") { + /* remember non-empty span elements with textEmphasis */ + if (llist.length > 0) { + llist[llist.length - 1].te.push(child); + } + } + } + child = child.nextSibling; + } + } +} + +function isSameLine(before1, after1, before2, after2) { + return ((after1 < after2) && (before1 > before2)) || ((after2 <= after1) && (before2 >= before1)); +} + +function HTMLStylingMapDefintion(qName, mapFunc) { + this.qname = qName; + this.map = mapFunc; +} + +let STYLING_MAP_DEFS = [ + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling backgroundColor", + function (context, dom_element, isd_element, attr) { + /* skip if transparent */ + if (attr[3] === 0) + return; + dom_element.style.backgroundColor = "rgba(" + + attr[0].toString() + "," + + attr[1].toString() + "," + + attr[2].toString() + "," + + (attr[3] / 255).toString() + + ")"; + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling color", + function (context, dom_element, isd_element, attr) { + dom_element.style.color = "rgba(" + + attr[0].toString() + "," + + attr[1].toString() + "," + + attr[2].toString() + "," + + (attr[3] / 255).toString() + + ")"; + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling direction", + function (context, dom_element, isd_element, attr) { + dom_element.style.direction = attr; + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling display", + function (context, dom_element, isd_element, attr) { + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling displayAlign", + function (context, dom_element, isd_element, attr) { + /* see https://css-tricks.com/snippets/css/a-guide-to-flexbox/ */ + /* TODO: is this affected by writing direction? */ + dom_element.style.display = "flex"; + dom_element.style.flexDirection = "column"; + if (attr === "before") { + dom_element.style.justifyContent = "flex-start"; + } else if (attr === "center") { + dom_element.style.justifyContent = "center"; + } else if (attr === "after") { + dom_element.style.justifyContent = "flex-end"; + } + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling extent", + function (context, dom_element, isd_element, attr) { + /* TODO: this is super ugly */ + context.regionH = attr.h.toUsedLength(context.w, context.h); + context.regionW = attr.w.toUsedLength(context.w, context.h); + /* + * CSS height/width are measured against the content rectangle, + * whereas TTML height/width include padding + */ + let hdelta = 0; + let wdelta = 0; + let p = isd_element.styleAttrs["http://www.w3.org/ns/ttml#styling padding"]; + if (!p) { + /* error */ + } else { + hdelta = p[0].toUsedLength(context.w, context.h) + p[2].toUsedLength(context.w, context.h); + wdelta = p[1].toUsedLength(context.w, context.h) + p[3].toUsedLength(context.w, context.h); + } + dom_element.style.height = (context.regionH - hdelta) + "px"; + dom_element.style.width = (context.regionW - wdelta) + "px"; + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling fontFamily", + function (context, dom_element, isd_element, attr) { + /* per IMSC1 */ + for (let attribute of attr) { + // monospaceSerif + // proportionalSansSerif + // monospace + // sansSerif + // serif + // monospaceSansSerif + // proportionalSerif + dom_element.classList.add("ttml-" + attribute); + } + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling shear", + function (context, dom_element, isd_element, attr) { + /* return immediately if tts:shear is 0% since CSS transforms are not inherited*/ + if (attr === 0) + return; + let angle = attr * -0.9; + /* context.writingMode is needed since writing mode is not inherited and sets the inline progression */ + if (context.bpd === "tb") { + dom_element.style.transform = "skewX(" + angle + "deg)"; + } else { + dom_element.style.transform = "skewY(" + angle + "deg)"; + } + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling fontSize", + function (context, dom_element, isd_element, attr) { + dom_element.style.fontSize = attr.toUsedLength(context.w, context.h) + "px"; + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling fontStyle", + function (context, dom_element, isd_element, attr) { + dom_element.style.fontStyle = attr; + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling fontWeight", + function (context, dom_element, isd_element, attr) { + dom_element.style.fontWeight = attr; + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling lineHeight", + function (context, dom_element, isd_element, attr) { + if (attr === "normal") { + dom_element.style.lineHeight = "normal"; + } else { + dom_element.style.lineHeight = attr.toUsedLength(context.w, context.h) + "px"; + } + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling opacity", + function (context, dom_element, isd_element, attr) { + dom_element.style.opacity = attr; + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling origin", + function (context, dom_element, isd_element, attr) { + dom_element.style.top = attr.h.toUsedLength(context.w, context.h) + "px"; + dom_element.style.left = attr.w.toUsedLength(context.w, context.h) + "px"; + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling overflow", + function (context, dom_element, isd_element, attr) { + dom_element.style.overflow = attr; + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling padding", + function (context, dom_element, isd_element, attr) { + /* attr: top,left,bottom,right*/ + /* style: top right bottom left*/ + let rslt = []; + rslt[0] = attr[0].toUsedLength(context.w, context.h) + "px"; + rslt[1] = attr[3].toUsedLength(context.w, context.h) + "px"; + rslt[2] = attr[2].toUsedLength(context.w, context.h) + "px"; + rslt[3] = attr[1].toUsedLength(context.w, context.h) + "px"; + dom_element.style.padding = rslt.join(" "); + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling position", + function (context, dom_element, isd_element, attr) { + dom_element.style.top = attr.h.toUsedLength(context.w, context.h) + "px"; + dom_element.style.left = attr.w.toUsedLength(context.w, context.h) + "px"; + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling rubyAlign", + function (context, dom_element, isd_element, attr) { + dom_element.style.rubyAlign = attr; + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling rubyPosition", + function (context, dom_element, isd_element, attr) { + /* skip if "outside", which is handled by applyRubyPosition() */ + if (attr === "before" || attr === "after") { + let pos; + if (context.bpd === "tb") { + pos = (attr === "before") ? "over" : "under"; + } else { + if (context.bpd === "rl") { + pos = (attr === "before") ? "over" : "under"; + } else { + pos = (attr === "before") ? "under" : "over"; + } + } + /* apply position to the parent dom_element, i.e. ruby or rtc */ + dom_element.parentElement.style.rubyPosition = pos; + } + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling showBackground", + null + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling textAlign", + function (context, dom_element, isd_element, attr) { + let ta; + let dir = isd_element.styleAttrs[imscStyles.byName.direction.qname]; + /* handle UAs that do not understand start or end */ + if (attr === "start") { + ta = (dir === "rtl") ? "right" : "left"; + } else if (attr === "end") { + ta = (dir === "rtl") ? "left" : "right"; + } else { + ta = attr; + } + dom_element.style.textAlign = ta; + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling textDecoration", + function (context, dom_element, isd_element, attr) { + dom_element.style.textDecoration = attr.join(" ").replace("lineThrough", "line-through"); + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling textOutline", + function (context, dom_element, isd_element, attr) { + /* defer to tts:textShadow */ + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling textShadow", + function (context, dom_element, isd_element, attr) { + let txto = isd_element.styleAttrs[imscStyles.byName.textOutline.qname]; + if (attr === "none" && txto === "none") { + dom_element.style.textShadow = ""; + } else { + let s = []; + if (txto !== "none") { + /* emulate text outline */ + s.push( + "rgba(" + + txto.color[0].toString() + "," + + txto.color[1].toString() + "," + + txto.color[2].toString() + "," + + (txto.color[3] / 255).toString() + + ") 0px 0px " + + txto.thickness.toUsedLength(context.w, context.h) + "px" + ); + } + /* add text shadow */ + if (attr !== "none") { + for (let attribute of attr) { + s.push(attribute.x_off.toUsedLength(context.w, context.h) + "px " + + attribute.y_off.toUsedLength(context.w, context.h) + "px " + + attribute.b_radius.toUsedLength(context.w, context.h) + "px " + + "rgba(" + + attribute.color[0].toString() + "," + + attribute.color[1].toString() + "," + + attribute.color[2].toString() + "," + + (attribute.color[3] / 255).toString() + + ")" + ); + } + } + dom_element.style.textShadow = s.join(","); + } + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling textCombine", + function (context, dom_element, isd_element, attr) { + dom_element.style.textCombineUpright = attr.join(" "); + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling textEmphasis", + function (context, dom_element, isd_element, attr) { + /* ignore color (not used in IMSC 1.1) */ + if (attr.style === "none") { + dom_element.style.textEmphasisStyle = "none"; + /* no need to set position, so return */ + return; + } else if (attr.style === "auto") { + dom_element.style.textEmphasisStyle = "filled"; + } else { + dom_element.style.textEmphasisStyle = attr.style + " " + attr.symbol; + } + /* ignore "outside" position (set in postprocessing) */ + if (attr.position === "before" || attr.position === "after") { + let pos; + if (context.bpd === "tb") { + pos = (attr.position === "before") ? "left over" : "left under"; + } else { + if (context.bpd === "rl") { + pos = (attr.position === "before") ? "right under" : "left under"; + } else { + pos = (attr.position === "before") ? "left under" : "right under"; + } + } + dom_element.style.textEmphasisPosition = pos; + } + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling unicodeBidi", + function (context, dom_element, isd_element, attr) { + let ub; + if (attr === 'bidiOverride') { + ub = "bidi-override"; + } else { + ub = attr; + } + dom_element.style.unicodeBidi = ub; + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling visibility", + function (context, dom_element, isd_element, attr) { + dom_element.style.visibility = attr; + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling wrapOption", + function (context, dom_element, isd_element, attr) { + if (attr === "wrap") { + if (isd_element.space === "preserve") { + dom_element.style.whiteSpace = "pre-wrap"; + } else { + dom_element.style.whiteSpace = "normal"; + } + } else { + if (isd_element.space === "preserve") { + dom_element.style.whiteSpace = "pre"; + } else { + dom_element.style.whiteSpace = "noWrap"; + } + } + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling writingMode", + function (context, dom_element, isd_element, attr) { + if (attr === "lrtb" || attr === "lr") { + context.writingMode = "horizontal-tb"; + } else if (attr === "rltb" || attr === "rl") { + context.writingMode = "horizontal-tb"; + } else if (attr === "tblr") { + context.writingMode = "vertical-lr"; + } else if (attr === "tbrl" || attr === "tb") { + context.writingMode = "vertical-rl"; + } + dom_element.style.writingMode = context.writingMode; + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml#styling zIndex", + function (context, dom_element, isd_element, attr) { + dom_element.style.zIndex = attr; + } + ), + new HTMLStylingMapDefintion( + "http://www.w3.org/ns/ttml/profile/imsc1#styling forcedDisplay", + function (context, dom_element, isd_element, attr) { + if (context.displayForcedOnlyMode && attr === false) { + dom_element.style.visibility = "hidden"; + } + } + ) +]; +let STYLMAP_BY_QNAME = {}; +for (let i in STYLING_MAP_DEFS) { + STYLMAP_BY_QNAME[STYLING_MAP_DEFS[i].qname] = STYLING_MAP_DEFS[i]; +} + +function reportError(errorHandler, msg) { + if (errorHandler && errorHandler.error && errorHandler.error(msg)) + throw msg; +} diff --git a/src/util/ttml/isd.js b/src/util/ttml/isd.js new file mode 100644 index 0000000..dbf8d54 --- /dev/null +++ b/src/util/ttml/isd.js @@ -0,0 +1,449 @@ +/* + * Copyright (c) 2016, Pierre-Anthony Lemieux <pal@sandflow.com> + * 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. + */ +import * as imscStyles from './styles'; +import * as imscUtils from './utils'; +import {reportError} from './utils'; + +export function generateISD(tt, offset, errorHandler) { + /* TODO check for tt and offset validity */ + /* create the ISD object from the IMSC1 doc */ + let isd = new ISD(tt); + /* context */ + let context = { + /*rubyfs: []*/ /* font size of the nearest textContainer or container */ + }; + /* process regions */ + for (let r in tt.head.layout.regions) { + /* post-order traversal of the body tree per [construct intermediate document] */ + let c = isdProcessContentElement(tt, offset, tt.head.layout.regions[r], tt.body, null, '', tt.head.layout.regions[r], errorHandler, context); + if (c !== null) { + /* add the region to the ISD */ + isd.contents.push(c.element); + } + } + return isd; +} + +/* set of styles not applicable to ruby container spans */ +let _rcs_na_styles = [ + imscStyles.byName.color.qname, + imscStyles.byName.textCombine.qname, + imscStyles.byName.textDecoration.qname, + imscStyles.byName.textEmphasis.qname, + imscStyles.byName.textOutline.qname, + imscStyles.byName.textShadow.qname +]; + +function isdProcessContentElement(doc, offset, region, body, parent, inherited_region_id, elem, errorHandler, context) { + /* prune if temporally inactive */ + if (offset < elem.begin || offset >= elem.end) { + return null; + } + /* + * set the associated region as specified by the regionID attribute, or the + * inherited associated region otherwise + */ + let associated_region_id = 'regionID' in elem && elem.regionID !== '' ? elem.regionID : inherited_region_id; + /* prune the element if either: + * - the element is not terminal and the associated region is neither the default + * region nor the parent region (this allows children to be associated with a + * region later on) + * - the element is terminal and the associated region is not the parent region + */ + /* TODO: improve detection of terminal elements since <region> has no contents */ + if (parent !== null /* are we in the region element */ && + associated_region_id !== region.id && + ( + (!('contents' in elem)) || + ('contents' in elem && elem.contents.length === 0) || + associated_region_id !== '' + ) + ) + return null; + /* create an ISD element, including applying specified styles */ + let isd_element = new ISDContentElement(elem); + /* apply set (animation) styling */ + if (elem.sets) { + for (let set of elem.sets) { + if (offset < set.begin || offset >= set.end) + continue; + isd_element.styleAttrs[set.qname] = set.value; + } + } + /* + * keep track of specified styling attributes so that we + * can compute them later + */ + let spec_attr = {}; + for (let qname in isd_element.styleAttrs) { + spec_attr[qname] = true; + /* special rule for tts:writingMode (section 7.29.1 of XSL) + * direction is set consistently with writingMode only + * if writingMode sets inline-direction to LTR or RTL + */ + if (qname === imscStyles.byName.writingMode.qname && + !(imscStyles.byName.direction.qname in isd_element.styleAttrs)) { + let wm = isd_element.styleAttrs[qname]; + if (wm === "lrtb" || wm === "lr") { + isd_element.styleAttrs[imscStyles.byName.direction.qname] = "ltr"; + } else if (wm === "rltb" || wm === "rl") { + isd_element.styleAttrs[imscStyles.byName.direction.qname] = "rtl"; + } + } + } + /* inherited styling */ + if (parent !== null) { + for (let j in imscStyles.all) { + let sa = imscStyles.all[j]; + /* textDecoration has special inheritance rules */ + if (sa.qname === imscStyles.byName.textDecoration.qname) { + /* handle both textDecoration inheritance and specification */ + let ps = parent.styleAttrs[sa.qname]; + let es = isd_element.styleAttrs[sa.qname]; + let outs = []; + if (es === undefined) { + outs = ps; + } else if (es.indexOf("none") === -1) { + if ((es.indexOf("noUnderline") === -1 && + ps.indexOf("underline") !== -1) || + es.indexOf("underline") !== -1) { + outs.push("underline"); + } + if ((es.indexOf("noLineThrough") === -1 && + ps.indexOf("lineThrough") !== -1) || + es.indexOf("lineThrough") !== -1) { + outs.push("lineThrough"); + } + if ((es.indexOf("noOverline") === -1 && + ps.indexOf("overline") !== -1) || + es.indexOf("overline") !== -1) { + outs.push("overline"); + } + } else { + outs.push("none"); + } + isd_element.styleAttrs[sa.qname] = outs; + } else if (sa.qname === imscStyles.byName.fontSize.qname && + !(sa.qname in isd_element.styleAttrs) && + isd_element.kind === 'span' && + isd_element.styleAttrs[imscStyles.byName.ruby.qname] === "textContainer") { + /* special inheritance rule for ruby text container font size */ + let ruby_fs = parent.styleAttrs[imscStyles.byName.fontSize.qname]; + isd_element.styleAttrs[sa.qname] = new imscUtils.ComputedLength( + 0.5 * ruby_fs.rw, + 0.5 * ruby_fs.rh); + } else if (sa.qname === imscStyles.byName.fontSize.qname && + !(sa.qname in isd_element.styleAttrs) && + isd_element.kind === 'span' && + isd_element.styleAttrs[imscStyles.byName.ruby.qname] === "text") { + /* special inheritance rule for ruby text font size */ + let parent_fs = parent.styleAttrs[imscStyles.byName.fontSize.qname]; + if (parent.styleAttrs[imscStyles.byName.ruby.qname] === "textContainer") { + isd_element.styleAttrs[sa.qname] = parent_fs; + } else { + isd_element.styleAttrs[sa.qname] = new imscUtils.ComputedLength( + 0.5 * parent_fs.rw, + 0.5 * parent_fs.rh); + } + } else if (sa.inherit && + (sa.qname in parent.styleAttrs) && + !(sa.qname in isd_element.styleAttrs)) { + isd_element.styleAttrs[sa.qname] = parent.styleAttrs[sa.qname]; + } + } + } + /* initial value styling */ + for (let k in imscStyles.all) { + let ivs = imscStyles.all[k]; + /* skip if value is already specified */ + if (ivs.qname in isd_element.styleAttrs) continue; + /* skip tts:position if tts:origin is specified */ + if (ivs.qname === imscStyles.byName.position.qname && + imscStyles.byName.origin.qname in isd_element.styleAttrs) + continue; + /* skip tts:origin if tts:position is specified */ + if (ivs.qname === imscStyles.byName.origin.qname && + imscStyles.byName.position.qname in isd_element.styleAttrs) + continue; + /* determine initial value */ + let iv = doc.head.styling.initials[ivs.qname] || ivs.initial; + /* apply initial value to elements other than region only if non-inherited */ + if (isd_element.kind === 'region' || (ivs.inherit === false && iv !== null)) { + isd_element.styleAttrs[ivs.qname] = ivs.parse(iv); + /* keep track of the style as specified */ + spec_attr[ivs.qname] = true; + } + } + /* compute styles (only for non-inherited styles) */ + /* TODO: get rid of spec_attr */ + for (let z in imscStyles.all) { + let cs = imscStyles.all[z]; + if (!(cs.qname in spec_attr)) continue; + if (cs.compute !== null) { + let cstyle = cs.compute( + /*doc, parent, element, attr, context*/ + doc, + parent, + isd_element, + isd_element.styleAttrs[cs.qname], + context + ); + if (cstyle !== null) { + isd_element.styleAttrs[cs.qname] = cstyle; + } else { + /* if the style cannot be computed, replace it by its initial value */ + isd_element.styleAttrs[cs.qname] = cs.compute( + /*doc, parent, element, attr, context*/ + doc, + parent, + isd_element, + cs.parse(cs.initial), + context + ); + reportError(errorHandler, "Style '" + cs.qname + "' on element '" + isd_element.kind + "' cannot be computed"); + } + } + } + /* tts:fontSize special ineritance for ruby */ + /* let isrubycontainer = false; + if (isd_element.kind === "span") { + let rtemp = isd_element.styleAttrs[imscStyles.byName.ruby.qname]; + if (rtemp === "container" || rtemp === "textContainer") { + isrubycontainer = true; + context.rubyfs.unshift(isd_element.styleAttrs[imscStyles.byName.fontSize.qname]); + } + } */ + /* prune if tts:display is none */ + if (isd_element.styleAttrs[imscStyles.byName.display.qname] === "none") + return null; + /* process contents of the element */ + let contents; + if (parent === null) { + /* we are processing the region */ + if (body === null) { + /* if there is no body, still process the region but with empty content */ + contents = []; + } else { + /*use the body element as contents */ + contents = [body]; + } + } else if ('contents' in elem) { + contents = elem.contents; + } + for (let x in contents) { + let c = isdProcessContentElement(doc, offset, region, body, isd_element, associated_region_id, contents[x], errorHandler, context); + /* + * keep child element only if they are non-null and their region match + * the region of this element + */ + if (c !== null) { + isd_element.contents.push(c.element); + } + } + /* compute used value of lineHeight="normal" */ + /* if (isd_element.styleAttrs[imscStyles.byName.lineHeight.qname] === "normal" ) { + isd_element.styleAttrs[imscStyles.byName.lineHeight.qname] = + isd_element.styleAttrs[imscStyles.byName.fontSize.qname] * 1.2; + } + */ + /* tts:fontSize special ineritance for ruby */ + /*if (isrubycontainer) { + context.rubyfs.shift(); + }*/ + /* remove styles that are not applicable */ + for (let qnameb in isd_element.styleAttrs) { + /* true if not applicable */ + let na = false; + /* special applicability of certain style properties to ruby container spans */ + /* TODO: in the future ruby elements should be translated to elements instead of kept as spans */ + if (isd_element.kind === 'span') { + let rsp = isd_element.styleAttrs[imscStyles.byName.ruby.qname]; + na = (rsp === 'container' || rsp === 'textContainer' || rsp === 'baseContainer') && + _rcs_na_styles.indexOf(qnameb) !== -1; + if (!na) { + na = rsp !== 'container' && + qnameb === imscStyles.byName.rubyAlign.qname; + } + if (!na) { + na = (!(rsp === 'textContainer' || rsp === 'text')) && + qnameb === imscStyles.byName.rubyPosition.qname; + } + } + /* normal applicability */ + if (!na) { + let da = imscStyles.byQName[qnameb]; + na = da.applies.indexOf(isd_element.kind) === -1; + } + if (na) { + delete isd_element.styleAttrs[qnameb]; + } + } + /* collapse white space if space is "default" */ + if (isd_element.kind === 'span' && isd_element.text && isd_element.space === "default") { + isd_element.text = isd_element.text.replace(/[\t\r\n ]+/g, ' '); + } + /* trim whitespace around explicit line breaks */ + if (isd_element.kind === 'p') { + let elist = []; + constructSpanList(isd_element, elist); + let l = 0; + let state = "after_br"; + let br_pos = 0; + while (true) { + if (state === "after_br") { + if (l >= elist.length || elist[l].kind === "br") { + state = "before_br"; + br_pos = l; + l--; + } else { + if (elist[l].space !== "preserve") { + elist[l].text = elist[l].text.replace(/^[\t\r\n ]+/g, ''); + } + if (elist[l].text.length > 0) { + state = "looking_br"; + l++; + } else { + elist.splice(l, 1); + } + } + } else if (state === "before_br") { + if (l < 0 || elist[l].kind === "br") { + state = "after_br"; + l = br_pos + 1; + if (l >= elist.length) break; + } else { + if (elist[l].space !== "preserve") { + elist[l].text = elist[l].text.replace(/[\t\r\n ]+$/g, ''); + } + if (elist[l].text.length > 0) { + state = "after_br"; + l = br_pos + 1; + if (l >= elist.length) break; + } else { + elist.splice(l, 1); + l--; + } + } + } else { + if (l >= elist.length || elist[l].kind === "br") { + state = "before_br"; + br_pos = l; + l--; + } else { + l++; + } + } + } + pruneEmptySpans(isd_element); + } + /* keep element if: + * * contains a background image + * * <br/> + * * if there are children + * * if it is an image + * * if <span> and has text + * * if region and showBackground = always + */ + if ((isd_element.kind === 'div' && imscStyles.byName.backgroundImage.qname in isd_element.styleAttrs) || + isd_element.kind === 'br' || + isd_element.kind === 'image' || + ('contents' in isd_element && isd_element.contents.length > 0) || + (isd_element.kind === 'span' && isd_element.text !== null) || + (isd_element.kind === 'region' && + isd_element.styleAttrs[imscStyles.byName.showBackground.qname] === 'always')) { + return { + region_id: associated_region_id, + element: isd_element + }; + } + return null; +} + +function constructSpanList(element, elist) { + if ('contents' in element) { + for (let content of element.contents) { + constructSpanList(content, elist); + } + } else if (element.kind === 'span' || element.kind === 'br') { + elist.push(element); + } +} + +function pruneEmptySpans(element) { + if (element.kind === 'br') { + return false; + } else if ('text' in element) { + return element.text.length === 0; + } else if ('contents' in element) { + let i = element.contents.length; + while (i--) { + if (pruneEmptySpans(element.contents[i])) { + element.contents.splice(i, 1); + } + } + return element.contents.length === 0; + } +} + +function ISD(tt) { + this.contents = []; + this.aspectRatio = tt.aspectRatio; +} + +function ISDContentElement(ttelem) { + /* assume the element is a region if it does not have a kind */ + this.kind = ttelem.kind || 'region'; + /* copy id */ + if (ttelem.id) { + this.id = ttelem.id; + } + /* deep copy of style attributes */ + this.styleAttrs = {}; + for (let sname in ttelem.styleAttrs) { + if (!ttelem.styleAttrs.hasOwnProperty(sname)) { + continue; + } + this.styleAttrs[sname] = + ttelem.styleAttrs[sname]; + } + /* copy src and type if image */ + if ('src' in ttelem) { + this.src = ttelem.src; + } + if ('type' in ttelem) { + this.type = ttelem.type; + } + /* TODO: clean this! + * TODO: ISDElement and document element should be better tied together */ + if ('text' in ttelem) { + this.text = ttelem.text; + } else if (this.kind === 'region' || 'contents' in ttelem) { + this.contents = []; + } + if ('space' in ttelem) { + this.space = ttelem.space; + } +} diff --git a/src/util/ttml/ismc.ts b/src/util/ttml/ismc.ts new file mode 100644 index 0000000..fae12ca --- /dev/null +++ b/src/util/ttml/ismc.ts @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2016, Pierre-Anthony Lemieux <pal@sandflow.com> + * 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. + */ + +import * as isd from './isd'; +import * as doc from './doc'; +import * as html from './html'; + +export interface TtmlDocument { + getMediaTimeEvents(): number[], + + getMediaTimeRange(): number[] +} + +export type ISD = unknown; +export type ISDState = unknown; + +/** + * Allows a client to provide callbacks to handle children of the <metadata> element + */ +export interface MetadataHandler { + /** + * Called when the opening tag of an element node is encountered. + * @param {string} ns Namespace URI of the element + * @param {string} name Local name of the element + * @param {Attribute[]} attributes List of attributes, each consisting of a `uri`, `name` and `value` + */ + onOpenTag(ns: string, name: string, attributes: Attribute[]): void, + + /** + * Called when the closing tag of an element node is encountered. + */ + onCloseTag(): void, + + /** + * Called when a text node is encountered. + * @param {string} contents Contents of the text node + */ + onText(contents: string): void, +} + +export interface Attribute { + uri: string, + name: string, + value: string +} + +/** + * Generic interface for handling events. The interface exposes four + * methods: + * * <pre>info</pre>: unusual event that does not result in an inconsistent state + * * <pre>warn</pre>: unexpected event that should not result in an inconsistent state + * * <pre>error</pre>: unexpected event that may result in an inconsistent state + * * <pre>fatal</pre>: unexpected event that results in an inconsistent state + * and termination of processing + * Each method takes a single <pre>string</pre> describing the event as argument, + * and returns a single <pre>boolean</pre>, which terminates processing if <pre>true</pre>. + */ +export interface ErrorHandler { + info(error: string): boolean + + warn(error: string): boolean + + error(error: string): boolean + + fatal(error: string): boolean +} + +/** + * Parses an IMSC1 document into an opaque in-memory representation that exposes + * a single method <pre>getMediaTimeEvents()</pre> 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. `metadataHandler` allows the caller to + * be called back when nodes are present in <metadata> elements. + */ +type fromXML = ( + xmlstring: string, + errorHandler?: ErrorHandler, + metadataHandler?: MetadataHandler +) => TtmlDocument | null; + +/** + * Creates a canonical representation of an IMSC1 document returned by <pre>imscDoc.fromXML()</pre> + * at a given absolute offset in seconds. This offset does not have to be one of the values returned + * by <pre>getMediaTimeEvents()</pre>. + */ +type generateISD = ( + tt: TtmlDocument, + offset?: number, + errorHandler?: ErrorHandler, +) => ISD; + +/** + * Renders an ISD object (returned by <pre>generateISD()</pre>) into a + * parent element, that must be attached to the DOM. The ISD will be rendered + * into a child <pre>div</pre> + * with heigh and width equal to the clientHeight and clientWidth of the element, + * unless explicitly specified otherwise by the caller. Images URIs specified + * by <pre>smpte:background</pre> attributes are mapped to image resource URLs + * by an <pre>imgResolver</pre> function. The latter takes the value of <code>smpte:background</code> + * attribute and an <code>img</code> DOM element as input, and is expected to + * set the <code>src</code> attribute of the <code>img</code> to the absolute URI of the image. + * <pre>displayForcedOnlyMode</pre> sets the (boolean) + * value of the IMSC1 displayForcedOnlyMode parameter. The function returns + * an opaque object that should passed in <code>previousISDState</code> when this function + * is called for the next ISD, otherwise <code>previousISDState</code> should be set to + * <code>null</code>. + */ +type renderHTML = ( + isd: ISD, + element: HTMLElement, + /** + * Function that maps <pre>smpte:background</pre> URIs to URLs resolving to image resource + * @param {string} <pre>smpte:background</pre> URI + */ + imgResolver?: (backgroundUri: string) => string, + height?: number, + width?: number, + displayForcedOnlyMode?: boolean, + errorHandler?: ErrorHandler, + previousISDState?: ISDState, + enableRollUp?: boolean +) => ISDState; + +export const generateISD: generateISD = isd.generateISD; +export const fromXML: fromXML = doc.fromXML; +export const renderHTML: renderHTML = html.render; diff --git a/src/util/ttml/names.js b/src/util/ttml/names.js new file mode 100644 index 0000000..53d6c68 --- /dev/null +++ b/src/util/ttml/names.js @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016, Pierre-Anthony Lemieux <pal@sandflow.com> + * 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. + */ +export const ns_tt = "http://www.w3.org/ns/ttml"; +export const ns_tts = "http://www.w3.org/ns/ttml#styling"; +export const ns_ttp = "http://www.w3.org/ns/ttml#parameter"; +export const ns_xml = "http://www.w3.org/XML/1998/namespace"; +export const ns_itts = "http://www.w3.org/ns/ttml/profile/imsc1#styling"; +export const ns_ittp = "http://www.w3.org/ns/ttml/profile/imsc1#parameter"; +export const ns_smpte = "http://www.smpte-ra.org/schemas/2052-1/2010/smpte-tt"; +export const ns_ebutts = "urn:ebu:tt:style"; diff --git a/src/util/ttml/styles.js b/src/util/ttml/styles.js new file mode 100644 index 0000000..d13ce0d --- /dev/null +++ b/src/util/ttml/styles.js @@ -0,0 +1,941 @@ +/* + * Copyright (c) 2016, Pierre-Anthony Lemieux <pal@sandflow.com> + * 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. + */ +import * as imscNames from './names'; +import * as imscUtils from './utils'; + +const DEFAULT_FONT_FAMILY = "proportionalSansSerif"; + +function StylingAttributeDefinition(ns, name, initialValue, appliesTo, isInherit, isAnimatable, parseFunc, computeFunc) { + this.name = name; + this.ns = ns; + this.qname = ns + " " + name; + this.inherit = isInherit; + this.animatable = isAnimatable; + this.initial = initialValue; + this.applies = appliesTo; + this.parse = parseFunc; + this.compute = computeFunc; +} + +export const all = [ + new StylingAttributeDefinition( + imscNames.ns_tts, + "backgroundColor", + "transparent", + ['body', 'div', 'p', 'region', 'span'], + false, + true, + imscUtils.parseColor, + null + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "color", + "white", + ['span'], + true, + true, + imscUtils.parseColor, + null + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "direction", + "ltr", + ['p', 'span'], + true, + true, + function (str) { + return str; + }, + null + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "display", + "auto", + ['body', 'div', 'p', 'region', 'span'], + false, + true, + function (str) { + return str; + }, + null + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "displayAlign", + "before", + ['region'], + false, + true, + function (str) { + return str; + }, + null + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "extent", + "auto", + ['tt', 'region'], + false, + true, + function (str) { + if (str === "auto") { + return str; + } else { + let s = str.split(" "); + if (s.length !== 2) + return null; + let w = imscUtils.parseLength(s[0]); + let h = imscUtils.parseLength(s[1]); + if (!h || !w) + return null; + return {'h': h, 'w': w}; + } + }, + function (doc, parent, element, attr, context) { + let h; + let w; + if (attr === "auto") { + h = new imscUtils.ComputedLength(0, 1); + } else { + h = imscUtils.toComputedLength( + attr.h.value, + attr.h.unit, + null, + doc.dimensions.h, + null, + doc.pxLength.h + ); + if (h === null) { + return null; + } + } + if (attr === "auto") { + w = new imscUtils.ComputedLength(1, 0); + } else { + w = imscUtils.toComputedLength( + attr.w.value, + attr.w.unit, + null, + doc.dimensions.w, + null, + doc.pxLength.w + ); + if (w === null) { + return null; + } + } + return {'h': h, 'w': w}; + } + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "fontFamily", + "default", + ['span'], + true, + true, + function (str) { + let ffs = str.split(","); + let rslt = []; + for (let element of ffs) { + if (element.charAt(0) !== "'" && element.charAt(0) !== '"') { + if (element === "default") { + /* per IMSC1 */ + rslt.push(DEFAULT_FONT_FAMILY); + } else { + rslt.push(element); + } + } else { + rslt.push(element); + } + } + return rslt; + }, + null + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "shear", + "0%", + ['p'], + true, + true, + imscUtils.parseLength, + function (doc, parent, element, attr) { + let fs; + if (attr.unit === "%") { + fs = Math.abs(attr.value) > 100 ? Math.sign(attr.value) * 100 : attr.value; + } else { + return null; + } + return fs; + } + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "fontSize", + "1c", + ['span'], + true, + true, + imscUtils.parseLength, + function (doc, parent, element, attr, context) { + let fs; + fs = imscUtils.toComputedLength( + attr.value, + attr.unit, + parent !== null ? parent.styleAttrs[byName.fontSize.qname] : doc.cellLength.h, + parent !== null ? parent.styleAttrs[byName.fontSize.qname] : doc.cellLength.h, + doc.cellLength.h, + doc.pxLength.h + ); + return fs; + } + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "fontStyle", + "normal", + ['span'], + true, + true, + function (str) { + /* TODO: handle font style */ + return str; + }, + null + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "fontWeight", + "normal", + ['span'], + true, + true, + function (str) { + /* TODO: handle font weight */ + return str; + }, + null + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "lineHeight", + "normal", + ['p'], + true, + true, + function (str) { + if (str === "normal") { + return str; + } else { + return imscUtils.parseLength(str); + } + }, + function (doc, parent, element, attr, context) { + let lh; + if (attr === "normal") { + /* inherit normal per https://github.com/w3c/ttml1/issues/220 */ + lh = attr; + } else { + lh = imscUtils.toComputedLength( + attr.value, + attr.unit, + element.styleAttrs[byName.fontSize.qname], + element.styleAttrs[byName.fontSize.qname], + doc.cellLength.h, + doc.pxLength.h + ); + if (lh === null) { + return null; + } + } + /* TODO: create a Length constructor */ + return lh; + } + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "opacity", + 1.0, + ['region'], + false, + true, + parseFloat, + null + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "origin", + "auto", + ['region'], + false, + true, + function (str) { + if (str === "auto") { + return str; + } else { + let s = str.split(" "); + if (s.length !== 2) + return null; + let w = imscUtils.parseLength(s[0]); + let h = imscUtils.parseLength(s[1]); + if (!h || !w) + return null; + return {'h': h, 'w': w}; + } + }, + function (doc, parent, element, attr, context) { + let h; + let w; + if (attr === "auto") { + h = new imscUtils.ComputedLength(0, 0); + } else { + h = imscUtils.toComputedLength( + attr.h.value, + attr.h.unit, + null, + doc.dimensions.h, + null, + doc.pxLength.h + ); + if (h === null) { + return null; + } + } + if (attr === "auto") { + w = new imscUtils.ComputedLength(0, 0); + } else { + w = imscUtils.toComputedLength( + attr.w.value, + attr.w.unit, + null, + doc.dimensions.w, + null, + doc.pxLength.w + ); + if (w === null) { + return null; + } + } + return {'h': h, 'w': w}; + } + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "overflow", + "hidden", + ['region'], + false, + true, + function (str) { + return str; + }, + null + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "padding", + "0px", + ['region'], + false, + true, + function (str) { + let s = str.split(" "); + if (s.length > 4) + return null; + let r = []; + for (let el of s) { + let l = imscUtils.parseLength(el); + if (!l) + return null; + r.push(l); + } + return r; + }, + function (doc, parent, element, attr, context) { + let padding; + /* TODO: make sure we are in region */ + /* + * expand padding shortcuts to + * [before, end, after, start] + * + */ + if (attr.length === 1) { + padding = [attr[0], attr[0], attr[0], attr[0]]; + } else if (attr.length === 2) { + padding = [attr[0], attr[1], attr[0], attr[1]]; + } else if (attr.length === 3) { + padding = [attr[0], attr[1], attr[2], attr[1]]; + } else if (attr.length === 4) { + padding = [attr[0], attr[1], attr[2], attr[3]]; + } else { + return null; + } + /* TODO: take into account tts:direction */ + /* + * transform [before, end, after, start] according to writingMode to + * [top,left,bottom,right] + * + */ + let dir = element.styleAttrs[byName.writingMode.qname]; + if (dir === "lrtb" || dir === "lr") { + padding = [padding[0], padding[3], padding[2], padding[1]]; + } else if (dir === "rltb" || dir === "rl") { + padding = [padding[0], padding[1], padding[2], padding[3]]; + } else if (dir === "tblr") { + padding = [padding[3], padding[0], padding[1], padding[2]]; + } else if (dir === "tbrl" || dir === "tb") { + padding = [padding[3], padding[2], padding[1], padding[0]]; + } else { + return null; + } + let out = []; + for (let i in padding) { + if (padding[i].value === 0) { + out[i] = new imscUtils.ComputedLength(0, 0); + } else { + out[i] = imscUtils.toComputedLength( + padding[i].value, + padding[i].unit, + element.styleAttrs[byName.fontSize.qname], + i === "0" || i === "2" ? element.styleAttrs[byName.extent.qname].h : element.styleAttrs[byName.extent.qname].w, + i === "0" || i === "2" ? doc.cellLength.h : doc.cellLength.w, + i === "0" || i === "2" ? doc.pxLength.h : doc.pxLength.w + ); + if (out[i] === null) return null; + } + } + return out; + } + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "position", + "top left", + ['region'], + false, + true, + function (str) { + return imscUtils.parsePosition(str); + }, + function (doc, parent, element, attr) { + let h; + let w; + h = imscUtils.toComputedLength( + attr.v.offset.value, + attr.v.offset.unit, + null, + new imscUtils.ComputedLength( + -element.styleAttrs[byName.extent.qname].h.rw, + doc.dimensions.h.rh - element.styleAttrs[byName.extent.qname].h.rh + ), + null, + doc.pxLength.h + ); + if (h === null) return null; + if (attr.v.edge === "bottom") { + h = new imscUtils.ComputedLength( + -h.rw - element.styleAttrs[byName.extent.qname].h.rw, + doc.dimensions.h.rh - h.rh - element.styleAttrs[byName.extent.qname].h.rh + ); + } + w = imscUtils.toComputedLength( + attr.h.offset.value, + attr.h.offset.unit, + null, + new imscUtils.ComputedLength( + doc.dimensions.w.rw - element.styleAttrs[byName.extent.qname].w.rw, + -element.styleAttrs[byName.extent.qname].w.rh + ), + null, + doc.pxLength.w + ); + if (h === null) return null; + if (attr.h.edge === "right") { + w = new imscUtils.ComputedLength( + doc.dimensions.w.rw - w.rw - element.styleAttrs[byName.extent.qname].w.rw, + -w.rh - element.styleAttrs[byName.extent.qname].w.rh + ); + } + return {'h': h, 'w': w}; + } + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "ruby", + "none", + ['span'], + false, + true, + function (str) { + return str; + }, + null + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "rubyAlign", + "center", + ['span'], + true, + true, + function (str) { + if (!(str === "center" || str === "spaceAround")) { + return null; + } + return str; + }, + null + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "rubyPosition", + "outside", + ['span'], + true, + true, + function (str) { + return str; + }, + null + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "rubyReserve", + "none", + ['p'], + true, + true, + function (str) { + let s = str.split(" "); + let r = [null, null]; + if (s.length === 0 || s.length > 2) + return null; + if (s[0] === "none" || + s[0] === "both" || + s[0] === "after" || + s[0] === "before" || + s[0] === "outside") { + r[0] = s[0]; + } else { + return null; + } + if (s.length === 2 && s[0] !== "none") { + let l = imscUtils.parseLength(s[1]); + if (l) { + r[1] = l; + } else { + return null; + } + } + return r; + }, + function (doc, parent, element, attr, context) { + if (attr[0] === "none") { + return attr; + } + let fs; + if (attr[1] === null) { + fs = new imscUtils.ComputedLength( + element.styleAttrs[byName.fontSize.qname].rw * 0.5, + element.styleAttrs[byName.fontSize.qname].rh * 0.5 + ); + } else { + fs = imscUtils.toComputedLength(attr[1].value, + attr[1].unit, + element.styleAttrs[byName.fontSize.qname], + element.styleAttrs[byName.fontSize.qname], + doc.cellLength.h, + doc.pxLength.h + ); + } + if (fs === null) return null; + return [attr[0], fs]; + } + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "showBackground", + "always", + ['region'], + false, + true, + function (str) { + return str; + }, + null + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "textAlign", + "start", + ['p'], + true, + true, + function (str) { + return str; + }, + function (doc, parent, element, attr, context) { + /* Section 7.16.9 of XSL */ + if (attr === "left") { + return "start"; + } else if (attr === "right") { + return "end"; + } else { + return attr; + } + } + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "textCombine", + "none", + ['span'], + true, + true, + function (str) { + let s = str.split(" "); + if (s.length === 1) { + if (s[0] === "none" || s[0] === "all") { + return [s[0]]; + } + } + return null; + }, + null + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "textDecoration", + "none", + ['span'], + true, + true, + function (str) { + return str.split(" "); + }, + null + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "textEmphasis", + "none", + ['span'], + true, + true, + function (str) { + let e = str.split(" "); + let rslt = {style: null, symbol: null, color: null, position: null}; + for (let el of e) { + if (el === "none" || el === "auto") { + rslt.style = el; + } else if (el === "filled" || + el === "open") { + rslt.style = el; + } else if (el === "circle" || + el === "dot" || + el === "sesame") { + rslt.symbol = el; + } else if (el === "current") { + rslt.color = el; + } else if (el === "outside" || el === "before" || el === "after") { + rslt.position = el; + } else { + rslt.color = imscUtils.parseColor(el); + if (rslt.color === null) + return null; + } + } + if (rslt.style == null && rslt.symbol == null) { + rslt.style = "auto"; + } else { + rslt.symbol = rslt.symbol || "circle"; + rslt.style = rslt.style || "filled"; + } + rslt.position = rslt.position || "outside"; + rslt.color = rslt.color || "current"; + return rslt; + }, + null + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "textOutline", + "none", + ['span'], + true, + true, + function (str) { + /* + * returns {c: <color>?, thichness: <length>} | "none" + * + */ + if (str === "none") { + return str; + } else { + let r = {}; + let s = str.split(" "); + if (s.length === 0 || s.length > 2) + return null; + let c = imscUtils.parseColor(s[0]); + r.color = c; + if (c !== null) + s.shift(); + if (s.length !== 1) + return null; + let l = imscUtils.parseLength(s[0]); + if (!l) + return null; + r.thickness = l; + return r; + } + }, + function (doc, parent, element, attr, context) { + /* + * returns {color: <color>, thickness: <norm length>} + * + */ + if (attr === "none") + return attr; + let rslt = {}; + if (attr.color === null) { + rslt.color = element.styleAttrs[byName.color.qname]; + } else { + rslt.color = attr.color; + } + rslt.thickness = imscUtils.toComputedLength( + attr.thickness.value, + attr.thickness.unit, + element.styleAttrs[byName.fontSize.qname], + element.styleAttrs[byName.fontSize.qname], + doc.cellLength.h, + doc.pxLength.h + ); + if (rslt.thickness === null) + return null; + return rslt; + } + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "textShadow", + "none", + ['span'], + true, + true, + imscUtils.parseTextShadow, + function (doc, parent, element, attr) { + /* + * returns [{x_off: <length>, y_off: <length>, b_radius: <length>, color: <color>}*] or "none" + * + */ + if (attr === "none") + return attr; + let r = []; + for (let el of attr) { + let shadow = {}; + shadow.x_off = imscUtils.toComputedLength( + el[0].value, + el[0].unit, + null, + element.styleAttrs[byName.fontSize.qname], + null, + doc.pxLength.w + ); + if (shadow.x_off === null) + return null; + shadow.y_off = imscUtils.toComputedLength( + el[1].value, + el[1].unit, + null, + element.styleAttrs[byName.fontSize.qname], + null, + doc.pxLength.h + ); + if (shadow.y_off === null) + return null; + if (el[2] === null) { + shadow.b_radius = 0; + } else { + shadow.b_radius = imscUtils.toComputedLength( + el[2].value, + el[2].unit, + null, + element.styleAttrs[byName.fontSize.qname], + null, + doc.pxLength.h + ); + if (shadow.b_radius === null) + return null; + } + if (el[3] === null) { + shadow.color = element.styleAttrs[byName.color.qname]; + } else { + shadow.color = el[3]; + } + r.push(shadow); + } + return r; + } + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "unicodeBidi", + "normal", + ['span', 'p'], + false, + true, + function (str) { + return str; + }, + null + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "visibility", + "visible", + ['body', 'div', 'p', 'region', 'span'], + true, + true, + function (str) { + return str; + }, + null + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "wrapOption", + "wrap", + ['span'], + true, + true, + function (str) { + return str; + }, + null + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "writingMode", + "lrtb", + ['region'], + false, + true, + function (str) { + return str; + }, + null + ), + new StylingAttributeDefinition( + imscNames.ns_tts, + "zIndex", + "auto", + ['region'], + false, + true, + function (str) { + let rslt; + if (str === 'auto') { + rslt = str; + } else { + rslt = parseInt(str); + if (isNaN(rslt)) { + rslt = null; + } + } + return rslt; + }, + null + ), + new StylingAttributeDefinition( + imscNames.ns_ebutts, + "linePadding", + "0c", + ['p'], + true, + false, + imscUtils.parseLength, + function (doc, parent, element, attr, context) { + return imscUtils.toComputedLength(attr.value, attr.unit, null, null, doc.cellLength.w, null); + } + ), + new StylingAttributeDefinition( + imscNames.ns_ebutts, + "multiRowAlign", + "auto", + ['p'], + true, + false, + function (str) { + return str; + }, + null + ), + new StylingAttributeDefinition( + imscNames.ns_smpte, + "backgroundImage", + null, + ['div'], + false, + false, + function (str) { + return str; + }, + null + ), + new StylingAttributeDefinition( + imscNames.ns_itts, + "forcedDisplay", + "false", + ['body', 'div', 'p', 'region', 'span'], + true, + true, + function (str) { + return str === 'true'; + }, + null + ), + new StylingAttributeDefinition( + imscNames.ns_itts, + "fillLineGap", + "false", + ['p'], + true, + true, + function (str) { + return str === 'true'; + }, + null + ) +]; +/* TODO: allow null parse function */ +export const byQName = Object.fromEntries(all.map(it => [it.qname, it])); +export const byName = Object.fromEntries(all.map(it => [it.name, it])); diff --git a/src/util/ttml/utils.js b/src/util/ttml/utils.js new file mode 100644 index 0000000..320460e --- /dev/null +++ b/src/util/ttml/utils.js @@ -0,0 +1,284 @@ +/* + * Copyright (c) 2016, Pierre-Anthony Lemieux <pal@sandflow.com> + * 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. + */ +/* + * Parses a TTML color expression + * + */ +let HEX_COLOR_RE = /#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})?/; +let DEC_COLOR_RE = /rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/; +let DEC_COLORA_RE = /rgba\(\s*(\d+),\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/; +let NAMED_COLOR = { + transparent: [0, 0, 0, 0], + black: [0, 0, 0, 255], + silver: [192, 192, 192, 255], + gray: [128, 128, 128, 255], + white: [255, 255, 255, 255], + maroon: [128, 0, 0, 255], + red: [255, 0, 0, 255], + purple: [128, 0, 128, 255], + fuchsia: [255, 0, 255, 255], + magenta: [255, 0, 255, 255], + green: [0, 128, 0, 255], + lime: [0, 255, 0, 255], + olive: [128, 128, 0, 255], + yellow: [255, 255, 0, 255], + navy: [0, 0, 128, 255], + blue: [0, 0, 255, 255], + teal: [0, 128, 128, 255], + aqua: [0, 255, 255, 255], + cyan: [0, 255, 255, 255] +}; + +export function parseColor(str) { + let m; + let r = null; + let nc = NAMED_COLOR[str.toLowerCase()]; + if (nc !== undefined) { + r = nc; + } else if ((m = HEX_COLOR_RE.exec(str)) !== null) { + r = [parseInt(m[1], 16), + parseInt(m[2], 16), + parseInt(m[3], 16), + (m[4] !== undefined ? parseInt(m[4], 16) : 255)]; + } else if ((m = DEC_COLOR_RE.exec(str)) !== null) { + r = [parseInt(m[1]), + parseInt(m[2]), + parseInt(m[3]), + 255]; + } else if ((m = DEC_COLORA_RE.exec(str)) !== null) { + r = [parseInt(m[1]), + parseInt(m[2]), + parseInt(m[3]), + parseInt(m[4])]; + } + return r; +} + +let LENGTH_RE = /^([+-]?\d*(?:\.\d+)?)(px|em|c|%|rh|rw)$/; + +export function parseLength(str) { + let m; + let r = null; + if ((m = LENGTH_RE.exec(str)) !== null) { + r = {value: parseFloat(m[1]), unit: m[2]}; + } + return r; +} + +export function parseTextShadow(str) { + let shadows = str.split(","); + let r = []; + for (let element of shadows) { + let shadow = element.split(" "); + if (shadow.length === 1 && shadow[0] === "none") { + return "none"; + } else if (shadow.length > 1 && shadow.length < 5) { + let out_shadow = [null, null, null, null]; + /* x offset */ + let l = parseLength(shadow.shift()); + if (l === null) + return null; + out_shadow[0] = l; + /* y offset */ + l = parseLength(shadow.shift()); + if (l === null) + return null; + out_shadow[1] = l; + /* is there a third component */ + if (shadow.length === 0) { + r.push(out_shadow); + continue; + } + l = parseLength(shadow[0]); + if (l !== null) { + out_shadow[2] = l; + shadow.shift(); + } + if (shadow.length === 0) { + r.push(out_shadow); + continue; + } + let c = parseColor(shadow[0]); + if (c === null) + return null; + out_shadow[3] = c; + r.push(out_shadow); + } + } + return r; +} + +export function parsePosition(str) { + /* see https://www.w3.org/TR/ttml2/#style-value-position */ + let s = str.split(" "); + let isKeyword = function (str) { + return str === "center" || + str === "left" || + str === "top" || + str === "bottom" || + str === "right"; + }; + if (s.length > 4) { + return null; + } + /* initial clean-up pass */ + for (let j = 0; j < s.length; j++) { + if (!isKeyword(s[j])) { + let l = parseLength(s[j]); + if (l === null) + return null; + s[j] = l; + } + } + /* position default */ + let pos = { + h: {edge: "left", offset: {value: 50, unit: "%"}}, + v: {edge: "top", offset: {value: 50, unit: "%"}} + }; + /* update position */ + for (let i = 0; i < s.length;) { + /* extract the current component */ + let comp = s[i++]; + if (isKeyword(comp)) { + /* we have a keyword */ + let offset = {value: 0, unit: "%"}; + /* peek at the next component */ + if (s.length !== 2 && i < s.length && (!isKeyword(s[i]))) { + /* followed by an offset */ + offset = s[i++]; + } + /* skip if center */ + if (comp === "right") { + pos.h.edge = comp; + pos.h.offset = offset; + } else if (comp === "bottom") { + pos.v.edge = comp; + pos.v.offset = offset; + } else if (comp === "left") { + pos.h.offset = offset; + } else if (comp === "top") { + pos.v.offset = offset; + } + } else if (s.length === 1 || s.length === 2) { + /* we have a bare value */ + if (i === 1) { + /* assign it to left edge if first bare value */ + pos.h.offset = comp; + } else { + /* assign it to top edge if second bare value */ + pos.v.offset = comp; + } + } else { + /* error condition */ + return null; + } + } + return pos; +} + +export function ComputedLength(rw, rh) { + this.rw = rw; + this.rh = rh; +} + +ComputedLength.prototype.toUsedLength = function (width, height) { + return width * this.rw + height * this.rh; +}; +ComputedLength.prototype.isZero = function () { + return this.rw === 0 && this.rh === 0; +}; + +/** + * Computes a specified length to a root container relative length + * + * @param {number} lengthVal Length value to be computed + * @param {string} lengthUnit Units of the length value + * @param {?ComputedLength} emScale length of 1em, or null if em is not allowed + * @param {?ComputedLength} percentScale length to which , or null if percentage is not allowed + * @param {?ComputedLength} cellScale length of 1c, or null if c is not allowed + * @param {?ComputedLength} pxScale length of 1px, or null if px is not allowed + * @return {ComputedLength} Computed length + */ +export function toComputedLength(lengthVal, lengthUnit, emScale, percentScale, cellScale, pxScale) { + if (lengthUnit === "%" && percentScale) { + return new ComputedLength( + percentScale.rw * lengthVal / 100, + percentScale.rh * lengthVal / 100 + ); + } else if (lengthUnit === "em" && emScale) { + return new ComputedLength( + emScale.rw * lengthVal, + emScale.rh * lengthVal + ); + } else if (lengthUnit === "c" && cellScale) { + return new ComputedLength( + lengthVal * cellScale.rw, + lengthVal * cellScale.rh + ); + } else if (lengthUnit === "px" && pxScale) { + return new ComputedLength( + lengthVal * pxScale.rw, + lengthVal * pxScale.rh + ); + } else if (lengthUnit === "rh") { + return new ComputedLength( + 0, + lengthVal / 100 + ); + } else if (lengthUnit === "rw") { + return new ComputedLength( + lengthVal / 100, + 0 + ); + } else { + return null; + } +} + +/* + * ERROR HANDLING UTILITY FUNCTIONS + * + */ +export function reportInfo(errorHandler, msg) { + if (errorHandler && errorHandler.info && errorHandler.info(msg)) + throw msg; +} + +export function reportWarning(errorHandler, msg) { + if (errorHandler && errorHandler.warn && errorHandler.warn(msg)) + throw msg; +} + +export function reportError(errorHandler, msg) { + if (errorHandler && errorHandler.error && errorHandler.error(msg)) + throw msg; +} + +export function reportFatal(errorHandler, msg) { + if (errorHandler && errorHandler.fatal) + errorHandler.fatal(msg); + throw msg; +} -- GitLab