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