Compare commits
143 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d3ef58d73 | |||
| d2db781f82 | |||
| 11d1b36fc3 | |||
| 509aa3b52d | |||
| d916a8dece | |||
| ff9fa3954d | |||
| 9b0e15cd78 | |||
| 795bef96ac | |||
| 45f081ba54 | |||
| 308715a495 | |||
| abbbd6bf04 | |||
| 569fce4072 | |||
| 5b88230c32 | |||
| 097854f30f | |||
| 6fc6bcc4b2 | |||
| 0045ebe3fb | |||
| 1cefb68b47 | |||
| 78bab74e11 | |||
| a43f2da4c9 | |||
| 7ff81409c7 | |||
| 0661ada8e6 | |||
| ec1c180173 | |||
| d5969cc2f5 | |||
| b23f6e4e9f | |||
| da1e7ed52b | |||
| e6d3195df0 | |||
| 81117ce07b | |||
| 6dbee54f6a | |||
| 5f9890d157 | |||
| db1201d84e | |||
| f3ab2bc57f | |||
| a4c617f1df | |||
| e1b993a0ae | |||
| 42db525b30 | |||
| 76691cc52b | |||
| 82f3706eff | |||
| a0ac2ba83b | |||
| b89b5f76bb | |||
| 2bfdcb582c | |||
| 604129a10d | |||
| 563141f1c4 | |||
| a0d49f4ee5 | |||
| 23445a82c4 | |||
| 0c51760fc5 | |||
| 2ce0d7985a | |||
| f8eb111f89 | |||
| 9e9ba88ede | |||
| 8ed209bba4 | |||
| 0260014524 | |||
| c6cafd10d4 | |||
| e46f0322ed | |||
| 0ecb96c42a | |||
| 191a34d060 | |||
| 484997e3fc | |||
| 7482220201 | |||
| 6606413eef | |||
| 6dce371011 | |||
| 6b2800ef70 | |||
| 29c187f076 | |||
| 7d7217007a | |||
| e783afc43c | |||
| 4183228262 | |||
| b92b63f94f | |||
| 4ddab3a2d9 | |||
| 3b381ac925 | |||
| 1d803dd1b8 | |||
| 074f3ec723 | |||
| caf6701c7b | |||
| 99d72711be | |||
| 7ed52215cb | |||
| 50e9140ee3 | |||
| 6f81c4dc17 | |||
| 7a1dc0582c | |||
| d9f2431414 | |||
| cec2f13361 | |||
| 6939e10c9b | |||
| 2ee04485d6 | |||
| d98f3ea94f | |||
| 83b6ae9f64 | |||
| 92397aa351 | |||
| 5e64329a8c | |||
| c0e0dec1fb | |||
| 90c781c4e2 | |||
| 74636c4098 | |||
| bc989a5761 | |||
| 586ff43bec | |||
| e856423ee1 | |||
| 8e903ba50e | |||
| 68d898764e | |||
| 9bde3b9982 | |||
| c4c935175e | |||
| c7986b0eab | |||
| a1dfc2f79d | |||
| bda06a8385 | |||
| 0dd0df60f5 | |||
| d4956466e4 | |||
| 0b4c66f88a | |||
| f21b90b061 | |||
| 1df6c44007 | |||
| 53f96a2367 | |||
| f6b4b705e0 | |||
| fc3c62af0a | |||
| fe2f7bde9e | |||
| 070ef1aee3 | |||
| d05e8015b4 | |||
| cb52d62158 | |||
| 232dddf6e5 | |||
| 53d9721f18 | |||
| 981132c08a | |||
| ff1739fe67 | |||
| 3df0b5bab7 | |||
| f9a8791280 | |||
| 3140391c16 | |||
| 6fa2b2c581 | |||
| c099015f6a | |||
| 9e039ee13d | |||
| 8a53a56157 | |||
| ff04dbab24 | |||
| a9ec919118 | |||
| 676f167180 | |||
| 3a277f9295 | |||
| c469780107 | |||
| 539fa58daf | |||
| 3df3eb7c11 | |||
| eebbad7fd1 | |||
| 0cd9dbe150 | |||
| 5b783850e8 | |||
| 0c2fecd1cc | |||
| d1b17204e9 | |||
| 0292acc20c | |||
| f1e820819c | |||
| c28c722263 | |||
| 64ac166485 | |||
| 8cabd87c60 | |||
| 6986531992 | |||
| 42282c2807 | |||
| f2765085c4 | |||
| 8e28a95e4e | |||
| 782d34c3eb | |||
| b2d9b757e2 | |||
| 41cb005ec6 | |||
| d84573038d | |||
| e047bed5b9 |
@@ -1,8 +1,8 @@
|
||||
dist/
|
||||
build/doc.js
|
||||
build/typescript/typescript.d.ts
|
||||
build/typescript/*.js
|
||||
|
||||
gulplib/doc.js
|
||||
gulplib/helpers.js
|
||||
gulplib/typescript/*.js
|
||||
dist/
|
||||
|
||||
lib/
|
||||
!lib/libjass.css
|
||||
|
||||
@@ -2,7 +2,8 @@ language: node_js
|
||||
node_js:
|
||||
- "0.10"
|
||||
- "0.12"
|
||||
- iojs
|
||||
- "4"
|
||||
- "5"
|
||||
before_script:
|
||||
- "./node_modules/.bin/gulp doc"
|
||||
- "node ./build.js doc"
|
||||
sudo: false
|
||||
|
||||
@@ -1,3 +1,30 @@
|
||||
### v0.11.0 - 2016/01/24
|
||||
- BREAKING CHANGE - WebRenderer.resize(width, height) used to have a broken implementation of letterboxing to move the subs div right or down. Now it's WebRenderer.resize(width, height, left, top) and expects the caller to calculate letterboxing itself and supply left and top accordingly. DefaultRenderer does it using the video resolution and users of WebRenderer can do the same.
|
||||
- BREAKING CHANGE - DefaultRenderer.resize() now ignores its parameters and always resizes to its video element's dimensions. It had already stopped resizing the video element when it was renamed from resizeVideo in v0.6.0, so it doesn't make sense to let it take a completely different width and height.
|
||||
- BREAKING CHANGE - Removed fullscreen support in DefaultRenderer. It started out as a hack using max z-index and works on even fewer browsers now. It probably didn't work for you anyway so it should be no big loss.
|
||||
- Implemented experimental support for \t
|
||||
- Added RendererSettings.fallbackFonts to set the fallback fonts for all styles. Defaults to 'Arial, Helvetica, sans-serif, "Segoe UI Symbol"'.
|
||||
- Better compatibility with loose ASS scripts - assume unnamed first section is Script Info, fall back to Default style for missing styles, recognize arbitrary-case property names, normalize asterisks in style names, etc.
|
||||
- Various font size improvements - faster calculation, fix for incorrect size when line-height is overridden by site CSS, fix for incorrect scaled sizes for letterboxed subs, fix for incorrect metrics for web fonts, etc. The last one requires that all web fonts be specified in RendererSettings.fontMap to be rendered accurately.
|
||||
- WebRenderer now supports using local() URLs in addition to url() in CSS font-face rules.
|
||||
- Added RendererSettings.useAttachedFonts. If true, TTF fonts attached to the script will be used in addition to fonts specified in RendererSettings.fontMap. This setting is false by default, and should only be enabled on trusted fonts since it uses a very naive base64 and TTF parser to extract the font names from the attachment. It also requires ES6 typed arrays - ArrayBuffer, DataView, Uint8Array, etc. in the environment.
|
||||
- Various pre-render, SVG filter and DOM perf improvements.
|
||||
- Fixed \fscx and \fscy to not scale shadows.
|
||||
- Fixed \fscx and \fscy to have optional values.
|
||||
- Fixed \fs+ and \fs- to have required values.
|
||||
- Fixed \r<target_style> to use the target style's alpha values instead of 1.
|
||||
- Fixed \fad subs to not flash after the fade-out ends with low-resolution clocks.
|
||||
- Fixed outlines to not be darker than they should be.
|
||||
- Fixed styles to not ignore the ScaleX and ScaleY properties in the script.
|
||||
- Fixed lack of sufficient space between normal and italic text.
|
||||
- Fixed SVG filters to interpolate in sRGB space instead of RGB.
|
||||
- Fixed ASS parser to complain if a script doesn't have a Script Info section at all.
|
||||
- The promise returned from ASS.from*() is now properly rejected due to errors from loading the script, instead of just remaining unresolved forever.
|
||||
- Fixed SRT parser to swallow UTF-8 BOM just like the ASS parser.
|
||||
- Fixed all clocks to suppress redundant ticks if the current timestamp hasn't change from the last tick.
|
||||
- Fixed {AutoClock, VideoClock}.{setEnabled, toggle} methods to actually enable / disable the high-resolution timer.
|
||||
|
||||
|
||||
### v0.10.0 - 2015/05/05
|
||||
- Implemented libjass.renderers.AutoClock, a clock that automatically ticks and generates clock events according to the state of an external driver.
|
||||
- Implemented \k
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[](https://travis-ci.org/Arnavion/libjass)
|
||||
|
||||
libjass is a JavaScript library written in TypeScript to render ASS subs on HTML5 video in the browser. [Check out the demo.](http://arnavion.github.io/libjass/demo/index.xhtml)
|
||||
libjass is a JavaScript library written in TypeScript to render ASS subs in the browser. [Check out the demo.](http://arnavion.github.io/libjass/demo/index.xhtml)
|
||||
|
||||
|
||||
### What's special about libjass?
|
||||
@@ -14,7 +14,13 @@ As a result, libjass is able to render subtitles with very low CPU usage. The do
|
||||
|
||||
### I want to use libjass for my website. What do I need to do?
|
||||
|
||||
You can install the latest release of libjass using npm with `npm install libjass` or using bower with `bower install https://github.com/Arnavion/libjass/releases/download/<release name>/libjass.zip`. Inside the package, you will find libjass.js and libjass.css, which you need to load on your website with your video.
|
||||
You can install the latest release of libjass
|
||||
|
||||
* using npm with `npm install libjass` and load with `var libjass = require("libjass");`
|
||||
* using bower with `bower install https://github.com/Arnavion/libjass/releases/download/<release name>/libjass.zip`
|
||||
* using jspm with `jspm install github:Arnavion/libjass` and load with `import libjass from "Arnavion/libjass";`
|
||||
|
||||
Inside the package, you will find libjass.js and libjass.css, which you need to load on your website with your video.
|
||||
|
||||
Alternatively, you can build libjass from source by cloning this repository and running `npm install`. This will install the dependencies and run the build. libjass.js and libjass.css will be found in the lib/ directory.
|
||||
|
||||
@@ -25,7 +31,7 @@ Only libjass.js and libjass.css are needed to use libjass on your website. The o
|
||||
|
||||
* The src/ directory contains the source of libjass. They are TypeScript files and get compiled into JavaScript for the browser using the TypeScript compiler.
|
||||
|
||||
* gulpfile.js is the build script. The build command will use this script to build libjass.js. The gulplib/ directory contains other files used for the build.
|
||||
* build.js is the build script. The build command will use this script to build libjass.js. The build/ directory contains other files used for the build.
|
||||
|
||||
* The tests/ directory contains unit and functional tests.
|
||||
|
||||
@@ -42,9 +48,9 @@ The API documentation is linked in the Links section below. Here's an overview:
|
||||
|
||||
* The renderer uses [window.requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame) as a source of timer ticks. In each tick, it determines the set of dialogues to be shown at the current video time, renders each of them as a div, and appendChild's the div into the appropriate layer+alignment div.
|
||||
|
||||
* The renderer can be told to dynamically change the size of the subtitles based on user input by calling [WebRenderer.resize()](http://arnavion.github.io/libjass/api.xhtml#libjass.renderers.WebRenderer.resize) DefaultRenderer also handles resizing the subtitles automatically when the user clicks the browser's native fullscreen-video button.
|
||||
* The renderer can be told to dynamically change the size of the subtitles based on user input by calling [WebRenderer.resize()](http://arnavion.github.io/libjass/api.xhtml#libjass.renderers.WebRenderer.resize)
|
||||
|
||||
* Lastly, the renderer contains an implementation of preloading fonts before playing the video. It uses a map of font names to URLs - this map can be conveniently created from a CSS file containing @font-face rules using [RendererSettings.makeFontMapFromStyleElement()](http://arnavion.github.io/libjass/api.xhtml#libjass.renderers.RendererSettings.makeFontMapFromStyleElement)
|
||||
* Lastly, the renderer contains an implementation of preloading fonts before playing the video. It uses a map of font names to URLs - this map can be conveniently created from a CSS file containing @font-face rules using [libjass.renderers.makeFontMapFromStyleElement()](http://arnavion.github.io/libjass/api.xhtml#libjass.renderers.makeFontMapFromStyleElement)
|
||||
|
||||
* For an example of using libjass, check out [the demo.](http://arnavion.github.io/libjass/demo/index.xhtml) It has comments explaining basic usage and pointers to some advanced usage.
|
||||
|
||||
@@ -61,6 +67,8 @@ The API documentation is linked in the Links section below. Here's an overview:
|
||||
|
||||
* WebRenderer and DefaultRenderer use [CSS3 animations](http://caniuse.com/#feat=css-animation) for effects like \mov and \fad.
|
||||
|
||||
* Using fonts attached to the script requires [ES6 typed arrays](http://caniuse.com/#feat=typedarrays) (ArrayBuffer, DataView, Uint8Array, etc).
|
||||
|
||||
|
||||
### Can I use libjass in node?
|
||||
|
||||
@@ -119,13 +127,13 @@ You can also join the IRC channel in the links section below and ask any questio
|
||||
### Supported features
|
||||
|
||||
* Styles: Italic, Bold, Underline, StrikeOut, FontName, FontSize, ScaleX, ScaleY, Spacing, PrimaryColor, OutlineColor, BackColor, Outline, Shadow, Alignment, MarginL, MarginR, MarginV
|
||||
* Tags: \i, \b, \u, \s, \bord, \xbord, \ybord, \shad, \xshad, \yshad, \be, \blur, \fn, \fs, \fscx, \fscy, \fsp, \frx, \fry, \frz, \fr, \fax, \fay, \c, \1c, \3c, \4c, \alpha, \1a, \3a, \4a, \an, \a, \k, \q, \r, \pos, \move, \fad, \fade, \p
|
||||
* Tags: \i, \b, \u, \s, \bord, \xbord, \ybord, \shad, \xshad, \yshad, \be, \blur, \fn, \fs, \fscx, \fscy, \fsp, \frx, \fry, \frz, \fr, \fax, \fay, \c, \1c, \3c, \4c, \alpha, \1a, \3a, \4a, \an, \a, \k, \q, \r, \pos, \move, \fad, \fade, \t (experimental), \p
|
||||
* Custom fonts, using CSS web fonts.
|
||||
|
||||
|
||||
### Known issues
|
||||
|
||||
* Unsupported tags: \fe, \2c, \2a, \K, \kf, \ko, \org, \t, \clip, \iclip
|
||||
* Unsupported tags: \fe, \2c, \2a, \K, \kf, \ko, \org, \clip, \iclip
|
||||
* \an4, \an5, \an6 aren't positioned correctly.
|
||||
* Smart line wrapping is not supported. Such lines are rendered as [wrapping style 1 (end-of-line wrapping).](http://docs.aegisub.org/3.0/ASS_Tags/#wrapstyle)
|
||||
* Lines with multiple rotations aren't rotated the same as VSFilter or libass. See [#14](https://github.com/Arnavion/libjass/issues/14)
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
var stream = require("stream");
|
||||
|
||||
var async = require("async");
|
||||
|
||||
var task = require("async-build");
|
||||
|
||||
(function () {
|
||||
var _TypeScript = null;
|
||||
var _UglifyJS = null;
|
||||
var _npm = null;
|
||||
|
||||
Object.defineProperties(global, {
|
||||
TypeScript: { get: function () { return _TypeScript || (_TypeScript = require("./build/typescript/index.js")); } },
|
||||
UglifyJS: { get: function () { return _UglifyJS || (_UglifyJS = require("./build/uglify.js")); } },
|
||||
npm: { get: function () { return _npm || (_npm = require("npm")); } },
|
||||
});
|
||||
})();
|
||||
|
||||
task("build-tools", function (callback) {
|
||||
async.every(["./build/typescript/typescript.d.ts", "./build/typescript/index.js", "./build/doc.js"], fs.exists.bind(fs), function (allExist) {
|
||||
if (allExist) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
var typescriptPath = path.join(require.resolve("typescript"), "..", "..");
|
||||
|
||||
async.waterfall([
|
||||
async.parallel.bind(async, [
|
||||
fs.readFile.bind(fs, path.join(typescriptPath, "lib", "typescript.d.ts"), "utf8"),
|
||||
fs.readFile.bind(fs, "./build/typescript/extras.d.ts", "utf8")
|
||||
]),
|
||||
function (results, callback) {
|
||||
var newDts = results[0] + "\n\n" + results[1];
|
||||
fs.writeFile("./build/typescript/typescript.d.ts", newDts, "utf8", callback);
|
||||
},
|
||||
function (callback) {
|
||||
npm.load(function () {
|
||||
npm.commands["run-script"](["build"], callback);
|
||||
});
|
||||
}
|
||||
], callback);
|
||||
});
|
||||
});
|
||||
|
||||
task("default", ["libjass.js", "libjass.min.js"]);
|
||||
|
||||
task("libjass.js", ["build-tools"], function (callback) {
|
||||
fs.exists("./lib/libjass.js", function (exists) {
|
||||
if (exists) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, task.src("./src/tsconfig.json")
|
||||
.pipe(TypeScript.build("./src/index.ts", "libjass"))
|
||||
.pipe(UglifyJS.build("./src/index", "libjass", ["AttachmentType", "BorderStyle", "ClockEvent", "Format", "WorkerCommands", "WrappingStyle"]))
|
||||
.pipe(task.dest("./lib")));
|
||||
});
|
||||
});
|
||||
|
||||
task("libjass.min.js", ["libjass.js"], function (callback) {
|
||||
fs.exists("./lib/libjass.min.js", function (exists) {
|
||||
if (exists) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, task.src(["./lib/libjass.js", "./lib/libjass.js.map"], { relativeTo: "./lib" })
|
||||
.pipe(UglifyJS.minify())
|
||||
.pipe(task.dest("./lib")));
|
||||
});
|
||||
});
|
||||
|
||||
task("clean", task.clean(["./lib/libjass.js", "./lib/libjass.js.map", "./lib/libjass.min.js", "./lib/libjass.min.js.map"]));
|
||||
|
||||
task("watch", ["build-tools"], function (callback) {
|
||||
npm.load(function () {
|
||||
var testRunning = false;
|
||||
var rerunTest = false;
|
||||
|
||||
var startTest = function () {
|
||||
npm.commands["run-script"](["test-lib"], function () {
|
||||
testRunning = false;
|
||||
|
||||
if (rerunTest) {
|
||||
startTest();
|
||||
rerunTest = false;
|
||||
}
|
||||
});
|
||||
|
||||
testRunning = true;
|
||||
};
|
||||
|
||||
var runTest = function () {
|
||||
if (!testRunning) {
|
||||
startTest();
|
||||
}
|
||||
else {
|
||||
rerunTest = true;
|
||||
}
|
||||
};
|
||||
|
||||
task.src("./src/tsconfig.json")
|
||||
.pipe(TypeScript.watch("./src/index.ts", "libjass"))
|
||||
.pipe(UglifyJS.watch("./src/index", "libjass", ["BorderStyle", "ClockEvent", "Format", "WorkerCommands", "WrappingStyle"]))
|
||||
.pipe(task.dest("./lib"))
|
||||
.pipe(new task.FileTransform(function (file) {
|
||||
if (file.path === "libjass.js") {
|
||||
runTest();
|
||||
}
|
||||
}));
|
||||
|
||||
task.watch("./tests/unit/", runTest);
|
||||
});
|
||||
});
|
||||
|
||||
task("test-lib", ["libjass.js"], function (callback) {
|
||||
npm.load(function () {
|
||||
npm.commands["run-script"](["test-lib"], callback);
|
||||
});
|
||||
});
|
||||
|
||||
task("test-minified", ["libjass.min.js"], function (callback) {
|
||||
npm.load(function () {
|
||||
npm.commands["run-script"](["test-minified"], callback);
|
||||
});
|
||||
});
|
||||
|
||||
// Start Selenium server with
|
||||
// java.exe -jar .\selenium-server-standalone-2.48.2.jar -Dwebdriver.ie.driver=IEDriverServer.exe -Dwebdriver.chrome.driver=chromedriver.exe
|
||||
task("test-browser", ["libjass.js"], function (callback) {
|
||||
npm.load(function () {
|
||||
npm.commands["run-script"](["test-browser"], callback);
|
||||
});
|
||||
});
|
||||
|
||||
task("test", ["test-lib", "test-minified"]);
|
||||
|
||||
task("demo", ["libjass.js"], function () {
|
||||
return task.src(["./lib/libjass.js", "./lib/libjass.js.map", "./lib/libjass.css"], { relativeTo: "./lib" }).pipe(task.dest("../libjass-gh-pages/demo/"));
|
||||
});
|
||||
|
||||
task("doc", ["make-doc", "test-doc"]);
|
||||
|
||||
task("make-doc", ["build-tools"], function () {
|
||||
var Doc = require("./build/doc.js");
|
||||
|
||||
return task.src("./src/tsconfig.json")
|
||||
.pipe(Doc.build("./api.xhtml", "./src/index.ts", "libjass"))
|
||||
.pipe(task.dest("../libjass-gh-pages/"));
|
||||
});
|
||||
|
||||
task("test-doc", ["make-doc", "libjass.js"], function (callback) {
|
||||
npm.load(function () {
|
||||
npm.commands["run-script"](["test-doc"], callback);
|
||||
});
|
||||
});
|
||||
|
||||
task("dist", ["libjass.js", "libjass.min.js", "test", "test-browser", "demo", "doc"], function (callback) {
|
||||
var inputFiles = [
|
||||
"./README.md", "./CHANGELOG.md", "./LICENSE",
|
||||
"./lib/libjass.js", "./lib/libjass.js.map",
|
||||
"./lib/libjass.min.js", "./lib/libjass.min.js.map",
|
||||
"./lib/libjass.css"
|
||||
];
|
||||
|
||||
var files = Object.create(null);
|
||||
inputFiles.forEach(function (filename) {
|
||||
files["./dist/" + path.basename(filename)] = filename;
|
||||
});
|
||||
|
||||
async.series([
|
||||
// Clean dist/
|
||||
task.clean(Object.keys(files).concat(["./dist/package.json"])),
|
||||
|
||||
// Create dist/ if necessary
|
||||
function (callback) {
|
||||
fs.mkdir("./dist", function (err) {
|
||||
if (err && err.code !== "EEXIST") {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
|
||||
// Copy all files except package.json
|
||||
async.forEachOf.bind(async, files, function (inputFilename, outputFilename, callback) {
|
||||
async.waterfall([fs.readFile.bind(fs, inputFilename), fs.writeFile.bind(fs, outputFilename)], callback);
|
||||
}),
|
||||
|
||||
// Copy package.json
|
||||
async.waterfall.bind(async, [
|
||||
fs.readFile.bind(fs, "./package.json"),
|
||||
function (data, callback) {
|
||||
try {
|
||||
var packageJson = JSON.parse(data);
|
||||
packageJson.devDependencies = undefined;
|
||||
packageJson.private = undefined;
|
||||
packageJson.scripts = undefined;
|
||||
packageJson.main = "libjass.js";
|
||||
}
|
||||
catch (ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, new Buffer(JSON.stringify(packageJson, null, "\t")));
|
||||
},
|
||||
fs.writeFile.bind(fs, "./dist/package.json")
|
||||
])
|
||||
], callback);
|
||||
});
|
||||
|
||||
task.runArgv(function (err) {
|
||||
if (err) {
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
@@ -21,9 +21,8 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { Transform } from "stream";
|
||||
import Vinyl = require("vinyl");
|
||||
|
||||
import { makeTransform } from "./helpers";
|
||||
import { File, FileTransform } from "async-build";
|
||||
|
||||
import * as AST from "./typescript/ast";
|
||||
import { Compiler } from "./typescript/compiler";
|
||||
@@ -32,7 +31,7 @@ import { walk } from "./typescript/walker";
|
||||
function flatten<T>(arr: T[][]): T[] {
|
||||
var result: T[] = [];
|
||||
|
||||
for (let a of arr) {
|
||||
for (const a of arr) {
|
||||
result = result.concat(a);
|
||||
}
|
||||
|
||||
@@ -221,6 +220,9 @@ function writeDescription(text: string): string {
|
||||
inCodeBlock = false;
|
||||
line = `</code></pre>${ line }`;
|
||||
}
|
||||
else if (line.length === 0 && !inCodeBlock) {
|
||||
line = "</p><p>";
|
||||
}
|
||||
|
||||
return line;
|
||||
}).join("\n");
|
||||
@@ -229,6 +231,12 @@ function writeDescription(text: string): string {
|
||||
result += '</code></pre>';
|
||||
}
|
||||
|
||||
result = `<p>${ result }</p>`.replace(/<p>\s*<\/p>/g, "").replace(/<p>\s+/g, "<p>").replace(/\s+<\/p>/g, "</p>");
|
||||
|
||||
if (result.substr("<p>".length).indexOf("<p>") === -1) {
|
||||
result = result.substring("<p>".length, result.length - "</p>".length);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -261,7 +269,7 @@ function functionToHtml(func: AST.Function): string[] {
|
||||
func.isStatic ? ' static' : ''}">`,
|
||||
` <dt class="name">${ toLink(func) }</dt>`,
|
||||
' <dd class="description">',
|
||||
` <p>${ writeDescription(func.description) }</p>`,
|
||||
` ${ writeDescription(func.description) }`,
|
||||
' </dd>',
|
||||
` <dd class="usage"><fieldset><legend /><pre><code>${
|
||||
sanitize(
|
||||
@@ -292,7 +300,7 @@ function interfaceToHtml(interfase: AST.Interface): string[] {
|
||||
interfase.baseTypes.map(baseType => baseType instanceof AST.TypeReference ? toLink(baseType) : baseType.name).join(', ')
|
||||
}` : '' }</dt>`,
|
||||
' <dd class="description">',
|
||||
` <p>${ writeDescription(interfase.description) }</p>`,
|
||||
` ${ writeDescription(interfase.description) }`,
|
||||
' </dd>',
|
||||
' <dd class="members">'
|
||||
].concat(flatten(members.map(member => {
|
||||
@@ -326,7 +334,7 @@ function classToHtml(clazz: AST.Class): string[] {
|
||||
(clazz.baseType !== null) ? ` extends ${ (clazz.baseType instanceof AST.TypeReference) ? toLink(<AST.TypeReference>clazz.baseType) : clazz.baseType.name }` : '' }${
|
||||
(clazz.interfaces.length > 0) ? ` implements ${ clazz.interfaces.map(interfase => interfase instanceof AST.TypeReference ? toLink(interfase) : interfase.name).join(', ') }` : ''}</dt>`,
|
||||
' <dd class="description">',
|
||||
` <p>${ writeDescription(clazz.description) }</p>`,
|
||||
` ${ writeDescription(clazz.description) }`,
|
||||
' </dd>',
|
||||
` <dd class="usage"><fieldset><legend /><pre><code>${
|
||||
sanitize(
|
||||
@@ -357,7 +365,7 @@ function enumToHtml(enumType: AST.Enum): string[] {
|
||||
`<dl id="${ toId(enumType) }" class="enum${ enumType.isPrivate ? ' private' : '' }">`,
|
||||
` <dt class="name">enum <a href="#${ sanitize(enumType.fullName) }">${ sanitize(enumType.name) }</a></dt>`,
|
||||
' <dd class="description">',
|
||||
` <p>${ writeDescription(enumType.description) }</p>`,
|
||||
` ${ writeDescription(enumType.description) }`,
|
||||
' </dd>'
|
||||
].concat([
|
||||
' <dd class="members">'
|
||||
@@ -365,7 +373,7 @@ function enumToHtml(enumType: AST.Enum): string[] {
|
||||
` <dl id="${ toId(member) }" class="member">`,
|
||||
` <dt class="name">${ toLink(member) } = ${ member.value }</dt>`,
|
||||
' <dd class="description">',
|
||||
` <p>${ writeDescription(member.description) }</p>`,
|
||||
` ${ writeDescription(member.description) }`,
|
||||
' </dd>',
|
||||
' </dl>'
|
||||
]))).concat([
|
||||
@@ -382,14 +390,14 @@ function propertyToHtml(property: AST.Property): string[] {
|
||||
].concat((property.getter === null) ? [] : [
|
||||
` <dt class="getter${ property.getter.isPrivate ? ' private' : '' }">Getter</dt>`,
|
||||
' <dd class="description">',
|
||||
` <p>${ writeDescription(property.getter.description) }</p>`,
|
||||
` ${ writeDescription(property.getter.description) }`,
|
||||
' </dd>',
|
||||
` <dd class="usage"><fieldset><legend /><pre><code>${ sanitize(`var result = ${ toUsageName(property) };`) }</code></pre></fieldset></dd>`,
|
||||
` <dd class="return type">${ sanitize(property.getter.type) }</dd>`
|
||||
]).concat((property.setter === null) ? [] : [
|
||||
` <dt class="setter${ property.setter.isPrivate ? ' private' : '' }">Setter</dt>`,
|
||||
' <dd class="description">',
|
||||
` <p>${ writeDescription(property.setter.description) }</p>`,
|
||||
` ${ writeDescription(property.setter.description) }`,
|
||||
' </dd>',
|
||||
` <dd class="usage"><fieldset><legend /><pre><code>${ sanitize(`${ toUsageName(property) } = value;`) }</code></pre></fieldset></dd>`
|
||||
].concat(writeParameters([new AST.Parameter("value", "", property.setter.type)]).map(indenter(1)))).concat([
|
||||
@@ -398,11 +406,11 @@ function propertyToHtml(property: AST.Property): string[] {
|
||||
]);
|
||||
}
|
||||
|
||||
export function gulp(outputFilePath: string, root: string, rootNamespaceName: string): any {
|
||||
export function build(outputFilePath: string, root: string, rootNamespaceName: string): FileTransform {
|
||||
var compiler = new Compiler();
|
||||
|
||||
return makeTransform(function (file: Vinyl): void {
|
||||
var self: Transform<Vinyl> = this;
|
||||
return new FileTransform(function (file: File): void {
|
||||
var self: FileTransform = this;
|
||||
|
||||
// Compile
|
||||
compiler.compile(file);
|
||||
@@ -422,7 +430,7 @@ export function gulp(outputFilePath: string, root: string, rootNamespaceName: st
|
||||
|
||||
moduleNames = moduleNames.filter(moduleName => Object.keys(modules[moduleName].members).length > 0);
|
||||
|
||||
self.push(new Vinyl({
|
||||
self.push({
|
||||
path: outputFilePath,
|
||||
contents: Buffer.concat([new Buffer(
|
||||
`<?xml version="1.0" encoding="utf-8" ?>
|
||||
@@ -585,7 +593,7 @@ export function gulp(outputFilePath: string, root: string, rootNamespaceName: st
|
||||
var namespace = namespaces[namespaceName];
|
||||
|
||||
var namespaceMembers: AST.NamespaceMember[] = [];
|
||||
for (let memberName of Object.keys(namespace.members)) {
|
||||
for (const memberName of Object.keys(namespace.members)) {
|
||||
namespaceMembers.push(namespace.members[memberName]);
|
||||
}
|
||||
|
||||
@@ -609,7 +617,7 @@ export function gulp(outputFilePath: string, root: string, rootNamespaceName: st
|
||||
var module = modules[moduleName];
|
||||
|
||||
var moduleMembers: AST.ModuleMemberWithoutReference[] = [];
|
||||
for (let memberName of Object.keys(module.members)) {
|
||||
for (const memberName of Object.keys(module.members)) {
|
||||
var member = module.members[memberName];
|
||||
if ((<AST.HasParent><any>member).parent === module) {
|
||||
moduleMembers.push(<AST.ModuleMemberWithoutReference>member);
|
||||
@@ -645,7 +653,7 @@ export function gulp(outputFilePath: string, root: string, rootNamespaceName: st
|
||||
var namespace = namespaces[namespaceName];
|
||||
|
||||
var namespaceMembers: AST.NamespaceMember[] = [];
|
||||
for (let memberName of Object.keys(namespace.members)) {
|
||||
for (const memberName of Object.keys(namespace.members)) {
|
||||
namespaceMembers.push(namespace.members[memberName]);
|
||||
}
|
||||
|
||||
@@ -670,7 +678,7 @@ export function gulp(outputFilePath: string, root: string, rootNamespaceName: st
|
||||
`
|
||||
));
|
||||
|
||||
for (let property of properties) {
|
||||
for (const property of properties) {
|
||||
result.push(new Buffer(propertyToHtml(property).map(indenter(5)).join("\n")));
|
||||
}
|
||||
|
||||
@@ -687,7 +695,7 @@ export function gulp(outputFilePath: string, root: string, rootNamespaceName: st
|
||||
`
|
||||
));
|
||||
|
||||
for (let func of functions) {
|
||||
for (const func of functions) {
|
||||
result.push(new Buffer(functionToHtml(func).map(indenter(5)).join("\n")));
|
||||
}
|
||||
|
||||
@@ -704,7 +712,7 @@ export function gulp(outputFilePath: string, root: string, rootNamespaceName: st
|
||||
`
|
||||
));
|
||||
|
||||
for (let interfase of interfaces) {
|
||||
for (const interfase of interfaces) {
|
||||
result.push(new Buffer(interfaceToHtml(interfase).map(indenter(5)).join("\n")));
|
||||
}
|
||||
|
||||
@@ -721,7 +729,7 @@ export function gulp(outputFilePath: string, root: string, rootNamespaceName: st
|
||||
`
|
||||
));
|
||||
|
||||
for (let clazz of classes) {
|
||||
for (const clazz of classes) {
|
||||
result.push(new Buffer(classToHtml(clazz).map(indenter(5)).join("\n")));
|
||||
}
|
||||
|
||||
@@ -738,7 +746,7 @@ export function gulp(outputFilePath: string, root: string, rootNamespaceName: st
|
||||
`
|
||||
));
|
||||
|
||||
for (let enumType of enums) {
|
||||
for (const enumType of enums) {
|
||||
result.push(new Buffer(enumToHtml(enumType).map(indenter(5)).join("\n")));
|
||||
}
|
||||
|
||||
@@ -758,7 +766,7 @@ export function gulp(outputFilePath: string, root: string, rootNamespaceName: st
|
||||
var module = modules[moduleName];
|
||||
|
||||
var moduleMembers: AST.ModuleMember[] = [];
|
||||
for (let memberName of Object.keys(module.members)) {
|
||||
for (const memberName of Object.keys(module.members)) {
|
||||
var member = module.members[memberName];
|
||||
if ((<AST.HasParent><any>member).parent === module) {
|
||||
moduleMembers.push(<AST.ModuleMember>member);
|
||||
@@ -790,7 +798,7 @@ export function gulp(outputFilePath: string, root: string, rootNamespaceName: st
|
||||
`
|
||||
));
|
||||
|
||||
for (let property of properties) {
|
||||
for (const property of properties) {
|
||||
result.push(new Buffer(propertyToHtml(property).map(indenter(5)).join("\n")));
|
||||
}
|
||||
|
||||
@@ -807,7 +815,7 @@ export function gulp(outputFilePath: string, root: string, rootNamespaceName: st
|
||||
`
|
||||
));
|
||||
|
||||
for (let func of functions) {
|
||||
for (const func of functions) {
|
||||
result.push(new Buffer(functionToHtml(func).map(indenter(5)).join("\n")));
|
||||
}
|
||||
|
||||
@@ -824,7 +832,7 @@ export function gulp(outputFilePath: string, root: string, rootNamespaceName: st
|
||||
`
|
||||
));
|
||||
|
||||
for (let interfase of interfaces) {
|
||||
for (const interfase of interfaces) {
|
||||
result.push(new Buffer(interfaceToHtml(interfase).map(indenter(5)).join("\n")));
|
||||
}
|
||||
|
||||
@@ -841,7 +849,7 @@ export function gulp(outputFilePath: string, root: string, rootNamespaceName: st
|
||||
`
|
||||
));
|
||||
|
||||
for (let clazz of classes) {
|
||||
for (const clazz of classes) {
|
||||
result.push(new Buffer(classToHtml(clazz).map(indenter(5)).join("\n")));
|
||||
}
|
||||
|
||||
@@ -858,7 +866,7 @@ export function gulp(outputFilePath: string, root: string, rootNamespaceName: st
|
||||
`
|
||||
));
|
||||
|
||||
for (let enumType of enums) {
|
||||
for (const enumType of enums) {
|
||||
result.push(new Buffer(enumToHtml(enumType).map(indenter(5)).join("\n")));
|
||||
}
|
||||
|
||||
@@ -880,6 +888,6 @@ export function gulp(outputFilePath: string, root: string, rootNamespaceName: st
|
||||
</html>
|
||||
`
|
||||
)]))
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Type definitions for Node.js v0.12.0
|
||||
// Project: http://nodejs.org/
|
||||
// Definitions by: Microsoft TypeScript <http://typescriptlang.org>, DefinitelyTyped <https://github.com/borisyankov/DefinitelyTyped>
|
||||
// Definitions: https://github.com/borisyankov/DefinitelyTyped
|
||||
|
||||
declare var require: {
|
||||
resolve(id: string): string;
|
||||
};
|
||||
|
||||
declare module "fs" {
|
||||
export function existsSync(filename: string): boolean;
|
||||
export function readFileSync(filename: string, options: { encoding: string }): string;
|
||||
}
|
||||
|
||||
declare module "path" {
|
||||
export function basename(p: string, ext?: string): string;
|
||||
export function extname(p: string): string;
|
||||
}
|
||||
@@ -22,30 +22,26 @@ import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { Transform } from "stream";
|
||||
import * as ts from "typescript";
|
||||
import Vinyl = require("vinyl");
|
||||
|
||||
import { File, FileTransform, FileWatcher } from "async-build";
|
||||
|
||||
import * as AST from "./ast";
|
||||
import { makeTransform } from "../helpers";
|
||||
import { walk } from "./walker";
|
||||
|
||||
export interface GulpCompilerHost extends ts.CompilerHost {
|
||||
setOutputStream(outputStream: Transform<Vinyl>): void;
|
||||
|
||||
setOutputPathsRelativeTo(path: string): void;
|
||||
export interface StreamingCompilerHost extends ts.CompilerHost, ts.ParseConfigHost {
|
||||
setOutputStream(outputStream: FileTransform): void;
|
||||
}
|
||||
|
||||
export class Compiler {
|
||||
private _projectRoot: string = null;
|
||||
private _program: ts.Program = null;
|
||||
|
||||
constructor(private _host: GulpCompilerHost = CompilerHost()) { }
|
||||
constructor(private _host: StreamingCompilerHost = new CompilerHost()) { }
|
||||
|
||||
compile(projectConfigFile: Vinyl) {
|
||||
compile(projectConfigFile: File) {
|
||||
this._projectRoot = path.dirname(projectConfigFile.path);
|
||||
|
||||
var projectConfig = parseConfigFile(JSON.parse(projectConfigFile.contents.toString()), this._projectRoot);
|
||||
|
||||
this._host.setOutputPathsRelativeTo(this._projectRoot);
|
||||
var projectConfig = ts.parseJsonConfigFileContent(JSON.parse(projectConfigFile.contents.toString()), this._host, this._projectRoot);
|
||||
|
||||
this._program = ts.createProgram(projectConfig.fileNames, projectConfig.options, this._host);
|
||||
|
||||
@@ -68,7 +64,7 @@ export class Compiler {
|
||||
}
|
||||
};
|
||||
|
||||
writeFiles(outputStream: Transform<Vinyl>) {
|
||||
writeFiles(outputStream: FileTransform) {
|
||||
this._host.setOutputStream(outputStream);
|
||||
|
||||
var emitDiagnostics = this._program.emit().diagnostics;
|
||||
@@ -91,7 +87,7 @@ export class Compiler {
|
||||
}
|
||||
|
||||
private _reportDiagnostics(diagnostics: ts.Diagnostic[]) {
|
||||
for (let diagnostic of diagnostics) {
|
||||
for (const diagnostic of diagnostics) {
|
||||
var message = "";
|
||||
|
||||
if (diagnostic.file) {
|
||||
@@ -109,124 +105,118 @@ export class Compiler {
|
||||
};
|
||||
}
|
||||
|
||||
const typeScriptModulePath = path.resolve("./node_modules/typescript/bin");
|
||||
const typeScriptModulePath = path.dirname(require.resolve("typescript"));
|
||||
|
||||
function CompilerHost(): GulpCompilerHost {
|
||||
var _outputStream: Transform<Vinyl> = null;
|
||||
var _outputPathsRelativeTo: string = null;
|
||||
class CompilerHost implements StreamingCompilerHost {
|
||||
protected _sourceFiles = Object.create(null);
|
||||
|
||||
return {
|
||||
setOutputStream(outputStream: Transform<Vinyl>): void {
|
||||
_outputStream = outputStream;
|
||||
},
|
||||
private _outputStream: FileTransform = null;
|
||||
|
||||
setOutputPathsRelativeTo(path: string): void {
|
||||
_outputPathsRelativeTo = path;
|
||||
},
|
||||
setOutputStream(outputStream: FileTransform): void {
|
||||
this._outputStream = outputStream;
|
||||
}
|
||||
|
||||
// ts.CompilerHost members
|
||||
// ts.ModuleResolutionHost members
|
||||
|
||||
getSourceFile(fileName: string, languageVersion: ts.ScriptTarget, onError: (message: string) => void): ts.SourceFile {
|
||||
try {
|
||||
var text = fs.readFileSync(fileName, { encoding: "utf8" });
|
||||
fileExists(fileName: string): boolean {
|
||||
return fs.existsSync(fileName);
|
||||
}
|
||||
|
||||
if (path.basename(fileName) === "lib.dom.d.ts") {
|
||||
var startOfES6Extensions = text.indexOf("/// IE11 ECMAScript Extensions");
|
||||
var endOfES6Extensions = text.indexOf("/// ECMAScript Internationalization API", startOfES6Extensions);
|
||||
text = text.substring(0, startOfES6Extensions) + text.substring(endOfES6Extensions);
|
||||
}
|
||||
}
|
||||
catch (ex) {
|
||||
if (onError) {
|
||||
onError(ex.message);
|
||||
}
|
||||
readFile(fileName: string): string {
|
||||
if (!this.fileExists(fileName)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return fs.readFileSync(fileName, { encoding: "utf8" });
|
||||
}
|
||||
|
||||
// ts.CompilerHost members
|
||||
|
||||
getSourceFile(fileName: string, languageVersion: ts.ScriptTarget, onError: (message: string) => void): ts.SourceFile {
|
||||
if (fileName in this._sourceFiles) {
|
||||
return this._sourceFiles[fileName];
|
||||
}
|
||||
|
||||
try {
|
||||
var text = fs.readFileSync(fileName, { encoding: "utf8" });
|
||||
var result = ts.createSourceFile(fileName, text, ts.ScriptTarget.ES5);
|
||||
this._sourceFiles[fileName] = result;
|
||||
}
|
||||
catch (ex) {
|
||||
if (onError) {
|
||||
onError(ex.message);
|
||||
}
|
||||
}
|
||||
|
||||
return (text !== undefined) ? ts.createSourceFile(fileName, text, ts.ScriptTarget.ES5) : undefined;
|
||||
},
|
||||
return result;
|
||||
}
|
||||
|
||||
getDefaultLibFileName: () => path.join(typeScriptModulePath, "lib.dom.d.ts"),
|
||||
getDefaultLibFileName(): string {
|
||||
return path.join(typeScriptModulePath, "lib.dom.d.ts");
|
||||
}
|
||||
|
||||
writeFile(fileName: string, data: string, writeByteOrderMark: boolean, onError: (message?: string) => void) {
|
||||
_outputStream.push(new Vinyl({
|
||||
base: _outputPathsRelativeTo,
|
||||
path: fileName,
|
||||
contents: new Buffer(data)
|
||||
}));
|
||||
},
|
||||
writeFile(fileName: string, data: string, writeByteOrderMark: boolean, onError: (message?: string) => void): void {
|
||||
this._outputStream.push({
|
||||
path: fileName,
|
||||
contents: new Buffer(data)
|
||||
});
|
||||
}
|
||||
|
||||
getCurrentDirectory: () => path.resolve("."),
|
||||
getCurrentDirectory(): string {
|
||||
return path.resolve(".");
|
||||
}
|
||||
|
||||
getCanonicalFileName: (fileName: string) => ts.normalizeSlashes(path.resolve(fileName)),
|
||||
getCanonicalFileName(fileName: string): string {
|
||||
return ts.normalizeSlashes(path.resolve(fileName));
|
||||
}
|
||||
|
||||
useCaseSensitiveFileNames: () => true,
|
||||
useCaseSensitiveFileNames(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
getNewLine: () => "\n",
|
||||
};
|
||||
getNewLine(): string {
|
||||
return "\n";
|
||||
}
|
||||
|
||||
// ts.ParseConfigHost members
|
||||
|
||||
readDirectory(rootDir: string, extension: string, exclude: string[]): string[] {
|
||||
return ts.sys.readDirectory(rootDir, extension, exclude).map(fileName => this.getCanonicalFileName(fileName));
|
||||
}
|
||||
}
|
||||
|
||||
function WatchCompilerHost(onChangeCallback: () => void): GulpCompilerHost {
|
||||
var compilerHost = CompilerHost();
|
||||
class WatchCompilerHost extends CompilerHost {
|
||||
private _fileWatcher = new FileWatcher(fileNames => this._onFilesChanged(fileNames));
|
||||
|
||||
var _onChangeCallback = onChangeCallback;
|
||||
private _filesChangedSinceLast: string[] = [];
|
||||
|
||||
var _sourceFiles = Object.create(null);
|
||||
constructor(private _onChangeCallback: () => void) {
|
||||
super();
|
||||
}
|
||||
|
||||
var _filesChangedSinceLast: string[] = [];
|
||||
|
||||
var _super_getSourceFile = compilerHost.getSourceFile;
|
||||
compilerHost.getSourceFile = (fileName, languageVersion, onError) => {
|
||||
if (fileName in _sourceFiles) {
|
||||
return _sourceFiles[fileName];
|
||||
}
|
||||
|
||||
var result = _super_getSourceFile(fileName, languageVersion, onError);
|
||||
getSourceFile(fileName: string, languageVersion: ts.ScriptTarget, onError: (message: string) => void): ts.SourceFile {
|
||||
var result = super.getSourceFile(fileName, languageVersion, onError);
|
||||
if (result !== undefined) {
|
||||
_sourceFiles[fileName] = result;
|
||||
this._fileWatcher.watchFile(fileName);
|
||||
}
|
||||
|
||||
watchFile(fileName);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
function watchFile(fileName: string): void {
|
||||
function watchFileCallback(currentFile: fs.Stats, previousFile: fs.Stats) {
|
||||
if (currentFile.mtime >= previousFile.mtime) {
|
||||
fileChangedCallback(fileName);
|
||||
}
|
||||
else {
|
||||
fs.unwatchFile(fileName, watchFileCallback);
|
||||
|
||||
fileChangedCallback(fileName);
|
||||
}
|
||||
private _onFilesChanged(fileNames: string[]) {
|
||||
for (const fileName of fileNames) {
|
||||
delete this._sourceFiles[fileName];
|
||||
}
|
||||
|
||||
fs.watchFile(fileName, { interval: 500 }, watchFileCallback);
|
||||
this._onChangeCallback();
|
||||
}
|
||||
|
||||
function fileChangedCallback(fileName: string): void {
|
||||
delete _sourceFiles[fileName];
|
||||
|
||||
if (_filesChangedSinceLast.length === 0) {
|
||||
setTimeout(() => {
|
||||
_filesChangedSinceLast = [];
|
||||
|
||||
_onChangeCallback();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
_filesChangedSinceLast.push(fileName);
|
||||
}
|
||||
|
||||
return compilerHost;
|
||||
}
|
||||
|
||||
export function gulp(root: string, rootNamespaceName: string): Transform<Vinyl> {
|
||||
export function build(root: string, rootNamespaceName: string): FileTransform {
|
||||
var compiler = new Compiler();
|
||||
|
||||
return makeTransform(function (projectConfigFile: Vinyl): void {
|
||||
var self: Transform<Vinyl> = this;
|
||||
return new FileTransform(function (projectConfigFile: File): void {
|
||||
var self: FileTransform = this;
|
||||
|
||||
console.log("Compiling " + projectConfigFile.path + "...");
|
||||
|
||||
@@ -241,9 +231,9 @@ export function gulp(root: string, rootNamespaceName: string): Transform<Vinyl>
|
||||
});
|
||||
}
|
||||
|
||||
export function watch(root: string, rootNamespaceName: string): Transform<Vinyl> {
|
||||
return makeTransform(function (projectConfigFile: Vinyl): void {
|
||||
var self: Transform<Vinyl> = this;
|
||||
export function watch(root: string, rootNamespaceName: string): FileTransform {
|
||||
return new FileTransform(function (projectConfigFile: File): void {
|
||||
var self: FileTransform = this;
|
||||
|
||||
function compile() {
|
||||
console.log("Compiling " + projectConfigFile.path + "...");
|
||||
@@ -254,14 +244,13 @@ export function watch(root: string, rootNamespaceName: string): Transform<Vinyl>
|
||||
|
||||
console.log("Compile succeeded.");
|
||||
|
||||
self.push(new Vinyl({
|
||||
base: this._outputPathsRelativeTo,
|
||||
self.push({
|
||||
path: "END",
|
||||
contents: new Buffer("")
|
||||
}));
|
||||
contents: ""
|
||||
});
|
||||
};
|
||||
|
||||
var compilerHost = WatchCompilerHost(() => {
|
||||
var compilerHost = new WatchCompilerHost(() => {
|
||||
try {
|
||||
compile();
|
||||
}
|
||||
@@ -281,7 +270,7 @@ export function watch(root: string, rootNamespaceName: string): Transform<Vinyl>
|
||||
function addJSDocComments(modules: { [name: string]: AST.Module }): void {
|
||||
function visitor(current: AST.Module | AST.ModuleMember | AST.InterfaceMember) {
|
||||
if (current instanceof AST.Module) {
|
||||
for (let memberName of Object.keys(current.members)) {
|
||||
for (const memberName of Object.keys(current.members)) {
|
||||
visitor(current.members[memberName]);
|
||||
}
|
||||
|
||||
@@ -354,48 +343,21 @@ function addJSDocComments(modules: { [name: string]: AST.Module }): void {
|
||||
var nodes: ts.Node[] = [];
|
||||
if (current.getter !== null) { nodes.push(current.getter.astNode); }
|
||||
if (current.setter !== null && nodes[0] !== current.setter.astNode) { nodes.push(current.setter.astNode); }
|
||||
for (let node of nodes) {
|
||||
(<any>node)["gulp-typescript-new-comment"] = newComments;
|
||||
for (const node of nodes) {
|
||||
(<any>node)["typescript-new-comment"] = newComments;
|
||||
}
|
||||
}
|
||||
else {
|
||||
(<any>(<AST.Class | AST.Interface | AST.Function | AST.Enum>current).astNode)["gulp-typescript-new-comment"] = newComments;
|
||||
(<any>(<AST.Class | AST.Interface | AST.Function | AST.Enum>current).astNode)["typescript-new-comment"] = newComments;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let moduleName of Object.keys(modules)) {
|
||||
for (const moduleName of Object.keys(modules)) {
|
||||
visitor(modules[moduleName]);
|
||||
}
|
||||
}
|
||||
|
||||
function parseConfigFile(json: { compilerOptions: ts.CompilerOptions }, basePath: string): { options: ts.CompilerOptions, fileNames: string[] } {
|
||||
var options = json.compilerOptions;
|
||||
options.module = (<any>ts).ModuleKind[options.module];
|
||||
options.target = (<any>ts).ScriptTarget[options.target];
|
||||
|
||||
var fileNames: string[] = [];
|
||||
|
||||
function walk(directory: string) {
|
||||
fs.readdirSync(directory).forEach(entry => {
|
||||
var entryPath = path.join(directory, entry);
|
||||
var stat = fs.lstatSync(entryPath);
|
||||
if (stat.isFile()) {
|
||||
if (path.extname(entry) === ".ts") {
|
||||
fileNames.push(entryPath);
|
||||
}
|
||||
}
|
||||
else if (stat.isDirectory()) {
|
||||
walk(entryPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
walk(basePath);
|
||||
|
||||
return { options, fileNames };
|
||||
}
|
||||
|
||||
class FakeSourceFile {
|
||||
public text: string;
|
||||
public lineMap: number[];
|
||||
@@ -405,7 +367,7 @@ class FakeSourceFile {
|
||||
this.lineMap = ts.getLineStarts(originalSourceFile).slice();
|
||||
}
|
||||
|
||||
addComment(originalComment: ts.CommentRange, newComments: string[]): ts.CommentRange {
|
||||
addComment(originalComment: ts.CommentRange, newComments: string[]): ts.CommentRange & { sourceFile: FakeSourceFile } {
|
||||
var pos = this.text.length;
|
||||
|
||||
this.text += "/**\n";
|
||||
@@ -420,42 +382,42 @@ class FakeSourceFile {
|
||||
originalCommentLines.splice(originalCommentLines.length - 1, 0, " *");
|
||||
}
|
||||
|
||||
for (let newComment of newComments) {
|
||||
for (const newComment of newComments) {
|
||||
originalCommentLines.splice(originalCommentLines.length - 1, 0, " * " + newComment);
|
||||
}
|
||||
|
||||
for (let newCommentLine of originalCommentLines) {
|
||||
for (const newCommentLine of originalCommentLines) {
|
||||
this.text += newCommentLine + "\n";
|
||||
this.lineMap.push(this.text.length);
|
||||
}
|
||||
|
||||
var end = this.text.length;
|
||||
|
||||
return { pos, end, hasTrailingNewLine: originalComment.hasTrailingNewLine, sourceFile: this };
|
||||
return { pos, end, hasTrailingNewLine: originalComment.hasTrailingNewLine, kind: ts.SyntaxKind.MultiLineCommentTrivia, sourceFile: this };
|
||||
}
|
||||
}
|
||||
|
||||
var fakeSourceFiles: { [name: string]: FakeSourceFile } = Object.create(null);
|
||||
|
||||
export var oldGetLeadingCommentRangesOfNode: (node: ts.Node, sourceFileOfNode: ts.SourceFile) => ts.CommentRange[] = ts.getLeadingCommentRangesOfNode.bind(ts);
|
||||
export var oldGetLeadingCommentRangesOfNode: typeof ts.getLeadingCommentRangesOfNode = ts.getLeadingCommentRangesOfNode.bind(ts);
|
||||
ts.getLeadingCommentRangesOfNode = (node: ts.Node, sourceFileOfNode: ts.SourceFile) => {
|
||||
sourceFileOfNode = sourceFileOfNode || ts.getSourceFileOfNode(node);
|
||||
|
||||
var originalComments = oldGetLeadingCommentRangesOfNode(node, sourceFileOfNode);
|
||||
|
||||
if (originalComments !== undefined && (<any>node)["gulp-typescript-new-comment"] !== undefined) {
|
||||
if (originalComments !== undefined && (<any>node)["typescript-new-comment"] !== undefined) {
|
||||
var fakeSourceFile = fakeSourceFiles[sourceFileOfNode.fileName];
|
||||
if (fakeSourceFile === undefined) {
|
||||
fakeSourceFile = fakeSourceFiles[sourceFileOfNode.fileName] = new FakeSourceFile(sourceFileOfNode);
|
||||
}
|
||||
|
||||
originalComments[originalComments.length - 1] = fakeSourceFile.addComment(originalComments[originalComments.length - 1], (<any>node)["gulp-typescript-new-comment"]);
|
||||
originalComments[originalComments.length - 1] = fakeSourceFile.addComment(originalComments[originalComments.length - 1], (<any>node)["typescript-new-comment"]);
|
||||
}
|
||||
|
||||
return originalComments;
|
||||
};
|
||||
|
||||
var oldWriteCommentRange = ts.writeCommentRange.bind(ts);
|
||||
var oldWriteCommentRange: typeof ts.writeCommentRange = ts.writeCommentRange.bind(ts);
|
||||
ts.writeCommentRange = (currentSourceFile: ts.SourceFile, writer: ts.EmitTextWriter, comment: ts.CommentRange, newLine: string) => {
|
||||
if ((<{ sourceFile: ts.SourceFile }><any>comment).sourceFile) {
|
||||
currentSourceFile = (<{ sourceFile: ts.SourceFile }><any>comment).sourceFile;
|
||||
@@ -0,0 +1,18 @@
|
||||
declare namespace ts {
|
||||
export interface EmitTextWriter { }
|
||||
|
||||
export interface IntrinsicType extends Type {
|
||||
intrinsicName: string;
|
||||
}
|
||||
|
||||
export function forEachValue<T, U>(map: Map<T>, callback: (value: T) => U): U;
|
||||
export function getClassExtendsHeritageClauseElement(node: ClassLikeDeclaration): ExpressionWithTypeArguments;
|
||||
export function getClassImplementsHeritageClauseElements(node: ClassDeclaration): NodeArray<ExpressionWithTypeArguments>;
|
||||
export function getInterfaceBaseTypeNodes(node: InterfaceDeclaration): NodeArray<ExpressionWithTypeArguments>;
|
||||
export function getLeadingCommentRangesOfNode(node: Node, sourceFileOfNode: SourceFile): CommentRange[];
|
||||
export function getLineStarts(sourceFile: SourceFile): number[];
|
||||
export function getSourceFileOfNode(node: Node): SourceFile;
|
||||
export function getTextOfNode(node: Node, includeTrivia?: boolean): string;
|
||||
export function normalizeSlashes(path: string): string;
|
||||
export function writeCommentRange(currentSourceFile: SourceFile, writer: EmitTextWriter, comment: CommentRange, newLine: string): void;
|
||||
}
|
||||
@@ -18,4 +18,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { gulp, watch } from "./compiler";
|
||||
export { build, watch } from "./compiler";
|
||||
@@ -77,7 +77,7 @@ class Walker {
|
||||
var module = this._scope.enter(this.modules[moduleName]);
|
||||
this._currentSourceFile = sourceFile;
|
||||
|
||||
for (let statement of sourceFile.statements) {
|
||||
for (const statement of sourceFile.statements) {
|
||||
this._walk(statement, module);
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ class Walker {
|
||||
case ts.SyntaxKind.ExpressionStatement:
|
||||
case ts.SyntaxKind.ForOfStatement:
|
||||
case ts.SyntaxKind.IfStatement:
|
||||
case ts.SyntaxKind.TypeAliasDeclaration:
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -362,13 +363,13 @@ class Walker {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let declaration of symbol.declarations) {
|
||||
for (const declaration of symbol.declarations) {
|
||||
this._walkClassMember(declaration, clazz);
|
||||
}
|
||||
});
|
||||
|
||||
ts.forEachValue(type.symbol.members, symbol => {
|
||||
for (let declaration of symbol.declarations) {
|
||||
for (const declaration of symbol.declarations) {
|
||||
this._walkClassMember(declaration, clazz);
|
||||
}
|
||||
});
|
||||
@@ -399,7 +400,7 @@ class Walker {
|
||||
parent.members[interfase.name] = interfase;
|
||||
|
||||
ts.forEachValue(type.symbol.members, symbol => {
|
||||
for (let declaration of symbol.declarations) {
|
||||
for (const declaration of symbol.declarations) {
|
||||
this._walkInterfaceMember(declaration, interfase);
|
||||
}
|
||||
});
|
||||
@@ -459,7 +460,7 @@ class Walker {
|
||||
}
|
||||
else if ((<ts.NamedImports>node.importClause.namedBindings).elements !== undefined) {
|
||||
// import { foo, bar } from "baz";
|
||||
for (let element of (<ts.NamedImports>node.importClause.namedBindings).elements) {
|
||||
for (const element of (<ts.NamedImports>node.importClause.namedBindings).elements) {
|
||||
var importedName = element.propertyName && element.propertyName.text || element.name.text;
|
||||
parent.members[element.name.text] = new AST.Reference(moduleName, importedName, true);
|
||||
}
|
||||
@@ -473,14 +474,14 @@ class Walker {
|
||||
if (node.moduleSpecifier !== undefined) {
|
||||
// export { foo } from "bar";
|
||||
var moduleName = this._resolve((<ts.LiteralExpression>node.moduleSpecifier).text, parent);
|
||||
for (let element of node.exportClause.elements) {
|
||||
for (const element of node.exportClause.elements) {
|
||||
var importedName = element.propertyName && element.propertyName.text || element.name.text;
|
||||
parent.members[element.name.text] = new AST.Reference(moduleName, importedName, false);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// export { foo };
|
||||
for (let element of node.exportClause.elements) {
|
||||
for (const element of node.exportClause.elements) {
|
||||
(<AST.CanBePrivate><any>parent.members[element.name.text]).isPrivate = false;
|
||||
}
|
||||
}
|
||||
@@ -524,7 +525,7 @@ class Walker {
|
||||
return "";
|
||||
}
|
||||
return match[1];
|
||||
}).filter(line => line.length > 0);
|
||||
});
|
||||
|
||||
var rootDescription = "";
|
||||
|
||||
@@ -538,10 +539,10 @@ class Walker {
|
||||
|
||||
var lastRead: { description: string } = null;
|
||||
|
||||
for (let line of lines) {
|
||||
for (const line of lines) {
|
||||
var firstWordMatch = line.match(/^\s*(\S+)(\s*)/);
|
||||
var firstWord = firstWordMatch[1];
|
||||
var remainingLine = line.substring(firstWordMatch[0].length);
|
||||
var firstWord = (firstWordMatch !== null) ? firstWordMatch[1] : "";
|
||||
var remainingLine = (firstWordMatch !== null) ? line.substring(firstWordMatch[0].length) : "";
|
||||
|
||||
if (firstWord[0] === "@") {
|
||||
lastRead = null;
|
||||
@@ -640,7 +641,7 @@ class Walker {
|
||||
return signatureDeclaration.typeParameters.map(typeParameter => typeParameter.name.text);
|
||||
}
|
||||
|
||||
private _getGenericsOfTypeReferenceNode(typeReferenceNode: ts.TypeReferenceNode | ts.HeritageClauseElement): (AST.UnresolvedType | AST.IntrinsicTypeReference)[] {
|
||||
private _getGenericsOfTypeReferenceNode(typeReferenceNode: ts.ExpressionWithTypeArguments): (AST.UnresolvedType | AST.IntrinsicTypeReference)[] {
|
||||
if (typeReferenceNode.typeArguments === undefined) {
|
||||
return [];
|
||||
}
|
||||
@@ -694,10 +695,10 @@ class Walker {
|
||||
}
|
||||
|
||||
link(rootNamespaceName: string): void {
|
||||
for (let moduleName of Object.keys(this.modules)) {
|
||||
for (const moduleName of Object.keys(this.modules)) {
|
||||
var module = this.modules[moduleName];
|
||||
|
||||
for (let memberName of Object.keys(module.members)) {
|
||||
for (const memberName of Object.keys(module.members)) {
|
||||
var member = module.members[memberName];
|
||||
|
||||
if (member instanceof AST.Class) {
|
||||
@@ -729,7 +730,7 @@ class Walker {
|
||||
|
||||
else if (member instanceof AST.Enum) {
|
||||
var value = 0;
|
||||
for (let enumMember of member.members) {
|
||||
for (const enumMember of member.members) {
|
||||
if (enumMember.value === null) {
|
||||
enumMember.value = value;
|
||||
}
|
||||
@@ -749,7 +750,7 @@ class Walker {
|
||||
}
|
||||
|
||||
private _moduleToNamespace(module: AST.Module): void {
|
||||
for (let memberName of Object.keys(module.members)) {
|
||||
for (const memberName of Object.keys(module.members)) {
|
||||
var member = module.members[memberName];
|
||||
|
||||
if (member instanceof AST.Reference) {
|
||||
@@ -769,7 +770,13 @@ class Walker {
|
||||
this.namespaces[newNamespace.fullName] = newNamespace;
|
||||
}
|
||||
|
||||
this._moduleToNamespace(this.modules[(<AST.Reference>member).moduleName]);
|
||||
var referencedModuleName = (<AST.Reference>member).moduleName;
|
||||
var referencedModule = this.modules[referencedModuleName];
|
||||
if (referencedModule === undefined && ((referencedModuleName + "/index") in this.modules)) {
|
||||
(<AST.Reference>member).moduleName = referencedModuleName = referencedModuleName + "/index";
|
||||
referencedModule = this.modules[referencedModuleName];
|
||||
}
|
||||
this._moduleToNamespace(referencedModule);
|
||||
|
||||
this._scope.leave();
|
||||
}
|
||||
@@ -844,7 +851,7 @@ export function walk(compiler: Compiler, root: string, rootNamespaceName: string
|
||||
var walker = new Walker(compiler);
|
||||
|
||||
// Walk
|
||||
for (let sourceFile of sourceFiles) {
|
||||
for (const sourceFile of sourceFiles) {
|
||||
if (
|
||||
path.basename(sourceFile.fileName) === "lib.core.d.ts" ||
|
||||
path.basename(sourceFile.fileName) === "lib.dom.d.ts" ||
|
||||
@@ -21,11 +21,9 @@
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
|
||||
var SourceMap = require("uglify-js/node_modules/source-map");
|
||||
var UglifyJS = require("uglify-js");
|
||||
var Vinyl = require("vinyl");
|
||||
|
||||
var makeTransform = require("./helpers.js").makeTransform;
|
||||
var FileTransform = require("async-build").FileTransform;
|
||||
|
||||
var Run = (function () {
|
||||
function Run(entry, outputLibraryName, unusedVarsToIgnore) {
|
||||
@@ -36,19 +34,20 @@ var Run = (function () {
|
||||
this._root = UglifyJS.parse(
|
||||
'(function (root, factory) {\n' +
|
||||
' var global = this;\n' +
|
||||
' if (typeof exports === "object" && typeof module === "object") {\n' +
|
||||
' module.exports = factory(global);\n' +
|
||||
' }\n' +
|
||||
' else if (typeof define === "function" && define.amd) {\n' +
|
||||
' define(function() {\n' +
|
||||
'\n' +
|
||||
' if (typeof define === "function" && define.amd) {\n' +
|
||||
' define([], function() {\n' +
|
||||
' return factory(global);\n' +
|
||||
' });\n' +
|
||||
' }\n' +
|
||||
' else if (typeof exports === "object" && typeof module === "object") {\n' +
|
||||
' module.exports = factory(global);\n' +
|
||||
' }\n' +
|
||||
' else if (typeof exports === "object") {\n' +
|
||||
' exports["libjass"] = factory(global);\n' +
|
||||
' exports.libjass = factory(global);\n' +
|
||||
' }\n' +
|
||||
' else {\n' +
|
||||
' root["libjass"] = factory(global);\n' +
|
||||
' root.libjass = factory(global);\n' +
|
||||
' }\n' +
|
||||
'})(this, function (global) {\n' +
|
||||
' "use strict";\n' +
|
||||
@@ -56,26 +55,18 @@ var Run = (function () {
|
||||
' var installedModules = Object.create(null);\n' +
|
||||
' function require(moduleId) {\n' +
|
||||
' if (installedModules[moduleId]) {\n' +
|
||||
' return installedModules[moduleId].exports;\n' +
|
||||
' return installedModules[moduleId];\n' +
|
||||
' }\n' +
|
||||
'\n' +
|
||||
' var module = installedModules[moduleId] = {\n' +
|
||||
' exports: Object.create(null),\n' +
|
||||
' id: moduleId,\n' +
|
||||
' loaded: false,\n' +
|
||||
' };\n' +
|
||||
'\n' +
|
||||
' modules[moduleId](module.exports, require);\n' +
|
||||
' module.loaded = true;\n' +
|
||||
' return module.exports;\n' +
|
||||
' var exports = installedModules[moduleId] = Object.create(null);\n' +
|
||||
' modules[moduleId](exports, require);\n' +
|
||||
' return exports;\n' +
|
||||
' }\n' +
|
||||
'\n' +
|
||||
' return require(0);\n' +
|
||||
' })([\n' +
|
||||
' ]);\n' +
|
||||
'});', {
|
||||
filename: this._outputLibraryName + ".js"
|
||||
});
|
||||
'});');
|
||||
|
||||
this._root.figure_out_scope({ screw_ie8: true });
|
||||
|
||||
@@ -85,7 +76,7 @@ var Run = (function () {
|
||||
|
||||
this._modules = Object.create(null);
|
||||
|
||||
this._rootSourceMap = new UjsSourceMap(this._outputLibraryName + ".js", "");
|
||||
this._rootSourceMap = UjsSourceMap({ file: this._outputLibraryName + ".js", root: "" });
|
||||
}
|
||||
|
||||
Run.prototype.addFile = function (file) {
|
||||
@@ -98,12 +89,14 @@ var Run = (function () {
|
||||
|
||||
var module = this._modules[moduleName];
|
||||
|
||||
var filenameForSourceMap = path.relative(path.join(this._entry, ".."), moduleName + ".ts").replace(/\\/g, "/");
|
||||
var filenameWithoutExtension = path.relative(path.join(this._entry, ".."), moduleName).replace(/\\/g, "/");
|
||||
var tsFilename = filenameWithoutExtension + ".ts";
|
||||
var jsFilename = filenameWithoutExtension + ".js";
|
||||
|
||||
switch (path.extname(file.path)) {
|
||||
case ".js":
|
||||
module.root = UglifyJS.parse(file.contents.toString(), {
|
||||
filename: filenameForSourceMap,
|
||||
filename: jsFilename,
|
||||
toplevel: null,
|
||||
});
|
||||
|
||||
@@ -130,8 +123,11 @@ var Run = (function () {
|
||||
}
|
||||
break;
|
||||
case ".map":
|
||||
this._rootSourceMap.addInput(filenameForSourceMap, fs.readFileSync(moduleName + ".ts", { encoding: "utf8" }), file.contents);
|
||||
module.codeBuffer = undefined;
|
||||
var sourceMapContents = JSON.parse(file.contents.toString());
|
||||
sourceMapContents.sources[0] = tsFilename;
|
||||
sourceMapContents.file = jsFilename;
|
||||
this._rootSourceMap.addInput(sourceMapContents);
|
||||
this._rootSourceMap.get().setSourceContent(tsFilename, fs.readFileSync(moduleName + ".ts", { encoding: "utf8" }));
|
||||
break;
|
||||
}
|
||||
};
|
||||
@@ -162,12 +158,20 @@ var Run = (function () {
|
||||
var importRelativePath = statement.definitions[0].value.args[0].value;
|
||||
var importAbsolutePath = path.join(moduleName, "..", importRelativePath).replace(/\\/g, "/");
|
||||
var stringArg = statement.definitions[0].value.args[0];
|
||||
statement.definitions[0].value.args[0] = new UglifyJS.AST_Number({ start: stringArg.start, end: stringArg.end, value: _this._modules[importAbsolutePath].id });
|
||||
var importedModule = _this._modules[importAbsolutePath];
|
||||
if (importedModule === undefined) {
|
||||
importedModule = _this._modules[importAbsolutePath + "/index"];
|
||||
}
|
||||
statement.definitions[0].value.args[0] = new UglifyJS.AST_Number({ start: stringArg.start, end: stringArg.end, value: importedModule.id });
|
||||
}
|
||||
else if (statement.definitions[0].name.name === "__extends") {
|
||||
var importAbsolutePath = path.join(_this._entry, "..", "utility", "extends").replace(/\\/g, "/");
|
||||
var importAbsolutePath = path.join(_this._entry, "..", "utility", "ts-helpers").replace(/\\/g, "/");
|
||||
statement.definitions[0].value = UglifyJS.parse('require(' + _this._modules[importAbsolutePath].id + ').__extends').body[0].body;
|
||||
}
|
||||
else if (statement.definitions[0].name.name === "__decorate") {
|
||||
var importAbsolutePath = path.join(_this._entry, "..", "utility", "ts-helpers").replace(/\\/g, "/");
|
||||
statement.definitions[0].value = UglifyJS.parse('require(' + _this._modules[importAbsolutePath].id + ').__decorate').body[0].body;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -267,6 +271,33 @@ var Run = (function () {
|
||||
}
|
||||
|
||||
|
||||
// Move var statements at the end of blocks (generated by TS for rest parameters) to the start of the block.
|
||||
// This is needed to prevent unreachable-code warnings from UJS
|
||||
this._root.walk(new UglifyJS.TreeWalker(function (node, descend) {
|
||||
if (
|
||||
node instanceof UglifyJS.AST_Block &&
|
||||
node.body[node.body.length - 1] instanceof UglifyJS.AST_Var
|
||||
) {
|
||||
node.body.unshift(node.body.pop());
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
// Split multiple vars per declaration into one var per declaration
|
||||
this._root.walk(new UglifyJS.TreeWalker(function (node, descend) {
|
||||
if (
|
||||
node instanceof UglifyJS.AST_Var &&
|
||||
node.definitions.length > 1 &&
|
||||
this.parent() instanceof UglifyJS.AST_Block
|
||||
) {
|
||||
var parent = this.parent().body;
|
||||
parent.splice.apply(parent, [parent.indexOf(node), 1].concat(node.definitions.map(function (definition) {
|
||||
return new UglifyJS.AST_Var({ start: node.start, end: node.end, definitions: [definition] });
|
||||
})));
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
// Rename all function arguments that begin with _ to not have the _.
|
||||
// This converts the TypeScript syntax of declaring private members in the constructor declaration `function Color(private _red: number, ...)` to `function Color(red, ...)`
|
||||
// so that it matches the JSDoc (and looks nicer).
|
||||
@@ -284,7 +315,7 @@ var Run = (function () {
|
||||
|
||||
// Output
|
||||
var output = {
|
||||
source_map: _this._rootSourceMap.asUjsOutputSourceMap(),
|
||||
source_map: _this._rootSourceMap,
|
||||
beautify: true,
|
||||
comments: function (node, comment) {
|
||||
// If this is the first license header, keep it.
|
||||
@@ -309,15 +340,15 @@ var Run = (function () {
|
||||
var stream = UglifyJS.OutputStream(output);
|
||||
this._root.print(stream);
|
||||
|
||||
outputStream.push(new Vinyl({
|
||||
outputStream.push({
|
||||
path: _this._outputLibraryName + ".js",
|
||||
contents: Buffer.concat([new Buffer(stream.toString()), new Buffer("\n//# sourceMappingURL=" + _this._outputLibraryName + ".js.map")])
|
||||
}));
|
||||
});
|
||||
|
||||
outputStream.push(new Vinyl({
|
||||
outputStream.push({
|
||||
path: _this._outputLibraryName + ".js.map",
|
||||
contents: _this._rootSourceMap.toBuffer()
|
||||
}));
|
||||
contents: new Buffer(_this._rootSourceMap.get().toString())
|
||||
});
|
||||
|
||||
// Print unused variables
|
||||
if (haveUnusedVars) {
|
||||
@@ -335,10 +366,10 @@ var Run = (function () {
|
||||
})();
|
||||
|
||||
module.exports = {
|
||||
gulp: function (entry, outputLibraryName, unusedVarsToIgnore) {
|
||||
build: function (entry, outputLibraryName, unusedVarsToIgnore) {
|
||||
var run = new Run(entry, outputLibraryName, unusedVarsToIgnore);
|
||||
|
||||
return makeTransform(function (file) {
|
||||
return new FileTransform(function (file) {
|
||||
run.addFile(file);
|
||||
}, function () {
|
||||
run.build(this);
|
||||
@@ -348,7 +379,7 @@ module.exports = {
|
||||
watch: function (entry, outputLibraryName, unusedVarsToIgnore) {
|
||||
var files = Object.create(null);
|
||||
|
||||
return makeTransform(function (file) {
|
||||
return new FileTransform(function (file) {
|
||||
if (file.path !== "END") {
|
||||
files[file.path] = file;
|
||||
}
|
||||
@@ -366,7 +397,7 @@ module.exports = {
|
||||
var codeFile = null;
|
||||
var sourceMapFile = null;
|
||||
|
||||
return makeTransform(function (file) {
|
||||
return new FileTransform(function (file) {
|
||||
switch (path.extname(file.path)) {
|
||||
case ".js":
|
||||
codeFile = file;
|
||||
@@ -408,62 +439,10 @@ module.exports = {
|
||||
root.figure_out_scope({ screw_ie8: true });
|
||||
root.compute_char_frequency();
|
||||
root.mangle_names({ screw_ie8: true });
|
||||
|
||||
|
||||
// Mangle private members
|
||||
var occurrences = Object.create(null);
|
||||
|
||||
root.walk(new UglifyJS.TreeWalker(function (node, descend) {
|
||||
if (
|
||||
node instanceof UglifyJS.AST_PropAccess &&
|
||||
typeof node.property === "string" &&
|
||||
node.property[0] === "_" &&
|
||||
node.property[1] !== "_" && // Doesn't start with two leading underscores
|
||||
node.property !== "_classTag" // webworker serializer uses this property by name, so it shouldn't be changed.
|
||||
) {
|
||||
var occurrence = occurrences[node.property];
|
||||
if (occurrence === undefined) {
|
||||
occurrences[node.property] = 1;
|
||||
}
|
||||
else {
|
||||
occurrences[node.property]++;
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
var identifiers = Object.keys(occurrences);
|
||||
identifiers.sort(function (first, second) { return occurrences[second] - occurrences[first]; });
|
||||
|
||||
var generatedIdentifiers = occurrences;
|
||||
|
||||
var validIdentifierCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_0123456789";
|
||||
var toIdentifier = function (index) {
|
||||
var result = validIdentifierCharacters[(index % validIdentifierCharacters.length)];
|
||||
index = (index / validIdentifierCharacters.length) | 0;
|
||||
|
||||
while (index > 0) {
|
||||
index--;
|
||||
result = validIdentifierCharacters[index % validIdentifierCharacters.length] + result;
|
||||
index = (index / validIdentifierCharacters.length) | 0;
|
||||
}
|
||||
|
||||
return "_" + result;
|
||||
};
|
||||
|
||||
identifiers.forEach(function (identifier, index) {
|
||||
generatedIdentifiers[identifier] = toIdentifier(index);
|
||||
root = UglifyJS.mangle_properties(root, {
|
||||
regex: /^_/
|
||||
});
|
||||
|
||||
root.walk(new UglifyJS.TreeWalker(function (node, descend) {
|
||||
if (
|
||||
node instanceof UglifyJS.AST_PropAccess &&
|
||||
typeof node.property === "string" &&
|
||||
node.property in generatedIdentifiers
|
||||
) {
|
||||
node.property = generatedIdentifiers[node.property];
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
// Output
|
||||
var firstLicenseHeaderFound = false; // To detect and preserve the first license header
|
||||
@@ -491,7 +470,7 @@ module.exports = {
|
||||
codeFile.path = codeFile.path.replace(/\.js$/, ".min.js");
|
||||
sourceMapFile.path = sourceMapFile.path.replace(/\.js\.map$/, ".min.js.map");
|
||||
|
||||
codeFile.contents = Buffer.concat([new Buffer(stream.toString()), new Buffer("\n//# sourceMappingURL="), new Buffer(sourceMapFile.relative)]);
|
||||
codeFile.contents = Buffer.concat([new Buffer(stream.toString()), new Buffer("\n//# sourceMappingURL="), new Buffer(sourceMapFile.path)]);
|
||||
this.push(codeFile);
|
||||
|
||||
var inputSourceMapObject = JSON.parse(sourceMapFile.contents.toString());
|
||||
@@ -510,68 +489,60 @@ module.exports = {
|
||||
}
|
||||
};
|
||||
|
||||
var UjsSourceMap = (function () {
|
||||
function UjsSourceMap(outputFilename, sourceRoot) {
|
||||
this._outputFilename = outputFilename;
|
||||
this._inputs = Object.create(null);
|
||||
var SourceMap = require.cache[require.resolve("uglify-js")].require("source-map");
|
||||
|
||||
this._generator = new SourceMap.SourceMapGenerator({
|
||||
file: outputFilename,
|
||||
sourceRoot: sourceRoot,
|
||||
});
|
||||
}
|
||||
function UjsSourceMap(options) {
|
||||
var orig_maps = Object.create(null);
|
||||
|
||||
UjsSourceMap.prototype.addInput = function (codeFilename, code, sourceMapContents) {
|
||||
this._inputs[codeFilename] = new SourceMap.SourceMapConsumer(sourceMapContents.toString());
|
||||
this._generator.setSourceContent(codeFilename, code);
|
||||
var generator = new SourceMap.SourceMapGenerator({
|
||||
file: options.file,
|
||||
sourceRoot: options.root,
|
||||
});
|
||||
|
||||
return {
|
||||
addInput: function (rawSourceMap) {
|
||||
var consumer = new SourceMap.SourceMapConsumer(rawSourceMap);
|
||||
orig_maps[consumer.file] = consumer;
|
||||
},
|
||||
add: function (source, gen_line, gen_col, orig_line, orig_col, name) {
|
||||
var originalMap = orig_maps[source];
|
||||
if (originalMap === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
var info = originalMap.originalPositionFor({
|
||||
line: orig_line,
|
||||
column: orig_col
|
||||
});
|
||||
|
||||
if (info.source === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
source = info.source;
|
||||
orig_line = info.line;
|
||||
orig_col = info.column;
|
||||
name = info.name || name;
|
||||
|
||||
generator.addMapping({
|
||||
generated : { line: gen_line, column: gen_col },
|
||||
original : { line: orig_line, column: orig_col },
|
||||
source : source,
|
||||
name : name
|
||||
});
|
||||
},
|
||||
get: function () { return generator; },
|
||||
toString: function () { return generator.toString(); },
|
||||
};
|
||||
|
||||
UjsSourceMap.prototype.asUjsOutputSourceMap = function () {
|
||||
var _this = this;
|
||||
|
||||
return {
|
||||
add: function (source, gen_line, gen_col, orig_line, orig_col, name) {
|
||||
if (source === _this._outputFilename || source === "?") {
|
||||
return;
|
||||
}
|
||||
var info = _this._inputs[source].originalPositionFor({
|
||||
line: orig_line,
|
||||
column: orig_col
|
||||
});
|
||||
|
||||
if (info.source === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_this._generator.addMapping({
|
||||
generated: { line: gen_line, column: gen_col },
|
||||
original: { line: info.line, column: info.column },
|
||||
source: source,
|
||||
name: info.name || name
|
||||
});
|
||||
},
|
||||
get: function () { return _this._generator; },
|
||||
toString: function () { return _this._generator.toString(); },
|
||||
};
|
||||
};
|
||||
|
||||
UjsSourceMap.prototype.toBuffer = function () {
|
||||
return new Buffer(this._generator.toString());
|
||||
};
|
||||
|
||||
return UjsSourceMap;
|
||||
})();
|
||||
}
|
||||
|
||||
var originalSymbolUnreferenced = UglifyJS.AST_Symbol.prototype.unreferenced;
|
||||
|
||||
// Workaround for https://github.com/mishoo/UglifyJS2/issues/789 - Nodes explicitly marked with ujs:unreferenced will not be warned for.
|
||||
UglifyJS.AST_Symbol.prototype.unreferenced = function () {
|
||||
if (this.start.comments_before.length > 0 && this.start.comments_before[0].value.trim() === "ujs:unreferenced") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.name === "module" || this.name === "exports") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return originalSymbolUnreferenced.call(this);
|
||||
};
|
||||
@@ -1,225 +0,0 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
|
||||
var gulp = require("gulp");
|
||||
|
||||
(function () {
|
||||
var _TypeScript = null;
|
||||
var _UglifyJS = null;
|
||||
var _npm = null;
|
||||
|
||||
Object.defineProperties(global, {
|
||||
TypeScript: { get: function () { return _TypeScript || (_TypeScript = require("./gulplib/typescript/index.js")); } },
|
||||
UglifyJS: { get: function () { return _UglifyJS || (_UglifyJS = require("./gulplib/uglify.js")); } },
|
||||
npm: { get: function () { return _npm || (_npm = require("npm")); } },
|
||||
});
|
||||
})();
|
||||
|
||||
gulp.task("default", ["libjass.js", "libjass.min.js"]);
|
||||
|
||||
gulp.task("libjass.js", ["build-gulplib"], function () {
|
||||
if (fs.existsSync("./lib/libjass.js")) {
|
||||
return;
|
||||
}
|
||||
|
||||
return gulp.src("./src/tsconfig.json")
|
||||
.pipe(TypeScript.gulp("./src/index.ts", "libjass"))
|
||||
.pipe(UglifyJS.gulp("./src/index", "libjass", ["BorderStyle", "ClockEvent", "Format", "WorkerCommands", "WrappingStyle"]))
|
||||
.pipe(gulp.dest("./lib"));
|
||||
});
|
||||
|
||||
gulp.task("libjass.min.js", ["libjass.js"], function () {
|
||||
if (fs.existsSync("./lib/libjass.min.js")) {
|
||||
return;
|
||||
}
|
||||
|
||||
return gulp.src(["./lib/libjass.js", "./lib/libjass.js.map"])
|
||||
.pipe(UglifyJS.minify())
|
||||
.pipe(gulp.dest("./lib"));
|
||||
});
|
||||
|
||||
gulp.task("clean", function () {
|
||||
["./lib/libjass.js", "./lib/libjass.js.map", "./lib/libjass.min.js", "./lib/libjass.min.js.map"].forEach(function (file) {
|
||||
try {
|
||||
fs.unlinkSync(file);
|
||||
}
|
||||
catch (ex) {
|
||||
if (ex.code !== "ENOENT") {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task("watch", ["clean", "build-gulplib"], function (callback) {
|
||||
var helpers = require("./gulplib/helpers.js");
|
||||
|
||||
npm.load(function () {
|
||||
var testRunning = false;
|
||||
var rerunTest = false;
|
||||
|
||||
var startTest = function () {
|
||||
npm.commands["run-script"](["test-lib"], function () {
|
||||
testRunning = false;
|
||||
|
||||
if (rerunTest) {
|
||||
startTest();
|
||||
rerunTest = false;
|
||||
}
|
||||
});
|
||||
|
||||
testRunning = true;
|
||||
};
|
||||
|
||||
var runTest = function () {
|
||||
if (!testRunning) {
|
||||
startTest();
|
||||
}
|
||||
else {
|
||||
rerunTest = true;
|
||||
}
|
||||
};
|
||||
|
||||
gulp.src("./src/tsconfig.json")
|
||||
.pipe(TypeScript.watch("./src/index.ts", "libjass"))
|
||||
.pipe(UglifyJS.watch("./src/index", "libjass", ["BorderStyle", "ClockEvent", "Format", "WorkerCommands", "WrappingStyle"]))
|
||||
.pipe(gulp.dest("./lib"))
|
||||
.pipe(helpers.makeTransform(function (file) {
|
||||
if (path.basename(file.path) === "libjass.js") {
|
||||
runTest();
|
||||
}
|
||||
}));
|
||||
|
||||
gulp.watch(["./tests/unit/*.js"], { debounceDelay: 1 }, function () {
|
||||
runTest();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task("test-lib", ["libjass.js"], function (callback) {
|
||||
npm.load(function () {
|
||||
npm.commands["run-script"](["test-lib"], callback);
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task("test-minified", ["libjass.min.js"], function (callback) {
|
||||
npm.load(function () {
|
||||
npm.commands["run-script"](["test-minified"], callback);
|
||||
});
|
||||
});
|
||||
|
||||
// Start Selenium server with
|
||||
// java.exe -jar .\selenium-server-standalone-2.45.0.jar "-Dwebdriver.ie.driver=$PWD\IEDriverServer.exe" "-Dwebdriver.chrome.driver=$PWD\chromedriver.exe"
|
||||
gulp.task("test-browser", ["libjass.js"], function (callback) {
|
||||
npm.load(function () {
|
||||
npm.commands["run-script"](["test-browser"], callback);
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task("test", ["test-lib", "test-minified"]);
|
||||
|
||||
gulp.task("demo", ["libjass.js"], function () {
|
||||
return gulp.src(["./lib/libjass.js", "./lib/libjass.js.map", "./lib/libjass.css"]).pipe(gulp.dest("../libjass-gh-pages/demo/"));
|
||||
});
|
||||
|
||||
gulp.task("doc", ["make-doc", "test-doc"]);
|
||||
|
||||
gulp.task("make-doc", ["libjass.js"], function () {
|
||||
var Doc = require("./gulplib/doc.js");
|
||||
|
||||
return gulp.src("./src/tsconfig.json")
|
||||
.pipe(Doc.gulp("./api.xhtml", "./src/index.ts", "libjass"))
|
||||
.pipe(gulp.dest("../libjass-gh-pages/"));
|
||||
});
|
||||
|
||||
gulp.task("test-doc", ["make-doc", "libjass.js"], function (callback) {
|
||||
npm.load(function () {
|
||||
npm.commands["run-script"](["test-doc"], callback);
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task("dist", ["clean", "default", "test", "test-browser", "demo", "doc"], function () {
|
||||
var inputFiles = [
|
||||
"./README.md", "./CHANGELOG.md", "./LICENSE",
|
||||
"./lib/libjass.js", "./lib/libjass.js.map",
|
||||
"./lib/libjass.min.js", "./lib/libjass.min.js.map",
|
||||
"./lib/libjass.css"
|
||||
];
|
||||
|
||||
var files = Object.create(null);
|
||||
inputFiles.forEach(function (filename) {
|
||||
files["./dist/" + path.basename(filename)] = filename;
|
||||
});
|
||||
|
||||
// Clean dist/
|
||||
Object.keys(files).concat("./dist/package.json").forEach(function (file) {
|
||||
try {
|
||||
fs.unlinkSync(file);
|
||||
}
|
||||
catch (ex) {
|
||||
if (ex.code !== "ENOENT") {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Create dist/ if necessary
|
||||
try {
|
||||
fs.mkdirSync("./dist");
|
||||
}
|
||||
catch (ex) {
|
||||
if (ex.code !== "EEXIST") {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy all files except package.json
|
||||
Object.keys(files).forEach(function (outputFilename) {
|
||||
var inputFilename = files[outputFilename];
|
||||
var contents = fs.readFileSync(inputFilename);
|
||||
fs.writeFileSync(outputFilename, contents);
|
||||
});
|
||||
|
||||
// Copy package.json
|
||||
var packageJson = fs.readFileSync("./package.json");
|
||||
packageJson = JSON.parse(packageJson);
|
||||
|
||||
packageJson.devDependencies = undefined;
|
||||
packageJson.private = undefined;
|
||||
packageJson.scripts = undefined;
|
||||
packageJson.main = "libjass.js";
|
||||
|
||||
packageJson = JSON.stringify(packageJson, null, "\t");
|
||||
fs.writeFileSync("./dist/package.json", packageJson, { encoding: "utf8" });
|
||||
});
|
||||
|
||||
gulp.task("build-gulplib", function (callback) {
|
||||
if (fs.existsSync("./gulplib/typescript/index.js") && fs.existsSync("./gulplib/doc.js")) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
npm.load(function () {
|
||||
npm.commands["run-script"](["build-gulplib"], callback);
|
||||
});
|
||||
});
|
||||
@@ -1,65 +0,0 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Transform } from "stream";
|
||||
|
||||
export function makeTransform<T>(transform: (chunk: T, encoding?: string, callback?: (error?: Error) => void) => void, flush?: (callback?: (error?: Error) => void) => void): Transform<T> {
|
||||
return new GulpTransformer<T>(transform, flush);
|
||||
}
|
||||
|
||||
class GulpTransformer<T> extends Transform<T> {
|
||||
constructor(transform?: (chunk: T, encoding?: string, callback?: (error?: Error) => void) => void, flush?: (callback?: (error?: Error) => void) => void) {
|
||||
super({ objectMode: true });
|
||||
|
||||
transform = transform || ((chunk, encoding, callback) => callback());
|
||||
|
||||
if (transform.length < 3) {
|
||||
this._transform = <any>((chunk: T, encoding: string, callback: (error?: Error) => void) => {
|
||||
try {
|
||||
transform.call(this, chunk, encoding);
|
||||
callback();
|
||||
}
|
||||
catch (ex) {
|
||||
callback(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
this._transform = transform.bind(this);
|
||||
}
|
||||
|
||||
flush = flush || (callback => callback());
|
||||
|
||||
if (flush.length < 1) {
|
||||
this._flush = ((callback: (error?: Error) => void) => {
|
||||
try {
|
||||
flush.call(this);
|
||||
callback();
|
||||
}
|
||||
catch (ex) {
|
||||
callback(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
this._flush = flush.bind(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
// Type definitions for Node.js v0.12.0
|
||||
// Project: http://nodejs.org/
|
||||
// Definitions by: Microsoft TypeScript <http://typescriptlang.org>, DefinitelyTyped <https://github.com/borisyankov/DefinitelyTyped>
|
||||
// Definitions: https://github.com/borisyankov/DefinitelyTyped
|
||||
|
||||
interface Buffer { }
|
||||
declare var Buffer: {
|
||||
new (str: string): Buffer;
|
||||
prototype: Buffer;
|
||||
concat(list: Buffer[]): Buffer;
|
||||
};
|
||||
|
||||
declare module "fs" {
|
||||
export function lstatSync(path: string): Stats;
|
||||
export function readdirSync(path: string): string[];
|
||||
export function readFileSync(filename: string, options: { encoding: string }): string;
|
||||
export function unwatchFile(filename: string, listener?: (curr: Stats, prev: Stats) => void): void;
|
||||
export function watchFile(filename: string, options: { interval?: number }, listener: (curr: Stats, prev: Stats) => void): void;
|
||||
|
||||
interface Stats {
|
||||
isDirectory(): boolean;
|
||||
isFile(): boolean;
|
||||
mtime: Date;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "path" {
|
||||
export function basename(p: string, ext?: string): string;
|
||||
export function dirname(p: string): string;
|
||||
export function extname(p: string): string;
|
||||
export function join(...paths: string[]): string;
|
||||
export function relative(from: string, to: string): string;
|
||||
export function resolve(...pathSegments: string[]): string;
|
||||
}
|
||||
|
||||
declare module "stream" {
|
||||
export class Transform<T> {
|
||||
constructor(opts?: { objectMode?: boolean; });
|
||||
|
||||
_transform(chunk: T, encoding: string, callback: (error?: Error) => void): void;
|
||||
_flush(callback: (error?: Error) => void): void;
|
||||
push(chunk: T, encoding?: string): boolean;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Type definitions for vinyl 0.4.3
|
||||
// Project: https://github.com/wearefractal/vinyl
|
||||
// Definitions by: vvakame <https://github.com/vvakame/>, jedmao <https://github.com/jedmao>
|
||||
// Definitions: https://github.com/borisyankov/DefinitelyTyped
|
||||
|
||||
declare module "vinyl" {
|
||||
class File {
|
||||
constructor(options?: { path: string; contents: Buffer; base?: string; });
|
||||
|
||||
path: string;
|
||||
contents: Buffer;
|
||||
}
|
||||
|
||||
export = File;
|
||||
}
|
||||
@@ -23,10 +23,6 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.libjass-wrapper.libjass-full-screen {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.libjass-subs {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
@@ -34,6 +30,8 @@
|
||||
|
||||
.libjass-subs, .libjass-subs * {
|
||||
pointer-events: none;
|
||||
-webkit-animation-fill-mode: both !important;
|
||||
animation-fill-mode: both !important;
|
||||
}
|
||||
|
||||
.libjass-subs.paused * {
|
||||
@@ -94,6 +92,7 @@
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.libjass-filters {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "libjass",
|
||||
"version": "0.10.0",
|
||||
"version": "0.12.0",
|
||||
"description": "A library to render ASS subtitles on HTML5 video in the browser.",
|
||||
"keywords": ["browser", "html5", "subtitles"],
|
||||
"homepage": "https://github.com/Arnavion/libjass",
|
||||
@@ -16,23 +16,24 @@
|
||||
},
|
||||
"main": "lib/libjass.js",
|
||||
"scripts": {
|
||||
"prepublish": "gulp clean default",
|
||||
"build-gulplib": "tsc ./gulplib/typescript/index.ts ./gulplib/doc.ts ./gulplib/typings.d.ts ./node_modules/typescript/bin/typescript.d.ts ./node_modules/typescript/bin/typescript_internal.d.ts -m commonjs -t es5 -noImplicitAny",
|
||||
"test": "gulp test",
|
||||
"test-lib": "intern-client config=tests/intern reporters=pretty",
|
||||
"test-minified": "intern-client config=tests/intern reporters=pretty minified=true",
|
||||
"prepublish": "node ./build.js clean default",
|
||||
"build": "tsc ./build/typescript/index.ts ./build/doc.ts ./build/node.d.ts ./build/typescript/typescript.d.ts ./node_modules/async-build/typings.d.ts -m commonjs -t es5 -noImplicitAny --moduleResolution classic",
|
||||
"test": "node ./build.js test",
|
||||
"test-lib": "intern-client config=tests/intern reporters=Pretty",
|
||||
"test-minified": "intern-client config=tests/intern reporters=Pretty minified=true",
|
||||
"test-browser": "intern-runner config=tests/intern",
|
||||
"test-doc": "intern-client config=tests/intern-doc reporters=pretty"
|
||||
"test-doc": "intern-client config=tests/intern-doc reporters=Pretty"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gulp": "3.x",
|
||||
"intern": "2.x",
|
||||
"npm": "2.x",
|
||||
"pngjs": "0.4.0",
|
||||
"sax": "0.6.x",
|
||||
"typescript": "1.5.0-alpha",
|
||||
"uglify-js": "2.x >=2.4.16",
|
||||
"vinyl": "latest"
|
||||
"async": "1.x >=1.4",
|
||||
"async-build": "0.2.0",
|
||||
"firefox-profile": "0.3.x",
|
||||
"intern": "3.x >=3.0.1",
|
||||
"npm": "3.x",
|
||||
"pngjs": "2.2.0",
|
||||
"sax": "1.x",
|
||||
"typescript": "1.7.5",
|
||||
"uglify-js": "2.x >=2.4.24"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -30,26 +30,27 @@ export { Map } from "./utility/map";
|
||||
import * as promise from "./utility/promise";
|
||||
export { Promise, DeferredPromise } from "./utility/promise";
|
||||
|
||||
import * as webworker from "./webworker/index";
|
||||
import * as webworker from "./webworker";
|
||||
export { webworker };
|
||||
|
||||
import * as parts from "./parts/index";
|
||||
import * as parts from "./parts";
|
||||
export { parts };
|
||||
|
||||
import * as parser from "./parser/index";
|
||||
import * as parser from "./parser";
|
||||
export { parser };
|
||||
|
||||
import * as renderers from "./renderers/index";
|
||||
import * as renderers from "./renderers";
|
||||
export { renderers };
|
||||
|
||||
export { ASS } from "./types/ass";
|
||||
export { Attachment, AttachmentType } from "./types/attachment";
|
||||
export { Dialogue } from "./types/dialogue";
|
||||
export { ScriptProperties } from "./types/script-properties";
|
||||
export { Style } from "./types/style";
|
||||
|
||||
export { BorderStyle, Format, WrappingStyle } from "./types/misc";
|
||||
|
||||
declare var exports: any;
|
||||
declare const exports: any;
|
||||
|
||||
Object.defineProperties(exports, {
|
||||
debugMode: {
|
||||
|
||||
@@ -29,13 +29,13 @@ import { Map } from "../utility/map";
|
||||
* @return {!Property}
|
||||
*/
|
||||
export function parseLineIntoProperty(line: string): Property {
|
||||
var colonPos = line.indexOf(":");
|
||||
const colonPos = line.indexOf(":");
|
||||
if (colonPos === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var name = line.substr(0, colonPos);
|
||||
var value = line.substr(colonPos + 1).replace(/^\s+/, "");
|
||||
const name = line.substr(0, colonPos);
|
||||
const value = line.substr(colonPos + 1).replace(/^\s+/, "");
|
||||
|
||||
return { name, value };
|
||||
}
|
||||
@@ -48,18 +48,18 @@ export function parseLineIntoProperty(line: string): Property {
|
||||
* @return {!TypedTemplate}
|
||||
*/
|
||||
export function parseLineIntoTypedTemplate(line: string, formatSpecifier: string[]): TypedTemplate {
|
||||
var property = parseLineIntoProperty(line);
|
||||
const property = parseLineIntoProperty(line);
|
||||
if (property === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var value = property.value.split(",");
|
||||
const value = property.value.split(",");
|
||||
|
||||
if (value.length > formatSpecifier.length) {
|
||||
value[formatSpecifier.length - 1] = value.slice(formatSpecifier.length - 1).join(",");
|
||||
}
|
||||
|
||||
var template = new Map<string, string>();
|
||||
const template = new Map<string, string>();
|
||||
formatSpecifier.forEach((formatKey, index) => {
|
||||
template.set(formatKey, value[index]);
|
||||
});
|
||||
|
||||
@@ -18,9 +18,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { debugMode } from "../settings";
|
||||
|
||||
import { ASS } from "../types/ass";
|
||||
import { Style } from "../types/style";
|
||||
import { Dialogue } from "../types/dialogue";
|
||||
import { Attachment, AttachmentType } from "../types/attachment";
|
||||
import { Property, TypedTemplate } from "../types/misc";
|
||||
|
||||
import { Map } from "../utility/map";
|
||||
@@ -30,6 +33,16 @@ import { Promise, DeferredPromise } from "../utility/promise";
|
||||
import { parseLineIntoProperty } from "./misc";
|
||||
import { Stream } from "./streams";
|
||||
|
||||
enum Section {
|
||||
ScriptInfo,
|
||||
Styles,
|
||||
Events,
|
||||
Fonts,
|
||||
Graphics,
|
||||
Other,
|
||||
EOF,
|
||||
}
|
||||
|
||||
/**
|
||||
* A parser that parses an {@link libjass.ASS} object from a {@link libjass.parser.Stream}.
|
||||
*
|
||||
@@ -41,10 +54,14 @@ export class StreamParser {
|
||||
private _deferred: DeferredPromise<ASS> = new DeferredPromise<ASS>();
|
||||
|
||||
private _shouldSwallowBom: boolean = true;
|
||||
private _currentSectionName: string = null;
|
||||
private _currentSection: Section = Section.ScriptInfo;
|
||||
private _currentAttachment: Attachment = null;
|
||||
|
||||
constructor(private _stream: Stream) {
|
||||
this._stream.nextLine().then(line => this._onNextLine(line));
|
||||
this._stream.nextLine().then(line => this._onNextLine(line), reason => {
|
||||
this._minimalDeferred.reject(reason);
|
||||
this._deferred.reject(reason);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,13 +79,49 @@ export class StreamParser {
|
||||
return this._deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
private get currentSection(): Section {
|
||||
return this._currentSection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
private set currentSection(value: Section) {
|
||||
if (this._currentAttachment !== null) {
|
||||
this._ass.addAttachment(this._currentAttachment);
|
||||
this._currentAttachment = null;
|
||||
}
|
||||
|
||||
if (this._currentSection === Section.ScriptInfo && value !== Section.ScriptInfo) {
|
||||
// Exiting script info section
|
||||
this._minimalDeferred.resolve(this._ass);
|
||||
}
|
||||
|
||||
if (value === Section.EOF) {
|
||||
const scriptProperties = this._ass.properties;
|
||||
if (scriptProperties.resolutionX === undefined || scriptProperties.resolutionY === undefined) {
|
||||
// Malformed script.
|
||||
this._minimalDeferred.reject("Malformed ASS script.");
|
||||
this._deferred.reject("Malformed ASS script.");
|
||||
}
|
||||
else {
|
||||
this._minimalDeferred.resolve(this._ass);
|
||||
this._deferred.resolve(this._ass);
|
||||
}
|
||||
}
|
||||
|
||||
this._currentSection = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} line
|
||||
*/
|
||||
private _onNextLine(line: string): void {
|
||||
if (line === null) {
|
||||
this._minimalDeferred.resolve(this._ass);
|
||||
this._deferred.resolve(this._ass);
|
||||
this.currentSection = Section.EOF;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -82,25 +135,41 @@ export class StreamParser {
|
||||
|
||||
this._shouldSwallowBom = false;
|
||||
|
||||
if (line === "" || line[0] === ";") {
|
||||
// Ignore empty lines and comments
|
||||
if (line === "") {
|
||||
// Ignore empty lines.
|
||||
}
|
||||
|
||||
else if (line[0] === "[" && line[line.length - 1] === "]") {
|
||||
// Start of new section
|
||||
else if (line[0] === ";" && this._currentAttachment === null) {
|
||||
// Lines starting with ; are comments, unless reading an attachment.
|
||||
}
|
||||
|
||||
if (this._currentSectionName === "Script Info") {
|
||||
// Exiting script info section
|
||||
this._minimalDeferred.resolve(this._ass);
|
||||
else if (line === "[Script Info]") {
|
||||
this.currentSection = Section.ScriptInfo;
|
||||
}
|
||||
else if (line === "[V4+ Styles]" || line === "[V4 Styles]") {
|
||||
this.currentSection = Section.Styles;
|
||||
}
|
||||
else if (line === "[Events]") {
|
||||
this.currentSection = Section.Events;
|
||||
}
|
||||
else if (line === "[Fonts]") {
|
||||
this.currentSection = Section.Fonts;
|
||||
}
|
||||
else if (line === "[Graphics]") {
|
||||
this.currentSection = Section.Graphics;
|
||||
}
|
||||
else {
|
||||
if (this._currentAttachment === null && line[0] === "[" && line[line.length - 1] === "]") {
|
||||
/* This looks like the start of a new section. The section name is unrecognized if it is.
|
||||
* Since there's no current attachment being parsed it's definitely the start of a new section.
|
||||
* If an attachment is being parsed, this might be part of the attachment.
|
||||
*/
|
||||
this.currentSection = Section.Other;
|
||||
}
|
||||
|
||||
this._currentSectionName = line.substring(1, line.length - 1);
|
||||
}
|
||||
|
||||
else {
|
||||
switch (this._currentSectionName) {
|
||||
case "Script Info":
|
||||
var property = parseLineIntoProperty(line);
|
||||
switch (this.currentSection) {
|
||||
case Section.ScriptInfo:
|
||||
const property = parseLineIntoProperty(line);
|
||||
if (property !== null) {
|
||||
switch (property.name) {
|
||||
case "PlayResX":
|
||||
@@ -119,9 +188,9 @@ export class StreamParser {
|
||||
}
|
||||
break;
|
||||
|
||||
case "V4+ Styles":
|
||||
case Section.Styles:
|
||||
if (this._ass.stylesFormatSpecifier === null) {
|
||||
var property = parseLineIntoProperty(line);
|
||||
const property = parseLineIntoProperty(line);
|
||||
if (property !== null && property.name === "Format") {
|
||||
this._ass.stylesFormatSpecifier = property.value.split(",").map(str => str.trim());
|
||||
}
|
||||
@@ -130,13 +199,20 @@ export class StreamParser {
|
||||
}
|
||||
}
|
||||
else {
|
||||
this._ass.addStyle(line);
|
||||
try {
|
||||
this._ass.addStyle(line);
|
||||
}
|
||||
catch (ex) {
|
||||
if (debugMode) {
|
||||
console.error(`Could not parse style from line ${ line } - ${ ex.stack || ex }`);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "Events":
|
||||
case Section.Events:
|
||||
if (this._ass.dialoguesFormatSpecifier === null) {
|
||||
var property = parseLineIntoProperty(line);
|
||||
const property = parseLineIntoProperty(line);
|
||||
if (property !== null && property.name === "Format") {
|
||||
this._ass.dialoguesFormatSpecifier = property.value.split(",").map(str => str.trim());
|
||||
}
|
||||
@@ -145,17 +221,62 @@ export class StreamParser {
|
||||
}
|
||||
}
|
||||
else {
|
||||
this._ass.addEvent(line);
|
||||
try {
|
||||
this._ass.addEvent(line);
|
||||
}
|
||||
catch (ex) {
|
||||
if (debugMode) {
|
||||
console.error(`Could not parse event from line ${ line } - ${ ex.stack || ex }`);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// Ignore other sections
|
||||
case Section.Fonts:
|
||||
case Section.Graphics:
|
||||
const startOfNewAttachmentRegex = (this.currentSection === Section.Fonts) ? /^fontname:(.+)/ : /^filename:(.+)/;
|
||||
const startOfNewAttachment = startOfNewAttachmentRegex.exec(line);
|
||||
|
||||
if (startOfNewAttachment !== null) {
|
||||
// Start of new attachment
|
||||
|
||||
if (this._currentAttachment !== null) {
|
||||
this._ass.addAttachment(this._currentAttachment);
|
||||
this._currentAttachment = null;
|
||||
}
|
||||
|
||||
this._currentAttachment = new Attachment(startOfNewAttachment[1].trim(), (this.currentSection === Section.Fonts) ? AttachmentType.Font : AttachmentType.Graphic);
|
||||
}
|
||||
else if (this._currentAttachment !== null) {
|
||||
try {
|
||||
this._currentAttachment.contents += uuencodedToBase64(line);
|
||||
}
|
||||
catch (ex) {
|
||||
if (debugMode) {
|
||||
console.error(`Encountered error while reading font ${ this._currentAttachment.filename }: %o`, ex);
|
||||
}
|
||||
|
||||
this._currentAttachment = null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Ignore.
|
||||
}
|
||||
break;
|
||||
|
||||
case Section.Other:
|
||||
// Ignore other sections.
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unhandled state ${ this.currentSection }`);
|
||||
}
|
||||
}
|
||||
|
||||
this._stream.nextLine().then(line => this._onNextLine(line));
|
||||
this._stream.nextLine().then(line => this._onNextLine(line), reason => {
|
||||
this._minimalDeferred.reject(reason);
|
||||
this._deferred.reject(reason);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,20 +289,24 @@ export class SrtStreamParser {
|
||||
private _ass: ASS = new ASS();
|
||||
private _deferred: DeferredPromise<ASS> = new DeferredPromise<ASS>();
|
||||
|
||||
private _shouldSwallowBom: boolean = true;
|
||||
|
||||
private _currentDialogueNumber: string = null;
|
||||
private _currentDialogueStart: string = null;
|
||||
private _currentDialogueEnd: string = null;
|
||||
private _currentDialogueText: string = null;
|
||||
|
||||
constructor(private _stream: Stream) {
|
||||
this._stream.nextLine().then(line => this._onNextLine(line));
|
||||
this._stream.nextLine().then(line => this._onNextLine(line), reason => {
|
||||
this._deferred.reject(reason);
|
||||
});
|
||||
|
||||
this._ass.properties.resolutionX = 1280;
|
||||
this._ass.properties.resolutionY = 720;
|
||||
this._ass.properties.wrappingStyle = 1;
|
||||
this._ass.properties.scaleBorderAndShadow = true;
|
||||
|
||||
var newStyle = new Style(new Map([["Name", "Default"]]));
|
||||
const newStyle = new Style(new Map([["Name", "Default"], ["FontSize", "36"]]));
|
||||
this._ass.styles.set(newStyle.name, newStyle);
|
||||
}
|
||||
|
||||
@@ -214,6 +339,12 @@ export class SrtStreamParser {
|
||||
line = line.substr(0, line.length - 1);
|
||||
}
|
||||
|
||||
if (line.charCodeAt(0) === 0xfeff && this._shouldSwallowBom) {
|
||||
line = line.substr(1);
|
||||
}
|
||||
|
||||
this._shouldSwallowBom = false;
|
||||
|
||||
if (line === "") {
|
||||
if (this._currentDialogueNumber !== null && this._currentDialogueStart !== null && this._currentDialogueEnd !== null && this._currentDialogueText !== null) {
|
||||
this._ass.dialogues.push(new Dialogue(new Map([
|
||||
@@ -233,7 +364,7 @@ export class SrtStreamParser {
|
||||
}
|
||||
}
|
||||
else if (this._currentDialogueStart === null && this._currentDialogueEnd === null) {
|
||||
var match = /^(\d\d:\d\d:\d\d,\d\d\d) --> (\d\d:\d\d:\d\d,\d\d\d)/.exec(line);
|
||||
const match = /^(\d\d:\d\d:\d\d,\d\d\d) --> (\d\d:\d\d:\d\d,\d\d\d)/.exec(line);
|
||||
if (match !== null) {
|
||||
this._currentDialogueStart = match[1].replace(",", ".");
|
||||
this._currentDialogueEnd = match[2].replace(",", ".");
|
||||
@@ -261,6 +392,43 @@ export class SrtStreamParser {
|
||||
}
|
||||
}
|
||||
|
||||
this._stream.nextLine().then(line => this._onNextLine(line));
|
||||
this._stream.nextLine().then(line => this._onNextLine(line), reason => {
|
||||
this._deferred.reject(reason);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a uuencoded string to a base64 string.
|
||||
*
|
||||
* @param {string} str
|
||||
* @return {string}
|
||||
*/
|
||||
function uuencodedToBase64(str: string): string {
|
||||
let result = "";
|
||||
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const charCode = str.charCodeAt(i) - 33;
|
||||
|
||||
if (charCode < 0 || charCode > 63) {
|
||||
throw new Error(`Out-of-range character code ${ charCode } at index ${ i } in string ${ str }`);
|
||||
}
|
||||
if (charCode < 26) {
|
||||
result += String.fromCharCode("A".charCodeAt(0) + charCode);
|
||||
}
|
||||
else if (charCode < 52) {
|
||||
result += String.fromCharCode("a".charCodeAt(0) + charCode - 26);
|
||||
}
|
||||
else if (charCode < 62) {
|
||||
result += String.fromCharCode("0".charCodeAt(0) + charCode - 52);
|
||||
}
|
||||
else if (charCode === 62) {
|
||||
result += "+";
|
||||
}
|
||||
else {
|
||||
result += "/";
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ export interface TextDecoderConstructor {
|
||||
prototype: TextDecoder;
|
||||
}
|
||||
|
||||
declare var global: {
|
||||
declare const global: {
|
||||
/**
|
||||
* @type {!TextDecoderConstructor}
|
||||
*/
|
||||
@@ -82,10 +82,10 @@ export class StringStream implements Stream {
|
||||
* @return {!Promise.<?string>} A promise that will be resolved with the next line, or null if the string has been completely read.
|
||||
*/
|
||||
nextLine(): Promise<string> {
|
||||
var result: Promise<string> = null;
|
||||
let result: Promise<string> = null;
|
||||
|
||||
if (this._readTill < this._str.length) {
|
||||
var nextNewLinePos = this._str.indexOf("\n", this._readTill);
|
||||
const nextNewLinePos = this._str.indexOf("\n", this._readTill);
|
||||
if (nextNewLinePos !== -1) {
|
||||
result = Promise.resolve(this._str.substring(this._readTill, nextNewLinePos));
|
||||
this._readTill = nextNewLinePos + 1;
|
||||
@@ -112,10 +112,12 @@ export class StringStream implements Stream {
|
||||
export class XhrStream implements Stream {
|
||||
private _readTill: number = 0;
|
||||
private _pendingDeferred: DeferredPromise<string> = null;
|
||||
private _failedError: ErrorEvent = null;
|
||||
|
||||
constructor(private _xhr: XMLHttpRequest) {
|
||||
_xhr.addEventListener("progress", event => this._onXhrProgress(event), false);
|
||||
_xhr.addEventListener("loadend", event => this._onXhrLoadEnd(event), false);
|
||||
_xhr.addEventListener("progress", () => this._onXhrProgress(), false);
|
||||
_xhr.addEventListener("load", () => this._onXhrLoad(), false);
|
||||
_xhr.addEventListener("error", event => this._onXhrError(event), false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,7 +128,7 @@ export class XhrStream implements Stream {
|
||||
throw new Error("XhrStream only supports one pending unfulfilled read at a time.");
|
||||
}
|
||||
|
||||
var deferred = this._pendingDeferred = new DeferredPromise<string>();
|
||||
const deferred = this._pendingDeferred = new DeferredPromise<string>();
|
||||
|
||||
this._tryResolveNextLine();
|
||||
|
||||
@@ -134,9 +136,27 @@ export class XhrStream implements Stream {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!ProgressEvent} event
|
||||
*/
|
||||
private _onXhrProgress(event: ProgressEvent): void {
|
||||
private _onXhrProgress(): void {
|
||||
if (this._pendingDeferred === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._xhr.readyState === XMLHttpRequest.DONE) {
|
||||
/* Suppress resolving next line here. Let the "load" or "error" event handlers do it.
|
||||
*
|
||||
* This is required because a failed XHR fires the progress event with readyState === DONE before it fires the error event.
|
||||
* This would confuse _tryResolveNextLine() into thinking the request succeeded with no data if it was called here.
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
this._tryResolveNextLine();
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
private _onXhrLoad(): void {
|
||||
if (this._pendingDeferred === null) {
|
||||
return;
|
||||
}
|
||||
@@ -145,9 +165,11 @@ export class XhrStream implements Stream {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!ProgressEvent} event
|
||||
* @param {!ErrorEvent} event
|
||||
*/
|
||||
private _onXhrLoadEnd(event: ProgressEvent): void {
|
||||
private _onXhrError(event: ErrorEvent): void {
|
||||
this._failedError = event;
|
||||
|
||||
if (this._pendingDeferred === null) {
|
||||
return;
|
||||
}
|
||||
@@ -158,9 +180,14 @@ export class XhrStream implements Stream {
|
||||
/**
|
||||
*/
|
||||
private _tryResolveNextLine(): void {
|
||||
var response = this._xhr.responseText;
|
||||
if (this._failedError !== null) {
|
||||
this._pendingDeferred.reject(this._failedError);
|
||||
return;
|
||||
}
|
||||
|
||||
var nextNewLinePos = response.indexOf("\n", this._readTill);
|
||||
const response = this._xhr.responseText;
|
||||
|
||||
const nextNewLinePos = response.indexOf("\n", this._readTill);
|
||||
if (nextNewLinePos !== -1) {
|
||||
this._pendingDeferred.resolve(response.substring(this._readTill, nextNewLinePos));
|
||||
this._readTill = nextNewLinePos + 1;
|
||||
@@ -168,8 +195,12 @@ export class XhrStream implements Stream {
|
||||
}
|
||||
|
||||
else if (this._xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (this._failedError !== null) {
|
||||
this._pendingDeferred.reject(this._failedError);
|
||||
}
|
||||
|
||||
// No more data. This is the last line.
|
||||
if (this._readTill < response.length) {
|
||||
else if (this._readTill < response.length) {
|
||||
this._pendingDeferred.resolve(response.substr(this._readTill));
|
||||
this._readTill = response.length;
|
||||
}
|
||||
@@ -207,7 +238,7 @@ export class BrowserReadableStream implements Stream {
|
||||
throw new Error("BrowserReadableStream only supports one pending unfulfilled read at a time.");
|
||||
}
|
||||
|
||||
var deferred = this._pendingDeferred = new DeferredPromise<string>();
|
||||
const deferred = this._pendingDeferred = new DeferredPromise<string>();
|
||||
|
||||
this._tryResolveNextLine();
|
||||
|
||||
@@ -217,7 +248,7 @@ export class BrowserReadableStream implements Stream {
|
||||
/**
|
||||
*/
|
||||
private _tryResolveNextLine(): void {
|
||||
var nextNewLinePos = this._buffer.indexOf("\n");
|
||||
const nextNewLinePos = this._buffer.indexOf("\n");
|
||||
if (nextNewLinePos !== -1) {
|
||||
this._pendingDeferred.resolve(this._buffer.substr(0, nextNewLinePos));
|
||||
this._buffer = this._buffer.substr(nextNewLinePos + 1);
|
||||
@@ -226,7 +257,7 @@ export class BrowserReadableStream implements Stream {
|
||||
|
||||
else {
|
||||
this._reader.read().then(next => {
|
||||
var { value, done } = next;
|
||||
const { value, done } = next;
|
||||
|
||||
if (!done) {
|
||||
this._buffer += this._decoder.decode(value, { stream: true });
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Attachment } from "../types/attachment";
|
||||
|
||||
import { Map } from "../utility/map";
|
||||
import { Set } from "../utility/set";
|
||||
|
||||
type DataReader = { dataView: DataView; position: number; };
|
||||
|
||||
enum DataType {
|
||||
Char,
|
||||
Uint16,
|
||||
Uint32,
|
||||
}
|
||||
|
||||
type StructMemberDefinition = { type: DataType; field?: string; };
|
||||
|
||||
const fieldDecorators = new Map<DataType, (proto: any, field: string) => void>();
|
||||
|
||||
@struct
|
||||
class OffsetTable {
|
||||
/** @type {number} */ @field(DataType.Uint16) majorVersion: number;
|
||||
/** @type {number} */ @field(DataType.Uint16) minorVersion: number;
|
||||
/** @type {number} */ @field(DataType.Uint16) numTables: number;
|
||||
/** @type {number} */ @field(DataType.Uint16) searchRange: number;
|
||||
/** @type {number} */ @field(DataType.Uint16) entrySelector: number;
|
||||
/** @type {number} */ @field(DataType.Uint16) rangeShift: number;
|
||||
|
||||
/** @type {function(!{ dataView: DataView, position: number }): OffsetTable} */
|
||||
static read: (reader: DataReader) => OffsetTable;
|
||||
}
|
||||
|
||||
@struct
|
||||
class TableRecord {
|
||||
/** @type {string} */ @field(DataType.Char) c1: string;
|
||||
/** @type {string} */ @field(DataType.Char) c2: string;
|
||||
/** @type {string} */ @field(DataType.Char) c3: string;
|
||||
/** @type {string} */ @field(DataType.Char) c4: string;
|
||||
/** @type {number} */ @field(DataType.Uint32) checksum: number;
|
||||
/** @type {number} */ @field(DataType.Uint32) offset: number;
|
||||
/** @type {number} */ @field(DataType.Uint32) length: number;
|
||||
|
||||
/** @type {function(!{ dataView: DataView, position: number }): TableRecord} */
|
||||
static read: (reader: DataReader) => TableRecord;
|
||||
}
|
||||
|
||||
@struct
|
||||
class NameTableHeader {
|
||||
/** @type {number} */ @field(DataType.Uint16) formatSelector: number;
|
||||
/** @type {number} */ @field(DataType.Uint16) count: number;
|
||||
/** @type {number} */ @field(DataType.Uint16) stringOffset: number;
|
||||
|
||||
/** @type {function(!{ dataView: DataView, position: number }): NameTableHeader} */
|
||||
static read: (reader: DataReader) => NameTableHeader;
|
||||
}
|
||||
|
||||
@struct
|
||||
class NameRecord {
|
||||
/** @type {number} */ @field(DataType.Uint16) platformId: number;
|
||||
/** @type {number} */ @field(DataType.Uint16) encodingId: number;
|
||||
/** @type {number} */ @field(DataType.Uint16) languageId: number;
|
||||
/** @type {number} */ @field(DataType.Uint16) nameId: number;
|
||||
/** @type {number} */ @field(DataType.Uint16) length: number;
|
||||
/** @type {number} */ @field(DataType.Uint16) offset: number;
|
||||
|
||||
/** @type {function(!{ dataView: DataView, position: number }): NameRecord} */
|
||||
static read: (reader: DataReader) => NameRecord;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the font names from the given font attachment.
|
||||
*
|
||||
* @param {!libjass.Attachment} attachment
|
||||
* @return {!libjass.Set.<string>}
|
||||
*/
|
||||
export function getTtfNames(attachment: Attachment): Set<string> {
|
||||
const decoded = atob(attachment.contents);
|
||||
|
||||
const bytes = new Uint8Array(new ArrayBuffer(decoded.length));
|
||||
|
||||
for (let i = 0; i < decoded.length; i++) {
|
||||
bytes[i] = decoded.charCodeAt(i);
|
||||
}
|
||||
|
||||
const reader = { dataView: new DataView(bytes.buffer), position: 0 };
|
||||
|
||||
const offsetTable = OffsetTable.read(reader);
|
||||
let nameTableRecord: TableRecord = null;
|
||||
for (let i = 0; i < offsetTable.numTables; i++) {
|
||||
const tableRecord = TableRecord.read(reader);
|
||||
if (tableRecord.c1 + tableRecord.c2 + tableRecord.c3 + tableRecord.c4 === "name") {
|
||||
nameTableRecord = tableRecord;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
reader.position = nameTableRecord.offset;
|
||||
const nameTableHeader = NameTableHeader.read(reader);
|
||||
|
||||
const result = new Set<string>();
|
||||
for (let i = 0; i < nameTableHeader.count; i++) {
|
||||
const nameRecord = NameRecord.read(reader);
|
||||
|
||||
switch (nameRecord.nameId) {
|
||||
case 1:
|
||||
case 4:
|
||||
case 6:
|
||||
const recordOffset = nameTableRecord.offset + nameTableHeader.stringOffset + nameRecord.offset;
|
||||
const nameBytes = bytes.subarray(recordOffset, recordOffset + nameRecord.length);
|
||||
|
||||
switch (nameRecord.platformId) {
|
||||
case 1: {
|
||||
let name = "";
|
||||
|
||||
for (let j = 0; j < nameBytes.length; j++) {
|
||||
name += String.fromCharCode(nameBytes[j]);
|
||||
}
|
||||
|
||||
result.add(name);
|
||||
}
|
||||
break;
|
||||
|
||||
case 3: {
|
||||
let name = "";
|
||||
|
||||
for (let j = 0; j < nameBytes.length; j += 2) {
|
||||
name += String.fromCharCode((nameBytes[j] << 8) + nameBytes[j + 1]);
|
||||
}
|
||||
|
||||
result.add(name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!function(new(): T)} clazz
|
||||
* @return {!function(new(): T)}
|
||||
*/
|
||||
function struct<T>(clazz: { new (): T; read(reader: DataReader): T; }): { new (): T; read(reader: DataReader): T; } {
|
||||
const fields: StructMemberDefinition[] = (<any>clazz).__fields;
|
||||
|
||||
clazz.read = (reader: DataReader) => {
|
||||
const result: any = new clazz();
|
||||
|
||||
for (const field of fields) {
|
||||
let value: any;
|
||||
switch (field.type) {
|
||||
case DataType.Char:
|
||||
value = String.fromCharCode(reader.dataView.getInt8(reader.position));
|
||||
reader.position += 1;
|
||||
break;
|
||||
|
||||
case DataType.Uint16:
|
||||
value = reader.dataView.getUint16(reader.position);
|
||||
reader.position += 2;
|
||||
break;
|
||||
|
||||
case DataType.Uint32:
|
||||
value = reader.dataView.getUint32(reader.position);
|
||||
reader.position += 4;
|
||||
break;
|
||||
}
|
||||
|
||||
result[field.field] = value;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return clazz;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} type
|
||||
* @return {function(T, string)}
|
||||
*/
|
||||
function field<T>(type: DataType): (proto: T, field: string) => void {
|
||||
let existingDecorator = fieldDecorators.get(type);
|
||||
if (existingDecorator === undefined) {
|
||||
existingDecorator =(proto: T, field: string) => {
|
||||
const ctor: { __fields?: StructMemberDefinition[] } = proto.constructor;
|
||||
if (ctor.__fields === undefined) {
|
||||
ctor.__fields = [];
|
||||
}
|
||||
|
||||
ctor.__fields.push({ type, field });
|
||||
};
|
||||
|
||||
fieldDecorators.set(type, existingDecorator);
|
||||
}
|
||||
|
||||
return existingDecorator;
|
||||
}
|
||||
@@ -90,6 +90,22 @@ export class Color {
|
||||
toString(): string {
|
||||
return `rgba(${ this._red }, ${ this._green }, ${ this._blue }, ${ this._alpha.toFixed(3) })`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new Color by interpolating the current color to the final color by the given progression.
|
||||
*
|
||||
* @param {!libjass.parts.Color} final
|
||||
* @param {number} progression
|
||||
* @return {!libjass.parts.Color}
|
||||
*/
|
||||
interpolate(final: Color, progression: number): Color {
|
||||
return new Color(
|
||||
this._red + progression * (final.red - this._red),
|
||||
this._green + progression * (final.green - this._green),
|
||||
this._blue + progression * (final.blue - this._blue),
|
||||
this._alpha + progression * (final.alpha - this._alpha)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -401,7 +417,7 @@ export class FontSize {
|
||||
/**
|
||||
* A font size increase tag {\fs+}
|
||||
*
|
||||
* @param {?number} value {\fs+###} -> difference (number)
|
||||
* @param {number} value {\fs+###} -> difference (number)
|
||||
*/
|
||||
export class FontSizePlus {
|
||||
constructor(private _value: number) { }
|
||||
@@ -409,7 +425,7 @@ export class FontSizePlus {
|
||||
/**
|
||||
* The value of this font size increase tag.
|
||||
*
|
||||
* @type {?number}
|
||||
* @type {number}
|
||||
*/
|
||||
get value(): number {
|
||||
return this._value;
|
||||
@@ -419,7 +435,7 @@ export class FontSizePlus {
|
||||
/**
|
||||
* A font size decrease tag {\fs-}
|
||||
*
|
||||
* @param {?number} value {\fs-###} -> difference (number)
|
||||
* @param {number} value {\fs-###} -> difference (number)
|
||||
*/
|
||||
export class FontSizeMinus {
|
||||
constructor(private _value: number) { }
|
||||
@@ -427,7 +443,7 @@ export class FontSizeMinus {
|
||||
/**
|
||||
* The value of this font size decrease tag.
|
||||
*
|
||||
* @type {?number}
|
||||
* @type {number}
|
||||
*/
|
||||
get value(): number {
|
||||
return this._value;
|
||||
@@ -1095,7 +1111,7 @@ export class Transform {
|
||||
/**
|
||||
* The starting time of this transform tag.
|
||||
*
|
||||
* @type {number}
|
||||
* @type {?number}
|
||||
*/
|
||||
get start(): number {
|
||||
return this._start;
|
||||
@@ -1104,7 +1120,7 @@ export class Transform {
|
||||
/**
|
||||
* The ending time of this transform tag.
|
||||
*
|
||||
* @type {number}
|
||||
* @type {?number}
|
||||
*/
|
||||
get end(): number {
|
||||
return this._end;
|
||||
@@ -1113,7 +1129,7 @@ export class Transform {
|
||||
/**
|
||||
* The acceleration of this transform tag.
|
||||
*
|
||||
* @type {number}
|
||||
* @type {?number}
|
||||
*/
|
||||
get accel(): number {
|
||||
return this._accel;
|
||||
@@ -1279,9 +1295,9 @@ export class DrawingInstructions {
|
||||
}
|
||||
}
|
||||
|
||||
var addToString = function (ctor: Function, ctorName: string) {
|
||||
const addToString = function (ctor: Function, ctorName: string) {
|
||||
if (!ctor.prototype.hasOwnProperty("toString")) {
|
||||
var propertyNames = Object.getOwnPropertyNames(ctor.prototype).filter(property => property !== "constructor");
|
||||
const propertyNames = Object.getOwnPropertyNames(ctor.prototype).filter(property => property !== "constructor");
|
||||
|
||||
ctor.prototype.toString = function () {
|
||||
return (
|
||||
@@ -1296,18 +1312,18 @@ var addToString = function (ctor: Function, ctorName: string) {
|
||||
|
||||
import { registerClassPrototype } from "../webworker/misc";
|
||||
|
||||
declare var exports: any;
|
||||
declare const exports: any;
|
||||
|
||||
for (let key of Object.keys(exports)) {
|
||||
var value: any = exports[key];
|
||||
for (const key of Object.keys(exports)) {
|
||||
const value: any = exports[key];
|
||||
if (value instanceof Function) {
|
||||
addToString(value, key);
|
||||
registerClassPrototype(value.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
for (let key of Object.keys(drawing)) {
|
||||
var value: any = (<any>drawing)[key];
|
||||
for (const key of Object.keys(drawing)) {
|
||||
const value: any = (<any>drawing)[key];
|
||||
if (value instanceof Function) {
|
||||
addToString(value, `Drawing${ key }`);
|
||||
registerClassPrototype(value.prototype);
|
||||
|
||||
@@ -24,10 +24,10 @@ import { Clock, ClockEvent } from "./base";
|
||||
import { ManualClock } from "./manual";
|
||||
|
||||
/**
|
||||
* An implementation of libjass.renderers.Clock that automatically ticks and generates {@link libjass.renderers.ClockEvent}s according to the state of an external driver.
|
||||
* An implementation of {@link libjass.renderers.Clock} that automatically ticks and generates {@link libjass.renderers.ClockEvent}s according to the state of an external driver.
|
||||
*
|
||||
* For example, if you're using libjass to render subtitles on a canvas with your own video controls, these video controls will function as the driver to this AutoClock.
|
||||
* It would call {@link libjass.renderers.AutoClock.play}, {@link libjass.renderers.AutoClock.play}, etc. when the user pressed the corresponding video controls.
|
||||
* It would call {@link libjass.renderers.AutoClock.play}, {@link libjass.renderers.AutoClock.pause}, etc. when the user pressed the corresponding video controls.
|
||||
*
|
||||
* The difference from ManualClock is that AutoClock does not require the driver to call something like {@link libjass.renderers.ManualClock.tick}. Instead it keeps its
|
||||
* own time with a high-resolution requestAnimationFrame-based timer.
|
||||
@@ -35,7 +35,7 @@ import { ManualClock } from "./manual";
|
||||
* If using libjass with a <video> element, consider using {@link libjass.renderers.VideoClock} that uses the video element as a driver.
|
||||
*
|
||||
* @param {function():number} getCurrentTime A callback that will be invoked to get the current time of the external driver.
|
||||
* @param {number} currentTimeUpdateMaxDelay If two calls to getCurrentTime are more than currentTimeUpdateMaxDelay milliseconds apart, then the external driver will be
|
||||
* @param {number} autoPauseAfter If two calls to getCurrentTime are more than autoPauseAfter milliseconds apart but return the same time, then the external driver will be
|
||||
* considered to have paused.
|
||||
*/
|
||||
export class AutoClock implements Clock {
|
||||
@@ -46,7 +46,7 @@ export class AutoClock implements Clock {
|
||||
private _lastKnownExternalTime: number = null;
|
||||
private _lastKnownExternalTimeObtainedAt: number = null;
|
||||
|
||||
constructor(private _getCurrentTime: () => number, private _currentTimeUpdateMaxDelay: number) { }
|
||||
constructor(private _getCurrentTime: () => number, private _autoPauseAfter: number) { }
|
||||
|
||||
/**
|
||||
* Tells the clock to start generating ticks.
|
||||
@@ -164,7 +164,12 @@ export class AutoClock implements Clock {
|
||||
* Toggle the clock.
|
||||
*/
|
||||
toggle(): void {
|
||||
this._manualClock.toggle();
|
||||
if (this._manualClock.enabled) {
|
||||
this.disable();
|
||||
}
|
||||
else {
|
||||
this.enable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -174,7 +179,12 @@ export class AutoClock implements Clock {
|
||||
* @return {boolean} True if the clock is now in the given state, false if it was already in that state.
|
||||
*/
|
||||
setEnabled(enabled: boolean): boolean {
|
||||
return this._manualClock.setEnabled(enabled);
|
||||
if (enabled) {
|
||||
return this.enable();
|
||||
}
|
||||
else {
|
||||
return this.disable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -197,12 +207,12 @@ export class AutoClock implements Clock {
|
||||
return;
|
||||
}
|
||||
|
||||
var currentTime = this._manualClock.currentTime;
|
||||
var currentExternalTime = this._getCurrentTime();
|
||||
const currentTime = this._manualClock.currentTime;
|
||||
const currentExternalTime = this._getCurrentTime();
|
||||
|
||||
if (!this._manualClock.paused) {
|
||||
if (this._lastKnownExternalTime !== null && currentExternalTime === this._lastKnownExternalTime) {
|
||||
if (timeStamp - this._lastKnownExternalTimeObtainedAt > this._currentTimeUpdateMaxDelay) {
|
||||
if (timeStamp - this._lastKnownExternalTimeObtainedAt > this._autoPauseAfter) {
|
||||
this._lastKnownExternalTimeObtainedAt = null;
|
||||
this._manualClock.pause();
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export class EventSource<T> {
|
||||
* @param {!Function} listener The listener
|
||||
*/
|
||||
addEventListener(type: T, listener: Function): void {
|
||||
var listeners = this._eventListeners.get(type);
|
||||
let listeners = this._eventListeners.get(type);
|
||||
|
||||
if (listeners === undefined) {
|
||||
this._eventListeners.set(type, listeners = []);
|
||||
@@ -54,9 +54,9 @@ export class EventSource<T> {
|
||||
* @param {!Array.<*>} args Arguments for the listeners of the event
|
||||
*/
|
||||
_dispatchEvent(type: T, args: Object[]): void {
|
||||
var listeners = this._eventListeners.get(type);
|
||||
const listeners = this._eventListeners.get(type);
|
||||
if (listeners !== undefined) {
|
||||
for (let listener of listeners) {
|
||||
for (const listener of listeners) {
|
||||
listener.apply(this, args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,10 +60,12 @@ export class ManualClock implements Clock, EventSource<ClockEvent> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._currentTime !== currentTime) {
|
||||
this.play();
|
||||
if (this._currentTime === currentTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.play();
|
||||
|
||||
this._currentTime = currentTime;
|
||||
this._dispatchEvent(ClockEvent.Tick, []);
|
||||
}
|
||||
@@ -188,12 +190,12 @@ export class ManualClock implements Clock, EventSource<ClockEvent> {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._enabled = false;
|
||||
|
||||
this.pause();
|
||||
|
||||
this.stop();
|
||||
|
||||
this._enabled = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,12 @@ export class VideoClock implements Clock {
|
||||
* Toggle the clock.
|
||||
*/
|
||||
toggle(): void {
|
||||
this._autoClock.toggle();
|
||||
if (this._autoClock.enabled) {
|
||||
this.disable();
|
||||
}
|
||||
else {
|
||||
this.enable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,7 +104,12 @@ export class VideoClock implements Clock {
|
||||
* @return {boolean} True if the clock is now in the given state, false if it was already in that state.
|
||||
*/
|
||||
setEnabled(enabled: boolean): boolean {
|
||||
return this._autoClock.setEnabled(enabled);
|
||||
if (enabled) {
|
||||
return this.enable();
|
||||
}
|
||||
else {
|
||||
return this.disable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,8 +24,6 @@ import { WebRenderer } from "./web/renderer";
|
||||
|
||||
import { ASS } from "../types/ass";
|
||||
|
||||
///<reference path="./default-references.d.ts" />
|
||||
|
||||
/**
|
||||
* A default renderer implementation.
|
||||
*
|
||||
@@ -34,8 +32,6 @@ import { ASS } from "../types/ass";
|
||||
* @param {libjass.renderers.RendererSettings} settings
|
||||
*/
|
||||
export class DefaultRenderer extends WebRenderer {
|
||||
private _videoIsFullScreen: boolean = false;
|
||||
|
||||
constructor(private _video: HTMLVideoElement, ass: ASS, settings?: RendererSettings) {
|
||||
super(ass, new VideoClock(_video), document.createElement("div"), settings);
|
||||
|
||||
@@ -44,49 +40,38 @@ export class DefaultRenderer extends WebRenderer {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* Resize the subtitles to the dimensions of the video element.
|
||||
*
|
||||
* @param {number} width
|
||||
* @param {number} height
|
||||
* This method accounts for letterboxing if the video element's size is not the same ratio as the video resolution.
|
||||
*/
|
||||
resizeVideo(width: number, height: number): void {
|
||||
console.warn("`DefaultRenderer.resizeVideo(width, height)` has been deprecated. Use `DefaultRenderer.resize(width, height)` instead.");
|
||||
this.resize(width, height);
|
||||
}
|
||||
resize(): void {
|
||||
// Handle letterboxing around the video. If the width or height are greater than the video can be, then consider that dead space.
|
||||
|
||||
protected _ready(): void {
|
||||
document.addEventListener("mozfullscreenchange", event => this._onFullScreenChange(document.mozFullScreenElement), false);
|
||||
document.addEventListener("webkitfullscreenchange", event => this._onFullScreenChange(document.webkitFullscreenElement), false);
|
||||
document.addEventListener("fullscreenchange", event => this._onFullScreenChange(document.fullscreenElement), false);
|
||||
const videoWidth = this._video.videoWidth;
|
||||
const videoHeight = this._video.videoHeight;
|
||||
const videoOffsetWidth = this._video.offsetWidth;
|
||||
const videoOffsetHeight = this._video.offsetHeight;
|
||||
|
||||
this.resize(this._video.offsetWidth, this._video.offsetHeight);
|
||||
const ratio = Math.min(videoOffsetWidth / videoWidth, videoOffsetHeight / videoHeight);
|
||||
const subsWrapperWidth = videoWidth * ratio;
|
||||
const subsWrapperHeight = videoHeight * ratio;
|
||||
const subsWrapperLeft = (videoOffsetWidth - subsWrapperWidth) / 2;
|
||||
const subsWrapperTop = (videoOffsetHeight - subsWrapperHeight) / 2;
|
||||
|
||||
super._ready();
|
||||
super.resize(subsWrapperWidth, subsWrapperHeight, subsWrapperLeft, subsWrapperTop);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Element} fullScreenElement
|
||||
* @deprecated
|
||||
*/
|
||||
private _onFullScreenChange(fullScreenElement: Element): void {
|
||||
if (fullScreenElement === undefined) {
|
||||
fullScreenElement = document.msFullscreenElement;
|
||||
}
|
||||
resizeVideo(): void {
|
||||
console.warn("`DefaultRenderer.resizeVideo(width, height)` has been deprecated. Use `DefaultRenderer.resize()` instead.");
|
||||
this.resize();
|
||||
}
|
||||
|
||||
if (fullScreenElement === this._video) {
|
||||
this.libjassSubsWrapper.classList.add("libjass-full-screen");
|
||||
protected _ready(): void {
|
||||
this.resize();
|
||||
|
||||
this.resize(screen.width, screen.height);
|
||||
|
||||
this._videoIsFullScreen = true;
|
||||
|
||||
this._dispatchEvent("fullScreenChange", [this._videoIsFullScreen]);
|
||||
}
|
||||
else if (fullScreenElement === null && this._videoIsFullScreen) {
|
||||
this.libjassSubsWrapper.classList.remove("libjass-full-screen");
|
||||
|
||||
this._videoIsFullScreen = false;
|
||||
|
||||
this._dispatchEvent("fullScreenChange", [this._videoIsFullScreen]);
|
||||
}
|
||||
super._ready();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,4 +26,5 @@ export { VideoClock } from "./clocks/video";
|
||||
export { DefaultRenderer } from "./default";
|
||||
export { NullRenderer } from "./null";
|
||||
export { WebRenderer } from "./web/renderer";
|
||||
export { RendererSettings } from "./settings";
|
||||
|
||||
export { RendererSettings, makeFontMapFromStyleElement } from "./settings";
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
import { Clock, ClockEvent } from "./clocks/base";
|
||||
|
||||
import { RendererSettings } from "./settings";
|
||||
import { RendererSettings, toRendererSettings } from "./settings";
|
||||
|
||||
import { verboseMode } from "../settings";
|
||||
|
||||
@@ -44,7 +44,7 @@ export class NullRenderer {
|
||||
constructor(private _ass: ASS, private _clock: Clock, settings?: RendererSettings) {
|
||||
this._id = ++NullRenderer._lastRendererId;
|
||||
|
||||
this._settings = RendererSettings.from(settings);
|
||||
this._settings = toRendererSettings(settings);
|
||||
|
||||
this._clock.addEventListener(ClockEvent.Play, () => this._onClockPlay());
|
||||
this._clock.addEventListener(ClockEvent.Tick, () => this._onClockTick());
|
||||
@@ -152,13 +152,13 @@ export class NullRenderer {
|
||||
* Runs when the clock's current time changed. This might be a result of either regular playback or seeking.
|
||||
*/
|
||||
protected _onClockTick(): void {
|
||||
var currentTime = this._clock.currentTime;
|
||||
const currentTime = this._clock.currentTime;
|
||||
|
||||
if (verboseMode) {
|
||||
console.log(`NullRenderer._onClockTick: currentTime = ${ currentTime }`);
|
||||
}
|
||||
|
||||
for (let dialogue of this._ass.dialogues) {
|
||||
for (const dialogue of this._ass.dialogues) {
|
||||
if (dialogue.end > currentTime) {
|
||||
if (dialogue.start <= currentTime) {
|
||||
// This dialogue is visible right now. Draw it.
|
||||
|
||||
@@ -23,16 +23,28 @@ import { Map } from "../utility/map";
|
||||
/**
|
||||
* Settings for the renderer.
|
||||
*/
|
||||
export class RendererSettings {
|
||||
export interface RendererSettings {
|
||||
/**
|
||||
* A map of font name to one or more URLs of that font. If provided, the fonts in this map are pre-loaded by the WebRenderer when it's created.
|
||||
*
|
||||
* If you have a <style> or <link> element on the page containing @font-face rules, you can use the {@link libjass.renderers.RendererSettings.makeFontMapFromStyleElement}
|
||||
* The key of each entry of the map is the font name used in the ASS script. There are three choices for the value:
|
||||
*
|
||||
* - A single string that you would use for the src attribute of a @font-face rule. Eg: `'url("/fonts.foo.ttf"), url("/fonts/foo-fallback.ttf"), local("Arial.ttf")'`
|
||||
*
|
||||
* - An array of the individual sources that you would use for the src attribute of a @font-face rule. Eg: `['url("/fonts.foo.ttf")', 'url("/fonts/foo-fallback.ttf")', 'local("Arial")']`
|
||||
*
|
||||
* - An array of URLs. Eg: `["/fonts.foo.ttf", "/fonts/foo-fallback.ttf"]`
|
||||
*
|
||||
* Only the first and second forms allow you to use local fonts. The third form only allows you to use remote fonts.
|
||||
*
|
||||
* If you have a <style> or <link> element on the page containing @font-face rules, you can use the {@link libjass.renderers.makeFontMapFromStyleElement}
|
||||
* convenience method to create a font map.
|
||||
*
|
||||
* @type {!Map.<string, !Array.<string>>}
|
||||
* Defaults to null.
|
||||
*
|
||||
* @type {!Map.<string, (string|!Array.<string>)>}
|
||||
*/
|
||||
fontMap: Map<string, string[]>;
|
||||
fontMap: Map<string, string | string[]>;
|
||||
|
||||
/**
|
||||
* Subtitles will be pre-rendered for this amount of time (seconds).
|
||||
@@ -64,73 +76,100 @@ export class RendererSettings {
|
||||
enableSvg: boolean;
|
||||
|
||||
/**
|
||||
* A convenience method to create a font map from a <style> or <link> element that contains @font-face rules. There should be one @font-face rule for each font name, mapping to a font file URL.
|
||||
* Comma-separated list of fonts to be used when font specified in ASS Styles not loaded.
|
||||
*
|
||||
* For example:
|
||||
* The value should be a valid CSS font-family property (i.e. comma-separated and individual names in quotes if necessary). Use empty string to disable fallback.
|
||||
*
|
||||
* @font-face {
|
||||
* font-family: "Helvetica";
|
||||
* src: url("/fonts/helvetica.ttf");
|
||||
* }
|
||||
* Defaults to 'Arial, Helvetica, sans-serif, "Segoe UI Symbol"'.
|
||||
*
|
||||
* @param {!LinkStyle} linkStyle
|
||||
* @return {!Map.<string, !Array.<string>>}
|
||||
* @type {string}
|
||||
*/
|
||||
static makeFontMapFromStyleElement(linkStyle: LinkStyle): Map<string, string[]> {
|
||||
var fontMap = new Map<string, string[]>();
|
||||
fallbackFonts: string;
|
||||
|
||||
var styleSheet = <CSSStyleSheet>linkStyle.sheet;
|
||||
var rules: CSSFontFaceRule[] = Array.prototype.filter.call(styleSheet.cssRules, (rule: CSSRule) => rule.type === CSSRule.FONT_FACE_RULE);
|
||||
for (let rule of rules) {
|
||||
var src = rule.style.getPropertyValue("src");
|
||||
/**
|
||||
* If true, attached TTF fonts in the ASS script will be used. The font is loaded as a data: URI. Requires ES6 typed arrays (ArrayBuffer, DataView, Uint8Array, etc).
|
||||
*
|
||||
* The font is naively parsed to extract the strings that will be used as the font family. Do not use this option with untrusted fonts or scripts.
|
||||
*
|
||||
* Defaults to false.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
useAttachedFonts: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an arbitrary object into a {@link libjass.renderers.RendererSettings} object.
|
||||
*
|
||||
* @param {*} object
|
||||
* @return {!libjass.renderers.RendererSettings}
|
||||
*/
|
||||
export function toRendererSettings(object?: any): RendererSettings {
|
||||
if (object === undefined || object === null) {
|
||||
object = {};
|
||||
}
|
||||
|
||||
const {
|
||||
fontMap = null,
|
||||
preRenderTime = 5,
|
||||
preciseOutlines = false,
|
||||
enableSvg = true,
|
||||
fallbackFonts = 'Arial, Helvetica, sans-serif, "Segoe UI Symbol"',
|
||||
useAttachedFonts = false
|
||||
} = <RendererSettings>object;
|
||||
|
||||
return {
|
||||
fontMap,
|
||||
preRenderTime,
|
||||
preciseOutlines,
|
||||
enableSvg,
|
||||
fallbackFonts,
|
||||
useAttachedFonts,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenience method to create a font map from a <style> or <link> element that contains @font-face rules. There should be one @font-face rule for each font name, mapping to a font file URL.
|
||||
*
|
||||
* For example:
|
||||
*
|
||||
* @font-face {
|
||||
* font-family: "Helvetica";
|
||||
* src: url("/fonts/helvetica.ttf"), local("Arial");
|
||||
* }
|
||||
*
|
||||
* @param {!LinkStyle} linkStyle
|
||||
* @return {!Map.<string, string>}
|
||||
*/
|
||||
export function makeFontMapFromStyleElement(linkStyle: LinkStyle): Map<string, string> {
|
||||
const fontMap = new Map<string, string>();
|
||||
|
||||
const styleSheet = <CSSStyleSheet>linkStyle.sheet;
|
||||
for (let i = 0; i < styleSheet.cssRules.length; i++) {
|
||||
const rule = styleSheet.cssRules[i];
|
||||
|
||||
if (isFontFaceRule(rule)) {
|
||||
const name = rule.style.getPropertyValue("font-family").match(/^["']?(.*?)["']?$/)[1];
|
||||
|
||||
let src = rule.style.getPropertyValue("src");
|
||||
if (!src) {
|
||||
src = rule.cssText.split("\n")
|
||||
.map(line => line.match(/src: ([^;]+);/))
|
||||
.map(line => line.match(/src:\s*([^;]+?)\s*;/))
|
||||
.filter(matches => matches !== null)
|
||||
.map(matches => matches[1])[0];
|
||||
}
|
||||
|
||||
var urls = src.split(/,\s*/).map(url => RendererSettings._stripQuotes(url.match(/^url\((.+)\)$/)[1]));
|
||||
if (urls.length > 0) {
|
||||
var name = RendererSettings._stripQuotes(rule.style.getPropertyValue("font-family"));
|
||||
var existingList = fontMap.get(name);
|
||||
if (existingList === undefined) {
|
||||
existingList = [];
|
||||
fontMap.set(name, existingList);
|
||||
}
|
||||
existingList.unshift.apply(existingList, urls);
|
||||
}
|
||||
fontMap.set(name, src);
|
||||
}
|
||||
|
||||
return fontMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an arbitrary object into a {@link libjass.renderers.RendererSettings} object.
|
||||
*
|
||||
* @param {*} object
|
||||
* @return {!libjass.renderers.RendererSettings}
|
||||
*/
|
||||
static from(object?: any): RendererSettings {
|
||||
if (object === undefined || object === null) {
|
||||
object = {};
|
||||
}
|
||||
|
||||
var { fontMap = null, preRenderTime = 5, preciseOutlines = false, enableSvg = true } = <RendererSettings>object;
|
||||
var result = new RendererSettings();
|
||||
result.fontMap = fontMap;
|
||||
result.preRenderTime = preRenderTime;
|
||||
result.preciseOutlines = preciseOutlines;
|
||||
result.enableSvg = enableSvg;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @return {string}
|
||||
*/
|
||||
private static _stripQuotes(str: string): string {
|
||||
return str.match(/^["']?(.*?)["']?$/)[1];
|
||||
}
|
||||
return fontMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!CSSRule} rule
|
||||
* @return {boolean}
|
||||
*/
|
||||
function isFontFaceRule(rule: CSSRule): rule is CSSFontFaceRule {
|
||||
return rule.type === CSSRule.FONT_FACE_RULE;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { Map } from "../../utility/map";
|
||||
* The collection can then be converted to a CSS3 representation.
|
||||
*
|
||||
* @param {!libjass.renderers.NullRenderer} renderer The renderer that this collection is associated with
|
||||
* @param {!HTMLStyleElement} style A <style> element to insert the animation rules into
|
||||
*/
|
||||
export class AnimationCollection {
|
||||
private static _nextId: number = 0;
|
||||
@@ -36,25 +37,15 @@ export class AnimationCollection {
|
||||
private _id: string;
|
||||
private _rate: number;
|
||||
|
||||
private _cssText: string = "";
|
||||
private _animationStyle: string = "";
|
||||
private _animationDelays: Map<string, number> = new Map<string, number>();
|
||||
private _numAnimations: number = 0;
|
||||
|
||||
constructor(renderer: NullRenderer) {
|
||||
constructor(renderer: NullRenderer, private _style: HTMLStyleElement) {
|
||||
this._id = `${ renderer.id }-${ AnimationCollection._nextId++ }`;
|
||||
this._rate = renderer.clock.rate;
|
||||
}
|
||||
|
||||
/**
|
||||
* This string contains the animation definitions and should be inserted into a <style> element.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
get cssText(): string {
|
||||
return this._cssText;
|
||||
}
|
||||
|
||||
/**
|
||||
* This string should be set as the "animation" CSS property of the target element.
|
||||
*
|
||||
@@ -80,10 +71,10 @@ export class AnimationCollection {
|
||||
* @param {!Array.<!libjass.renderers.Keyframe>} keyframes
|
||||
*/
|
||||
add(timingFunction: string, keyframes: Keyframe[]): void {
|
||||
var start: number = null;
|
||||
var end: number = null;
|
||||
let start: number = null;
|
||||
let end: number = null;
|
||||
|
||||
for (let keyframe of keyframes) {
|
||||
for (const keyframe of keyframes) {
|
||||
if (start === null) {
|
||||
start = keyframe.time;
|
||||
}
|
||||
@@ -91,9 +82,9 @@ export class AnimationCollection {
|
||||
end = keyframe.time;
|
||||
}
|
||||
|
||||
var ruleCssText = "";
|
||||
let ruleCssText = "";
|
||||
|
||||
for (let keyframe of keyframes) {
|
||||
for (const keyframe of keyframes) {
|
||||
ruleCssText +=
|
||||
` ${ (100 * ((end - start === 0) ? 1 : ((keyframe.time - start) / (end - start)))).toFixed(3) }% {
|
||||
`;
|
||||
@@ -109,18 +100,17 @@ export class AnimationCollection {
|
||||
`;
|
||||
}
|
||||
|
||||
var animationName = `animation-${ this._id }-${ this._numAnimations++ }`;
|
||||
const animationName = `animation-${ this._id }-${ this._numAnimations++ }`;
|
||||
|
||||
this._cssText +=
|
||||
this._style.appendChild(document.createTextNode(
|
||||
`@-webkit-keyframes ${ animationName } {
|
||||
${ ruleCssText }
|
||||
}
|
||||
}`));
|
||||
|
||||
@keyframes ${ animationName } {
|
||||
this._style.appendChild(document.createTextNode(
|
||||
`@keyframes ${ animationName } {
|
||||
${ ruleCssText }
|
||||
}
|
||||
|
||||
`;
|
||||
}`));
|
||||
|
||||
if (this._animationStyle !== "") {
|
||||
this._animationStyle += ",";
|
||||
|
||||
@@ -18,9 +18,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { domParser } from "./dom-parser";
|
||||
|
||||
import * as parts from "../../parts/index";
|
||||
import * as parts from "../../parts";
|
||||
|
||||
/**
|
||||
* This class represents an ASS drawing - a set of drawing instructions between {\p} tags.
|
||||
@@ -55,40 +53,46 @@ export class DrawingStyles {
|
||||
* @param {!libjass.parts.Color} fillColor
|
||||
* @return {!SVGSVGElement}
|
||||
*/
|
||||
toSVG(drawingInstructions: parts.DrawingInstructions, fillColor: parts.Color): SVGElement {
|
||||
var scaleFactor = Math.pow(2, this._scale - 1);
|
||||
var scaleX = this._outputScaleX / scaleFactor;
|
||||
var scaleY = this._outputScaleY / scaleFactor;
|
||||
toSVG(drawingInstructions: parts.DrawingInstructions, fillColor: parts.Color): SVGSVGElement {
|
||||
const scaleFactor = Math.pow(2, this._scale - 1);
|
||||
const scaleX = this._outputScaleX / scaleFactor;
|
||||
const scaleY = this._outputScaleY / scaleFactor;
|
||||
|
||||
var path = "";
|
||||
var bboxWidth = 0;
|
||||
var bboxHeight = 0;
|
||||
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||
|
||||
for (let instruction of drawingInstructions.instructions) {
|
||||
let bboxWidth = 0;
|
||||
let bboxHeight = 0;
|
||||
|
||||
for (const instruction of drawingInstructions.instructions) {
|
||||
if (instruction instanceof parts.drawing.MoveInstruction) {
|
||||
path += ` M ${ instruction.x } ${ instruction.y + this._baselineOffset }`;
|
||||
path.pathSegList.appendItem(path.createSVGPathSegMovetoAbs(instruction.x, instruction.y + this._baselineOffset));
|
||||
bboxWidth = Math.max(bboxWidth, instruction.x);
|
||||
bboxHeight = Math.max(bboxHeight, instruction.y + this._baselineOffset);
|
||||
}
|
||||
else if (instruction instanceof parts.drawing.LineInstruction) {
|
||||
path += ` L ${ instruction.x } ${ instruction.y + this._baselineOffset }`;
|
||||
path.pathSegList.appendItem(path.createSVGPathSegLinetoAbs(instruction.x, instruction.y + this._baselineOffset));
|
||||
bboxWidth = Math.max(bboxWidth, instruction.x);
|
||||
bboxHeight = Math.max(bboxHeight, instruction.y + this._baselineOffset);
|
||||
}
|
||||
else if (instruction instanceof parts.drawing.CubicBezierCurveInstruction) {
|
||||
path += ` C ${ instruction.x1 } ${ instruction.y1 + this._baselineOffset }, ${ instruction.x2 } ${ instruction.y2 + this._baselineOffset }, ${ instruction.x3 } ${ instruction.y3 + this._baselineOffset }`;
|
||||
path.pathSegList.appendItem(path.createSVGPathSegCurvetoCubicAbs(instruction.x3, instruction.y3 + this._baselineOffset, instruction.x1, instruction.y1 + this._baselineOffset, instruction.x2, instruction.y2 + this._baselineOffset));
|
||||
bboxWidth = Math.max(bboxWidth, instruction.x1, instruction.x2, instruction.x3);
|
||||
bboxHeight = Math.max(bboxHeight, instruction.y1 + this._baselineOffset, instruction.y2 + this._baselineOffset, instruction.y3 + this._baselineOffset);
|
||||
}
|
||||
}
|
||||
|
||||
var result =
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="${ (bboxWidth * scaleX).toFixed(3) }px" height="${ (bboxHeight * scaleY).toFixed(3) }px">
|
||||
<g transform="scale(${ scaleX.toFixed(3) } ${ scaleY.toFixed(3) })">
|
||||
<path d="${ path }" fill="${ fillColor.toString() }" />
|
||||
</g>
|
||||
</svg>`;
|
||||
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
svg.setAttribute("version", "1.1");
|
||||
svg.width.baseVal.valueAsString = `${ (bboxWidth * scaleX).toFixed(3) }px`;
|
||||
svg.height.baseVal.valueAsString = `${ (bboxHeight * scaleY).toFixed(3) }px`;
|
||||
|
||||
return <SVGSVGElement>domParser.parseFromString(result, "image/svg+xml").childNodes[0];
|
||||
const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
||||
svg.appendChild(g);
|
||||
g.setAttribute("transform", `scale(${ scaleX.toFixed(3) } ${ scaleY.toFixed(3) })`);
|
||||
|
||||
g.appendChild(path);
|
||||
path.setAttribute("fill", fillColor.toString());
|
||||
|
||||
return svg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Map } from "../../utility/map";
|
||||
import { Promise } from "../../utility/promise";
|
||||
|
||||
/**
|
||||
* @param {string} fontFamily
|
||||
* @param {number} fontSize
|
||||
* @param {string} fallbackFonts
|
||||
* @param {!HTMLDivElement} fontSizeElement
|
||||
*/
|
||||
function prepareFontSizeElement(fontFamily: string, fontSize: number, fallbackFonts: string, fontSizeElement: HTMLDivElement): void {
|
||||
let fonts = `"${ fontFamily }"`;
|
||||
if (fallbackFonts !== "") {
|
||||
fonts += `, ${ fallbackFonts }`;
|
||||
}
|
||||
|
||||
fontSizeElement.style.fontFamily = fonts;
|
||||
fontSizeElement.style.fontSize = `${ fontSize }px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} fontFamily
|
||||
* @param {number} fontSize
|
||||
* @param {string} fallbackFonts
|
||||
* @param {!HTMLDivElement} fontSizeElement
|
||||
* @return {!Promise.<number>}
|
||||
*/
|
||||
function lineHeightForFontSize(fontFamily: string, fontSize: number, fallbackFonts: string, fontSizeElement: HTMLDivElement): Promise<number> {
|
||||
prepareFontSizeElement(fontFamily, fontSize, fallbackFonts, fontSizeElement);
|
||||
|
||||
return new Promise(resolve => setTimeout(() => resolve(fontSizeElement.offsetHeight), 1000));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} fontFamily
|
||||
* @param {number} fontSize
|
||||
* @param {string} fallbackFonts
|
||||
* @param {!HTMLDivElement} fontSizeElement
|
||||
* @return {number}
|
||||
*/
|
||||
function lineHeightForFontSizeSync(fontFamily: string, fontSize: number, fallbackFonts: string, fontSizeElement: HTMLDivElement): number {
|
||||
prepareFontSizeElement(fontFamily, fontSize, fallbackFonts, fontSizeElement);
|
||||
|
||||
return fontSizeElement.offsetHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} lowerLineHeight
|
||||
* @param {number} upperLineHeight
|
||||
* @return {[number, number]}
|
||||
*/
|
||||
function fontMetricsFromLineHeights(lowerLineHeight: number, upperLineHeight: number): [number, number] {
|
||||
return [lowerLineHeight, (360 - 180) / (upperLineHeight - lowerLineHeight)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates font metrics for the given font family.
|
||||
*
|
||||
* @param {string} fontFamily
|
||||
* @param {string} fallbackFonts
|
||||
* @param {!HTMLDivElement} fontSizeElement
|
||||
* @return {!Promise.<number>}
|
||||
*/
|
||||
export function calculateFontMetrics(fontFamily: string, fallbackFonts: string, fontSizeElement: HTMLDivElement): Promise<[number, number]> {
|
||||
return lineHeightForFontSize(fontFamily, 180, fallbackFonts, fontSizeElement).then(lowerLineHeight =>
|
||||
lineHeightForFontSize(fontFamily, 360, fallbackFonts, fontSizeElement).then(upperLineHeight =>
|
||||
fontMetricsFromLineHeights(lowerLineHeight, upperLineHeight)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} lineHeight
|
||||
* @param {number} lowerLineHeight
|
||||
* @param {number} factor
|
||||
* @return {number}
|
||||
*/
|
||||
function fontSizeFromMetrics(lineHeight: number, lowerLineHeight: number, factor: number): number {
|
||||
return 180 + (lineHeight - lowerLineHeight) * factor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses linear interpolation to calculate the CSS font size that would give the specified line height for the specified font family.
|
||||
*
|
||||
* WARNING: If fontMetricsCache doesn't already contain a cached value for this font family, and it is not a font already installed on the user's device, then this function
|
||||
* may return wrong values. To avoid this, make sure to preload the font using the {@link libjass.renderers.RendererSettings.fontMap} property when constructing the renderer.
|
||||
*
|
||||
* @param {string} fontFamily
|
||||
* @param {number} lineHeight
|
||||
* @param {string} fallbackFonts
|
||||
* @param {!HTMLDivElement} fontSizeElement
|
||||
* @param {!Map.<string, [number, number]>} fontMetricsCache
|
||||
* @return {number}
|
||||
*/
|
||||
export function fontSizeForLineHeight(fontFamily: string, lineHeight: number, fallbackFonts: string, fontSizeElement: HTMLDivElement, fontMetricsCache: Map<string, [number, number]>): number {
|
||||
let existingMetrics = fontMetricsCache.get(fontFamily);
|
||||
if (existingMetrics === undefined) {
|
||||
const lowerLineHeight = lineHeightForFontSizeSync(fontFamily, 180, fallbackFonts, fontSizeElement);
|
||||
const upperLineHeight = lineHeightForFontSizeSync(fontFamily, 360, fallbackFonts, fontSizeElement);
|
||||
fontMetricsCache.set(fontFamily, existingMetrics = fontMetricsFromLineHeights(lowerLineHeight, upperLineHeight));
|
||||
}
|
||||
|
||||
const [lowerLineHeight, factor] = existingMetrics;
|
||||
return fontSizeFromMetrics(lineHeight, lowerLineHeight, factor);
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
import { AnimationCollection } from "./animation-collection";
|
||||
import { DrawingStyles } from "./drawing-styles";
|
||||
import { calculateFontMetrics } from "./font-size";
|
||||
import { Keyframe } from "./keyframe";
|
||||
import { SpanStyles } from "./span-styles";
|
||||
|
||||
@@ -28,24 +29,63 @@ import { Clock, EventSource } from "../clocks/base";
|
||||
import { NullRenderer } from "../null";
|
||||
import { RendererSettings } from "../settings";
|
||||
|
||||
import * as parts from "../../parts/index";
|
||||
import { getTtfNames } from "../../parser/ttf";
|
||||
|
||||
import * as parts from "../../parts";
|
||||
|
||||
import { debugMode } from "../../settings";
|
||||
|
||||
import { ASS } from "../../types/ass";
|
||||
import { AttachmentType } from "../../types/attachment";
|
||||
import { Dialogue } from "../../types/dialogue";
|
||||
import { WrappingStyle } from "../../types/misc";
|
||||
|
||||
import { mixin } from "../../utility/mixin";
|
||||
import { Map } from "../../utility/map";
|
||||
import { Promise, any as Promise_any, first as Promise_first, lastly as Promise_finally } from "../../utility/promise";
|
||||
import { Set } from "../../utility/set";
|
||||
import { Promise } from "../../utility/promise";
|
||||
|
||||
///<reference path="./references.d.ts" />
|
||||
declare const global: {
|
||||
document: {
|
||||
fonts?: FontFaceSet;
|
||||
};
|
||||
};
|
||||
|
||||
interface FontFaceSet {
|
||||
/**
|
||||
* @param {!FontFace} fontFace
|
||||
* @return {!FontFaceSet}
|
||||
*/
|
||||
add(fontFace: FontFace): FontFaceSet;
|
||||
|
||||
/**
|
||||
* @param {function(!FontFace, !FontFace, !FontFaceSet)} callbackfn A function that is called with each value in the set.
|
||||
* @param {*} thisArg
|
||||
*/
|
||||
forEach(callbackfn: (fontFace: FontFace, index: FontFace, set: FontFaceSet) => void, thisArg?: any): void;
|
||||
}
|
||||
|
||||
interface FontFace {
|
||||
/** @type {string} */
|
||||
family: string;
|
||||
|
||||
/**
|
||||
* @return {!Promise.<!FontFace>}
|
||||
*/
|
||||
load(): Promise<FontFace>;
|
||||
}
|
||||
|
||||
declare var FontFace: {
|
||||
new (family: string, source: string): FontFace;
|
||||
};
|
||||
|
||||
const fontSrcUrlRegex = /^(url|local)\(["']?(.+?)["']?\)$/;
|
||||
|
||||
/**
|
||||
* A renderer implementation that draws subtitles to the given <div>
|
||||
*
|
||||
* After the renderer fires its ready event, {@link libjass.renderers.WebRenderer.resize} must be called to initialize its size before starting the clock.
|
||||
*
|
||||
* @param {!libjass.ASS} ass
|
||||
* @param {!libjass.renderers.Clock} clock
|
||||
* @param {!HTMLDivElement} libjassSubsWrapper Subtitles will be rendered to this <div>
|
||||
@@ -53,11 +93,13 @@ import { Promise } from "../../utility/promise";
|
||||
*/
|
||||
export class WebRenderer extends NullRenderer implements EventSource<string> {
|
||||
private _subsWrapper: HTMLDivElement;
|
||||
private _subsWrapperWidth: number; // this._subsWrapper.offsetWidth is expensive, so cache this.
|
||||
|
||||
private _layerWrappers: HTMLDivElement[] = [];
|
||||
private _layerAlignmentWrappers: HTMLDivElement[][] = [];
|
||||
private _fontSizeElement: HTMLDivElement;
|
||||
private _animationStyleElement: HTMLStyleElement;
|
||||
private _svgDefsElement: SVGDefsElement;
|
||||
|
||||
private _fontMetricsCache: Map<string, [number, number]> = new Map<string, [number, number]>();
|
||||
|
||||
private _currentSubs: Map<Dialogue, HTMLDivElement> = new Map<Dialogue, HTMLDivElement>();
|
||||
private _preRenderedSubs: Map<number, PreRenderedSub> = new Map<number, PreRenderedSub>();
|
||||
@@ -68,7 +110,7 @@ export class WebRenderer extends NullRenderer implements EventSource<string> {
|
||||
constructor(ass: ASS, clock: Clock, private _libjassSubsWrapper: HTMLDivElement, settings?: RendererSettings) {
|
||||
super(ass, clock, (() => {
|
||||
if (!(_libjassSubsWrapper instanceof HTMLDivElement)) {
|
||||
var temp = settings;
|
||||
const temp = settings;
|
||||
settings = <any>_libjassSubsWrapper;
|
||||
_libjassSubsWrapper = <any>temp;
|
||||
console.warn("WebRenderer's constructor now takes libjassSubsWrapper as the third parameter and settings as the fourth parameter. Please update the caller.");
|
||||
@@ -88,54 +130,179 @@ export class WebRenderer extends NullRenderer implements EventSource<string> {
|
||||
this._fontSizeElement.className = "libjass-font-measure";
|
||||
this._fontSizeElement.appendChild(document.createTextNode("M"));
|
||||
|
||||
this._animationStyleElement = document.createElement("style");
|
||||
this._animationStyleElement.id = `libjass-animation-styles-${ this.id }`;
|
||||
this._animationStyleElement.type = "text/css";
|
||||
document.querySelector("head").appendChild(this._animationStyleElement);
|
||||
|
||||
var svgElement = <SVGSVGElement>document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
this._libjassSubsWrapper.appendChild(svgElement);
|
||||
svgElement.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
||||
svgElement.setAttribute("version", "1.1");
|
||||
svgElement.setAttribute("class", "libjass-filters");
|
||||
svgElement.setAttribute("width", "0");
|
||||
svgElement.setAttribute("height", "0");
|
||||
|
||||
this._svgDefsElement = <SVGDefsElement>document.createElementNS("http://www.w3.org/2000/svg", "defs");
|
||||
svgElement.appendChild(this._svgDefsElement);
|
||||
|
||||
// Preload fonts
|
||||
|
||||
var urlsToPreload = new Set<string>();
|
||||
if (this.settings.fontMap !== null) {
|
||||
this.settings.fontMap.forEach(srcs => {
|
||||
for (let src of srcs) {
|
||||
urlsToPreload.add(src);
|
||||
if (debugMode) {
|
||||
console.log(`Preloading fonts...`);
|
||||
}
|
||||
|
||||
const preloadFontPromises: Promise<any>[] = [];
|
||||
|
||||
const fontFetchPromisesCache = new Map<string, Promise<any>>();
|
||||
|
||||
const fontMap = (this.settings.fontMap === null) ? new Map<string, string | string[]>() : this.settings.fontMap;
|
||||
|
||||
const attachedFontsMap = new Map<string, string[]>();
|
||||
if (this.settings.useAttachedFonts) {
|
||||
ass.attachments.forEach(attachment => {
|
||||
if (attachment.type !== AttachmentType.Font) {
|
||||
return;
|
||||
}
|
||||
|
||||
let ttfNames: Set<string> = null;
|
||||
try {
|
||||
ttfNames = getTtfNames(attachment);
|
||||
}
|
||||
catch (ex) {
|
||||
console.error(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
const attachmentUrl = `data:application/x-font-ttf;base64,${ attachment.contents }`;
|
||||
|
||||
ttfNames.forEach(name => {
|
||||
let correspondingFontMapEntry = fontMap.get(name);
|
||||
if (correspondingFontMapEntry !== undefined) {
|
||||
// Also defined in fontMap.
|
||||
if (typeof correspondingFontMapEntry !== "string") {
|
||||
// Entry in fontMap is an array. Append this URL to it.
|
||||
correspondingFontMapEntry.push(attachmentUrl);
|
||||
}
|
||||
else {
|
||||
/* The entry in fontMap is a string. Don't append this URL to it. Instead, put it in attachedFontsMap now
|
||||
* and it'll be merged with the entry from fontMap later. If it was added here, and later the string needed
|
||||
* to be split on commas, then the commas in the data URI would break the result.
|
||||
*/
|
||||
let existingList = attachedFontsMap.get(name);
|
||||
if (existingList === undefined) {
|
||||
attachedFontsMap.set(name, existingList = []);
|
||||
}
|
||||
existingList.push(attachmentUrl);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Not defined in fontMap. Add it there.
|
||||
fontMap.set(name, [attachmentUrl]);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (debugMode) {
|
||||
console.log(`Preloading ${ urlsToPreload.size } fonts...`);
|
||||
}
|
||||
fontMap.forEach((srcs, fontFamily) => {
|
||||
let fontFamilyMetricsPromise: Promise<[number, number]>;
|
||||
|
||||
var xhrPromises: Promise<void>[] = [];
|
||||
urlsToPreload.forEach(url => {
|
||||
xhrPromises.push(new Promise<void>((resolve, reject) => {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.addEventListener("load", () => {
|
||||
if (debugMode) {
|
||||
console.log(`Preloaded ${ url }.`);
|
||||
if (global.document.fonts && global.document.fonts.add) {
|
||||
// value should be string. If it's string[], combine it into string
|
||||
let source =
|
||||
(typeof srcs === "string") ?
|
||||
srcs :
|
||||
srcs.map(src =>
|
||||
(src.match(fontSrcUrlRegex) !== null) ?
|
||||
src :
|
||||
`url("${ src }")`).join(", ");
|
||||
|
||||
const attachedFontUrls = attachedFontsMap.get(fontFamily);
|
||||
if (attachedFontUrls !== undefined) {
|
||||
for (const url of attachedFontUrls) {
|
||||
source += `, url("${ url }")`;
|
||||
}
|
||||
}
|
||||
|
||||
resolve(null);
|
||||
let existingFontFaces: FontFace[] = [];
|
||||
global.document.fonts.forEach(fontFace => {
|
||||
if (fontFace.family === fontFamily || fontFace.family === `"${ fontFamily }"`) {
|
||||
existingFontFaces.push(fontFace);
|
||||
}
|
||||
});
|
||||
xhr.open("GET", url, true);
|
||||
xhr.send();
|
||||
}));
|
||||
|
||||
let fontFetchPromise: Promise<FontFace>;
|
||||
if (existingFontFaces.length === 0) {
|
||||
const fontFace = new FontFace(fontFamily, source);
|
||||
const quotedFontFace = new FontFace(`"${ fontFamily }"`, source);
|
||||
|
||||
global.document.fonts.add(fontFace);
|
||||
global.document.fonts.add(quotedFontFace);
|
||||
|
||||
fontFetchPromise = Promise_any([fontFace.load(), quotedFontFace.load()])
|
||||
}
|
||||
else {
|
||||
fontFetchPromise = Promise_any(existingFontFaces.map(fontFace => fontFace.load()));
|
||||
}
|
||||
|
||||
fontFamilyMetricsPromise = this._calculateFontMetricsAfterFetch(fontFamily, fontFetchPromise.catch(reason => {
|
||||
console.warn(`Fetching fonts for ${ fontFamily } at ${ source } failed: %o`, reason);
|
||||
return null;
|
||||
}));
|
||||
}
|
||||
else {
|
||||
// value should be string[]. If it's string, split it into string[]
|
||||
let urls =
|
||||
((typeof srcs === "string") ?
|
||||
srcs.split(/,/) :
|
||||
srcs).map(url => url.trim()).map(url => {
|
||||
const match = url.match(fontSrcUrlRegex);
|
||||
|
||||
if (match === null) {
|
||||
// A URL
|
||||
return url;
|
||||
}
|
||||
|
||||
if (match[1] === "local") {
|
||||
// A local() URL. Don't fetch it.
|
||||
return null;
|
||||
}
|
||||
|
||||
// A url() URL. Extract the raw URL.
|
||||
return match[2];
|
||||
}).filter(url => url !== null);
|
||||
|
||||
const attachedFontUrls = attachedFontsMap.get(fontFamily);
|
||||
if (attachedFontUrls !== undefined) {
|
||||
urls = urls.concat(attachedFontUrls);
|
||||
}
|
||||
|
||||
const thisFontFamilysFetchPromises =
|
||||
urls.map(url => {
|
||||
let fontFetchPromise = fontFetchPromisesCache.get(url);
|
||||
if (fontFetchPromise === undefined) {
|
||||
fontFetchPromise =
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.addEventListener("load", () => {
|
||||
if (debugMode) {
|
||||
console.log(`Preloaded ${ url }.`);
|
||||
}
|
||||
|
||||
resolve(null);
|
||||
});
|
||||
xhr.addEventListener("error", err => {
|
||||
reject(err);
|
||||
});
|
||||
xhr.open("GET", url, true);
|
||||
xhr.send();
|
||||
});
|
||||
|
||||
fontFetchPromisesCache.set(url, fontFetchPromise);
|
||||
}
|
||||
|
||||
return fontFetchPromise;
|
||||
});
|
||||
|
||||
const allFontsFetchedPromise =
|
||||
(thisFontFamilysFetchPromises.length === 0) ?
|
||||
Promise.resolve<void>(null) :
|
||||
Promise_first(thisFontFamilysFetchPromises).catch(reason => {
|
||||
console.warn(`Fetching fonts for ${ fontFamily } at ${ urls.join(", ") } failed: %o`, reason);
|
||||
return null;
|
||||
});
|
||||
|
||||
fontFamilyMetricsPromise = this._calculateFontMetricsAfterFetch(fontFamily, allFontsFetchedPromise);
|
||||
}
|
||||
|
||||
preloadFontPromises.push(fontFamilyMetricsPromise.then(metrics => this._fontMetricsCache.set(fontFamily, metrics)));
|
||||
});
|
||||
|
||||
Promise.all(xhrPromises).then(() => {
|
||||
Promise.all(preloadFontPromises).then(() => {
|
||||
if (debugMode) {
|
||||
console.log("All fonts have been preloaded.");
|
||||
}
|
||||
@@ -156,34 +323,25 @@ export class WebRenderer extends NullRenderer implements EventSource<string> {
|
||||
*
|
||||
* @param {number} width
|
||||
* @param {number} height
|
||||
* @param {number=0} left
|
||||
* @param {number=0} top
|
||||
*/
|
||||
resize(width: number, height: number): void {
|
||||
resize(width: number, height: number, left: number = 0, top: number = 0): void {
|
||||
this._removeAllSubs();
|
||||
|
||||
var ratio = Math.min(width / this.ass.properties.resolutionX, height / this.ass.properties.resolutionY);
|
||||
var subsWrapperWidth = this.ass.properties.resolutionX * ratio;
|
||||
var subsWrapperHeight = this.ass.properties.resolutionY * ratio;
|
||||
this._subsWrapper.style.width = `${ subsWrapperWidth.toFixed(3) }px`;
|
||||
this._subsWrapper.style.height = `${ subsWrapperHeight.toFixed(3) }px`;
|
||||
this._subsWrapper.style.left = `${ ((width - subsWrapperWidth) / 2).toFixed(3) }px`;
|
||||
this._subsWrapper.style.top = `${ ((height - subsWrapperHeight) / 2).toFixed(3) }px`;
|
||||
this._subsWrapper.style.width = `${ width.toFixed(3) }px`;
|
||||
this._subsWrapper.style.height = `${ height.toFixed(3) }px`;
|
||||
this._subsWrapper.style.left = `${ left.toFixed(3) }px`;
|
||||
this._subsWrapper.style.top = `${ top.toFixed(3) }px`;
|
||||
|
||||
this._scaleX = subsWrapperWidth / this.ass.properties.resolutionX;
|
||||
this._scaleY = subsWrapperHeight / this.ass.properties.resolutionY;
|
||||
this._subsWrapperWidth = width;
|
||||
|
||||
this._scaleX = width / this.ass.properties.resolutionX;
|
||||
this._scaleY = height / this.ass.properties.resolutionY;
|
||||
|
||||
// Any dialogues which have been pre-rendered will need to be pre-rendered again.
|
||||
this._preRenderedSubs.clear();
|
||||
|
||||
if (this._animationStyleElement !== null) {
|
||||
while (this._animationStyleElement.firstChild !== null) {
|
||||
this._animationStyleElement.removeChild(this._animationStyleElement.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
while (this._svgDefsElement.firstChild !== null) {
|
||||
this._svgDefsElement.removeChild(this._svgDefsElement.firstChild);
|
||||
}
|
||||
|
||||
// this.currentTime will be -1 if resize() is called before the clock begins playing for the first time. In this situation, there is no need to force a redraw.
|
||||
if (this.clock.currentTime !== -1) {
|
||||
this._onClockTick();
|
||||
@@ -194,30 +352,52 @@ export class WebRenderer extends NullRenderer implements EventSource<string> {
|
||||
* The magic happens here. The subtitle div is rendered and stored. Call {@link libjass.renderers.WebRenderer.draw} to get a clone of the div to display.
|
||||
*
|
||||
* @param {!libjass.Dialogue} dialogue
|
||||
* @return {PreRenderedSub}
|
||||
*/
|
||||
preRender(dialogue: Dialogue): void {
|
||||
if (this._preRenderedSubs.has(dialogue.id)) {
|
||||
return;
|
||||
preRender(dialogue: Dialogue): PreRenderedSub {
|
||||
const alreadyPreRenderedSub = this._preRenderedSubs.get(dialogue.id);
|
||||
if (alreadyPreRenderedSub) {
|
||||
return alreadyPreRenderedSub;
|
||||
}
|
||||
|
||||
var sub = document.createElement("div");
|
||||
const currentTimeRelativeToDialogueStart = this.clock.currentTime - dialogue.start;
|
||||
|
||||
if (dialogue.containsTransformTag && currentTimeRelativeToDialogueStart < 0) {
|
||||
// draw() expects this function to always return non-null, but it only calls this function when currentTimeRelativeToDialogueStart would be >= 0
|
||||
return null;
|
||||
}
|
||||
|
||||
const sub = document.createElement("div");
|
||||
|
||||
sub.style.marginLeft = `${ (this._scaleX * dialogue.style.marginLeft).toFixed(3) }px`;
|
||||
sub.style.marginRight = `${ (this._scaleX * dialogue.style.marginRight).toFixed(3) }px`;
|
||||
sub.style.marginTop = sub.style.marginBottom = `${ (this._scaleY * dialogue.style.marginVertical).toFixed(3) }px`;
|
||||
sub.style.minWidth = `${ (this._subsWrapper.offsetWidth - this._scaleX * (dialogue.style.marginLeft + dialogue.style.marginRight)).toFixed(3) }px`;
|
||||
sub.style.minWidth = `${ (this._subsWrapperWidth - this._scaleX * (dialogue.style.marginLeft + dialogue.style.marginRight)).toFixed(3) }px`;
|
||||
|
||||
var dialogueAnimationCollection = new AnimationCollection(this);
|
||||
const dialogueAnimationStylesElement = document.createElement("style");
|
||||
dialogueAnimationStylesElement.id = `libjass-animation-styles-${ this.id }-${ dialogue.id }`;
|
||||
dialogueAnimationStylesElement.type = "text/css";
|
||||
|
||||
var currentSpan: HTMLSpanElement = null;
|
||||
var currentSpanStyles = new SpanStyles(this, dialogue, this._scaleX, this._scaleY, this.settings, this._fontSizeElement, this._svgDefsElement);
|
||||
const dialogueAnimationCollection = new AnimationCollection(this, dialogueAnimationStylesElement);
|
||||
|
||||
var currentAnimationCollection: AnimationCollection = null;
|
||||
const svgElement = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
svgElement.setAttribute("version", "1.1");
|
||||
svgElement.setAttribute("class", "libjass-filters");
|
||||
svgElement.width.baseVal.valueAsString = "0";
|
||||
svgElement.height.baseVal.valueAsString = "0";
|
||||
|
||||
var previousAddNewLine = false; // If two or more \N's are encountered in sequence, then all but the first will be created using currentSpanStyles.makeNewLine() instead
|
||||
var startNewSpan = (addNewLine: boolean): void => {
|
||||
if (currentSpan !== null && currentSpan.textContent !== "") {
|
||||
sub.appendChild(currentSpanStyles.setStylesOnSpan(currentSpan, currentAnimationCollection, this._animationStyleElement));
|
||||
const svgDefsElement = document.createElementNS("http://www.w3.org/2000/svg", "defs");
|
||||
svgElement.appendChild(svgDefsElement);
|
||||
|
||||
let currentSpan: HTMLSpanElement = null;
|
||||
const currentSpanStyles = new SpanStyles(this, dialogue, this._scaleX, this._scaleY, this.settings, this._fontSizeElement, svgDefsElement, this._fontMetricsCache);
|
||||
|
||||
let currentAnimationCollection: AnimationCollection = null;
|
||||
|
||||
let previousAddNewLine = false; // If two or more \N's are encountered in sequence, then all but the first will be created using currentSpanStyles.makeNewLine() instead
|
||||
const startNewSpan = (addNewLine: boolean): void => {
|
||||
if (currentSpan !== null && currentSpan.hasChildNodes()) {
|
||||
sub.appendChild(currentSpanStyles.setStylesOnSpan(currentSpan, currentAnimationCollection));
|
||||
}
|
||||
|
||||
if (currentAnimationCollection !== null) {
|
||||
@@ -234,19 +414,19 @@ export class WebRenderer extends NullRenderer implements EventSource<string> {
|
||||
}
|
||||
|
||||
currentSpan = document.createElement("span");
|
||||
currentAnimationCollection = new AnimationCollection(this);
|
||||
currentAnimationCollection = new AnimationCollection(this, dialogueAnimationStylesElement);
|
||||
|
||||
previousAddNewLine = addNewLine;
|
||||
};
|
||||
startNewSpan(false);
|
||||
|
||||
var currentDrawingStyles: DrawingStyles = new DrawingStyles(this._scaleX, this._scaleY);
|
||||
const currentDrawingStyles = new DrawingStyles(this._scaleX, this._scaleY);
|
||||
|
||||
var wrappingStyle = this.ass.properties.wrappingStyle;
|
||||
let wrappingStyle = this.ass.properties.wrappingStyle;
|
||||
|
||||
var karaokeTimesAccumulator = 0;
|
||||
let karaokeTimesAccumulator = 0;
|
||||
|
||||
for (let part of dialogue.parts) {
|
||||
for (const part of dialogue.parts) {
|
||||
if (part instanceof parts.Italic) {
|
||||
currentSpanStyles.italic = part.value;
|
||||
}
|
||||
@@ -461,6 +641,132 @@ export class WebRenderer extends NullRenderer implements EventSource<string> {
|
||||
]))]);
|
||||
}
|
||||
|
||||
else if (part instanceof parts.Transform) {
|
||||
const progression =
|
||||
(currentTimeRelativeToDialogueStart <= part.start) ? 0 :
|
||||
(currentTimeRelativeToDialogueStart >= part.end) ? 1 :
|
||||
Math.pow((currentTimeRelativeToDialogueStart - part.start) / (part.end - part.start), part.accel);
|
||||
|
||||
for (const tag of part.tags) {
|
||||
if (tag instanceof parts.Border) {
|
||||
currentSpanStyles.outlineWidth += progression * (tag.value - currentSpanStyles.outlineWidth);
|
||||
currentSpanStyles.outlineHeight += progression * (tag.value - currentSpanStyles.outlineHeight);
|
||||
}
|
||||
|
||||
else if (tag instanceof parts.BorderX) {
|
||||
currentSpanStyles.outlineWidth += progression * (tag.value - currentSpanStyles.outlineWidth);
|
||||
}
|
||||
|
||||
else if (tag instanceof parts.BorderY) {
|
||||
currentSpanStyles.outlineHeight += progression * (tag.value - currentSpanStyles.outlineHeight);
|
||||
}
|
||||
|
||||
else if (tag instanceof parts.Shadow) {
|
||||
currentSpanStyles.shadowDepthX += progression * (tag.value - currentSpanStyles.shadowDepthX);
|
||||
currentSpanStyles.shadowDepthY += progression * (tag.value - currentSpanStyles.shadowDepthY);
|
||||
}
|
||||
|
||||
else if (tag instanceof parts.ShadowX) {
|
||||
currentSpanStyles.shadowDepthX += progression * (tag.value - currentSpanStyles.shadowDepthX);
|
||||
}
|
||||
|
||||
else if (tag instanceof parts.ShadowY) {
|
||||
currentSpanStyles.shadowDepthY += progression * (tag.value - currentSpanStyles.shadowDepthY);
|
||||
}
|
||||
|
||||
else if (tag instanceof parts.Blur) {
|
||||
currentSpanStyles.blur += progression * (tag.value - currentSpanStyles.blur);
|
||||
}
|
||||
|
||||
else if (tag instanceof parts.GaussianBlur) {
|
||||
currentSpanStyles.gaussianBlur += progression * (tag.value - currentSpanStyles.gaussianBlur);
|
||||
}
|
||||
|
||||
else if (tag instanceof parts.FontSize) {
|
||||
currentSpanStyles.fontSize += progression * (tag.value - currentSpanStyles.fontSize);
|
||||
}
|
||||
|
||||
else if (tag instanceof parts.FontSizePlus) {
|
||||
currentSpanStyles.fontSize += progression * tag.value;
|
||||
}
|
||||
|
||||
else if (tag instanceof parts.FontSizeMinus) {
|
||||
currentSpanStyles.fontSize -= progression * tag.value;
|
||||
}
|
||||
|
||||
else if (tag instanceof parts.FontScaleX) {
|
||||
currentSpanStyles.fontScaleX += progression * (tag.value - currentSpanStyles.fontScaleX);
|
||||
}
|
||||
|
||||
else if (tag instanceof parts.FontScaleY) {
|
||||
currentSpanStyles.fontScaleY += progression * (tag.value - currentSpanStyles.fontScaleY);
|
||||
}
|
||||
|
||||
else if (tag instanceof parts.LetterSpacing) {
|
||||
currentSpanStyles.letterSpacing += progression * (tag.value - currentSpanStyles.letterSpacing);
|
||||
}
|
||||
|
||||
else if (tag instanceof parts.RotateX) {
|
||||
currentSpanStyles.rotationX += progression * (tag.value - currentSpanStyles.rotationX);
|
||||
}
|
||||
|
||||
else if (tag instanceof parts.RotateY) {
|
||||
currentSpanStyles.rotationY += progression * (tag.value - currentSpanStyles.rotationY);
|
||||
}
|
||||
|
||||
else if (tag instanceof parts.RotateZ) {
|
||||
currentSpanStyles.rotationZ += progression * (tag.value - currentSpanStyles.rotationZ);
|
||||
}
|
||||
|
||||
else if (tag instanceof parts.SkewX) {
|
||||
currentSpanStyles.skewX += progression * (tag.value - currentSpanStyles.skewX);
|
||||
}
|
||||
|
||||
else if (tag instanceof parts.SkewY) {
|
||||
currentSpanStyles.skewY += progression * (tag.value - currentSpanStyles.skewY);
|
||||
}
|
||||
|
||||
else if (tag instanceof parts.PrimaryColor) {
|
||||
currentSpanStyles.primaryColor = currentSpanStyles.primaryColor.interpolate(tag.value, progression);
|
||||
}
|
||||
|
||||
else if (tag instanceof parts.SecondaryColor) {
|
||||
currentSpanStyles.secondaryColor = currentSpanStyles.secondaryColor.interpolate(tag.value, progression);
|
||||
}
|
||||
|
||||
else if (tag instanceof parts.OutlineColor) {
|
||||
currentSpanStyles.outlineColor = currentSpanStyles.outlineColor.interpolate(tag.value, progression);
|
||||
}
|
||||
|
||||
else if (tag instanceof parts.ShadowColor) {
|
||||
currentSpanStyles.shadowColor = currentSpanStyles.shadowColor.interpolate(tag.value, progression);
|
||||
}
|
||||
|
||||
else if (tag instanceof parts.Alpha) {
|
||||
currentSpanStyles.primaryAlpha += progression * (tag.value - currentSpanStyles.primaryAlpha);
|
||||
currentSpanStyles.secondaryAlpha += progression * (tag.value - currentSpanStyles.secondaryAlpha);
|
||||
currentSpanStyles.outlineAlpha += progression * (tag.value - currentSpanStyles.outlineAlpha);
|
||||
currentSpanStyles.shadowAlpha += progression * (tag.value - currentSpanStyles.shadowAlpha);
|
||||
}
|
||||
|
||||
else if (tag instanceof parts.PrimaryAlpha) {
|
||||
currentSpanStyles.primaryAlpha += progression * (tag.value - currentSpanStyles.primaryAlpha);
|
||||
}
|
||||
|
||||
else if (tag instanceof parts.SecondaryAlpha) {
|
||||
currentSpanStyles.secondaryAlpha += progression * (tag.value - currentSpanStyles.secondaryAlpha);
|
||||
}
|
||||
|
||||
else if (tag instanceof parts.OutlineAlpha) {
|
||||
currentSpanStyles.outlineAlpha += progression * (tag.value - currentSpanStyles.outlineAlpha);
|
||||
}
|
||||
|
||||
else if (tag instanceof parts.ShadowAlpha) {
|
||||
currentSpanStyles.shadowAlpha += progression * (tag.value - currentSpanStyles.shadowAlpha);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else if (part instanceof parts.DrawingMode) {
|
||||
if (part.scale !== 0) {
|
||||
currentDrawingStyles.scale = part.scale;
|
||||
@@ -477,7 +783,7 @@ export class WebRenderer extends NullRenderer implements EventSource<string> {
|
||||
}
|
||||
|
||||
else if (part instanceof parts.Text) {
|
||||
currentSpan.appendChild(document.createTextNode(part.value));
|
||||
currentSpan.appendChild(document.createTextNode(part.value + "\u200C"));
|
||||
startNewSpan(false);
|
||||
}
|
||||
|
||||
@@ -491,12 +797,12 @@ export class WebRenderer extends NullRenderer implements EventSource<string> {
|
||||
}
|
||||
}
|
||||
|
||||
for (let part of dialogue.parts) {
|
||||
for (const part of dialogue.parts) {
|
||||
if (part instanceof parts.Position || part instanceof parts.Move) {
|
||||
var transformOrigin = WebRenderer._transformOrigins[dialogue.alignment];
|
||||
const transformOrigin = WebRenderer._transformOrigins[dialogue.alignment];
|
||||
|
||||
var divTransformStyle = `translate(${ -transformOrigin[0] }%, ${ -transformOrigin[1] }%) translate(-${ sub.style.marginLeft }, -${ sub.style.marginTop })`;
|
||||
var transformOriginString = `${ transformOrigin[0] }% ${ transformOrigin[1] }%`;
|
||||
const divTransformStyle = `translate(${ -transformOrigin[0] }%, ${ -transformOrigin[1] }%) translate(-${ sub.style.marginLeft }, -${ sub.style.marginTop })`;
|
||||
const transformOriginString = `${ transformOrigin[0] }% ${ transformOrigin[1] }%`;
|
||||
|
||||
sub.style.webkitTransform = divTransformStyle;
|
||||
sub.style.webkitTransformOrigin = transformOriginString;
|
||||
@@ -533,14 +839,26 @@ export class WebRenderer extends NullRenderer implements EventSource<string> {
|
||||
}
|
||||
}
|
||||
|
||||
this._animationStyleElement.appendChild(document.createTextNode(dialogueAnimationCollection.cssText));
|
||||
|
||||
sub.style.webkitAnimation = dialogueAnimationCollection.animationStyle;
|
||||
sub.style.animation = dialogueAnimationCollection.animationStyle;
|
||||
|
||||
sub.setAttribute("data-dialogue-id", `${ this.id }-${ dialogue.id }`);
|
||||
|
||||
this._preRenderedSubs.set(dialogue.id, { sub, animationDelays: dialogueAnimationCollection.animationDelays });
|
||||
if (dialogueAnimationStylesElement.textContent !== "") {
|
||||
sub.appendChild(dialogueAnimationStylesElement);
|
||||
}
|
||||
|
||||
if (svgDefsElement.hasChildNodes()) {
|
||||
sub.appendChild(svgElement);
|
||||
}
|
||||
|
||||
const result = { sub, animationDelays: dialogueAnimationCollection.animationDelays };
|
||||
|
||||
if (!dialogue.containsTransformTag) {
|
||||
this._preRenderedSubs.set(dialogue.id, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -550,7 +868,7 @@ export class WebRenderer extends NullRenderer implements EventSource<string> {
|
||||
* @param {!libjass.Dialogue} dialogue
|
||||
*/
|
||||
draw(dialogue: Dialogue): void {
|
||||
if (this._currentSubs.has(dialogue)) {
|
||||
if (this._currentSubs.has(dialogue) && !dialogue.containsTransformTag) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -558,29 +876,24 @@ export class WebRenderer extends NullRenderer implements EventSource<string> {
|
||||
console.log(dialogue.toString());
|
||||
}
|
||||
|
||||
var preRenderedSub = this._preRenderedSubs.get(dialogue.id);
|
||||
let preRenderedSub = this._preRenderedSubs.get(dialogue.id);
|
||||
|
||||
if (preRenderedSub === undefined) {
|
||||
if (debugMode) {
|
||||
console.warn("This dialogue was not pre-rendered. Call preRender() before calling draw() so that draw() is faster.");
|
||||
}
|
||||
|
||||
this.preRender(dialogue);
|
||||
preRenderedSub = this._preRenderedSubs.get(dialogue.id);
|
||||
preRenderedSub = this.preRender(dialogue);
|
||||
|
||||
if (debugMode) {
|
||||
console.log(dialogue.toString());
|
||||
}
|
||||
}
|
||||
|
||||
var result = <HTMLDivElement>preRenderedSub.sub.cloneNode(true);
|
||||
const result = <HTMLDivElement>preRenderedSub.sub.cloneNode(true);
|
||||
|
||||
var applyAnimationDelays = (node: HTMLElement) => {
|
||||
var animationNames = node.style.animationName || node.style.webkitAnimationName;
|
||||
const applyAnimationDelays = (node: HTMLElement) => {
|
||||
const animationNames = node.style.animationName || node.style.webkitAnimationName;
|
||||
if (animationNames !== undefined && animationNames !== "") {
|
||||
var animationDelays = animationNames.split(",").map(name => {
|
||||
const animationDelays = animationNames.split(",").map(name => {
|
||||
name = name.trim();
|
||||
var delay = preRenderedSub.animationDelays.get(name);
|
||||
const delay = preRenderedSub.animationDelays.get(name);
|
||||
return `${ ((delay + dialogue.start - this.clock.currentTime) / this.clock.rate).toFixed(3) }s`;
|
||||
}).join(", ");
|
||||
|
||||
@@ -589,22 +902,22 @@ export class WebRenderer extends NullRenderer implements EventSource<string> {
|
||||
}
|
||||
}
|
||||
applyAnimationDelays(result);
|
||||
var animatedDescendants = result.querySelectorAll('[style*="animation:"]');
|
||||
for (var i = 0; i < animatedDescendants.length; i++) {
|
||||
const animatedDescendants = result.querySelectorAll('[style*="animation:"]');
|
||||
for (let i = 0; i < animatedDescendants.length; i++) {
|
||||
applyAnimationDelays(<HTMLElement>animatedDescendants[i]);
|
||||
}
|
||||
|
||||
var layer = dialogue.layer;
|
||||
var alignment = (result.style.position === "absolute") ? 0 : dialogue.alignment; // Alignment 0 is for absolutely-positioned subs
|
||||
const layer = dialogue.layer;
|
||||
const alignment = (result.style.position === "absolute") ? 0 : dialogue.alignment; // Alignment 0 is for absolutely-positioned subs
|
||||
|
||||
// Create the layer wrapper div and the alignment div inside it if not already created
|
||||
if (this._layerWrappers[layer] === undefined) {
|
||||
var layerWrapper = document.createElement("div");
|
||||
const layerWrapper = document.createElement("div");
|
||||
layerWrapper.className = `layer layer${ layer }`;
|
||||
|
||||
// Find the next greater layer div and insert this div before that one
|
||||
var insertBeforeElement: HTMLDivElement = null;
|
||||
for (var insertBeforeLayer = layer + 1; insertBeforeLayer < this._layerWrappers.length && insertBeforeElement === null; insertBeforeLayer++) {
|
||||
let insertBeforeElement: HTMLDivElement = null;
|
||||
for (let insertBeforeLayer = layer + 1; insertBeforeLayer < this._layerWrappers.length && insertBeforeElement === null; insertBeforeLayer++) {
|
||||
if (this._layerWrappers[insertBeforeLayer] !== undefined) {
|
||||
insertBeforeElement = this._layerWrappers[insertBeforeLayer];
|
||||
}
|
||||
@@ -617,13 +930,13 @@ export class WebRenderer extends NullRenderer implements EventSource<string> {
|
||||
}
|
||||
|
||||
if (this._layerAlignmentWrappers[layer][alignment] === undefined) {
|
||||
var layerAlignmentWrapper = document.createElement("div");
|
||||
const layerAlignmentWrapper = document.createElement("div");
|
||||
layerAlignmentWrapper.className = `an an${ alignment }`;
|
||||
|
||||
// Find the next greater layer,alignment div and insert this div before that one
|
||||
var layerWrapper = this._layerWrappers[layer];
|
||||
var insertBeforeElement: HTMLDivElement = null;
|
||||
for (var insertBeforeAlignment = alignment + 1; insertBeforeAlignment < this._layerAlignmentWrappers[layer].length && insertBeforeElement === null; insertBeforeAlignment++) {
|
||||
const layerWrapper = this._layerWrappers[layer];
|
||||
let insertBeforeElement: HTMLDivElement = null;
|
||||
for (let insertBeforeAlignment = alignment + 1; insertBeforeAlignment < this._layerAlignmentWrappers[layer].length && insertBeforeElement === null; insertBeforeAlignment++) {
|
||||
if (this._layerAlignmentWrappers[layer][insertBeforeAlignment] !== undefined) {
|
||||
insertBeforeElement = this._layerAlignmentWrappers[layer][insertBeforeAlignment];
|
||||
}
|
||||
@@ -636,6 +949,15 @@ export class WebRenderer extends NullRenderer implements EventSource<string> {
|
||||
|
||||
this._layerAlignmentWrappers[layer][alignment].appendChild(result);
|
||||
|
||||
// Workaround for IE
|
||||
const dialogueAnimationStylesElement = result.getElementsByTagName("style")[0];
|
||||
if (dialogueAnimationStylesElement !== undefined) {
|
||||
const sheet = <CSSStyleSheet>dialogueAnimationStylesElement.sheet;
|
||||
if (sheet.cssRules.length === 0) {
|
||||
sheet.cssText = dialogueAnimationStylesElement.textContent;
|
||||
}
|
||||
}
|
||||
|
||||
this._currentSubs.set(dialogue, result);
|
||||
}
|
||||
|
||||
@@ -652,10 +974,10 @@ export class WebRenderer extends NullRenderer implements EventSource<string> {
|
||||
protected _onClockTick(): void {
|
||||
// Remove dialogues that should be removed before adding new ones via super._onClockTick()
|
||||
|
||||
var currentTime = this.clock.currentTime;
|
||||
const currentTime = this.clock.currentTime;
|
||||
|
||||
this._currentSubs.forEach((sub: HTMLDivElement, dialogue: Dialogue) => {
|
||||
if (dialogue.start > currentTime || dialogue.end < currentTime) {
|
||||
if (dialogue.start > currentTime || dialogue.end < currentTime || dialogue.containsTransformTag) {
|
||||
this._currentSubs.delete(dialogue);
|
||||
this._removeSub(sub);
|
||||
}
|
||||
@@ -681,22 +1003,27 @@ export class WebRenderer extends NullRenderer implements EventSource<string> {
|
||||
|
||||
// Any dialogues which have been pre-rendered will need to be pre-rendered again.
|
||||
this._preRenderedSubs.clear();
|
||||
|
||||
if (this._animationStyleElement !== null) {
|
||||
while (this._animationStyleElement.firstChild !== null) {
|
||||
this._animationStyleElement.removeChild(this._animationStyleElement.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
while (this._svgDefsElement.firstChild !== null) {
|
||||
this._svgDefsElement.removeChild(this._svgDefsElement.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
protected _ready(): void {
|
||||
this._dispatchEvent("ready", []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} fontFamily
|
||||
* @param {!Promise.<*>} fontFetchPromise
|
||||
* @return {!Promise.<[number, number]>}
|
||||
*/
|
||||
private _calculateFontMetricsAfterFetch(fontFamily: string, fontFetchPromise: Promise<any>): Promise<[number, number]> {
|
||||
return fontFetchPromise.then(() => {
|
||||
const fontSizeElement = <HTMLDivElement>this._fontSizeElement.cloneNode(true);
|
||||
this._libjassSubsWrapper.appendChild(fontSizeElement);
|
||||
return Promise_finally(calculateFontMetrics(fontFamily, this.settings.fallbackFonts, fontSizeElement), () => {
|
||||
this._libjassSubsWrapper.removeChild(fontSizeElement);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!HTMLDivElement} sub
|
||||
*/
|
||||
|
||||
@@ -19,21 +19,20 @@
|
||||
*/
|
||||
|
||||
import { AnimationCollection } from "./animation-collection";
|
||||
import { domParser } from "./dom-parser";
|
||||
|
||||
import { fontSizeForLineHeight } from "./font-size";
|
||||
|
||||
import { WebRenderer } from "./renderer";
|
||||
|
||||
import { RendererSettings } from "../settings";
|
||||
|
||||
import { Color } from "../../parts/index";
|
||||
import { Color } from "../../parts";
|
||||
|
||||
import { Style } from "../../types/style";
|
||||
import { Dialogue } from "../../types/dialogue";
|
||||
|
||||
import { Map } from "../../utility/map";
|
||||
|
||||
///<reference path="./web-references.d.ts" />
|
||||
|
||||
/**
|
||||
* This class represents the style attribute of a span.
|
||||
* As a Dialogue's div is rendered, individual parts are added to span's, and this class is used to maintain the style attribute of those.
|
||||
@@ -45,10 +44,9 @@ import { Map } from "../../utility/map";
|
||||
* @param {!libjass.renderers.RendererSettings} settings The renderer settings
|
||||
* @param {!HTMLDivElement} fontSizeElement A <div> element to measure font sizes with
|
||||
* @param {!SVGDefsElement} svgDefsElement An SVG <defs> element to append filter definitions to
|
||||
* @param {!Map<string, [number, number]>} fontMetricsCache Font metrics cache
|
||||
*/
|
||||
export class SpanStyles {
|
||||
private static _fontSizeCache: Map<string, Map<number, number>> = new Map<string, Map<number, number>>();
|
||||
|
||||
private _id: string;
|
||||
private _defaultStyle: Style;
|
||||
|
||||
@@ -93,7 +91,7 @@ export class SpanStyles {
|
||||
|
||||
private _nextFilterId = 0;
|
||||
|
||||
constructor(renderer: WebRenderer, dialogue: Dialogue, private _scaleX: number, private _scaleY: number, private _settings: RendererSettings, private _fontSizeElement: HTMLDivElement, private _svgDefsElement: SVGDefsElement) {
|
||||
constructor(renderer: WebRenderer, dialogue: Dialogue, private _scaleX: number, private _scaleY: number, private _settings: RendererSettings, private _fontSizeElement: HTMLDivElement, private _svgDefsElement: SVGDefsElement, private _fontMetricsCache: Map<string, [number, number]>) {
|
||||
this._id = `${ renderer.id }-${ dialogue.id }`;
|
||||
this._defaultStyle = dialogue.style;
|
||||
|
||||
@@ -141,10 +139,10 @@ export class SpanStyles {
|
||||
this.outlineColor = newStyle.outlineColor;
|
||||
this.shadowColor = newStyle.shadowColor;
|
||||
|
||||
this.primaryAlpha = null;
|
||||
this.secondaryAlpha = null;
|
||||
this.outlineAlpha = null;
|
||||
this.shadowAlpha = null;
|
||||
this.primaryAlpha = newStyle.primaryColor.alpha;
|
||||
this.secondaryAlpha = newStyle.secondaryColor.alpha;
|
||||
this.outlineAlpha = newStyle.outlineColor.alpha;
|
||||
this.shadowAlpha = newStyle.shadowColor.alpha;
|
||||
|
||||
this.blur = null;
|
||||
this.gaussianBlur = null;
|
||||
@@ -155,13 +153,12 @@ export class SpanStyles {
|
||||
*
|
||||
* @param {!HTMLSpanElement} span
|
||||
* @param {!AnimationCollection} animationCollection
|
||||
* @param {!HTMLStyleElement} animationStyleElement
|
||||
* @return {!HTMLSpanElement} The resulting <span> with the CSS styles applied. This may be a wrapper around the input <span> if the styles were applied using SVG filters.
|
||||
*/
|
||||
setStylesOnSpan(span: HTMLSpanElement, animationCollection: AnimationCollection, animationStyleElement: HTMLStyleElement): HTMLSpanElement {
|
||||
var isTextOnlySpan = span.childNodes[0] instanceof Text;
|
||||
setStylesOnSpan(span: HTMLSpanElement, animationCollection: AnimationCollection): HTMLSpanElement {
|
||||
const isTextOnlySpan = span.childNodes[0] instanceof Text;
|
||||
|
||||
var fontStyleOrWeight = "";
|
||||
let fontStyleOrWeight = "";
|
||||
if (this._italic) {
|
||||
fontStyleOrWeight += "italic ";
|
||||
}
|
||||
@@ -171,17 +168,34 @@ export class SpanStyles {
|
||||
else if (this._bold !== false) {
|
||||
fontStyleOrWeight += this._bold + " ";
|
||||
}
|
||||
var fontSize: string;
|
||||
if (isTextOnlySpan) {
|
||||
fontSize = (this._scaleY * SpanStyles._getFontSize(this._fontName, this._fontSize * this._fontScaleY, this._fontSizeElement)).toFixed(3);
|
||||
}
|
||||
else {
|
||||
fontSize = (this._scaleY * SpanStyles._getFontSize(this._fontName, this._fontSize, this._fontSizeElement)).toFixed(3);
|
||||
}
|
||||
var lineHeight = (this._scaleY * this._fontSize).toFixed(3);
|
||||
span.style.font = `${ fontStyleOrWeight }${ fontSize }px/${ lineHeight }px "${ this._fontName }"`;
|
||||
const fontSize = (
|
||||
this._scaleY *
|
||||
fontSizeForLineHeight(this._fontName, this._fontSize * (isTextOnlySpan ? this._fontScaleX : 1), this._settings.fallbackFonts, this._fontSizeElement, this._fontMetricsCache)
|
||||
).toFixed(3);
|
||||
const lineHeight = (this._scaleY * this._fontSize).toFixed(3);
|
||||
|
||||
var textDecoration = "";
|
||||
let fonts = this._fontName;
|
||||
|
||||
// Quote the font family unless it's a generic family, as those must never be quoted
|
||||
switch (fonts) {
|
||||
case "cursive":
|
||||
case "fantasy":
|
||||
case "monospace":
|
||||
case "sans-serif":
|
||||
case "serif":
|
||||
break;
|
||||
default:
|
||||
fonts = `"${ fonts }"`;
|
||||
break;
|
||||
}
|
||||
|
||||
if (this._settings.fallbackFonts !== "") {
|
||||
fonts += `, ${ this._settings.fallbackFonts }`;
|
||||
}
|
||||
|
||||
span.style.font = `${ fontStyleOrWeight }${ fontSize }px/${ lineHeight }px ${ fonts }`;
|
||||
|
||||
let textDecoration = "";
|
||||
if (this._underline) {
|
||||
textDecoration = "underline";
|
||||
}
|
||||
@@ -190,7 +204,7 @@ export class SpanStyles {
|
||||
}
|
||||
span.style.textDecoration = textDecoration.trim();
|
||||
|
||||
var transform = "";
|
||||
let transform = "";
|
||||
if (isTextOnlySpan) {
|
||||
if (this._fontScaleY !== this._fontScaleX) {
|
||||
transform += `scaleY(${ (this._fontScaleY / this._fontScaleX).toFixed(3) }) `;
|
||||
@@ -214,8 +228,8 @@ export class SpanStyles {
|
||||
transform += `rotateZ(${ -1 * this._rotationZ }deg) `;
|
||||
}
|
||||
if (this._skewX !== null || this._skewY !== null) {
|
||||
var skewX = SpanStyles._valueOrDefault(this._skewX, 0);
|
||||
var skewY = SpanStyles._valueOrDefault(this._skewY, 0);
|
||||
const skewX = SpanStyles._valueOrDefault(this._skewX, 0);
|
||||
const skewY = SpanStyles._valueOrDefault(this._skewY, 0);
|
||||
transform += `matrix(1, ${ skewY }, ${ skewX }, 1, 0, 0) `;
|
||||
}
|
||||
if (transform !== "") {
|
||||
@@ -228,147 +242,40 @@ export class SpanStyles {
|
||||
|
||||
span.style.letterSpacing = `${ (this._scaleX * this._letterSpacing).toFixed(3) }px`;
|
||||
|
||||
var primaryColor = this._primaryColor.withAlpha(this._primaryAlpha);
|
||||
span.style.color = primaryColor.toString();
|
||||
const outlineWidth = this._scaleX * this._outlineWidth;
|
||||
const outlineHeight = this._scaleY * this._outlineHeight;
|
||||
|
||||
var outlineColor = this._outlineColor.withAlpha(this._outlineAlpha);
|
||||
|
||||
var outlineWidth = this._scaleX * this._outlineWidth;
|
||||
var outlineHeight = this._scaleY * this._outlineHeight;
|
||||
|
||||
var outlineFilter = '';
|
||||
var blurFilter = '';
|
||||
|
||||
if (this._settings.enableSvg) {
|
||||
var filterId = `svg-filter-${ this._id }-${ this._nextFilterId++ }`;
|
||||
|
||||
if (outlineWidth > 0 || outlineHeight > 0) {
|
||||
/* Construct an elliptical border by merging together many rectangles. The border is creating using dilate morphology filters, but these only support
|
||||
* generating rectangles. http://lists.w3.org/Archives/Public/public-fx/2012OctDec/0003.html
|
||||
*/
|
||||
|
||||
var mergeOutlinesFilter = '';
|
||||
|
||||
var outlineNumber = 0;
|
||||
|
||||
var increment = (!this._settings.preciseOutlines && this._gaussianBlur > 0) ? this._gaussianBlur : 1;
|
||||
|
||||
((addOutline: (x: number, y: number) => void) => {
|
||||
if (outlineWidth <= outlineHeight) {
|
||||
if (outlineWidth > 0) {
|
||||
for (var x = 0; x <= outlineWidth; x += increment) {
|
||||
addOutline(x, outlineHeight / outlineWidth * Math.sqrt(outlineWidth * outlineWidth - x * x));
|
||||
}
|
||||
if (x !== outlineWidth + increment) {
|
||||
addOutline(outlineWidth, 0);
|
||||
}
|
||||
}
|
||||
else {
|
||||
addOutline(0, outlineHeight);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (outlineHeight > 0) {
|
||||
for (var y = 0; y <= outlineHeight; y += increment) {
|
||||
addOutline(outlineWidth / outlineHeight * Math.sqrt(outlineHeight * outlineHeight - y * y), y);
|
||||
}
|
||||
if (y !== outlineHeight + increment) {
|
||||
addOutline(0, outlineHeight);
|
||||
}
|
||||
}
|
||||
else {
|
||||
addOutline(outlineWidth, 0);
|
||||
}
|
||||
}
|
||||
})((x: number, y: number): void => {
|
||||
outlineFilter +=
|
||||
` <feMorphology in="SourceAlpha" operator="dilate" radius="${ x.toFixed(3) } ${ y.toFixed(3) }" result="outline${ outlineNumber }" />
|
||||
`;
|
||||
|
||||
mergeOutlinesFilter +=
|
||||
` <feMergeNode in="outline${ outlineNumber }" />
|
||||
`;
|
||||
|
||||
outlineNumber++;
|
||||
});
|
||||
|
||||
outlineFilter +=
|
||||
` <feMerge result="outline">
|
||||
${ mergeOutlinesFilter }
|
||||
</feMerge>
|
||||
<feFlood flood-color="${ outlineColor.toString() }" />
|
||||
<feComposite operator="in" in2="outline" />
|
||||
`;
|
||||
}
|
||||
|
||||
if (this._gaussianBlur > 0) {
|
||||
blurFilter +=
|
||||
` <feGaussianBlur stdDeviation="${ this._gaussianBlur }" />
|
||||
`;
|
||||
}
|
||||
for (var i = 0; i < this._blur; i++) {
|
||||
blurFilter +=
|
||||
` <feConvolveMatrix kernelMatrix="1 2 1 2 4 2 1 2 1" edgeMode="none" />
|
||||
`;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (outlineWidth > 0 || outlineHeight > 0) {
|
||||
var outlineCssString = "";
|
||||
|
||||
((addOutline: (x: number, y: number) => void) => {
|
||||
for (var x = 0; x <= outlineWidth; x++) {
|
||||
var maxY = (outlineWidth === 0) ? outlineHeight : outlineHeight * Math.sqrt(1 - ((x * x) / (outlineWidth * outlineWidth)));
|
||||
for (var y = 0; y <= maxY; y++) {
|
||||
addOutline(x, y);
|
||||
|
||||
if (x !== 0) {
|
||||
addOutline(-x, y);
|
||||
}
|
||||
|
||||
if (y !== 0) {
|
||||
addOutline(x, -y);
|
||||
}
|
||||
|
||||
if (x !== 0 && y !== 0) {
|
||||
addOutline(-x, -y);
|
||||
}
|
||||
}
|
||||
}
|
||||
})((x: number, y: number): void => {
|
||||
outlineCssString += `, ${ outlineColor.toString() } ${ x }px ${ y }px ${ this._gaussianBlur.toFixed(3) }px`;
|
||||
});
|
||||
|
||||
span.style.textShadow = outlineCssString.substr(", ".length);
|
||||
}
|
||||
}
|
||||
|
||||
var filterWrapperSpan = document.createElement("span");
|
||||
const filterWrapperSpan = document.createElement("span");
|
||||
filterWrapperSpan.appendChild(span);
|
||||
|
||||
if (outlineFilter !== '' || blurFilter !== '') {
|
||||
var filterString =
|
||||
`<filter xmlns="http://www.w3.org/2000/svg" id="${ filterId }" x="-50%" width="200%" y="-50%" height="200%">
|
||||
${ outlineFilter }
|
||||
${ blurFilter }
|
||||
<feMerge>
|
||||
<feMergeNode />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
`;
|
||||
let primaryColor = this._primaryColor.withAlpha(this._primaryAlpha);
|
||||
let outlineColor = this._outlineColor.withAlpha(this._outlineAlpha);
|
||||
let shadowColor = this._shadowColor.withAlpha(this._shadowAlpha);
|
||||
|
||||
var filterElement = domParser.parseFromString(filterString, "image/svg+xml").childNodes[0];
|
||||
// If we're in non-SVG mode and all colors have the same alpha, then set all colors to alpha === 1 and set the common alpha as the span's opacity property instead
|
||||
if (
|
||||
!this._settings.enableSvg &&
|
||||
((outlineWidth === 0 && outlineHeight === 0) || outlineColor.alpha === primaryColor.alpha) &&
|
||||
((this._shadowDepthX === 0 && this._shadowDepthY === 0) || shadowColor.alpha === primaryColor.alpha)
|
||||
) {
|
||||
primaryColor = this._primaryColor.withAlpha(1);
|
||||
outlineColor = this._outlineColor.withAlpha(1);
|
||||
shadowColor = this._shadowColor.withAlpha(1);
|
||||
|
||||
this._svgDefsElement.appendChild(filterElement);
|
||||
span.style.opacity = this._primaryAlpha.toFixed(3);
|
||||
}
|
||||
|
||||
filterWrapperSpan.style.webkitFilter = `url("#${ filterId }")`;
|
||||
filterWrapperSpan.style.filter = `url("#${ filterId }")`;
|
||||
span.style.color = primaryColor.toString();
|
||||
|
||||
if (this._settings.enableSvg) {
|
||||
this._setSvgOutlineOnSpan(filterWrapperSpan, outlineWidth, outlineHeight, outlineColor, this._primaryAlpha);
|
||||
}
|
||||
else {
|
||||
this._setTextShadowOutlineOnSpan(span, outlineWidth, outlineHeight, outlineColor);
|
||||
}
|
||||
|
||||
if (this._shadowDepthX !== 0 || this._shadowDepthY !== 0) {
|
||||
var shadowColor = this._shadowColor.withAlpha(this._shadowAlpha);
|
||||
var shadowCssString = `${ shadowColor.toString() } ${ (this._shadowDepthX * this._scaleX / this._fontScaleX).toFixed(3) }px ${ (this._shadowDepthY * this._scaleY / this._fontScaleY).toFixed(3) }px 0px`;
|
||||
const shadowCssString = `${ shadowColor.toString() } ${ (this._shadowDepthX * this._scaleX).toFixed(3) }px ${ (this._shadowDepthY * this._scaleY).toFixed(3) }px 0px`;
|
||||
if (span.style.textShadow === "") {
|
||||
span.style.textShadow = shadowCssString;
|
||||
}
|
||||
@@ -382,19 +289,233 @@ ${ blurFilter }
|
||||
filterWrapperSpan.style.display = "inline-block";
|
||||
}
|
||||
|
||||
animationStyleElement.appendChild(document.createTextNode(animationCollection.cssText));
|
||||
|
||||
span.style.webkitAnimation = animationCollection.animationStyle;
|
||||
span.style.animation = animationCollection.animationStyle;
|
||||
|
||||
return filterWrapperSpan;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!HTMLSpanElement} filterWrapperSpan
|
||||
* @param {number} outlineWidth
|
||||
* @param {number} outlineHeight
|
||||
* @param {!libjass.parts.Color} outlineColor
|
||||
* @param {number} primaryAlpha
|
||||
*/
|
||||
private _setSvgOutlineOnSpan(filterWrapperSpan: HTMLSpanElement, outlineWidth: number, outlineHeight: number, outlineColor: Color, primaryAlpha: number): void {
|
||||
const filterId = `svg-filter-${ this._id }-${ this._nextFilterId++ }`;
|
||||
|
||||
const filterElement = document.createElementNS("http://www.w3.org/2000/svg", "filter");
|
||||
filterElement.id = filterId;
|
||||
filterElement.x.baseVal.valueAsString = "-50%";
|
||||
filterElement.width.baseVal.valueAsString = "200%";
|
||||
filterElement.y.baseVal.valueAsString = "-50%";
|
||||
filterElement.height.baseVal.valueAsString = "200%";
|
||||
|
||||
if (outlineWidth > 0 || outlineHeight > 0) {
|
||||
/* Construct an elliptical border by merging together many rectangles. The border is creating using dilate morphology filters, but these only support
|
||||
* generating rectangles. http://lists.w3.org/Archives/Public/public-fx/2012OctDec/0003.html
|
||||
*/
|
||||
|
||||
let outlineNumber = 0;
|
||||
|
||||
const increment = (!this._settings.preciseOutlines && this._gaussianBlur > 0) ? this._gaussianBlur : 1;
|
||||
|
||||
((addOutline: (x: number, y: number) => void) => {
|
||||
if (outlineWidth <= outlineHeight) {
|
||||
if (outlineWidth > 0) {
|
||||
let x: number;
|
||||
for (x = 0; x <= outlineWidth; x += increment) {
|
||||
addOutline(x, outlineHeight / outlineWidth * Math.sqrt(outlineWidth * outlineWidth - x * x));
|
||||
}
|
||||
if (x !== outlineWidth + increment) {
|
||||
addOutline(outlineWidth, 0);
|
||||
}
|
||||
}
|
||||
else {
|
||||
addOutline(0, outlineHeight);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (outlineHeight > 0) {
|
||||
let y: number;
|
||||
for (y = 0; y <= outlineHeight; y += increment) {
|
||||
addOutline(outlineWidth / outlineHeight * Math.sqrt(outlineHeight * outlineHeight - y * y), y);
|
||||
}
|
||||
if (y !== outlineHeight + increment) {
|
||||
addOutline(0, outlineHeight);
|
||||
}
|
||||
}
|
||||
else {
|
||||
addOutline(outlineWidth, 0);
|
||||
}
|
||||
}
|
||||
})((x: number, y: number): void => {
|
||||
const outlineFilter = document.createElementNS("http://www.w3.org/2000/svg", "feMorphology");
|
||||
filterElement.appendChild(outlineFilter);
|
||||
outlineFilter.in1.baseVal = "source";
|
||||
outlineFilter.operator.baseVal = SVGFEMorphologyElement.SVG_MORPHOLOGY_OPERATOR_DILATE;
|
||||
outlineFilter.radiusX.baseVal = x;
|
||||
outlineFilter.radiusY.baseVal = y;
|
||||
outlineFilter.result.baseVal = `outline${ outlineNumber }`;
|
||||
|
||||
outlineNumber++;
|
||||
});
|
||||
|
||||
// Start with SourceAlpha. Leave the alpha as 0 if it's 0, and set it to 1 if it's greater than 0
|
||||
const source = document.createElementNS("http://www.w3.org/2000/svg", "feComponentTransfer");
|
||||
filterElement.insertBefore(source, filterElement.firstElementChild);
|
||||
source.in1.baseVal = "SourceAlpha";
|
||||
source.result.baseVal = "source";
|
||||
|
||||
const sourceAlphaTransferNode = document.createElementNS("http://www.w3.org/2000/svg", "feFuncA");
|
||||
source.appendChild(sourceAlphaTransferNode);
|
||||
sourceAlphaTransferNode.type.baseVal = SVGComponentTransferFunctionElement.SVG_FECOMPONENTTRANSFER_TYPE_LINEAR;
|
||||
|
||||
/* The alphas of all colored pixels of the SourceAlpha should be made as close to 1 as possible. This way the summed outlines below will be uniformly dark.
|
||||
* Multiply the pixels by 1 / primaryAlpha so that the primaryAlpha pixels become 1. A higher value would make the outline larger and too sharp,
|
||||
* leading to jagged outer edge and transparent space around the inner edge between itself and the SourceGraphic.
|
||||
*/
|
||||
sourceAlphaTransferNode.slope.baseVal = (primaryAlpha === 0) ? 1 : (1 / primaryAlpha);
|
||||
|
||||
sourceAlphaTransferNode.intercept.baseVal = 0;
|
||||
|
||||
// Merge the individual outlines
|
||||
const mergedOutlines = document.createElementNS("http://www.w3.org/2000/svg", "feMerge");
|
||||
filterElement.appendChild(mergedOutlines);
|
||||
|
||||
for (let i = 0; i < outlineNumber; i++) {
|
||||
const outlineReferenceNode = document.createElementNS("http://www.w3.org/2000/svg", "feMergeNode");
|
||||
mergedOutlines.appendChild(outlineReferenceNode);
|
||||
outlineReferenceNode.in1.baseVal = `outline${ i }`;
|
||||
}
|
||||
|
||||
// Color it with the outline color
|
||||
const coloredSource = document.createElementNS("http://www.w3.org/2000/svg", "feComponentTransfer");
|
||||
filterElement.appendChild(coloredSource);
|
||||
coloredSource.setAttribute("color-interpolation-filters", "sRGB");
|
||||
|
||||
const outlineRedTransferNode = document.createElementNS("http://www.w3.org/2000/svg", "feFuncR");
|
||||
coloredSource.appendChild(outlineRedTransferNode);
|
||||
outlineRedTransferNode.type.baseVal = SVGComponentTransferFunctionElement.SVG_FECOMPONENTTRANSFER_TYPE_LINEAR;
|
||||
outlineRedTransferNode.slope.baseVal = 0;
|
||||
outlineRedTransferNode.intercept.baseVal = outlineColor.red / 255 * outlineColor.alpha;
|
||||
|
||||
const outlineGreenTransferNode = document.createElementNS("http://www.w3.org/2000/svg", "feFuncG");
|
||||
coloredSource.appendChild(outlineGreenTransferNode);
|
||||
outlineGreenTransferNode.type.baseVal = SVGComponentTransferFunctionElement.SVG_FECOMPONENTTRANSFER_TYPE_LINEAR;
|
||||
outlineGreenTransferNode.slope.baseVal = 0;
|
||||
outlineGreenTransferNode.intercept.baseVal = outlineColor.green / 255 * outlineColor.alpha;
|
||||
|
||||
const outlineBlueTransferNode = document.createElementNS("http://www.w3.org/2000/svg", "feFuncB");
|
||||
coloredSource.appendChild(outlineBlueTransferNode);
|
||||
outlineBlueTransferNode.type.baseVal = SVGComponentTransferFunctionElement.SVG_FECOMPONENTTRANSFER_TYPE_LINEAR;
|
||||
outlineBlueTransferNode.slope.baseVal = 0;
|
||||
outlineBlueTransferNode.intercept.baseVal = outlineColor.blue / 255 * outlineColor.alpha;
|
||||
|
||||
const outlineAlphaTransferNode = document.createElementNS("http://www.w3.org/2000/svg", "feFuncA");
|
||||
coloredSource.appendChild(outlineAlphaTransferNode);
|
||||
outlineAlphaTransferNode.type.baseVal = SVGComponentTransferFunctionElement.SVG_FECOMPONENTTRANSFER_TYPE_LINEAR;
|
||||
outlineAlphaTransferNode.slope.baseVal = outlineColor.alpha;
|
||||
outlineAlphaTransferNode.intercept.baseVal = 0;
|
||||
|
||||
// Blur the merged outline
|
||||
if (this._gaussianBlur > 0) {
|
||||
const gaussianBlurFilter = document.createElementNS("http://www.w3.org/2000/svg", "feGaussianBlur");
|
||||
filterElement.appendChild(gaussianBlurFilter);
|
||||
gaussianBlurFilter.stdDeviationX.baseVal = this._gaussianBlur;
|
||||
gaussianBlurFilter.stdDeviationY.baseVal = this._gaussianBlur;
|
||||
}
|
||||
for (let i = 0; i < this._blur; i++) {
|
||||
const blurFilter = document.createElementNS("http://www.w3.org/2000/svg", "feConvolveMatrix");
|
||||
filterElement.appendChild(blurFilter);
|
||||
blurFilter.setAttribute("kernelMatrix", "1 2 1 2 4 2 1 2 1");
|
||||
blurFilter.edgeMode.baseVal = SVGFEConvolveMatrixElement.SVG_EDGEMODE_NONE;
|
||||
}
|
||||
|
||||
// Cut out the source, so only the outline remains
|
||||
const outlineCutoutNode = document.createElementNS("http://www.w3.org/2000/svg", "feComposite");
|
||||
filterElement.appendChild(outlineCutoutNode);
|
||||
outlineCutoutNode.in2.baseVal = "source";
|
||||
outlineCutoutNode.operator.baseVal = SVGFECompositeElement.SVG_FECOMPOSITE_OPERATOR_OUT;
|
||||
|
||||
// Merge the outline with the SourceGraphic
|
||||
const mergedOutlineAndSourceGraphic = document.createElementNS("http://www.w3.org/2000/svg", "feMerge");
|
||||
filterElement.appendChild(mergedOutlineAndSourceGraphic);
|
||||
|
||||
const outlineReferenceNode = document.createElementNS("http://www.w3.org/2000/svg", "feMergeNode");
|
||||
mergedOutlineAndSourceGraphic.appendChild(outlineReferenceNode);
|
||||
|
||||
const sourceGraphicReferenceNode = document.createElementNS("http://www.w3.org/2000/svg", "feMergeNode");
|
||||
mergedOutlineAndSourceGraphic.appendChild(sourceGraphicReferenceNode);
|
||||
sourceGraphicReferenceNode.in1.baseVal = "SourceGraphic";
|
||||
}
|
||||
else {
|
||||
// Blur the source graphic directly
|
||||
if (this._gaussianBlur > 0) {
|
||||
const gaussianBlurFilter = document.createElementNS("http://www.w3.org/2000/svg", "feGaussianBlur");
|
||||
filterElement.appendChild(gaussianBlurFilter);
|
||||
gaussianBlurFilter.stdDeviationX.baseVal = this._gaussianBlur;
|
||||
gaussianBlurFilter.stdDeviationY.baseVal = this._gaussianBlur;
|
||||
}
|
||||
for (let i = 0; i < this._blur; i++) {
|
||||
const blurFilter = document.createElementNS("http://www.w3.org/2000/svg", "feConvolveMatrix");
|
||||
filterElement.appendChild(blurFilter);
|
||||
blurFilter.setAttribute("kernelMatrix", "1 2 1 2 4 2 1 2 1");
|
||||
blurFilter.edgeMode.baseVal = SVGFEConvolveMatrixElement.SVG_EDGEMODE_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
if (filterElement.childElementCount > 0) {
|
||||
this._svgDefsElement.appendChild(filterElement);
|
||||
|
||||
filterWrapperSpan.style.webkitFilter = `url("#${ filterId }")`;
|
||||
filterWrapperSpan.style.filter = `url("#${ filterId }")`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!HTMLSpanElement} span
|
||||
* @param {number} outlineWidth
|
||||
* @param {number} outlineHeight
|
||||
* @param {!libjass.parts.Color} outlineColor
|
||||
*/
|
||||
private _setTextShadowOutlineOnSpan(span: HTMLSpanElement, outlineWidth: number, outlineHeight: number, outlineColor: Color): void {
|
||||
if (outlineWidth > 0 || outlineHeight > 0) {
|
||||
let outlineCssString = "";
|
||||
|
||||
((addOutline: (x: number, y: number) => void) => {
|
||||
for (let x = 0; x <= outlineWidth; x++) {
|
||||
const maxY = (outlineWidth === 0) ? outlineHeight : outlineHeight * Math.sqrt(1 - ((x * x) / (outlineWidth * outlineWidth)));
|
||||
for (let y = 0; y <= maxY; y++) {
|
||||
addOutline(x, y);
|
||||
|
||||
if (x !== 0) {
|
||||
addOutline(-x, y);
|
||||
}
|
||||
|
||||
if (y !== 0) {
|
||||
addOutline(x, -y);
|
||||
}
|
||||
|
||||
if (x !== 0 && y !== 0) {
|
||||
addOutline(-x, -y);
|
||||
}
|
||||
}
|
||||
}
|
||||
})((x: number, y: number): void => {
|
||||
outlineCssString += `, ${ outlineColor.toString() } ${ x }px ${ y }px ${ this._gaussianBlur.toFixed(3) }px`;
|
||||
});
|
||||
|
||||
span.style.textShadow = outlineCssString.substr(", ".length);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!HTMLBRElement}
|
||||
*/
|
||||
makeNewLine(): HTMLBRElement {
|
||||
var result = document.createElement("br");
|
||||
const result = document.createElement("br");
|
||||
result.style.lineHeight = `${ (this._scaleY * this._fontSize).toFixed(3) }px`;
|
||||
return result;
|
||||
}
|
||||
@@ -435,6 +556,15 @@ ${ blurFilter }
|
||||
this._strikeThrough = SpanStyles._valueOrDefault(value, this._defaultStyle.strikeThrough);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the outline width property.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get outlineWidth() {
|
||||
return this._outlineWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the outline width property. null defaults it to the style's original outline width value.
|
||||
*
|
||||
@@ -444,6 +574,15 @@ ${ blurFilter }
|
||||
this._outlineWidth = SpanStyles._valueOrDefault(value, this._defaultStyle.outlineThickness);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the outline width property.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get outlineHeight() {
|
||||
return this._outlineWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the outline height property. null defaults it to the style's original outline height value.
|
||||
*
|
||||
@@ -454,7 +593,16 @@ ${ blurFilter }
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the outline width property. null defaults it to the style's original shadow depth X value.
|
||||
* Gets the shadow width property.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get shadowDepthX() {
|
||||
return this._shadowDepthX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the shadow width property. null defaults it to the style's original shadow depth value.
|
||||
*
|
||||
* @type {?number}
|
||||
*/
|
||||
@@ -463,7 +611,16 @@ ${ blurFilter }
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the shadow height property. null defaults it to the style's original shadow depth Y value.
|
||||
* Gets the shadow height property.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get shadowDepthY() {
|
||||
return this._shadowDepthY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the shadow height property. null defaults it to the style's original shadow depth value.
|
||||
*
|
||||
* @type {?number}
|
||||
*/
|
||||
@@ -471,6 +628,15 @@ ${ blurFilter }
|
||||
this._shadowDepthY = SpanStyles._valueOrDefault(value, this._defaultStyle.shadowDepth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the blur property.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get blur() {
|
||||
return this._blur;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the blur property. null defaults it to 0.
|
||||
*
|
||||
@@ -480,6 +646,15 @@ ${ blurFilter }
|
||||
this._blur = SpanStyles._valueOrDefault(value, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Gaussian blur property.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get gaussianBlur() {
|
||||
return this._gaussianBlur;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Gaussian blur property. null defaults it to 0.
|
||||
*
|
||||
@@ -498,6 +673,15 @@ ${ blurFilter }
|
||||
this._fontName = SpanStyles._valueOrDefault(value, this._defaultStyle.fontName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the font size property.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get fontSize() {
|
||||
return this._fontSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the font size property. null defaults it to the default style's value.
|
||||
*
|
||||
@@ -507,6 +691,15 @@ ${ blurFilter }
|
||||
this._fontSize = SpanStyles._valueOrDefault(value, this._defaultStyle.fontSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the horizontal font scaling property.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get fontScaleX() {
|
||||
return this._fontScaleX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the horizontal font scaling property. null defaults it to the default style's value.
|
||||
*
|
||||
@@ -516,6 +709,15 @@ ${ blurFilter }
|
||||
this._fontScaleX = SpanStyles._valueOrDefault(value, this._defaultStyle.fontScaleX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the vertical font scaling property.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get fontScaleY() {
|
||||
return this._fontScaleY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the vertical font scaling property. null defaults it to the default style's value.
|
||||
*
|
||||
@@ -525,6 +727,15 @@ ${ blurFilter }
|
||||
this._fontScaleY = SpanStyles._valueOrDefault(value, this._defaultStyle.fontScaleY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the letter spacing property.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get letterSpacing() {
|
||||
return this._letterSpacing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the letter spacing property. null defaults it to the default style's value.
|
||||
*
|
||||
@@ -534,6 +745,15 @@ ${ blurFilter }
|
||||
this._letterSpacing = SpanStyles._valueOrDefault(value, this._defaultStyle.letterSpacing);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the X-axis rotation property.
|
||||
*
|
||||
* @type {?number}
|
||||
*/
|
||||
get rotationX() {
|
||||
return this._rotationX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the X-axis rotation property.
|
||||
*
|
||||
@@ -543,6 +763,15 @@ ${ blurFilter }
|
||||
this._rotationX = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Y-axis rotation property.
|
||||
*
|
||||
* @type {?number}
|
||||
*/
|
||||
get rotationY() {
|
||||
return this._rotationY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Y-axis rotation property.
|
||||
*
|
||||
@@ -552,6 +781,15 @@ ${ blurFilter }
|
||||
this._rotationY = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Z-axis rotation property.
|
||||
*
|
||||
* @type {?number}
|
||||
*/
|
||||
get rotationZ() {
|
||||
return this._rotationZ;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Z-axis rotation property.
|
||||
*
|
||||
@@ -561,6 +799,15 @@ ${ blurFilter }
|
||||
this._rotationZ = SpanStyles._valueOrDefault(value, this._defaultStyle.rotationZ);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the X-axis skew property.
|
||||
*
|
||||
* @type {?number}
|
||||
*/
|
||||
get skewX() {
|
||||
return this._skewX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the X-axis skew property.
|
||||
*
|
||||
@@ -570,6 +817,15 @@ ${ blurFilter }
|
||||
this._skewX = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Y-axis skew property.
|
||||
*
|
||||
* @type {?number}
|
||||
*/
|
||||
get skewY() {
|
||||
return this._skewY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Y-axis skew property.
|
||||
*
|
||||
@@ -615,6 +871,15 @@ ${ blurFilter }
|
||||
this._secondaryColor = SpanStyles._valueOrDefault(value, this._defaultStyle.secondaryColor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the outline color property.
|
||||
*
|
||||
* @type {!libjass.Color}
|
||||
*/
|
||||
get outlineColor(): Color {
|
||||
return this._outlineColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the outline color property. null defaults it to the default style's value.
|
||||
*
|
||||
@@ -624,6 +889,15 @@ ${ blurFilter }
|
||||
this._outlineColor = SpanStyles._valueOrDefault(value, this._defaultStyle.outlineColor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the shadow color property.
|
||||
*
|
||||
* @type {!libjass.Color}
|
||||
*/
|
||||
get shadowColor(): Color {
|
||||
return this._shadowColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the shadow color property. null defaults it to the default style's value.
|
||||
*
|
||||
@@ -636,7 +910,7 @@ ${ blurFilter }
|
||||
/**
|
||||
* Gets the primary alpha property.
|
||||
*
|
||||
* @type {?number}
|
||||
* @type {number}
|
||||
*/
|
||||
get primaryAlpha(): number {
|
||||
return this._primaryAlpha;
|
||||
@@ -648,13 +922,13 @@ ${ blurFilter }
|
||||
* @type {?number}
|
||||
*/
|
||||
set primaryAlpha(value: number) {
|
||||
this._primaryAlpha = value;
|
||||
this._primaryAlpha = SpanStyles._valueOrDefault(value, this._defaultStyle.primaryColor.alpha);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the secondary alpha property.
|
||||
*
|
||||
* @type {?number}
|
||||
* @type {number}
|
||||
*/
|
||||
get secondaryAlpha(): number {
|
||||
return this._secondaryAlpha;
|
||||
@@ -666,7 +940,16 @@ ${ blurFilter }
|
||||
* @type {?number}
|
||||
*/
|
||||
set secondaryAlpha(value: number) {
|
||||
this._secondaryAlpha = value;
|
||||
this._secondaryAlpha = SpanStyles._valueOrDefault(value, this._defaultStyle.secondaryColor.alpha);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the outline alpha property.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get outlineAlpha(): number {
|
||||
return this._outlineAlpha;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -675,7 +958,16 @@ ${ blurFilter }
|
||||
* @type {?number}
|
||||
*/
|
||||
set outlineAlpha(value: number) {
|
||||
this._outlineAlpha = value;
|
||||
this._outlineAlpha = SpanStyles._valueOrDefault(value, this._defaultStyle.outlineColor.alpha);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the shadow alpha property.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get shadowAlpha(): number {
|
||||
return this._shadowAlpha;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -684,30 +976,8 @@ ${ blurFilter }
|
||||
* @type {?number}
|
||||
*/
|
||||
set shadowAlpha(value: number) {
|
||||
this._shadowAlpha = value;
|
||||
this._shadowAlpha = SpanStyles._valueOrDefault(value, this._defaultStyle.shadowColor.alpha);
|
||||
}
|
||||
|
||||
private static _valueOrDefault = <T>(newValue: T, defaultValue: T): T => ((newValue !== null) ? newValue : defaultValue);
|
||||
|
||||
/**
|
||||
* @param {string} fontFamily
|
||||
* @param {number} lineHeight
|
||||
* @param {!HTMLDivElement} fontSizeElement
|
||||
* @return {number}
|
||||
*/
|
||||
private static _getFontSize(fontFamily: string, lineHeight: number, fontSizeElement: HTMLDivElement): number {
|
||||
var existingFontSizeMap = SpanStyles._fontSizeCache.get(fontFamily);
|
||||
if (existingFontSizeMap === undefined) {
|
||||
SpanStyles._fontSizeCache.set(fontFamily, existingFontSizeMap = new Map<number, number>());
|
||||
}
|
||||
|
||||
var existingFontSize = existingFontSizeMap.get(lineHeight);
|
||||
if (existingFontSize === undefined) {
|
||||
fontSizeElement.style.fontFamily = fontFamily;
|
||||
fontSizeElement.style.fontSize = `${ lineHeight }px`;
|
||||
existingFontSizeMap.set(lineHeight, existingFontSize = lineHeight * lineHeight / fontSizeElement.offsetHeight);
|
||||
}
|
||||
|
||||
return existingFontSize;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
interface Document {
|
||||
createElementNS(namespaceURI: "http://www.w3.org/2000/svg", qualifiedName: "defs"): SVGDefsElement;
|
||||
createElementNS(namespaceURI: "http://www.w3.org/2000/svg", qualifiedName: "feComponentTransfer"): SVGFEComponentTransferElement;
|
||||
createElementNS(namespaceURI: "http://www.w3.org/2000/svg", qualifiedName: "feComposite"): SVGFECompositeElement;
|
||||
createElementNS(namespaceURI: "http://www.w3.org/2000/svg", qualifiedName: "feConvolveMatrix"): SVGFEConvolveMatrixElement;
|
||||
createElementNS(namespaceURI: "http://www.w3.org/2000/svg", qualifiedName: "feFuncA"): SVGFEFuncAElement;
|
||||
createElementNS(namespaceURI: "http://www.w3.org/2000/svg", qualifiedName: "feFuncB"): SVGFEFuncBElement;
|
||||
createElementNS(namespaceURI: "http://www.w3.org/2000/svg", qualifiedName: "feFuncG"): SVGFEFuncGElement;
|
||||
createElementNS(namespaceURI: "http://www.w3.org/2000/svg", qualifiedName: "feFuncR"): SVGFEFuncRElement;
|
||||
createElementNS(namespaceURI: "http://www.w3.org/2000/svg", qualifiedName: "feGaussianBlur"): SVGFEGaussianBlurElement;
|
||||
createElementNS(namespaceURI: "http://www.w3.org/2000/svg", qualifiedName: "feMerge"): SVGFEMergeElement;
|
||||
createElementNS(namespaceURI: "http://www.w3.org/2000/svg", qualifiedName: "feMergeNode"): SVGFEMergeNodeElement;
|
||||
createElementNS(namespaceURI: "http://www.w3.org/2000/svg", qualifiedName: "feMorphology"): SVGFEMorphologyElement;
|
||||
createElementNS(namespaceURI: "http://www.w3.org/2000/svg", qualifiedName: "filter"): SVGFilterElement;
|
||||
createElementNS(namespaceURI: "http://www.w3.org/2000/svg", qualifiedName: "g"): SVGGElement;
|
||||
createElementNS(namespaceURI: "http://www.w3.org/2000/svg", qualifiedName: "path"): SVGPathElement;
|
||||
createElementNS(namespaceURI: "http://www.w3.org/2000/svg", qualifiedName: "svg"): SVGSVGElement;
|
||||
}
|
||||
|
||||
interface SVGFEComponentTransferElement {
|
||||
appendChild(newChild: SVGFEFuncAElement): SVGFEFuncAElement;
|
||||
appendChild(newChild: SVGFEFuncBElement): SVGFEFuncBElement;
|
||||
appendChild(newChild: SVGFEFuncGElement): SVGFEFuncGElement;
|
||||
appendChild(newChild: SVGFEFuncRElement): SVGFEFuncRElement;
|
||||
}
|
||||
|
||||
interface SVGFEMergeElement {
|
||||
appendChild(newChild: SVGFEMergeNodeElement): SVGFEMergeNodeElement;
|
||||
}
|
||||
@@ -23,14 +23,14 @@
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
export var debugMode: boolean = false;
|
||||
export let debugMode: boolean = false;
|
||||
|
||||
/**
|
||||
* Verbose debug mode. When true, libjass logs some more debug messages. This setting is independent of {@link libjass.debugMode}
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
export var verboseMode: boolean = false;
|
||||
export let verboseMode: boolean = false;
|
||||
|
||||
/**
|
||||
* Sets the debug mode.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"module": "CommonJS",
|
||||
"noImplicitAny": true,
|
||||
"sourceMap": true,
|
||||
"target": "ES5"
|
||||
"target": "ES5",
|
||||
"experimentalDecorators": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Attachment } from "./attachment";
|
||||
import { Dialogue } from "./dialogue";
|
||||
import { Style } from "./style";
|
||||
import { ScriptProperties } from "./script-properties";
|
||||
@@ -26,15 +27,15 @@ import { Format } from "./misc";
|
||||
|
||||
import { verboseMode } from "../settings";
|
||||
|
||||
import * as parser from "../parser/index";
|
||||
import * as parser from "../parser";
|
||||
import { parseLineIntoTypedTemplate } from "../parser/misc";
|
||||
import { ReadableStream, TextDecoder, TextDecoderConstructor } from "../parser/streams";
|
||||
|
||||
import { Map } from "../utility/map";
|
||||
import { Promise } from "../utility/promise";
|
||||
|
||||
declare var global: {
|
||||
fetch?(url: string): Promise<{ body: ReadableStream }>;
|
||||
declare const global: {
|
||||
fetch?(url: string): Promise<{ body: ReadableStream; ok?: boolean; status?: number; }>;
|
||||
ReadableStream?: { prototype: ReadableStream; };
|
||||
TextDecoder?: TextDecoderConstructor;
|
||||
};
|
||||
@@ -46,6 +47,7 @@ export class ASS {
|
||||
private _properties: ScriptProperties = new ScriptProperties();
|
||||
private _styles: Map<string, Style> = new Map<string, Style>();
|
||||
private _dialogues: Dialogue[] = [];
|
||||
private _attachments: Attachment[] = [];
|
||||
|
||||
private _stylesFormatSpecifier: string[] = null;
|
||||
private _dialoguesFormatSpecifier: string[] = null;
|
||||
@@ -77,6 +79,15 @@ export class ASS {
|
||||
return this._dialogues;
|
||||
}
|
||||
|
||||
/**
|
||||
* The attachments of this script.
|
||||
*
|
||||
* @type {!Array.<!libjass.Attachment>}
|
||||
*/
|
||||
get attachments(): Attachment[] {
|
||||
return this._attachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* The format specifier for the styles section.
|
||||
*
|
||||
@@ -118,6 +129,8 @@ export class ASS {
|
||||
if (arguments.length === 1) {
|
||||
throw new Error("Constructor `new ASS(rawASS)` has been deprecated. Use `ASS.fromString(rawASS)` instead.");
|
||||
}
|
||||
|
||||
this._styles.set("Default", new Style(new Map([["Name", "Default"]])));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,21 +139,21 @@ export class ASS {
|
||||
* @param {string} line The line from the script that contains the new style.
|
||||
*/
|
||||
addStyle(line: string): void {
|
||||
var styleLine = parseLineIntoTypedTemplate(line, this._stylesFormatSpecifier);
|
||||
const styleLine = parseLineIntoTypedTemplate(line, this._stylesFormatSpecifier);
|
||||
if (styleLine === null || styleLine.type !== "Style") {
|
||||
return;
|
||||
}
|
||||
|
||||
var styleTemplate = styleLine.template;
|
||||
const styleTemplate = styleLine.template;
|
||||
|
||||
if (verboseMode) {
|
||||
var repr = "";
|
||||
let repr = "";
|
||||
styleTemplate.forEach((value, key) => repr += `${ key } = ${ value }, `);
|
||||
console.log(`Read style: ${ repr }`);
|
||||
}
|
||||
|
||||
// Create the dialogue and add it to the dialogues array
|
||||
var style = new Style(styleTemplate);
|
||||
const style = new Style(styleTemplate);
|
||||
this._styles.set(style.name, style);
|
||||
}
|
||||
|
||||
@@ -150,15 +163,15 @@ export class ASS {
|
||||
* @param {string} line The line from the script that contains the new event.
|
||||
*/
|
||||
addEvent(line: string): void {
|
||||
var dialogueLine = parseLineIntoTypedTemplate(line, this._dialoguesFormatSpecifier);
|
||||
const dialogueLine = parseLineIntoTypedTemplate(line, this._dialoguesFormatSpecifier);
|
||||
if (dialogueLine === null || dialogueLine.type !== "Dialogue") {
|
||||
return;
|
||||
}
|
||||
|
||||
var dialogueTemplate = dialogueLine.template;
|
||||
const dialogueTemplate = dialogueLine.template;
|
||||
|
||||
if (verboseMode) {
|
||||
var repr = "";
|
||||
let repr = "";
|
||||
dialogueTemplate.forEach((value, key) => repr += `${ key } = ${ value }, `);
|
||||
console.log(`Read dialogue: ${ repr }`);
|
||||
}
|
||||
@@ -167,6 +180,15 @@ export class ASS {
|
||||
this.dialogues.push(new Dialogue(dialogueTemplate, this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an attachment to this ASS script.
|
||||
*
|
||||
* @param {!libjass.Attachment} attachment
|
||||
*/
|
||||
addAttachment(attachment: Attachment): void {
|
||||
this._attachments.push(attachment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an ASS object from the raw text of an ASS script.
|
||||
*
|
||||
@@ -204,19 +226,34 @@ export class ASS {
|
||||
* @return {!Promise.<!libjass.ASS>} A promise that will be resolved with the ASS object when it has been fully parsed
|
||||
*/
|
||||
static fromUrl(url: string, type: Format = Format.ASS): Promise<ASS> {
|
||||
let fetchPromise: Promise<ASS>;
|
||||
|
||||
if (
|
||||
typeof global.fetch === "function" &&
|
||||
typeof global.ReadableStream === "function" && typeof global.ReadableStream.prototype.getReader === "function" &&
|
||||
typeof global.TextDecoder === "function"
|
||||
) {
|
||||
return global.fetch(url).then(response => ASS.fromReadableStream(response.body, "utf-8", type));
|
||||
fetchPromise = global.fetch(url).then(response => {
|
||||
if (response.ok === false || (response.ok === undefined && (response.status < 200 || response.status > 299))) {
|
||||
throw new Error(`HTTP request for ${ url } failed with status code ${ response.status }`);
|
||||
}
|
||||
|
||||
return ASS.fromReadableStream(response.body, "utf-8", type);
|
||||
});
|
||||
}
|
||||
else {
|
||||
fetchPromise = Promise.reject<ASS>(new Error("Not supported."));
|
||||
}
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
var result = ASS.fromStream(new parser.XhrStream(xhr), type);
|
||||
xhr.open("GET", url, true);
|
||||
xhr.send();
|
||||
return result;
|
||||
return fetchPromise.catch(reason => {
|
||||
console.warn("fetch() failed, falling back to XHR: %o", reason);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
const result = ASS.fromStream(new parser.XhrStream(xhr), type);
|
||||
xhr.open("GET", url, true);
|
||||
xhr.send();
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The type of an attachment.
|
||||
*/
|
||||
export enum AttachmentType {
|
||||
Font,
|
||||
Graphic,
|
||||
}
|
||||
|
||||
/**
|
||||
* This class represents an attachment in a {@link libjass.ASS} script.
|
||||
*
|
||||
* @param {string} filename The filename of this attachment.
|
||||
* @param {number} type The type of this attachment.
|
||||
*/
|
||||
export class Attachment {
|
||||
private _contents: string = "";
|
||||
|
||||
constructor(private _filename: string, private _type: AttachmentType) { }
|
||||
|
||||
/**
|
||||
* The filename of this attachment.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get filename(): string {
|
||||
return this._filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of this attachment.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get type(): AttachmentType {
|
||||
return this._type;
|
||||
}
|
||||
|
||||
/**
|
||||
* The contents of this attachment in base64 encoding.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get contents(): string {
|
||||
return this._contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* The contents of this attachment in base64 encoding.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
set contents(value: string) {
|
||||
this._contents = value;
|
||||
}
|
||||
}
|
||||
@@ -23,9 +23,11 @@ import { Style } from "./style";
|
||||
|
||||
import { valueOrDefault } from "./misc";
|
||||
|
||||
import { parseLineIntoTypedTemplate } from "../parser/misc";
|
||||
|
||||
import { parse } from "../parser/parse";
|
||||
|
||||
import * as parts from "../parts/index";
|
||||
import * as parts from "../parts";
|
||||
|
||||
import { debugMode } from "../settings";
|
||||
|
||||
@@ -40,7 +42,7 @@ import { Map } from "../utility/map";
|
||||
* @param {string} template["End"] The end time
|
||||
* @param {string} template["Layer"] The layer number
|
||||
* @param {string} template["Text"] The text of this dialogue
|
||||
* @param {ASS} ass The ASS object to which this dialogue belongs
|
||||
* @param {!libjass.ASS} ass The ASS object to which this dialogue belongs
|
||||
*/
|
||||
export class Dialogue {
|
||||
private static _lastDialogueId = -1;
|
||||
@@ -58,23 +60,45 @@ export class Dialogue {
|
||||
private _rawPartsString: string;
|
||||
private _parts: parts.Part[] = null;
|
||||
|
||||
private _sub: HTMLDivElement = null;
|
||||
private _containsTransformTag: boolean = false;
|
||||
|
||||
constructor(template: Map<string, string>, ass: ASS) {
|
||||
this._id = ++Dialogue._lastDialogueId;
|
||||
|
||||
var style = template.get("Style");
|
||||
this._style = ass.styles.get(style);
|
||||
if (this._style === undefined) {
|
||||
throw new Error(`Unrecognized style ${ style }`);
|
||||
{
|
||||
const normalizedTemplate = new Map<string, string>();
|
||||
template.forEach((value, key) => {
|
||||
normalizedTemplate.set(key.toLowerCase(), value);
|
||||
});
|
||||
template = normalizedTemplate;
|
||||
}
|
||||
|
||||
this._start = Dialogue._toTime(template.get("Start"));
|
||||
this._end = Dialogue._toTime(template.get("End"));
|
||||
this._id = ++Dialogue._lastDialogueId;
|
||||
|
||||
this._layer = Math.max(valueOrDefault(template, "Layer", parseInt, value => !isNaN(value), "0"), 0);
|
||||
let styleName = template.get("style");
|
||||
if (styleName !== undefined && styleName !== null && styleName.constructor === String) {
|
||||
styleName = styleName.replace(/^\*+/, "");
|
||||
if (styleName.match(/^Default$/i) !== null) {
|
||||
styleName = "Default";
|
||||
}
|
||||
}
|
||||
|
||||
this._rawPartsString = template.get("Text");
|
||||
this._style = ass.styles.get(styleName);
|
||||
if (this._style === undefined) {
|
||||
if (debugMode) {
|
||||
console.warn(`Unrecognized style ${ styleName }. Falling back to "Default"`);
|
||||
}
|
||||
|
||||
this._style = ass.styles.get("Default");
|
||||
}
|
||||
if (this._style === undefined) {
|
||||
throw new Error(`Unrecognized style ${ styleName }`);
|
||||
}
|
||||
|
||||
this._start = Dialogue._toTime(template.get("start"));
|
||||
this._end = Dialogue._toTime(template.get("end"));
|
||||
|
||||
this._layer = Math.max(valueOrDefault(template, "layer", parseInt, value => !isNaN(value), "0"), 0);
|
||||
|
||||
this._rawPartsString = template.get("text");
|
||||
if (this._rawPartsString === undefined || this._rawPartsString === null || this._rawPartsString.constructor !== String) {
|
||||
throw new Error("Dialogue doesn't have any text.");
|
||||
}
|
||||
@@ -151,6 +175,19 @@ export class Dialogue {
|
||||
return this._parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience getter for whether this dialogue contains a {\t} tag.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
get containsTransformTag(): boolean {
|
||||
if (this._parts === null) {
|
||||
this._parsePartsString();
|
||||
}
|
||||
|
||||
return this._containsTransformTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string} A simple representation of this dialogue's properties and parts.
|
||||
*/
|
||||
@@ -189,11 +226,13 @@ export class Dialogue {
|
||||
part.tags
|
||||
);
|
||||
}
|
||||
|
||||
this._containsTransformTag = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (debugMode) {
|
||||
var possiblyIncorrectParses = this._parts.filter(part => part instanceof parts.Comment && part.value.indexOf("\\") !== -1);
|
||||
const possiblyIncorrectParses = this._parts.filter(part => part instanceof parts.Comment && part.value.indexOf("\\") !== -1);
|
||||
if (possiblyIncorrectParses.length > 0) {
|
||||
console.warn(
|
||||
`Possible incorrect parse:
|
||||
|
||||
@@ -85,19 +85,19 @@ export interface TypedTemplate {
|
||||
* @return {T}
|
||||
*/
|
||||
export function valueOrDefault<T>(template: Map<string, string>, key: string, converter: (str: string) => T, validator: (value: T) => boolean, defaultValue: string): T {
|
||||
var value = template.get(key);
|
||||
const value = template.get(key);
|
||||
if (value === undefined) {
|
||||
return converter(defaultValue);
|
||||
}
|
||||
|
||||
try {
|
||||
var numValue = converter(value);
|
||||
const result = converter(value);
|
||||
|
||||
if (validator !== null && !validator(numValue)) {
|
||||
if (validator !== null && !validator(result)) {
|
||||
throw new Error("Validation failed.");
|
||||
}
|
||||
|
||||
return numValue;
|
||||
return result;
|
||||
}
|
||||
catch (ex) {
|
||||
throw new Error(`Property ${ key } has invalid value ${ value } - ${ ex.stack }`);
|
||||
|
||||
@@ -22,7 +22,7 @@ import { valueOrDefault, BorderStyle } from "./misc";
|
||||
|
||||
import { parse } from "../parser/parse";
|
||||
|
||||
import { Color } from "../parts/index";
|
||||
import { Color } from "../parts";
|
||||
|
||||
import { Map } from "../utility/map";
|
||||
|
||||
@@ -85,41 +85,50 @@ export class Style {
|
||||
private _marginVertical: number;
|
||||
|
||||
constructor(template: Map<string, string>) {
|
||||
this._name = template.get("Name");
|
||||
{
|
||||
const normalizedTemplate = new Map<string, string>();
|
||||
template.forEach((value, key) => {
|
||||
normalizedTemplate.set(key.toLowerCase(), value);
|
||||
});
|
||||
template = normalizedTemplate;
|
||||
}
|
||||
|
||||
this._name = template.get("name");
|
||||
if (this._name === undefined || this._name === null || this._name.constructor !== String) {
|
||||
throw new Error("Style doesn't have a name.");
|
||||
}
|
||||
this._name = this._name.replace(/^\*+/, "");
|
||||
|
||||
this._italic = template.get("Italic") === "-1";
|
||||
this._bold = template.get("Bold") === "-1";
|
||||
this._underline = template.get("Underline") === "-1";
|
||||
this._strikeThrough = template.get("StrikeOut") === "-1";
|
||||
this._italic = !!valueOrDefault(template, "italic", parseFloat, value => !isNaN(value), "0");
|
||||
this._bold = !!valueOrDefault(template, "bold", parseFloat, value => !isNaN(value), "0");
|
||||
this._underline = !!valueOrDefault(template, "underline", parseFloat, value => !isNaN(value), "0");
|
||||
this._strikeThrough = !!valueOrDefault(template, "strikeout", parseFloat, value => !isNaN(value), "0");
|
||||
|
||||
this._fontName = template.get("Fontname");
|
||||
this._fontSize = valueOrDefault(template, "Fontsize", parseFloat, value => !isNaN(value), "50");
|
||||
this._fontName = valueOrDefault(template, "fontname", str => str, value => value.constructor === String, "sans-serif");
|
||||
this._fontSize = valueOrDefault(template, "fontsize", parseFloat, value => !isNaN(value), "18");
|
||||
|
||||
this._fontScaleX = valueOrDefault(template, "ScaleX", parseFloat, value => !isNaN(value), "100") / 100;
|
||||
this._fontScaleY = valueOrDefault(template, "ScaleY", parseFloat, value => !isNaN(value), "100") / 100;
|
||||
this._fontScaleX = valueOrDefault(template, "scalex", parseFloat, value => value >= 0, "100") / 100;
|
||||
this._fontScaleY = valueOrDefault(template, "scaley", parseFloat, value => value >= 0, "100") / 100;
|
||||
|
||||
this._letterSpacing = valueOrDefault(template, "Spacing", parseFloat, value => !isNaN(value), "0");
|
||||
this._letterSpacing = valueOrDefault(template, "spacing", parseFloat, value => value >= 0, "0");
|
||||
|
||||
this._rotationZ = valueOrDefault(template, "Angle", parseFloat, value => !isNaN(value), "0");
|
||||
this._rotationZ = valueOrDefault(template, "angle", parseFloat, value => !isNaN(value), "0");
|
||||
|
||||
this._primaryColor = valueOrDefault(template, "PrimaryColour", str => <Color>parse(str, "colorWithAlpha"), null, "&H0000FFFF");
|
||||
this._secondaryColor = valueOrDefault(template, "SecondaryColour", str => <Color>parse(str, "colorWithAlpha"), null, "&H00000000");
|
||||
this._outlineColor = valueOrDefault(template, "OutlineColour", str => <Color>parse(str, "colorWithAlpha"), null, "&H00000000");
|
||||
this._shadowColor = valueOrDefault(template, "BackColour", str => <Color>parse(str, "colorWithAlpha"), null, "&H00000000");
|
||||
this._primaryColor = valueOrDefault(template, "primarycolour", str => <Color>parse(str, "colorWithAlpha"), null, "&H00FFFFFF");
|
||||
this._secondaryColor = valueOrDefault(template, "secondarycolour", str => <Color>parse(str, "colorWithAlpha"), null, "&H00FFFF00");
|
||||
this._outlineColor = valueOrDefault(template, "outlinecolour", str => <Color>parse(str, "colorWithAlpha"), null, "&H00000000");
|
||||
this._shadowColor = valueOrDefault(template, "backcolour", str => <Color>parse(str, "colorWithAlpha"), null, "&H80000000");
|
||||
|
||||
this._outlineThickness = valueOrDefault(template, "Outline", parseFloat, value => !isNaN(value), "1");
|
||||
this._borderStyle = valueOrDefault(template, "BorderStyle", parseInt, value => !isNaN(value), "1");
|
||||
this._outlineThickness = valueOrDefault(template, "outline", parseFloat, value => value >= 0, "2");
|
||||
this._borderStyle = valueOrDefault(template, "borderstyle", parseInt, value => (<any>BorderStyle)[(<any>BorderStyle)[value]] === value, "1");
|
||||
|
||||
this._shadowDepth = valueOrDefault(template, "Shadow", parseFloat, value => !isNaN(value), "1");
|
||||
this._shadowDepth = valueOrDefault(template, "shadow", parseFloat, value => value >= 0, "3");
|
||||
|
||||
this._alignment = valueOrDefault(template, "Alignment", parseInt, value => !isNaN(value), "2");
|
||||
this._alignment = valueOrDefault(template, "alignment", parseInt, value => value >= 1 && value <= 9, "2");
|
||||
|
||||
this._marginLeft = valueOrDefault(template, "MarginL", parseFloat, value => !isNaN(value), "80");
|
||||
this._marginRight = valueOrDefault(template, "MarginR", parseFloat, value => !isNaN(value), "80");
|
||||
this._marginVertical = valueOrDefault(template, "MarginV", parseFloat, value => !isNaN(value), "35");
|
||||
this._marginLeft = valueOrDefault(template, "marginl", parseFloat, value => !isNaN(value), "20");
|
||||
this._marginRight = valueOrDefault(template, "marginr", parseFloat, value => !isNaN(value), "20");
|
||||
this._marginVertical = valueOrDefault(template, "marginv", parseFloat, value => !isNaN(value), "20");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,156 +18,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Map implementation for browsers that don't support it. Only supports keys which are of Number or String type, or which have a property called "id".
|
||||
*
|
||||
* Keys and values are stored as properties of an object, with property names derived from the key type.
|
||||
*
|
||||
* @param {!Array.<!Array.<*>>=} iterable Only an array of elements (where each element is a 2-tuple of key and value) is supported.
|
||||
*/
|
||||
class SimpleMap<K, V> {
|
||||
private _keys: { [key: string]: K };
|
||||
private _values: { [key: string]: V };
|
||||
private _size: number;
|
||||
|
||||
constructor(iterable?: [K, V][]) {
|
||||
this.clear();
|
||||
|
||||
if (iterable === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(iterable)) {
|
||||
throw new Error("Non-array iterables are not supported by the SimpleMap constructor.");
|
||||
}
|
||||
|
||||
for (let element of iterable) {
|
||||
this.set(element[0], element[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {K} key
|
||||
* @return {V}
|
||||
*/
|
||||
get(key: K): V {
|
||||
var property = this._keyToProperty(key);
|
||||
|
||||
if (property === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this._values[property];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {K} key
|
||||
* @return {boolean}
|
||||
*/
|
||||
has(key: K): boolean {
|
||||
var property = this._keyToProperty(key);
|
||||
|
||||
if (property === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return property in this._keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {K} key
|
||||
* @param {V} value
|
||||
* @return {libjass.Map.<K, V>} This map
|
||||
*/
|
||||
set(key: K, value: V): Map<K, V> {
|
||||
var property = this._keyToProperty(key);
|
||||
|
||||
if (property === null) {
|
||||
throw new Error("This Map implementation only supports Number and String keys, or keys with an id property.");
|
||||
}
|
||||
|
||||
if (!(property in this._keys)) {
|
||||
this._size++;
|
||||
}
|
||||
|
||||
this._keys[property] = key;
|
||||
this._values[property] = value;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {K} key
|
||||
* @return {boolean} true if the key was present before being deleted, false otherwise
|
||||
*/
|
||||
delete(key: K): boolean {
|
||||
var property = this._keyToProperty(key);
|
||||
|
||||
if (property === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var result = property in this._keys;
|
||||
|
||||
if (result) {
|
||||
delete this._keys[property];
|
||||
delete this._values[property];
|
||||
this._size--;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
clear(): void {
|
||||
this._keys = Object.create(null);
|
||||
this._values = Object.create(null);
|
||||
this._size = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function(V, K, libjass.Map.<K, V>)} callbackfn A function that is called with each key and value in the map.
|
||||
* @param {*} thisArg
|
||||
*/
|
||||
forEach(callbackfn: (value: V, index: K, map: Map<K, V>) => void, thisArg?: any): void {
|
||||
for (let property of Object.keys(this._keys)) {
|
||||
callbackfn.call(thisArg, this._values[property], this._keys[property], this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
get size(): number {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given key into a property name for the internal map.
|
||||
*
|
||||
* @param {K} key
|
||||
* @return {string}
|
||||
*/
|
||||
private _keyToProperty(key: K): string {
|
||||
if (typeof key === "number") {
|
||||
return `#${ key }`;
|
||||
}
|
||||
|
||||
if (typeof key === "string") {
|
||||
return `'${ key }`;
|
||||
}
|
||||
|
||||
if ((<any>key).id !== undefined) {
|
||||
return `!${ (<any>key).id }`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
declare var global: {
|
||||
Map?: typeof SimpleMap;
|
||||
declare const global: {
|
||||
Map?: typeof Map;
|
||||
};
|
||||
|
||||
export interface Map<K, V> {
|
||||
@@ -224,6 +76,154 @@ export var Map: {
|
||||
prototype: Map<any, any>;
|
||||
} = global.Map;
|
||||
|
||||
/**
|
||||
* Map implementation for browsers that don't support it. Only supports keys which are of Number or String type, or which have a property called "id".
|
||||
*
|
||||
* Keys and values are stored as properties of an object, with property names derived from the key type.
|
||||
*
|
||||
* @param {!Array.<!Array.<*>>=} iterable Only an array of elements (where each element is a 2-tuple of key and value) is supported.
|
||||
*/
|
||||
class SimpleMap<K, V> {
|
||||
private _keys: { [key: string]: K };
|
||||
private _values: { [key: string]: V };
|
||||
private _size: number;
|
||||
|
||||
constructor(iterable?: [K, V][]) {
|
||||
this.clear();
|
||||
|
||||
if (iterable === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(iterable)) {
|
||||
throw new Error("Non-array iterables are not supported by the SimpleMap constructor.");
|
||||
}
|
||||
|
||||
for (const element of iterable) {
|
||||
this.set(element[0], element[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {K} key
|
||||
* @return {V}
|
||||
*/
|
||||
get(key: K): V {
|
||||
const property = this._keyToProperty(key);
|
||||
|
||||
if (property === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this._values[property];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {K} key
|
||||
* @return {boolean}
|
||||
*/
|
||||
has(key: K): boolean {
|
||||
const property = this._keyToProperty(key);
|
||||
|
||||
if (property === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return property in this._keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {K} key
|
||||
* @param {V} value
|
||||
* @return {libjass.Map.<K, V>} This map
|
||||
*/
|
||||
set(key: K, value: V): Map<K, V> {
|
||||
const property = this._keyToProperty(key);
|
||||
|
||||
if (property === null) {
|
||||
throw new Error("This Map implementation only supports Number and String keys, or keys with an id property.");
|
||||
}
|
||||
|
||||
if (!(property in this._keys)) {
|
||||
this._size++;
|
||||
}
|
||||
|
||||
this._keys[property] = key;
|
||||
this._values[property] = value;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {K} key
|
||||
* @return {boolean} true if the key was present before being deleted, false otherwise
|
||||
*/
|
||||
delete(key: K): boolean {
|
||||
const property = this._keyToProperty(key);
|
||||
|
||||
if (property === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = property in this._keys;
|
||||
|
||||
if (result) {
|
||||
delete this._keys[property];
|
||||
delete this._values[property];
|
||||
this._size--;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
clear(): void {
|
||||
this._keys = Object.create(null);
|
||||
this._values = Object.create(null);
|
||||
this._size = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function(V, K, libjass.Map.<K, V>)} callbackfn A function that is called with each key and value in the map.
|
||||
* @param {*} thisArg
|
||||
*/
|
||||
forEach(callbackfn: (value: V, index: K, map: Map<K, V>) => void, thisArg?: any): void {
|
||||
for (const property of Object.keys(this._keys)) {
|
||||
callbackfn.call(thisArg, this._values[property], this._keys[property], this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
get size(): number {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given key into a property name for the internal map.
|
||||
*
|
||||
* @param {K} key
|
||||
* @return {string}
|
||||
*/
|
||||
private _keyToProperty(key: K): string {
|
||||
if (typeof key === "number") {
|
||||
return `#${ key }`;
|
||||
}
|
||||
|
||||
if (typeof key === "string") {
|
||||
return `'${ key }`;
|
||||
}
|
||||
|
||||
if ((<any>key).id !== undefined) {
|
||||
return `!${ (<any>key).id }`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (Map === undefined || typeof Map.prototype.forEach !== "function" || (() => {
|
||||
try {
|
||||
return new Map([[1, "foo"], [2, "bar"]]).size !== 2;
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
* @param {!Array.<*>} mixins
|
||||
*/
|
||||
export function mixin(clazz: any, mixins: any[]): void {
|
||||
for (let mixin of mixins) {
|
||||
for (let name of Object.getOwnPropertyNames(mixin.prototype)) {
|
||||
for (const mixin of mixins) {
|
||||
for (const name of Object.getOwnPropertyNames(mixin.prototype)) {
|
||||
clazz.prototype[name] = mixin.prototype[name];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,131 +18,193 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
declare const global: {
|
||||
Promise?: typeof Promise;
|
||||
MutationObserver?: typeof MutationObserver;
|
||||
WebkitMutationObserver?: typeof MutationObserver;
|
||||
process?: {
|
||||
nextTick(callback: () => void): void;
|
||||
}
|
||||
};
|
||||
|
||||
export interface Thenable<T> {
|
||||
/** @type {function(this:!Thenable.<T>, function(T|!Thenable.<T>), function(*))} */
|
||||
then: ThenableThen<T>;
|
||||
}
|
||||
|
||||
export interface ThenableThen<T> {
|
||||
/** @type {function(this:!Thenable.<T>, function(T|!Thenable.<T>), function(*))} */
|
||||
(resolve: (resolution: T | Thenable<T>) => void, reject: (reason: any) => void): void;
|
||||
}
|
||||
|
||||
export interface Promise<T> extends Thenable<T> {
|
||||
/**
|
||||
* @param {?function(T):!Thenable.<U>} onFulfilled
|
||||
* @param {?function(*):(U|!Thenable.<U>)} onRejected
|
||||
* @return {!Promise.<U>}
|
||||
*/
|
||||
then<U>(onFulfilled?: (value: T) => Thenable<U>, onRejected?: (reason: any) => U | Thenable<U>): Promise<U>;
|
||||
|
||||
/**
|
||||
* @param {?function(T):U} onFulfilled
|
||||
* @param {?function(*):(U|!Thenable.<U>)} onRejected
|
||||
* @return {!Promise.<U>}
|
||||
*/
|
||||
then<U>(onFulfilled?: (value: T) => U, onRejected?: (reason: any) => U | Thenable<U>): Promise<U>;
|
||||
|
||||
/**
|
||||
* @param {function(*):(T|!Thenable.<T>)} onRejected
|
||||
* @return {!Promise.<T>}
|
||||
*/
|
||||
catch(onRejected?: (reason: any) => T | Thenable<T>): Promise<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to the global implementation of Promise if the environment has one, else set to {@link ./utility/promise.SimplePromise}
|
||||
*
|
||||
* Set it to null to force {@link ./utility/promise.SimplePromise} to be used even if a global Promise is present.
|
||||
*
|
||||
* @type {function(new:Promise)}
|
||||
*/
|
||||
export var Promise: {
|
||||
new <T>(init: (resolve: (value: T | Thenable<T>) => void, reject: (reason: any) => void) => void): Promise<T>;
|
||||
prototype: Promise<any>;
|
||||
resolve<T>(value: T | Thenable<T>): Promise<T>;
|
||||
reject<T>(reason: any): Promise<T>;
|
||||
all<T>(values: (T | Thenable<T>)[]): Promise<T[]>;
|
||||
race<T>(values: (T | Thenable<T>)[]): Promise<T>;
|
||||
} = global.Promise;
|
||||
|
||||
// Based on https://github.com/petkaantonov/bluebird/blob/1b1467b95442c12378d0ea280ede61d640ab5510/src/schedule.js
|
||||
const enqueueJob: (callback: () => void) => void = (function () {
|
||||
const MutationObserver = global.MutationObserver || global.WebkitMutationObserver;
|
||||
if (global.process !== undefined && typeof global.process.nextTick === "function") {
|
||||
return (callback: () => void) => {
|
||||
global.process.nextTick(callback);
|
||||
};
|
||||
}
|
||||
else if (MutationObserver !== undefined) {
|
||||
const pending: (() => void)[] = [];
|
||||
let currentlyPending = false;
|
||||
|
||||
const div = document.createElement("div");
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
const processing = pending.splice(0, pending.length);
|
||||
|
||||
for (const callback of processing) {
|
||||
callback();
|
||||
}
|
||||
|
||||
currentlyPending = false;
|
||||
|
||||
if (pending.length > 0) {
|
||||
div.classList.toggle("foo");
|
||||
currentlyPending = true;
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(div, { attributes: true });
|
||||
|
||||
return (callback: () => void) => {
|
||||
pending.push(callback);
|
||||
|
||||
if (!currentlyPending) {
|
||||
div.classList.toggle("foo");
|
||||
currentlyPending = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
else {
|
||||
return (callback: () => void) => setTimeout(callback, 0);
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* Promise implementation for browsers that don't support it.
|
||||
*
|
||||
* @param {function(function(T), function(*))} resolver
|
||||
* @param {function(function(T|!Thenable.<T>), function(*))} executor
|
||||
*/
|
||||
class SimplePromise<T> {
|
||||
private _state: SimplePromiseState = SimplePromiseState.PENDING;
|
||||
|
||||
private _thens: { propagateFulfilling: (value: T) => void; propagateRejection: (reason: any) => void }[] = [];
|
||||
private _propagateIsPending: boolean = false;
|
||||
private _fulfillReactions: FulfilledPromiseReaction<T, any>[] = [];
|
||||
private _rejectReactions: RejectedPromiseReaction<any>[] = [];
|
||||
|
||||
private _alreadyFulfilledValue: T = null;
|
||||
private _alreadyRejectedReason: any = null;
|
||||
private _fulfilledValue: T = null;
|
||||
private _rejectedReason: any = null;
|
||||
|
||||
constructor(resolver: (resolve: (value: T | Promise<T>) => void, reject: (reason: any) => void) => void) {
|
||||
constructor(executor: (resolve: (resolution: T | Thenable<T>) => void, reject: (reason: any) => void) => void) {
|
||||
if (typeof executor !== "function") {
|
||||
throw new TypeError(`typeof executor !== "function"`);
|
||||
}
|
||||
|
||||
const { resolve, reject } = this._createResolvingFunctions();
|
||||
try {
|
||||
resolver(value => this._resolve(value), reason => this._reject(reason));
|
||||
executor(resolve, reject);
|
||||
}
|
||||
catch (ex) {
|
||||
this._reject(ex);
|
||||
reject(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?function(T):(U|Promise.<U>)} fulfilledHandler
|
||||
* @param {?function(*):(U|Promise.<U>)} rejectedHandler
|
||||
* @param {?function(T):(U|!Thenable.<U>)} onFulfilled
|
||||
* @param {?function(*):(U|!Thenable.<U>)} onRejected
|
||||
* @return {!Promise.<U>}
|
||||
*/
|
||||
then<U>(fulfilledHandler?: (value: T) => U | Promise<U>, rejectedHandler?: (reason: any) => U | Promise<U>): Promise<U> {
|
||||
fulfilledHandler = (typeof fulfilledHandler === "function") ? fulfilledHandler : null;
|
||||
rejectedHandler = (typeof rejectedHandler === "function") ? rejectedHandler : null;
|
||||
then<U>(onFulfilled: (value: T) => U | Thenable<U>, onRejected: (reason: any) => U | Thenable<U>): Promise<U> {
|
||||
const resultCapability = new DeferredPromise<U>();
|
||||
|
||||
if (fulfilledHandler === null && rejectedHandler === null) {
|
||||
return <any>this;
|
||||
if (typeof onFulfilled !== "function") {
|
||||
onFulfilled = (value: T) => <U><any>value;
|
||||
}
|
||||
|
||||
if (fulfilledHandler === null) {
|
||||
fulfilledHandler = value => <U><any>value;
|
||||
if (typeof onRejected !== "function") {
|
||||
onRejected = (reason: any): U => { throw reason; };
|
||||
}
|
||||
|
||||
if (rejectedHandler === null) {
|
||||
rejectedHandler = (reason): U => { throw reason; };
|
||||
const fulfillReaction: FulfilledPromiseReaction<T, U> = {
|
||||
capabilities: resultCapability,
|
||||
handler: onFulfilled,
|
||||
};
|
||||
|
||||
const rejectReaction: RejectedPromiseReaction<U> = {
|
||||
capabilities: resultCapability,
|
||||
handler: onRejected,
|
||||
};
|
||||
|
||||
switch (this._state) {
|
||||
case SimplePromiseState.PENDING:
|
||||
this._fulfillReactions.push(fulfillReaction);
|
||||
this._rejectReactions.push(rejectReaction);
|
||||
break;
|
||||
|
||||
case SimplePromiseState.FULFILLED:
|
||||
this._enqueueFulfilledReactionJob(fulfillReaction, this._fulfilledValue);
|
||||
break;
|
||||
|
||||
case SimplePromiseState.REJECTED:
|
||||
this._enqueueRejectedReactionJob(rejectReaction, this._rejectedReason);
|
||||
break;
|
||||
}
|
||||
|
||||
var result = new Promise<U>((resolve, reject) => {
|
||||
this._thens.push({
|
||||
propagateFulfilling: value => {
|
||||
try {
|
||||
resolve(fulfilledHandler(value));
|
||||
}
|
||||
catch (ex) {
|
||||
reject(ex);
|
||||
}
|
||||
}, propagateRejection: reason => {
|
||||
try {
|
||||
resolve(rejectedHandler(reason));
|
||||
}
|
||||
catch (ex) {
|
||||
reject(ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this._propagateResolution();
|
||||
|
||||
return result;
|
||||
return resultCapability.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function(*):(T|Promise.<T>)} rejectedHandler
|
||||
* @param {function(*):(T|!Thenable.<T>)} onRejected
|
||||
* @return {!Promise.<T>}
|
||||
*/
|
||||
catch(rejectedHandler?: (reason: any) => T | Promise<T>): Promise<T> {
|
||||
return this.then(null, rejectedHandler);
|
||||
catch(onRejected?: (reason: any) => T | Thenable<T>): Promise<T> {
|
||||
return this.then(null, onRejected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isFulfilled(): boolean {
|
||||
return this._state === SimplePromiseState.FULFILLED;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isRejected(): boolean {
|
||||
return this._state === SimplePromiseState.REJECTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
isPending(): boolean {
|
||||
return this._state === SimplePromiseState.PENDING;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {T}
|
||||
*/
|
||||
value(): T {
|
||||
if (this._state !== SimplePromiseState.FULFILLED) {
|
||||
throw new Error("This promise is not in FULFILLED state.");
|
||||
}
|
||||
|
||||
return this._alreadyFulfilledValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {*}
|
||||
*/
|
||||
reason(): any {
|
||||
if (this._state !== SimplePromiseState.REJECTED) {
|
||||
throw new Error("This promise is not in FULFILLED state.");
|
||||
}
|
||||
|
||||
return this._alreadyRejectedReason;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {T|!Promise.<T>} value
|
||||
* @param {T|!Thenable.<T>} value
|
||||
* @return {!Promise.<T>}
|
||||
*/
|
||||
static resolve<T>(value: T | Promise<T>): Promise<T> {
|
||||
static resolve<T>(value: T | Thenable<T>): Promise<T> {
|
||||
if (value instanceof SimplePromise) {
|
||||
return value;
|
||||
}
|
||||
@@ -151,14 +213,22 @@ class SimplePromise<T> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Array.<T|!Promise.<T>>} values
|
||||
* @param {*} reason
|
||||
* @return {!Promise.<T>}
|
||||
*/
|
||||
static reject<T>(reason: any): Promise<T> {
|
||||
return new Promise<T>((/* ujs:unreferenced */ resolve, reject) => reject(reason));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Array.<T|!Thenable.<T>>} values
|
||||
* @return {!Promise.<!Array.<T>>}
|
||||
*/
|
||||
static all<T>(values: (T | Promise<T>)[]): Promise<T[]> {
|
||||
static all<T>(values: (T | Thenable<T>)[]): Promise<T[]> {
|
||||
return new Promise<T[]>((resolve, reject) => {
|
||||
var result: T[] = [];
|
||||
const result: T[] = [];
|
||||
|
||||
var numUnresolved = values.length;
|
||||
let numUnresolved = values.length;
|
||||
if (numUnresolved === 0) {
|
||||
resolve(result);
|
||||
return;
|
||||
@@ -171,52 +241,86 @@ class SimplePromise<T> {
|
||||
if (numUnresolved === 0) {
|
||||
resolve(result);
|
||||
}
|
||||
}), reject);
|
||||
}, reject));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {T|!Promise.<T>} value
|
||||
* @param {!Array.<T|!Thenable.<T>>} values
|
||||
* @return {!Promise.<T>}
|
||||
*/
|
||||
private _resolve(value: T | Promise<T>): void {
|
||||
var alreadyCalled = false;
|
||||
static race<T>(values: (T | Thenable<T>)[]): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
for (const value of values) {
|
||||
Promise.resolve(value).then(resolve, reject);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {{ resolve(T|!Thenable.<T>), reject(*) }}
|
||||
*/
|
||||
private _createResolvingFunctions(): { resolve(resolution: T | Thenable<T>): void; reject(reason: any): void; } {
|
||||
let alreadyResolved = false;
|
||||
|
||||
const resolve = (resolution: T | Thenable<T>): void => {
|
||||
if (alreadyResolved) {
|
||||
return;
|
||||
}
|
||||
|
||||
alreadyResolved = true;
|
||||
|
||||
if (resolution === this) {
|
||||
this._reject(new TypeError(`resolution === this`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (resolution === null || (typeof resolution !== "object" && typeof resolution !== "function")) {
|
||||
this._fulfill(<T>resolution);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var then = (<Thenable<T>>resolution).then;
|
||||
}
|
||||
catch (ex) {
|
||||
this._reject(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof then !== "function") {
|
||||
this._fulfill(<T>resolution);
|
||||
return;
|
||||
}
|
||||
|
||||
enqueueJob(() => this._resolveWithThenable(<Thenable<T>>resolution, then));
|
||||
};
|
||||
|
||||
const reject = (reason: any): void => {
|
||||
if (alreadyResolved) {
|
||||
return;
|
||||
}
|
||||
|
||||
alreadyResolved = true;
|
||||
|
||||
this._reject(reason);
|
||||
};
|
||||
|
||||
return { resolve, reject };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Thenable.<T>} thenable
|
||||
* @param {{function(this:!Thenable.<T>, function(T|!Thenable.<T>), function(*))}} then
|
||||
*/
|
||||
private _resolveWithThenable(thenable: Thenable<T>, then: ThenableThen<T>): void {
|
||||
const { resolve, reject } = this._createResolvingFunctions();
|
||||
|
||||
try {
|
||||
if (value === this) {
|
||||
throw new TypeError("2.3.1");
|
||||
}
|
||||
|
||||
var thenMethod = SimplePromise._getThenMethod<T>(value);
|
||||
if (thenMethod === null) {
|
||||
this._fulfill(<T>value);
|
||||
return;
|
||||
}
|
||||
|
||||
thenMethod.call(
|
||||
<Promise<T>>value,
|
||||
(value: T) => {
|
||||
if (alreadyCalled) {
|
||||
return;
|
||||
}
|
||||
alreadyCalled = true;
|
||||
|
||||
this._resolve(value);
|
||||
},
|
||||
(reason: any) => {
|
||||
if (alreadyCalled) {
|
||||
return;
|
||||
}
|
||||
alreadyCalled = true;
|
||||
|
||||
this._reject(reason);
|
||||
});
|
||||
then.call(thenable, resolve, reject);
|
||||
}
|
||||
catch (ex) {
|
||||
if (alreadyCalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._reject(ex);
|
||||
reject(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,127 +328,103 @@ class SimplePromise<T> {
|
||||
* @param {T} value
|
||||
*/
|
||||
private _fulfill(value: T): void {
|
||||
if (this._state !== SimplePromiseState.PENDING) {
|
||||
return;
|
||||
}
|
||||
const reactions = this._fulfillReactions;
|
||||
|
||||
this._fulfilledValue = value;
|
||||
this._fulfillReactions = undefined;
|
||||
this._rejectReactions = undefined;
|
||||
this._state = SimplePromiseState.FULFILLED;
|
||||
this._alreadyFulfilledValue = value;
|
||||
|
||||
this._propagateResolution();
|
||||
for (const reaction of reactions) {
|
||||
this._enqueueFulfilledReactionJob(reaction, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {*} reason
|
||||
*/
|
||||
private _reject(reason: any): void {
|
||||
if (this._state !== SimplePromiseState.PENDING) {
|
||||
return;
|
||||
}
|
||||
const reactions = this._rejectReactions;
|
||||
|
||||
this._rejectedReason = reason;
|
||||
this._fulfillReactions = undefined;
|
||||
this._rejectReactions = undefined;
|
||||
this._state = SimplePromiseState.REJECTED;
|
||||
this._alreadyRejectedReason = reason;
|
||||
|
||||
this._propagateResolution();
|
||||
for (const reaction of reactions) {
|
||||
this._enqueueRejectedReactionJob(reaction, reason);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!*} obj
|
||||
* @return {?function(function(T):(T|!Promise.<T>), function(*):(T|!Promise.<T>)):!Promise.<T>}
|
||||
* @param {!FulfilledPromiseReaction.<T, *>} reaction
|
||||
* @param {T} value
|
||||
*/
|
||||
private static _getThenMethod<T>(obj: any): (fulfilledHandler: (value: T) => T | Promise<T>, rejectedHandler: (reason: any) => T | Promise<T>) => Promise<T> {
|
||||
if (typeof obj !== "object" && typeof obj !== "function") {
|
||||
return null;
|
||||
}
|
||||
private _enqueueFulfilledReactionJob(reaction: FulfilledPromiseReaction<T, any>, value: T): void {
|
||||
enqueueJob(() => {
|
||||
const { capabilities: { resolve, reject }, handler } = reaction;
|
||||
|
||||
if (obj === null || obj === undefined) {
|
||||
return null;
|
||||
}
|
||||
let handlerResult: any | Thenable<any>;
|
||||
|
||||
var then: any = obj.then;
|
||||
if (typeof then !== "function") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return then;
|
||||
}
|
||||
|
||||
/**
|
||||
* Propagates the result of the current promise to all its children.
|
||||
*/
|
||||
private _propagateResolution(): void {
|
||||
if (this._state === SimplePromiseState.PENDING) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._propagateIsPending) {
|
||||
return;
|
||||
}
|
||||
this._propagateIsPending = true;
|
||||
|
||||
SimplePromise._nextTick(() => {
|
||||
this._propagateIsPending = false;
|
||||
|
||||
if (this._state === SimplePromiseState.FULFILLED) {
|
||||
while (this._thens.length > 0) {
|
||||
var nextThen = this._thens.shift();
|
||||
nextThen.propagateFulfilling(this._alreadyFulfilledValue);
|
||||
}
|
||||
try {
|
||||
handlerResult = handler(value);
|
||||
}
|
||||
else if (this._state === SimplePromiseState.REJECTED) {
|
||||
while (this._thens.length > 0) {
|
||||
var nextThen = this._thens.shift();
|
||||
nextThen.propagateRejection(this._alreadyRejectedReason);
|
||||
}
|
||||
catch (ex) {
|
||||
reject(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(handlerResult);
|
||||
});
|
||||
}
|
||||
|
||||
// Based on https://github.com/petkaantonov/bluebird/blob/1b1467b95442c12378d0ea280ede61d640ab5510/src/schedule.js
|
||||
private static _nextTick: (callback: () => void) => void = (function () {
|
||||
var MutationObserver = global.MutationObserver || global.WebkitMutationObserver;
|
||||
if (global.process !== undefined && typeof global.process.nextTick === "function") {
|
||||
return (callback: () => void) => {
|
||||
global.process.nextTick(callback);
|
||||
};
|
||||
}
|
||||
else if (MutationObserver !== undefined) {
|
||||
var pending: (() => void)[] = [];
|
||||
var currentlyPending = false;
|
||||
/**
|
||||
* @param {!RejectedPromiseReaction.<*>} reaction
|
||||
* @param {*} reason
|
||||
*/
|
||||
private _enqueueRejectedReactionJob(reaction: RejectedPromiseReaction<any>, reason: any): void {
|
||||
enqueueJob(() => {
|
||||
const { capabilities: { resolve, reject }, handler } = reaction;
|
||||
|
||||
var div = document.createElement("div");
|
||||
let handlerResult: any | Thenable<any>;
|
||||
|
||||
var observer = new MutationObserver(() => {
|
||||
var processing = pending;
|
||||
pending = [];
|
||||
try {
|
||||
handlerResult = handler(reason);
|
||||
}
|
||||
catch (ex) {
|
||||
reject(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
for (let callback of processing) {
|
||||
callback();
|
||||
}
|
||||
resolve(handlerResult);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
currentlyPending = false;
|
||||
if (Promise === undefined) {
|
||||
Promise = SimplePromise;
|
||||
}
|
||||
|
||||
if (pending.length > 0) {
|
||||
div.classList.toggle("foo");
|
||||
currentlyPending = true;
|
||||
}
|
||||
});
|
||||
interface FulfilledPromiseReaction<T, U> {
|
||||
/** @type {!libjass.DeferredPromise.<U>} */
|
||||
capabilities: DeferredPromise<U>;
|
||||
|
||||
observer.observe(div, { attributes: true });
|
||||
/**
|
||||
* @param {T} value
|
||||
* @return {U|!Thenable.<U>}
|
||||
*/
|
||||
handler(value: T): U | Thenable<U>;
|
||||
}
|
||||
|
||||
return (callback: () => void) => {
|
||||
pending.push(callback);
|
||||
interface RejectedPromiseReaction<U> {
|
||||
/** @type {!libjass.DeferredPromise.<U>} */
|
||||
capabilities: DeferredPromise<U>;
|
||||
|
||||
if (!currentlyPending) {
|
||||
div.classList.toggle("foo");
|
||||
currentlyPending = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
else {
|
||||
return (callback: () => void) => setTimeout(callback, 0);
|
||||
}
|
||||
})();
|
||||
/**
|
||||
* @param {*} reason
|
||||
* @return {U|!Thenable.<U>}
|
||||
*/
|
||||
handler(reason: any): U | Thenable<U>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -356,48 +436,6 @@ enum SimplePromiseState {
|
||||
REJECTED = 2,
|
||||
}
|
||||
|
||||
declare var global: {
|
||||
Promise?: typeof SimplePromise;
|
||||
MutationObserver?: typeof MutationObserver;
|
||||
WebkitMutationObserver?: typeof MutationObserver;
|
||||
process?: {
|
||||
nextTick(callback: () => void): void;
|
||||
}
|
||||
};
|
||||
|
||||
export interface Promise<T> {
|
||||
/**
|
||||
* @param {?function(T):(U|Promise.<U>)} fulfilledHandler
|
||||
* @param {?function(*):(U|Promise.<U>)} rejectedHandler
|
||||
* @return {!Promise.<U>}
|
||||
*/
|
||||
then<U>(fulfilledHandler?: (value: T) => U | Promise<U>, rejectedHandler?: (reason: any) => U | Promise<U>): Promise<U>;
|
||||
|
||||
/**
|
||||
* @param {function(*):(T|Promise.<T>)} rejectedHandler
|
||||
* @return {!Promise.<T>}
|
||||
*/
|
||||
catch(rejectedHandler?: (reason: any) => T | Promise<T>): Promise<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to the global implementation of Promise if the environment has one, else set to {@link ./utility/promise.SimplePromise}
|
||||
*
|
||||
* Set it to null to force {@link ./utility/promise.SimplePromise} to be used even if a global Promise is present.
|
||||
*
|
||||
* @type {function(new:Promise)}
|
||||
*/
|
||||
export var Promise: {
|
||||
new <T>(init: (resolve: (value?: T | Promise<T>) => void, reject: (reason?: any) => void) => void): Promise<T>;
|
||||
prototype: Promise<any>;
|
||||
resolve<T>(value: T | Promise<T>): Promise<T>;
|
||||
all<T>(values: (T | Promise<T>)[]): Promise<T[]>;
|
||||
} = global.Promise;
|
||||
|
||||
if (Promise === undefined) {
|
||||
Promise = SimplePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Promise implementation used by libjass to the provided one. If null, {@link ./utility/promise.SimplePromise} is used.
|
||||
*
|
||||
@@ -417,13 +455,23 @@ export function setImplementation(value: typeof Promise): void {
|
||||
*/
|
||||
export class DeferredPromise<T> {
|
||||
private _promise: Promise<T>;
|
||||
private _resolve: (value: T) => void;
|
||||
private _reject: (reason: any) => void;
|
||||
|
||||
/**
|
||||
* @type {function(T|!Thenable.<T>)}
|
||||
*/
|
||||
resolve: (value: T | Thenable<T>) => void;
|
||||
|
||||
/**
|
||||
* @type {function(*)} reason
|
||||
*/
|
||||
reject: (reason: any) => void;
|
||||
|
||||
constructor() {
|
||||
this._promise = new Promise<T>((resolve, reject) => {
|
||||
this._resolve = resolve;
|
||||
this._reject = reject;
|
||||
Object.defineProperties(this, {
|
||||
resolve: { value: resolve, enumerable: true },
|
||||
reject: { value: reject, enumerable: true },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -433,18 +481,56 @@ export class DeferredPromise<T> {
|
||||
get promise(): Promise<T> {
|
||||
return this._promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {T} value
|
||||
*/
|
||||
resolve(value: T): void {
|
||||
this._resolve(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {*} reason
|
||||
*/
|
||||
reject(reason: any): void {
|
||||
this._reject(reason);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves to the first (in iteration order) promise that fulfills, and rejects if all the promises reject.
|
||||
*
|
||||
* @param {!Array.<!Promise.<T>>} promises
|
||||
* @return {!Promise.<T>}
|
||||
*/
|
||||
export function first<T>(promises: Promise<T>[]): Promise<T> {
|
||||
return first_rec(promises, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Array.<!Promise.<T>>} promises
|
||||
* @param {!Array.<*>} previousRejections
|
||||
* @return {!Promise.<T>}
|
||||
*/
|
||||
function first_rec<T>(promises: Promise<T>[], previousRejections: any[]): Promise<T> {
|
||||
if (promises.length === 0) {
|
||||
return Promise.reject(previousRejections);
|
||||
}
|
||||
|
||||
const [head, ...tail] = promises;
|
||||
return head.catch(reason => first_rec(tail, previousRejections.concat(reason)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves to the first (in time order) promise that fulfills, and rejects if all the promises reject.
|
||||
*
|
||||
* @param {!Array.<!Promise.<T>>} promises
|
||||
* @return {!Promise.<T>}
|
||||
*/
|
||||
export function any<T>(promises: Promise<T>[]): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) =>
|
||||
Promise.all<any>(promises.map(promise => promise.then(resolve, reason => reason))).then(reject));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that runs the given callback when the promise has resolved regardless of whether it fulfilled or rejected.
|
||||
*
|
||||
* @param {!Promise.<T>} promise
|
||||
* @param {function()} body
|
||||
* @return {!Promise.<T>}
|
||||
*/
|
||||
export function lastly<T>(promise: Promise<T>, body: () => void): Promise<T> {
|
||||
return promise.then<any>(value => {
|
||||
body();
|
||||
return value;
|
||||
}, reason => {
|
||||
body();
|
||||
throw reason;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,113 +18,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Set implementation for browsers that don't support it. Only supports Number and String elements.
|
||||
*
|
||||
* Elements are stored as properties of an object, with names derived from their type.
|
||||
*
|
||||
* @param {!Array.<T>=} iterable Only an array of values is supported.
|
||||
*/
|
||||
class SimpleSet<T> {
|
||||
private _elements: { [key: string]: T };
|
||||
private _size: number;
|
||||
|
||||
constructor(iterable?: T[]) {
|
||||
this.clear();
|
||||
|
||||
if (iterable === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(iterable)) {
|
||||
throw new Error("Non-array iterables are not supported by the SimpleSet constructor.");
|
||||
}
|
||||
|
||||
for (let value of iterable) {
|
||||
this.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {T} value
|
||||
* @return {libjass.Set.<T>} This set
|
||||
*/
|
||||
add(value: T): Set<T> {
|
||||
var property = this._toProperty(value);
|
||||
|
||||
if (property === null) {
|
||||
throw new Error("This Set implementation only supports Number and String values.");
|
||||
}
|
||||
|
||||
if (!(property in this._elements)) {
|
||||
this._size++;
|
||||
}
|
||||
|
||||
this._elements[property] = value;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
clear(): void {
|
||||
this._elements = Object.create(null);
|
||||
this._size = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {T} value
|
||||
* @return {boolean}
|
||||
*/
|
||||
has(value: T): boolean {
|
||||
var property = this._toProperty(value);
|
||||
|
||||
if (property === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return property in this._elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function(T, T, libjass.Set.<T>)} callbackfn A function that is called with each value in the set.
|
||||
* @param {*} thisArg
|
||||
*/
|
||||
forEach(callbackfn: (value: T, index: T, set: Set<T>) => void, thisArg?: any): void {
|
||||
for (let property of Object.keys(this._elements)) {
|
||||
var element = this._elements[property];
|
||||
callbackfn.call(thisArg, element, element, this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
get size(): number {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given value into a property name for the internal map.
|
||||
*
|
||||
* @param {T} value
|
||||
* @return {string}
|
||||
*/
|
||||
private _toProperty(value: T): string {
|
||||
if (typeof value === "number") {
|
||||
return `#${ value }`;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
return `'${ value }`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
declare var global: {
|
||||
Set?: typeof SimpleSet;
|
||||
declare const global: {
|
||||
Set?: typeof Set;
|
||||
};
|
||||
|
||||
export interface Set<T> {
|
||||
@@ -168,6 +63,111 @@ export var Set: {
|
||||
prototype: Set<any>;
|
||||
} = global.Set;
|
||||
|
||||
/**
|
||||
* Set implementation for browsers that don't support it. Only supports Number and String elements.
|
||||
*
|
||||
* Elements are stored as properties of an object, with names derived from their type.
|
||||
*
|
||||
* @param {!Array.<T>=} iterable Only an array of values is supported.
|
||||
*/
|
||||
class SimpleSet<T> {
|
||||
private _elements: { [key: string]: T };
|
||||
private _size: number;
|
||||
|
||||
constructor(iterable?: T[]) {
|
||||
this.clear();
|
||||
|
||||
if (iterable === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(iterable)) {
|
||||
throw new Error("Non-array iterables are not supported by the SimpleSet constructor.");
|
||||
}
|
||||
|
||||
for (const value of iterable) {
|
||||
this.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {T} value
|
||||
* @return {libjass.Set.<T>} This set
|
||||
*/
|
||||
add(value: T): Set<T> {
|
||||
const property = this._toProperty(value);
|
||||
|
||||
if (property === null) {
|
||||
throw new Error("This Set implementation only supports Number and String values.");
|
||||
}
|
||||
|
||||
if (!(property in this._elements)) {
|
||||
this._size++;
|
||||
}
|
||||
|
||||
this._elements[property] = value;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
clear(): void {
|
||||
this._elements = Object.create(null);
|
||||
this._size = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {T} value
|
||||
* @return {boolean}
|
||||
*/
|
||||
has(value: T): boolean {
|
||||
const property = this._toProperty(value);
|
||||
|
||||
if (property === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return property in this._elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function(T, T, libjass.Set.<T>)} callbackfn A function that is called with each value in the set.
|
||||
* @param {*} thisArg
|
||||
*/
|
||||
forEach(callbackfn: (value: T, index: T, set: Set<T>) => void, thisArg?: any): void {
|
||||
for (const property of Object.keys(this._elements)) {
|
||||
const element = this._elements[property];
|
||||
callbackfn.call(thisArg, element, element, this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
get size(): number {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given value into a property name for the internal map.
|
||||
*
|
||||
* @param {T} value
|
||||
* @return {string}
|
||||
*/
|
||||
private _toProperty(value: T): string {
|
||||
if (typeof value === "number") {
|
||||
return `#${ value }`;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
return `'${ value }`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (Set === undefined || typeof Set.prototype.forEach !== "function" || (() => {
|
||||
try {
|
||||
return new Set([1, 2]).size !== 2;
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class inheritance shim.
|
||||
*
|
||||
* @param {!Function} derived
|
||||
* @param {!Function} base
|
||||
*/
|
||||
export function __extends(derived: any, base: any): void {
|
||||
for (const property in base) {
|
||||
if (base.hasOwnProperty(property)) {
|
||||
derived[property] = base[property];
|
||||
}
|
||||
}
|
||||
|
||||
function __() {
|
||||
this.constructor = derived;
|
||||
}
|
||||
|
||||
__.prototype = base.prototype;
|
||||
|
||||
derived.prototype = new (<any>__)();
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorator shim.
|
||||
*
|
||||
* @param {!Array.<!Function>} decorators
|
||||
* @param {!*} target
|
||||
* @param {string=} key
|
||||
* @return {*}
|
||||
*/
|
||||
export function __decorate(decorators: Function[], target: any, key?: string): any {
|
||||
if (arguments.length < 3) {
|
||||
return decorateClass(<any>decorators.reverse(), target);
|
||||
}
|
||||
else {
|
||||
decorateField(<any>decorators.reverse(), target, key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class decorator shim.
|
||||
*
|
||||
* @param {!Array.<function(function(new(): T)): function(new(): T)>} decorators
|
||||
* @param {function(new(): T)} clazz
|
||||
* @return {function(new(): T)}
|
||||
*/
|
||||
function decorateClass<T>(decorators: ((clazz: { new (...args: any[]): T }) => { new (...args: any[]): T })[], clazz: { new (...args: any[]): T }): { new (...args: any[]): T } {
|
||||
for (const decorator of decorators) {
|
||||
clazz = decorator(clazz) || clazz;
|
||||
}
|
||||
|
||||
return clazz;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class member decorator shim.
|
||||
*
|
||||
* @param {!Array.<function(T, string)>} decorators
|
||||
* @param {!T} proto
|
||||
* @param {string} name
|
||||
*/
|
||||
function decorateField<T>(decorators: ((proto: T, name: string) => void)[], proto: T, name: string): void {
|
||||
for (const decorator of decorators) {
|
||||
decorator(proto, name);
|
||||
}
|
||||
}
|
||||
@@ -144,11 +144,11 @@ export class WorkerChannelImpl implements WorkerChannel {
|
||||
* @return {!Promise.<*>}
|
||||
*/
|
||||
request(command: WorkerCommands, parameters: any): Promise<any> {
|
||||
var deferred = new DeferredPromise<any>();
|
||||
var requestId = ++WorkerChannelImpl._lastRequestId;
|
||||
const deferred = new DeferredPromise<any>();
|
||||
const requestId = ++WorkerChannelImpl._lastRequestId;
|
||||
this._pendingRequests.set(requestId, deferred);
|
||||
|
||||
var requestMessage: WorkerRequestMessage = { requestId, command, parameters };
|
||||
const requestMessage: WorkerRequestMessage = { requestId, command, parameters };
|
||||
this._comm.postMessage(serialize(requestMessage));
|
||||
|
||||
return deferred.promise;
|
||||
@@ -158,7 +158,7 @@ export class WorkerChannelImpl implements WorkerChannel {
|
||||
* @param {number} requestId
|
||||
*/
|
||||
cancelRequest(requestId: number): void {
|
||||
var deferred = this._pendingRequests.get(requestId);
|
||||
const deferred = this._pendingRequests.get(requestId);
|
||||
if (deferred === undefined) {
|
||||
return;
|
||||
}
|
||||
@@ -171,7 +171,7 @@ export class WorkerChannelImpl implements WorkerChannel {
|
||||
* @param {!WorkerResponseMessage} message
|
||||
*/
|
||||
private _respond(message: WorkerResponseMessage): void {
|
||||
var { requestId, error, result } = message;
|
||||
let { requestId, error, result } = message;
|
||||
if (error instanceof Error) {
|
||||
error = { message: error.message, stack: error.stack };
|
||||
}
|
||||
@@ -182,12 +182,12 @@ export class WorkerChannelImpl implements WorkerChannel {
|
||||
* @param {string} rawMessage
|
||||
*/
|
||||
private _onMessage(rawMessage: string): void {
|
||||
var message = <{ command: WorkerCommands }>deserialize(rawMessage);
|
||||
const message = <{ command: WorkerCommands }>deserialize(rawMessage);
|
||||
|
||||
if (message.command === WorkerCommands.Response) {
|
||||
var responseMessage = <WorkerResponseMessage><any>message;
|
||||
const responseMessage = <WorkerResponseMessage><any>message;
|
||||
|
||||
var deferred = this._pendingRequests.get(responseMessage.requestId);
|
||||
const deferred = this._pendingRequests.get(responseMessage.requestId);
|
||||
if (deferred !== undefined) {
|
||||
this._pendingRequests.delete(responseMessage.requestId);
|
||||
if (responseMessage.error === null) {
|
||||
@@ -199,10 +199,10 @@ export class WorkerChannelImpl implements WorkerChannel {
|
||||
}
|
||||
}
|
||||
else {
|
||||
var requestMessage = <WorkerRequestMessage>message;
|
||||
var requestId = requestMessage.requestId;
|
||||
const requestMessage = <WorkerRequestMessage>message;
|
||||
const requestId = requestMessage.requestId;
|
||||
|
||||
var commandCallback = getWorkerCommandHandler(requestMessage.command);
|
||||
const commandCallback = getWorkerCommandHandler(requestMessage.command);
|
||||
if (commandCallback === undefined) {
|
||||
this._respond({ requestId, error: new Error(`No handler registered for command ${ requestMessage.command }`), result: null });
|
||||
return;
|
||||
@@ -216,4 +216,4 @@ export class WorkerChannelImpl implements WorkerChannel {
|
||||
}
|
||||
}
|
||||
|
||||
registerWorkerCommand(WorkerCommands.Ping, parameters => new Promise(resolve => resolve()));
|
||||
registerWorkerCommand(WorkerCommands.Ping, parameters => new Promise<void>(resolve => resolve(null)));
|
||||
|
||||
@@ -27,14 +27,16 @@ export { WorkerChannel } from "./channel";
|
||||
|
||||
export { WorkerCommands } from "./commands";
|
||||
|
||||
declare var exports: any;
|
||||
declare const exports: any;
|
||||
|
||||
/**
|
||||
* Indicates whether web workers are supposed in this environment or not.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
export var supported = typeof Worker !== "undefined";
|
||||
export const supported = typeof Worker !== "undefined";
|
||||
|
||||
const _scriptNode = (typeof document !== "undefined" && document.currentScript !== undefined) ? document.currentScript : null;
|
||||
|
||||
/**
|
||||
* Create a new web worker and returns a {@link libjass.webworker.WorkerChannel} to it.
|
||||
@@ -47,12 +49,7 @@ export function createWorker(scriptPath: string = _scriptNode.src): WorkerChanne
|
||||
return new WorkerChannelImpl(new Worker(scriptPath));
|
||||
}
|
||||
|
||||
var _scriptNode: HTMLScriptElement = null;
|
||||
if (typeof document !== "undefined" && document.currentScript !== undefined) {
|
||||
_scriptNode = document.currentScript;
|
||||
}
|
||||
|
||||
declare var global: any;
|
||||
declare const global: any;
|
||||
|
||||
if (typeof WorkerGlobalScope !== "undefined" && global instanceof WorkerGlobalScope) {
|
||||
// This is a web worker. Set up a channel to talk back to the main thread.
|
||||
|
||||
@@ -23,9 +23,9 @@ import { Map } from "../utility/map";
|
||||
import { WorkerCommands } from "./commands";
|
||||
import { WorkerCommandHandler } from "./channel";
|
||||
|
||||
var workerCommands = new Map<WorkerCommands, WorkerCommandHandler>();
|
||||
const workerCommands = new Map<WorkerCommands, WorkerCommandHandler>();
|
||||
|
||||
var classPrototypes = new Map<number, any>();
|
||||
const classPrototypes = new Map<number, any>();
|
||||
|
||||
/**
|
||||
* Registers a handler for the given worker command.
|
||||
@@ -78,8 +78,8 @@ export function serialize(obj: any): string {
|
||||
export function deserialize(str: string): any {
|
||||
return JSON.parse(str, (/* ujs:unreferenced */ key: string, value: any) => {
|
||||
if (value && value._classTag !== undefined) {
|
||||
var hydratedValue = Object.create(classPrototypes.get(value._classTag));
|
||||
for (let key of Object.keys(value)) {
|
||||
const hydratedValue = Object.create(classPrototypes.get(value._classTag));
|
||||
for (const key of Object.keys(value)) {
|
||||
if (key !== "_classTag") {
|
||||
hydratedValue[key] = value[key];
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ define(["intern!tdd", "intern/chai!assert", "intern/dojo/node!fs", "intern/dojo/
|
||||
assert.equal(brokenLinks.length, 0, "Broken link(s): " + brokenLinks.map(function (href) { return '"' + href + '"'; }).join(", "));
|
||||
});
|
||||
|
||||
tdd.test("@{link} tags in the source", function () {
|
||||
tdd.test("{@link} tags in the source", function () {
|
||||
var regex = /\{@link ([^}]+)\}/g;
|
||||
var brokenLinks = [];
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
[Script Info]
|
||||
Title:
|
||||
ScriptType: v4.00+
|
||||
WrapStyle: 0
|
||||
PlayResX: 256
|
||||
PlayResY: 144
|
||||
Scroll Position: 0
|
||||
Active Line: 0
|
||||
Video Zoom Percent: 1
|
||||
ScaledBorderAndShadow: yes
|
||||
|
||||
[V4+ Styles]
|
||||
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
||||
Style: Default,Arial,100,&H7F0000FF,&H00FFFFFF,&H7F000000,&H7F000000,0,0,0,0,100,100,0,0,1,2,0,2,15,15,15,1
|
||||
|
||||
[Events]
|
||||
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
||||
Dialogue: 0,0:00:00.00,0:10:00.00,Default,,0,0,0,,X
|
||||
@@ -18,35 +18,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
interface CSSStyleDeclaration {
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
webkitAnimation: string;
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
webkitAnimationDelay: string;
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
webkitAnimationName: string;
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
webkitFilter: string;
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
webkitTransform: string;
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
webkitTransformOrigin: string;
|
||||
}
|
||||
|
||||
define(["intern!tdd", "require", "tests/support/test-page"], function (tdd, require, TestPage) {
|
||||
tdd.suite("alpha", function () {
|
||||
tdd.test("Basic", function () {
|
||||
var testPage = new TestPage(this.remote, require.toUrl("tests/support/browser-test-page.html"), "/tests/functional/alpha/alpha.ass", 256, 144, "rgb(47, 163, 254)");
|
||||
return testPage
|
||||
.prepare()
|
||||
.then(function (testPage) { return testPage.seekAndCompareScreenshot(1, require.toUrl("./alpha-1.png")); });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
define(["intern!tdd", "intern/chai!assert", "require", "intern/dojo/node!leadfoot/helpers/pollUntil"], function (tdd, assert, require, pollUntil) {
|
||||
tdd.suite("Auto clock", function () {
|
||||
tdd.test("Operations", function () {
|
||||
this.remote.session.setExecuteAsyncTimeout(10000);
|
||||
|
||||
return this.remote
|
||||
.get(require.toUrl("tests/support/browser-test-page.html"))
|
||||
.then(pollUntil('return (document.readyState === "complete") ? true : null;', 100))
|
||||
.execute(function () {
|
||||
window.driverStartTime = 0;
|
||||
window.driverStartTimeAt = new Date();
|
||||
window.driver = function () {
|
||||
return (new Date() - driverStartTimeAt) / 1000 + driverStartTime;
|
||||
};
|
||||
|
||||
window.clock = new libjass.renderers.AutoClock(driver);
|
||||
|
||||
window.events = [];
|
||||
|
||||
clock.addEventListener(libjass.renderers.ClockEvent.Play, function () {
|
||||
events.push("play");
|
||||
});
|
||||
|
||||
clock.addEventListener(libjass.renderers.ClockEvent.Tick, function () {
|
||||
events.push("tick");
|
||||
});
|
||||
|
||||
clock.addEventListener(libjass.renderers.ClockEvent.Pause, function () {
|
||||
events.push("pause");
|
||||
});
|
||||
|
||||
clock.addEventListener(libjass.renderers.ClockEvent.Stop, function () {
|
||||
events.push("stop");
|
||||
});
|
||||
|
||||
return { enabled: clock.enabled, paused: clock.paused, rate: clock.rate, events: events.slice() };
|
||||
})
|
||||
.then(function (clock) {
|
||||
assert.strictEqual(clock.enabled, true);
|
||||
assert.strictEqual(clock.paused, true);
|
||||
assert.strictEqual(clock.rate, 1);
|
||||
assert.deepEqual(clock.events, []);
|
||||
})
|
||||
.executeAsync(function (callback) {
|
||||
clock.play();
|
||||
|
||||
setTimeout(function () {
|
||||
callback({ enabled: clock.enabled, paused: clock.paused, events: events.slice() });
|
||||
}, 1000);
|
||||
})
|
||||
.then(function (clock) {
|
||||
assert.strictEqual(clock.enabled, true);
|
||||
assert.strictEqual(clock.paused, false);
|
||||
|
||||
assert(clock.events.length >= 2);
|
||||
assert.strictEqual(clock.events[0], "play");
|
||||
for (var i = 1; i < clock.events.length; i++) {
|
||||
assert.strictEqual(clock.events[i], "tick");
|
||||
}
|
||||
})
|
||||
.execute(function () {
|
||||
events = [];
|
||||
|
||||
clock.disable();
|
||||
|
||||
return { enabled: clock.enabled, paused: clock.paused, events: events.slice() };
|
||||
})
|
||||
.then(function (clock) {
|
||||
assert.strictEqual(clock.enabled, false);
|
||||
assert.strictEqual(clock.paused, true);
|
||||
assert.deepEqual(clock.events, ["pause", "stop"]);
|
||||
})
|
||||
.execute(function () {
|
||||
events = [];
|
||||
|
||||
clock.enable();
|
||||
|
||||
return { enabled: clock.enabled, paused: clock.paused, events: events.slice() };
|
||||
})
|
||||
.then(function (clock) {
|
||||
assert.strictEqual(clock.enabled, true);
|
||||
assert.strictEqual(clock.paused, true);
|
||||
assert.deepEqual(clock.events, []);
|
||||
})
|
||||
.executeAsync(function (callback) {
|
||||
setTimeout(function () {
|
||||
callback({ enabled: clock.enabled, paused: clock.paused, events: events.slice() });
|
||||
}, 1000);
|
||||
})
|
||||
.then(function (clock) {
|
||||
assert.strictEqual(clock.enabled, true);
|
||||
assert.strictEqual(clock.paused, false);
|
||||
|
||||
assert(clock.events.length >= 2);
|
||||
assert.strictEqual(clock.events[0], "play");
|
||||
for (var i = 1; i < clock.events.length; i++) {
|
||||
assert.strictEqual(clock.events[i], "tick");
|
||||
}
|
||||
})
|
||||
.execute(function () {
|
||||
events = [];
|
||||
|
||||
driverStartTime = 30000;
|
||||
clock.seeking();
|
||||
|
||||
var result = { enabled: clock.enabled, paused: clock.paused, currentTime: clock.currentTime, events: events.slice() };
|
||||
events = [];
|
||||
return result;
|
||||
})
|
||||
.then(function (clock) {
|
||||
assert.strictEqual(clock.enabled, true);
|
||||
assert.strictEqual(clock.paused, true);
|
||||
assert(clock.currentTime >= 30);
|
||||
assert.deepEqual(clock.events, ["pause", "stop", "play", "tick", "pause"]);
|
||||
})
|
||||
.executeAsync(function (callback) {
|
||||
setTimeout(function () {
|
||||
callback({ enabled: clock.enabled, paused: clock.paused, currentTime: clock.currentTime, events: events.slice() });
|
||||
}, 1000);
|
||||
})
|
||||
.then(function (clock) {
|
||||
assert.strictEqual(clock.enabled, true);
|
||||
assert.strictEqual(clock.paused, false);
|
||||
assert(clock.currentTime >= 31);
|
||||
|
||||
assert(clock.events.length >= 2);
|
||||
assert.strictEqual(clock.events[0], "play");
|
||||
for (var i = 1; i < clock.events.length; i++) {
|
||||
assert.strictEqual(clock.events[i], "tick");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
@@ -0,0 +1,18 @@
|
||||
[Script Info]
|
||||
Title:
|
||||
ScriptType: v4.00+
|
||||
WrapStyle: 0
|
||||
PlayResX: 256
|
||||
PlayResY: 144
|
||||
Scroll Position: 0
|
||||
Active Line: 0
|
||||
Video Zoom Percent: 1
|
||||
ScaledBorderAndShadow: yes
|
||||
|
||||
[V4+ Styles]
|
||||
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
||||
Style: Default,Arial,36,&H000000FF,&H00FFFFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,1.2,2,15,15,15,1
|
||||
|
||||
[Events]
|
||||
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
||||
Dialogue: 0,0:00:00.00,0:10:00.00,Default,,0,0,0,,xxx{\fscx50}xxx{\fscx\fscy50}xxx{\fscx50\fscy50}xxx
|
||||
@@ -18,8 +18,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/** @type {!DomParser} */
|
||||
export var domParser: DOMParser = null;
|
||||
if (typeof DOMParser !== "undefined") {
|
||||
domParser = new DOMParser();
|
||||
}
|
||||
define(["intern!tdd", "require", "tests/support/test-page"], function (tdd, require, TestPage) {
|
||||
tdd.suite("fscx and fscy", function () {
|
||||
tdd.test("Basic", function () {
|
||||
var testPage = new TestPage(this.remote, require.toUrl("tests/support/browser-test-page.html"), "/tests/functional/fsc/fsc.ass", 256, 144);
|
||||
return testPage
|
||||
.prepare()
|
||||
.then(function (testPage) { return testPage.seekAndCompareScreenshot(1, require.toUrl("./fsc-1.png")); });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.7 KiB |
@@ -19,7 +19,7 @@
|
||||
*/
|
||||
|
||||
define(["intern!tdd", "require", "tests/support/test-page"], function (tdd, require, TestPage) {
|
||||
tdd.suite("WebDriver", function () {
|
||||
tdd.suite("kfx", function () {
|
||||
tdd.test("Basic", function () {
|
||||
var testPage = new TestPage(this.remote, require.toUrl("tests/support/browser-test-page.html"), "/tests/functional/kfx/kfx.ass", 256, 144);
|
||||
return testPage
|
||||
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,25 @@
|
||||
[Script Info]
|
||||
Title:
|
||||
ScriptType: v4.00+
|
||||
WrapStyle: 0
|
||||
PlayResX: 256
|
||||
PlayResY: 144
|
||||
Scroll Position: 0
|
||||
Active Line: 0
|
||||
Video Zoom Percent: 1
|
||||
ScaledBorderAndShadow: yes
|
||||
|
||||
[V4+ Styles]
|
||||
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
||||
Style: Default,Arial,36,&H000000FF,&H00FFFFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,1.2,2,15,15,15,1
|
||||
Style: Default2,Arial,36,&H000000FF,&H00FFFFFF,&H7F000000,&H00000000,0,0,0,0,100,100,0,0,1,2,1.2,2,15,15,15,1
|
||||
Style: Default3,Arial,36,&H9E0000FF,&H9EFFFFFF,&H9E000000,&H9E000000,0,0,0,0,100,100,0,0,1,2,1.2,2,15,15,15,1
|
||||
|
||||
[Events]
|
||||
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
||||
Dialogue: 1,0:00:00.00,0:00:01.00,Default,,0,0,0,,test
|
||||
Dialogue: 1,0:00:01.00,0:00:02.00,Default2,,0,0,0,,test
|
||||
Dialogue: 1,0:00:02.00,0:00:03.00,Default,,0,0,0,,{\alpha&9E&}test
|
||||
Dialogue: 1,0:00:02.00,0:00:03.00,Default3,,0,0,0,,test
|
||||
Dialogue: 1,0:00:03.00,0:00:04.00,Default2,,0,0,0,,{\alpha&9E&}test
|
||||
Dialogue: 1,0:00:03.00,0:00:04.00,Default3,,0,0,0,,test
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
define(["intern!tdd", "require", "tests/support/test-page"], function (tdd, require, TestPage) {
|
||||
tdd.suite("Outlines", function () {
|
||||
tdd.test("Basic", function () {
|
||||
var testPage = new TestPage(this.remote, require.toUrl("tests/support/browser-test-page.html"), "/tests/functional/outlines/outlines.ass", 256, 144);
|
||||
return testPage
|
||||
.prepare()
|
||||
.then(function (testPage) { return testPage.seekAndCompareScreenshot(0.5, require.toUrl("./outlines-1.png")); })
|
||||
.then(function (testPage) { return testPage.seekAndCompareScreenshot(1.5, require.toUrl("./outlines-2.png")); })
|
||||
.then(function (testPage) { return testPage.seekAndCompareScreenshot(2.5, require.toUrl("./outlines-3.png")); })
|
||||
.then(function (testPage) { return testPage.seekAndCompareScreenshot(3.5, require.toUrl("./outlines-4.png")); });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
[Script Info]
|
||||
Title:
|
||||
ScriptType: v4.00+
|
||||
WrapStyle: 0
|
||||
PlayResX: 256
|
||||
PlayResY: 144
|
||||
Scroll Position: 0
|
||||
Active Line: 0
|
||||
Video Zoom Percent: 1
|
||||
ScaledBorderAndShadow: yes
|
||||
|
||||
[V4+ Styles]
|
||||
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
||||
Style: Default,Arial,36,&H7F0000FF,&H00FFFFFF,&H7F00FF00,&H00000000,0,0,0,0,100,100,0,0,1,2,1.2,2,15,15,15,1
|
||||
Style: Default2,Arial,36,&HF00000FF,&H00FFFFFF,&HF000FF00,&H00000000,0,0,0,0,100,100,0,0,1,2,1.2,2,15,15,15,1
|
||||
|
||||
[Events]
|
||||
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
||||
Dialogue: 1,0:00:00.00,0:00:01.00,Default,,0,0,0,,a{\alpha&00&}b{\rDefault2}c{\alpha&D0&}d
|
||||