62 Commits
Author SHA1 Message Date
Arnavion c6053f59a1 Nested rotations. 2014-08-17 22:09:16 -07:00
Arnavion 73755e0c8a libjass.parser.parse_assScriptProperty() now properly handles a parent that's not a script section. 2014-08-17 03:59:41 -07:00
Arnavion f951475103 Replaced custom map-like interfaces with actual Map. 2014-08-17 03:59:40 -07:00
Arnavion 77474771e6 Fixed bug in SRT-to-ASS conversion not converting all HTML tags. 2014-08-17 03:51:29 -07:00
Arnavion 55f9cb42fe Store animation delays separately instead of parsing them from the sub's style properties. 2014-08-17 03:51:27 -07:00
Arnavion a8addddbd6 Begin v0.9.0 2014-08-16 21:58:29 -07:00
Arnavion ccd4a53cfb Updated mocha to v1.21.x 2014-08-16 21:52:38 -07:00
Arnavion 3928bf57f0 Added ASS.addEvent() to dynamically add dialogues to an ASS object.
Fixes #16
2014-08-16 21:33:26 -07:00
Arnavion dd6d008b74 Parse format-specifier-enabled properties inside parse_assScriptProperty itself instead of relying on caller. 2014-08-16 21:28:56 -07:00
Arnavion 45aabac358 Allow passing in a custom parse tree root to libjass.parser.parse() 2014-08-16 21:28:56 -07:00
Arnavion 55c9dc10ca Log how long it to parse the script in debug mode. 2014-08-16 18:47:17 -07:00
Arnavion 32c232308f Update ManualClock with new API. 2014-08-08 23:02:26 -07:00
Arnavion 2eaa6e451d Cleanup and typos. 2014-08-08 22:51:12 -07:00
Arnavion 03ed6091c2 Handle unknown types gracefully in type linker. 2014-08-08 22:32:32 -07:00
Arnavion 43c3e26596 Use a checkbox to toggle subs instead of a button. 2014-08-08 22:22:44 -07:00
Arnavion 9e584bb75b Better clock state machine and API. 2014-08-08 22:22:13 -07:00
Arnavion e62f150f32 Added explanatory comments to example. 2014-08-08 20:23:53 -07:00
Arnavion 24ccc5200c Set resolution option labels even if video metadata is already loaded, and other tiny fixes. 2014-08-08 20:14:56 -07:00
Arnavion c627e72b03 Use XHR's load event instead of readystatechange + readyState === DONE 2014-08-08 20:02:46 -07:00
Arnavion 6eeb634a36 Minor improvements. 2014-07-31 22:56:38 -07:00
Arnavion 2349b82f0f Fastpath for ParseNode._peek() 2014-07-31 22:56:10 -07:00
Arnavion 443714878b Simply logic for parsing whitespace or text nodes while parsing dialogue parts. 2014-07-31 22:55:11 -07:00
Arnavion 7a6f956b49 Remember the format specifier for script sections that have it. Persist the format specifier for ASS dialogues.
Required for #16
2014-07-31 22:53:45 -07:00
Arnavion 73e464e357 Parse newlines as part of parsing a script header/comment/property instead of requiring script section to swallow it. 2014-07-31 22:52:31 -07:00
Arnavion 71d58e686c Don't create objects before they're needed. 2014-07-31 22:51:38 -07:00
Arnavion adaba54be0 Revert "Work around build break because of source-map 0.1.35"
This reverts commit 54b4245482.

Fixed in source-map 0.1.37.
2014-07-13 02:16:39 -07:00
Arnavion 5be310a6ba Added RendererSettings.preciseOutlines
When false, outlines are rendered in increments of the gaussian blur (if any). Vastly boosts performance for large-outline-large-blur lines and doesn't look appreciably different.
2014-07-13 02:14:09 -07:00
Arnavion ee45e0379e Added default value of RendererSettings.preRenderTime to JSDoc. 2014-07-13 02:13:04 -07:00
Arnavion 848898c8d0 Don't rename "_classTag" property in minifier. webworker serializer refers to it by name. 2014-07-11 00:46:10 -07:00
Arnavion deda90698b UJS.SourceMap.root defaults to null. 2014-07-09 20:29:59 -07:00
Arnavion 774b218aff Use a set to hold font URLs to avoid duplicates. 2014-07-09 20:28:11 -07:00
Arnavion 6bb4873d67 Implemented SimpleSet.size 2014-07-09 20:27:26 -07:00
Arnavion 54b4245482 Work around build break because of source-map 0.1.35
Depending on https://github.com/mozilla/source-map/issues/123 it's either a bug in source-map or in TypeScript.
2014-07-09 00:21:52 -07:00
Arnavion 39d75c189d Implemented UglifyJS directive to suppress unreferenced symbol warnings. 2014-07-08 22:27:41 -07:00
Arnavion ae56620f91 Updated mocha to v1.20.x 2014-07-04 23:41:32 -07:00
Arnavion ee07ca1d7c Removed workaround for uglify-js < 2.4.14 2014-07-04 23:41:19 -07:00
Arnavion b6ca859f4b Fixed typo. 2014-07-04 23:20:07 -07:00
Arnavion c4f87d54bf Added support for \fs+ and \fs- 2014-07-04 23:19:41 -07:00
Arnavion b662698e0c Increase range of filter effects from 150% to 200% 2014-07-03 01:21:49 -07:00
Arnavion 0198fb235c Fixed some things detected by Safer Typescript compiler. 2014-07-03 00:57:06 -07:00
Arnavion fc5dce86ea Increase range of filter effects from 120% to 150% 2014-06-20 21:31:27 -07:00
Arnavion d918b1256d Explicitly set text alignment on absolutely-positioned subs because they'll go in a .an0 <div> and so won't get alignment CSS text-align. 2014-06-02 22:25:57 -07:00
Arnavion c65305fbe8 Fixed typo. 2014-06-02 21:49:00 -07:00
Arnavion 111c4199ff Fixed min-width calculation to correctly sum horizontal margins. 2014-06-02 21:23:25 -07:00
Arnavion 8bc205fc01 Added support for interfaces to documentation generator. 2014-05-31 02:17:49 -07:00
Arnavion e69221af7d Web worker improvements.
- Don't restrict usage to only one worker channel per library instance.
- WorkerChannel.request() now returns a promise instead of needing a callback passed in.
2014-05-31 01:42:22 -07:00
Arnavion 6ba048e55a Added web worker test for libjass.parser.parse() 2014-05-29 00:35:12 -07:00
Arnavion 8dda6d67e6 Web worker support for calling libjass.parser.parse() 2014-05-29 00:35:10 -07:00
Arnavion 7c6793be8c Implemented a mechanism for running tasks in a web worker. 2014-05-29 00:35:09 -07:00
Arnavion 123339519d Added browser support for async tests and mocha's setup() function. 2014-05-29 00:35:08 -07:00
Arnavion 12374765ed Added support for Map.size to SimpleMap 2014-05-29 00:35:06 -07:00
Arnavion 255447b147 Updated documentation generator to use the new TypeScript API. 2014-05-29 00:35:05 -07:00
Arnavion 32507ad644 Fixed not being able to parse tags without values. 2014-05-28 22:00:04 -07:00
Arnavion d06777d2b3 Removed private member from interface. 2014-05-27 20:46:13 -07:00
Arnavion b727f3c6e2 Added support for adding and removing files while watching.
Removed astWatcher support for watch since it's not something I care to have run in incremental compiles.
2014-05-22 23:21:53 -07:00
Arnavion 140393fcca Compiler was resolving files using the old file contents instead of the updated file contents, which would suppress reference errors until the next compile. 2014-05-22 12:35:38 -07:00
Arnavion 2d308b7130 Support top-level classes and free functions in AST walker.
While libjass will never have these, they can occur because of what's actually a compile error. Thus while the AST walker will succeed, the subsequent call to compiler.compile() will fail.
2014-05-21 00:49:10 -07:00
Arnavion 6a282416a8 Split test runner into its own file that doesn't need gulp to run.
This allows the tests child process to start nearly instantly since it doesn't have to spend a few seconds loading gulp.
2014-05-21 00:49:08 -07:00
Arnavion 40ebcfc6b7 Tell gulp.src not to read file contents since we read the files ourselves. 2014-05-21 00:21:58 -07:00
Arnavion d23e57d215 Added better watch implementation of TypeScript gulp task that only re-compiled changed files. 2014-05-21 00:21:57 -07:00
Arnavion 77ca69e1d3 Added information about using the parser in node. 2014-05-15 15:51:30 -07:00
Arnavion c2ff6dc369 Begin v0.8.0 2014-05-15 15:16:30 -07:00
24 changed files with 2182 additions and 995 deletions
+24
View File
@@ -45,6 +45,30 @@ The API documentation is linked in the Links section below. Here's an overview:
* Lastly, the renderer contains an implementation of preloading fonts before playing the video. It uses a map of font names to URLs - index.js creates this map from the @font-face rules in fonts.css.
### Can I use libjass in node?
libjass's parser works in node.
```javascript
> var libjass = require("./libjass.js")
undefined
> var parts = libjass.parser.parse("{\\an8}Are {\\i1}you{\\i0} the one who stole the clock?!", "dialogueParts")
undefined
> parts.join(" ")
'Alignment { value: 8 } Text { value: Are } Italic { value: true } Text { value: you } Italic { value: false } Text { value: the one who stole the clock?! }'
> parts.length
6
> parts[0].toString()
'Alignment { value: 8 }'
> parts[0] instanceof libjass.parts.Alignment
true
> parts[0].value
8
```
[libjass.parser.parse](http://arnavion.github.io/libjass/api.xhtml#libjass.parser.parse) parses the first parameter using the second parameter as the rule name. The rule names are derived from the methods on the [ParserRun class](http://arnavion.github.io/libjass/api.xhtml#libjass.parser.ParserRun). For example, [dialogueParts](http://arnavion.github.io/libjass/api.xhtml#libjass.parser.ParserRun.parse_dialogueParts) can be used to get an array of [libjass.parts](http://arnavion.github.io/libjass/api.xhtml#libjass.parts) objects that represent the parts of an ASS dialogue line.
### Can I contribute?
Yes! Feature requests, suggestions, bug reports and pull requests are welcome! I'm especially looking for details and edge-cases of the ASS syntax that libjass doesn't support.
+57 -86
View File
@@ -18,64 +18,70 @@
* limitations under the License.
*/
var childProcess = require("child_process");
var fs = require("fs");
var gulp = require("gulp");
var path = require("path");
var gulp = require("gulp");
var Vinyl = require("vinyl");
var Transform = require("./gulplib/helpers.js").Transform;
var helpers = require("./gulplib/helpers.js");
var Transform = helpers.Transform;
var SingletonChildProcess = helpers.SingletonChildProcess;
Object.defineProperties(global, {
Doc: { get: function () { return require("./gulplib/doc.js"); } },
Mocha: { get: function () { return require("mocha"); } },
TypeScript: { get: function () { return require("./gulplib/typescript.js"); } },
UglifyJS: { get: function () { return require("./gulplib/uglify.js"); } },
});
gulp.task("default", ["libjass.js", "libjass.min.js"]);
function ASTModifer(namespaces) {
var visitor = function (current) {
var newText = [];
if (current instanceof TypeScript.AST.Namespace) {
current.members.forEach(visitor);
}
else if (current instanceof TypeScript.AST.Constructor) {
newText.push("@constructor");
if (current.baseType !== null) {
newText.push("@extends {" + current.baseType.fullName + "}");
}
if (current.parent !== null) {
newText.push("@memberOf " + current.parent.fullName);
}
current.members.forEach(visitor);
}
if (current.isPrivate) {
newText.push("@private");
}
if (current.isStatic) {
newText.push("@static");
}
if (newText.length > 0) {
current.astNode["gulp-typescript-new-comment"] = newText;
}
};
Object.keys(namespaces).forEach(function (namespaceName) { visitor(namespaces[namespaceName]); });
}
gulp.task("libjass.js", function (callback) {
if (fs.existsSync("./libjass.js")) {
return callback(null);
}
return gulp.src("./libjass.ts").pipe(TypeScript.gulp("/libjass.js", "/libjass.js.map", function (namespaces) {
var visitor = function (current) {
var newText = [];
if (current instanceof TypeScript.AST.Namespace) {
current.members.forEach(visitor);
}
else if (current instanceof TypeScript.AST.Constructor) {
newText.push("@constructor");
if (current.baseType !== null) {
newText.push("@extends {" + current.baseType.fullName + "}");
}
if (current.parent !== null) {
newText.push("@memberOf " + current.parent.fullName);
}
current.members.forEach(visitor);
}
if (current.isPrivate) {
newText.push("@private");
}
if (current.isStatic) {
newText.push("@static");
}
if (newText.length > 0) {
current.astNode["gulp-typescript-new-comment"] = newText;
}
};
Object.keys(namespaces).forEach(function (namespaceName) { visitor(namespaces[namespaceName]); });
})).pipe(UglifyJS.fixup()).pipe(gulp.dest("."));
return gulp.src("./libjass.ts", { read: false })
.pipe(TypeScript.gulp("/libjass.js", "/libjass.js.map", ASTModifer))
.pipe(UglifyJS.fixup())
.pipe(gulp.dest("."));
});
gulp.task("libjass.min.js", ["libjass.js"], function (callback) {
@@ -101,56 +107,21 @@ gulp.task("clean", function () {
});
});
gulp.task("test", ["libjass.js"], function (callback) {
var mocha = new Mocha({
ui: "tdd",
reporter: "spec"
});
gulp.task("watch", ["clean"], function (callback) {
var gulpTestProcess = SingletonChildProcess(path.resolve("./gulplib/test-runner.js"));
gulp.src("./tests/test-*.js").pipe(Transform(function (file) {
mocha.addFile(file.path);
}, function () {
mocha.run(function (failures) {
if (failures > 0) {
callback(new Error(failures + " test(s) failed."));
}
else {
callback(null);
}
});
gulp.src("./libjass.ts", { read: false })
.pipe(TypeScript.watch("/libjass.js", "/libjass.js.map"))
.pipe(UglifyJS.fixup())
.pipe(gulp.dest("."))
.pipe(Transform(function (file) {
if (path.basename(file.path) === "libjass.js") {
gulpTestProcess();
}
}));
});
gulp.task("watch", function (callback) {
var commandLine = path.resolve("./node_modules/.bin/gulp") + " clean libjass.js test";
var subProcess = null;
var rerun = false;
var spawnSubProcess = function () {
subProcess = childProcess.exec(commandLine);
subProcess.stdout.pipe(process.stdout);
subProcess.stderr.pipe(process.stderr);
subProcess.addListener("exit", function (code, signal) {
subProcess = null;
if (rerun) {
spawnSubProcess();
}
rerun = false;
});
};
gulp.watch(["./*.ts", "./tests/*.js"], { debounceDelay: 1 }, function () {
if (subProcess === null) {
spawnSubProcess();
}
else {
rerun = true;
}
gulp.watch(["./tests/test-*.js"], { debounceDelay: 1 }, function () {
gulpTestProcess();
});
});
+70 -12
View File
@@ -38,13 +38,17 @@ Array.prototype.concatMany = function (arr) {
};
module.exports = function () {
return Transform(function (file) {
// Parse
var compiler = new TypeScript.Compiler();
var compiler = new TypeScript.Compiler();
var resolvedFiles = compiler.addFiles([file.path]);
var filenames = [];
var allFiles = [];
var namespaces = TypeScript.AST.walk(compiler, resolvedFiles);
return Transform(function (file, encoding) {
filenames.push(file.path);
allFiles.push.apply(allFiles, compiler.addFile(file));
}, function () {
// Walk
var namespaces = TypeScript.AST.walk(compiler, allFiles);
// Make HTML
var sorter = (function () {
@@ -61,7 +65,7 @@ module.exports = function () {
return 0;
};
var types = [TypeScript.AST.Variable, TypeScript.AST.Property, TypeScript.AST.Function, TypeScript.AST.Constructor];
var types = [TypeScript.AST.Variable, TypeScript.AST.Property, TypeScript.AST.Function, TypeScript.AST.Interface, TypeScript.AST.Constructor];
var typeSorter = function (value1, value2) {
var type1Index = -1;
var type2Index = -1;
@@ -106,9 +110,9 @@ module.exports = function () {
namespaces[namespaceName].members.sort(sorter);
namespaces[namespaceName].members.filter(function (value) {
return value instanceof TypeScript.AST.Constructor;
}).forEach(function (constructor) {
constructor.members.sort(sorter);
return value instanceof TypeScript.AST.Interface || value instanceof TypeScript.AST.Constructor;
}).forEach(function (interfaceOrConstructor) {
interfaceOrConstructor.members.sort(sorter);
});
});
@@ -160,6 +164,35 @@ module.exports = function () {
]).map(indenter(indent));
};
var writeInterface = function (interface, indent) {
return [
'<dl class="interface' +
(interface.isPrivate ? ' private' : '') +
'" id="' +
sanitize(interface.fullName) +
'">',
' <dt class="name">' + writeInterfaceName(interface) + '</dt>',
' <dd class="description">',
' <p>' + sanitize(interface.description) + '</p>',
' </dd>'
].concat([
' <dd class="members">'
]).concatMany(interface.members.map(function (member) {
if (member instanceof TypeScript.AST.Variable) {
return writeVariable(member, 2);
}
if (member instanceof TypeScript.AST.Function) {
return writeFunction(member, 2);
}
if (member instanceof TypeScript.AST.Property) {
return writeProperty(member, 2);
}
})).concat([
' </dd>',
'</dl>'
]).map(indenter(indent));
};
var writeConstructor = function (constructor, indent) {
return [
'<dl class="constructor' +
@@ -221,6 +254,14 @@ module.exports = function () {
return writeCallableName(func);
};
var writeInterfaceName = function (interface) {
return (
'interface ' +
writeCallableName(interface) +
((interface.baseType !== null) ? (' extends <a href="#' + sanitize(interface.baseType.fullName) + '">' + sanitize(interface.baseType.name) + '</a>') : '')
);
};
var writeConstructorName = function (constructor) {
return (
'class ' +
@@ -455,16 +496,16 @@ module.exports = function () {
' border-bottom: 1px solid black;',
' }',
'',
' .variable, .function, .constructor, .getter, .setter {',
' .variable, .function, .interface, .constructor, .getter, .setter {',
' margin-left: 30px;',
' padding: 10px;',
' }',
'',
' section > .variable:nth-child(2n), section > .function:nth-child(2n), section > .constructor:nth-child(2n) {',
' section > .variable:nth-child(2n), section > .function:nth-child(2n), section > .interface:nth-child(2n), section > .constructor:nth-child(2n) {',
' background-color: rgb(221, 250, 238);',
' }',
'',
' section > .variable:nth-child(2n + 1), section > .function:nth-child(2n + 1), section > .constructor:nth-child(2n + 1) {',
' section > .variable:nth-child(2n + 1), section > .function:nth-child(2n + 1), section > .interface:nth-child(2n + 1), section > .constructor:nth-child(2n + 1) {',
' background-color: rgb(244, 250, 221);',
' }',
'',
@@ -485,6 +526,7 @@ module.exports = function () {
' margin: 0;',
' }',
'',
' .interface .variable, .interface .function, .interface .getter, .interface .setter,',
' .constructor .variable, .constructor .function, .constructor .getter, .constructor .setter {',
' background-color: rgb(250, 241, 221);',
' }',
@@ -557,6 +599,10 @@ module.exports = function () {
return value instanceof TypeScript.AST.Function;
});
var interfaces = namespaces[namespaceName].members.filter(function (value) {
return value instanceof TypeScript.AST.Interface;
});
var constructors = namespaces[namespaceName].members.filter(function (value) {
return value instanceof TypeScript.AST.Constructor;
});
@@ -591,6 +637,18 @@ module.exports = function () {
]);
}
if (interfaces.length > 0) {
result = result.concat([
' <section>',
' <h2>Interfaces</h2>'
]).concatMany(interfaces.map(function (value) {
return writeInterface(value, 5);
})).concat([
' </section>',
''
]);
}
if (constructors.length > 0) {
result = result.concat([
' <section>',
+29
View File
@@ -18,6 +18,7 @@
* limitations under the License.
*/
var childProcess = require("child_process");
var stream = require("stream");
var util = require("util");
@@ -57,3 +58,31 @@ var Transform = function (transform, flush) {
util.inherits(Transform, stream.Transform);
exports.Transform = Transform;
exports.SingletonChildProcess = function (filename) {
var subProcess = null;
var rerun = false;
var spawnSubProcess = function () {
subProcess = childProcess.fork(filename);
subProcess.addListener("exit", function (code, signal) {
subProcess = null;
if (rerun) {
spawnSubProcess();
}
rerun = false;
});
};
return function () {
if (subProcess === null) {
spawnSubProcess();
}
else {
rerun = true;
}
};
};
+41
View File
@@ -0,0 +1,41 @@
/**
* 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 Mocha = require("mocha");
console.log("Running tests...");
var mocha = new Mocha({
ui: "tdd",
reporter: "spec"
});
fs.readdirSync("./tests/")
.filter(function (filename) { return filename.match(/^test-.*\.js$/) !== null; })
.forEach(function (filename) { mocha.addFile(path.resolve("./tests", filename)); });
mocha.run(function (failures) {
if (failures > 0) {
throw new Error(failures + " test(s) failed.");
}
});
+246 -63
View File
@@ -53,48 +53,70 @@ innerCompilerSettings = TypeScript.ImmutableCompilationSettings.fromCompilationS
var Compiler = function () {
function Compiler() {
this._diagnostics = [];
this._scriptSnapshots = Object.create(null);
};
Compiler.prototype.addFiles = function (files) {
var _this = this;
var resolutionResults = TypeScript.ReferenceResolver.resolve(files, {
resolveRelativePath: function (file, directory) {
return path.resolve(directory, file);
},
fileExists: fs.existsSync.bind(fs),
getScriptSnapshot: this._getScriptSnapshot.bind(this),
getParentDirectory: path.dirname.bind(path)
}, true);
resolutionResults.resolvedFiles.unshift({
path: path.resolve(typeScriptModulePath, "lib.d.ts"),
referencedFiles: [],
importedFiles: []
});
this._addDiagnostics(resolutionResults.diagnostics);
this._innerCompiler = new TypeScript.TypeScriptCompiler(new TypeScript.NullLogger(), innerCompilerSettings);
resolutionResults.resolvedFiles.forEach(function (resolvedFile) {
_this._innerCompiler.addFile(resolvedFile.path, _this._getScriptSnapshot(resolvedFile.path), 0, 0, false, resolvedFile.referencedFiles);
});
this.addFile(path.resolve(typeScriptModulePath, "lib.d.ts"));
};
return resolutionResults.resolvedFiles;
Compiler.prototype.addFile = function (file) {
var _this = this;
var filename = file;
if (file.constructor !== String) {
filename = file.path;
}
var newFiles = [];
if (this._innerCompiler.getDocument(filename) === null) {
// This is a new file. Add all the resolutionResults that are new files (including this one).
var resolutionResults = this._resolve(filename);
resolutionResults.resolvedFiles.forEach(function (resolvedFile) {
if (_this._innerCompiler.getDocument(resolvedFile.path) === null) {
_this._innerCompiler.addFile(resolvedFile.path, _this._getScriptSnapshot(resolvedFile.path), 0, 0, false, resolvedFile.referencedFiles);
newFiles.push(resolvedFile.path);
}
});
}
else {
// This is an existing file that's being updated.
this._innerCompiler.updateFile(filename, this._newScriptSnapshot(filename), 0, false, null);
var resolutionResults = this._resolve(filename);
resolutionResults.resolvedFiles.forEach(function (resolvedFile) {
if (_this._innerCompiler.getDocument(resolvedFile.path) === null) {
_this._innerCompiler.addFile(resolvedFile.path, _this._getScriptSnapshot(resolvedFile.path), 0, 0, false, resolvedFile.referencedFiles);
newFiles.push(resolvedFile.path);
}
});
}
return newFiles;
};
Compiler.prototype.removeFile = function (filename) {
this._innerCompiler.removeFile(filename);
};
Compiler.prototype.compile = function (outputCodePath, outputSourceMapPath) {
var iterator = this._innerCompiler.compile(path.resolve.bind(path));
var outputFiles = [];
var error = false;
while (iterator.moveNext()) {
var result = iterator.current();
this._addDiagnostics(result.diagnostics);
error = error || this._writeDiagnostics(result.diagnostics);
result.outputFiles.forEach(function (outputFile) {
var outputPath = null;
@@ -120,19 +142,36 @@ var Compiler = function () {
});
}
if (this._diagnostics.some(function (diagnostic) { return diagnostic.info().category === 1; })) {
if (error) {
throw new Error("There were one or more errors.");
}
return outputFiles;
};
Compiler.prototype.getDocument = function (resolvedFilePath) {
return this._innerCompiler.getDocument(resolvedFilePath);
Compiler.prototype.getDocument = function (filename) {
return this._innerCompiler.getDocument(filename);
};
Compiler.prototype._addDiagnostics = function (newDiagnostics) {
newDiagnostics.forEach(function (diagnostic) {
Compiler.prototype._resolve = function (filename) {
var resolutionResults = TypeScript.ReferenceResolver.resolve([filename], {
resolveRelativePath: function (file, directory) {
return path.resolve(directory, file);
},
fileExists: fs.existsSync.bind(fs),
getScriptSnapshot: this._getScriptSnapshot.bind(this),
getParentDirectory: path.dirname.bind(path)
}, true);
if (this._writeDiagnostics(resolutionResults.diagnostics)) {
throw new Error("File resolution failed for " + JSON.stringify(filename));
}
return resolutionResults;
};
Compiler.prototype._writeDiagnostics = function (diagnostics) {
diagnostics.forEach(function (diagnostic) {
var message = diagnostic.message();
if (diagnostic.fileName()) {
@@ -142,27 +181,23 @@ var Compiler = function () {
console.error(message);
});
this._diagnostics.push.apply(this._diagnostics, newDiagnostics);
return diagnostics.some(function (diagnostic) { return diagnostic.info().category === TypeScript.DiagnosticCategory.Error; });
};
Compiler.prototype._getScriptSnapshot = function (filename) {
var scriptSnapshot = this._scriptSnapshots[filename];
if (!scriptSnapshot) {
var fileContents;
try {
fileContents = fs.readFileSync(filename, { encoding: "utf8" });
}
catch (ex) {
this._addDiagnostics([new TypeScript.Diagnostic(null, null, 0, 0, TypeScript.DiagnosticCode.Cannot_read_file_0_1, [filename, ex.message])]);
fileContents = "";
}
scriptSnapshot = this._scriptSnapshots[filename] = TypeScript.ScriptSnapshot.fromString(fileContents);
if (scriptSnapshot) {
return scriptSnapshot;
}
return scriptSnapshot;
return this._newScriptSnapshot(filename);
};
Compiler.prototype._newScriptSnapshot = function (filename) {
var fileContents = fs.readFileSync(filename, { encoding: "utf8" });
return this._scriptSnapshots[filename] = TypeScript.ScriptSnapshot.fromString(fileContents);
};
return Compiler;
@@ -170,23 +205,27 @@ var Compiler = function () {
exports.Compiler = Compiler;
function flatten(arrays) {
return arrays.reduce(function (previous, current) { return previous.concat(current); }, []);
}
exports.gulp = function (outputCodePath, outputSourceMapPath, astModifier) {
var files = [];
var compiler = new Compiler();
var filenames = [];
var allFiles = [];
return Transform(function (file, encoding) {
files.push(file.path);
filenames.push(file.path);
allFiles.push.apply(allFiles, compiler.addFile(file));
}, function () {
var _this = this;
try {
console.log("Compiling " + JSON.stringify(files) + "...");
var compiler = new Compiler();
var resolvedFiles = compiler.addFiles(files);
console.log("Compiling " + JSON.stringify(filenames) + "...");
if (astModifier !== undefined) {
astModifier(exports.AST.walk(compiler, resolvedFiles));
astModifier(exports.AST.walk(compiler, allFiles));
}
var outputFiles = compiler.compile(outputCodePath, outputSourceMapPath);
@@ -208,7 +247,91 @@ exports.gulp = function (outputCodePath, outputSourceMapPath, astModifier) {
});
};
var __extends = function(d, b, m) {
exports.watch = function (outputCodePath, outputSourceMapPath) {
var compiler = new Compiler();
var filenames = [];
var allFiles = [];
return Transform(function (file) {
filenames.push(file.path);
allFiles.push.apply(allFiles, compiler.addFile(file));
}, function (callback) {
var _this = this;
var outputFiles = compiler.compile(outputCodePath, outputSourceMapPath);
console.log("Compile succeeded.");
console.log("Listening for changes...");
allFiles.forEach(function (filename) {
watchFile(filename, fileChangedCallback, fileDeletedCallback);
});
function fileChangedCallback(filename) {
try {
compiler.addFile(filename).forEach(function (changed) {
if (changed !== filename) {
watchFile(changed, fileChangedCallback, fileDeletedCallback);
}
});
compile();
}
catch (ex) {
console.error("Compile failed." + ex.stack);
}
}
function fileDeletedCallback(filename) {
try {
compiler.removeFile(filename);
compile();
}
catch (ex) {
console.error("Compile failed." + ex.stack);
}
}
function compile() {
console.log("Compiling " + JSON.stringify(filenames) + "...");
var outputFiles = compiler.compile(outputCodePath, outputSourceMapPath);
console.log("Compile succeeded.");
outputFiles.forEach(function (file) {
_this.push(file);
});
}
});
};
var watchFile = function (filename, onChange, onDelete) {
var alreadyCalled = false;
function watchFileCallback(currentFile, previousFile) {
if (currentFile.mtime >= previousFile.mtime) {
if (alreadyCalled) { return; }
alreadyCalled = true;
setTimeout(function () { alreadyCalled = false; }, 100);
onChange(filename);
}
else {
fs.unwatchFile(filename, watchFileCallback);
onDelete(filename);
}
}
fs.watchFile(filename, { interval: 500 }, watchFileCallback);
};
var __extends = function(d, b) {
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
function __() {
this.constructor = d;
@@ -275,6 +398,28 @@ var Function = function (_super) {
return Function;
}(Scoped);
var Interface = function (_super) {
__extends(Interface, _super);
function Interface(name, astNode, description, generics, baseType, isPrivate) {
_super.call(this, name);
this.astNode = astNode;
this.description = description;
this.generics = generics;
this.baseType = baseType;
this.isPrivate = isPrivate;
this.members = [];
};
return Interface;
}(Scoped);
var Constructor = function (_super) {
__extends(Constructor, _super);
@@ -383,6 +528,7 @@ var NodeType = function () {
NodeType[NodeType["Getter"] = 2] = "Getter";
NodeType[NodeType["Setter"] = 3] = "Setter";
NodeType[NodeType["Variable"] = 4] = "Variable";
NodeType[NodeType["Interface"] = 5] = "Interface";
return NodeType;
}();
@@ -451,6 +597,36 @@ var Walker = function (_super) {
});
};
Walker.prototype.visitInterfaceDeclaration = function (node) {
var name = node.identifier.text();
var docNode = node.modifiers.item || node.interfaceKeyword;
var doc = this._parseJSDoc(docNode, NodeType.Interface);
if (doc === null) {
return;
}
var extendsClause = node.heritageClauses.toArray().filter(function (heritageClause) {
return heritageClause.extendsOrImplementsKeyword.tokenKind === TypeScript.SyntaxKind.ExtendsKeyword;
})[0] || null;
var baseType = null;
if (extendsClause !== null) {
baseType = extendsClause.typeNames.item.fullText().trim();
}
var modifiers = ((node.modifiers.item ? [node.modifiers.item] : node.modifiers.nodeOrTokens) || []).map(function (modifier) { return modifier.tokenKind; });
var isPrivate = !modifiers.some(function (modifier) { return modifier === TypeScript.SyntaxKind["ExportKeyword"]; });
var interface = this._scope.enter(new Interface(name, node, doc.rootDescription, doc.generics, baseType, isPrivate));
if (interface.parent !== null) {
interface.parent.members.push(interface);
}
_super.prototype.visitInterfaceDeclaration.call(this, node);
this._scope.leave();
};
Walker.prototype.visitClassDeclaration = function (node) {
var name = node.identifier.text();
@@ -472,7 +648,9 @@ var Walker = function (_super) {
var clazz = this._scope.enter(new Constructor(name, node, doc.rootDescription, doc.generics, doc.parameters, baseType, doc.isAbstract, isPrivate));
clazz.parent.members.push(clazz);
if (clazz.parent !== null) {
clazz.parent.members.push(clazz);
}
_super.prototype.visitClassDeclaration.call(this, node);
@@ -514,7 +692,9 @@ var Walker = function (_super) {
var freeFunction = this._scope.enter(new Function(name, node, doc.rootDescription, doc.generics, doc.parameters, doc.returnType, doc.isAbstract, isPrivate, isStatic));
freeFunction.parent.members.push(freeFunction);
if (freeFunction.parent !== null) {
freeFunction.parent.members.push(freeFunction);
}
this._scope.leave();
};
@@ -671,7 +851,7 @@ var Walker = function (_super) {
return [type, remainingLine];
};
var firstWordMatch = line.match(/^(\S+)(\s*)/);
var firstWordMatch = line.match(/^\s*(\S+)(\s*)/);
var firstWord = firstWordMatch[1];
var remainingLine = line.substring(firstWordMatch[0].length);
@@ -761,19 +941,19 @@ var Walker = function (_super) {
return Walker;
}(TypeScript.PositionTrackingWalker);
var walk = function (compiler, resolvedFiles) {
var walk = function (compiler, resolvedFilenames) {
var walker = new Walker();
// Walk
resolvedFiles.slice(1).forEach(function (resolvedFile) {
var document = compiler.getDocument(resolvedFile.path);
resolvedFilenames.forEach(function (resolvedFilename) {
var document = compiler.getDocument(resolvedFilename);
var sourceUnit = document.syntaxTree().sourceUnit();
sourceUnit.accept(walker);
});
// Link base types
var linkVisitor = function (current) {
if (current instanceof Constructor) {
if (current instanceof Interface || current instanceof Constructor) {
if (current.baseType !== null && current.baseType.constructor === String) {
var existingBaseType = null;
for (var ns = current.parent; existingBaseType === null; ns = ns.parent) {
@@ -798,6 +978,7 @@ var walk = function (compiler, resolvedFiles) {
if (existingBaseType === null) {
console.warn("Base type [" + current.baseType + "] of type [" + current.fullName + "] not found.");
current.baseType = { name: current.baseType, fullName: current.baseType };
}
else {
current.baseType = existingBaseType;
@@ -817,6 +998,7 @@ var walk = function (compiler, resolvedFiles) {
exports.AST = {
Namespace: Namespace,
Function: Function,
Interface: Interface,
Constructor: Constructor,
Property: Property,
Getter: Getter,
@@ -863,6 +1045,7 @@ TypeScript.Emitter.prototype.emitComments = function (ast, pre, onlyPinnedOrTrip
ast["gulp-typescript-new-comment"].forEach(function (line) {
fullText += "* " + line + "\n";
});
ast["gulp-typescript-new-comment"] = undefined;
trivia._fullText = fullText + originalFullText.substr(commentEndIndex);
trivia._fullWidth = trivia._fullText.length;
+242 -234
View File
@@ -39,149 +39,153 @@ module.exports = {
sourceMapFile = file;
break;
}
}, function () {
UglifyJS.base54.reset();
if (codeFile !== null && sourceMapFile !== null) {
UglifyJS.base54.reset();
// Parse
var root = UglifyJS.parse(codeFile.contents.toString(), {
filename: path.basename(codeFile.path),
toplevel: null
});
// Parse
var root = UglifyJS.parse(codeFile.contents.toString(), {
filename: path.basename(codeFile.path),
toplevel: null
});
root = root.wrap_enclose(['((typeof module !== "undefined") && module.exports) || (this.libjass = {}):libjass', '((typeof global !== "undefined") && global) || this:global']);
root = root.wrap_enclose(['(typeof module !== "undefined") ? module.exports : (this.libjass = {}):libjass', '(typeof global !== "undefined") ? global : this:global']);
root.figure_out_scope({ screw_ie8: true });
root.figure_out_scope({ screw_ie8: true });
// Remove some things from the AST
var nodesToRemove;
// Remove some things from the AST
var nodesToRemove;
nodesToRemove = [];
nodesToRemove = [];
// 1. All the top-level "var libjass;"
root.walk(new UglifyJS.TreeWalker(function (node, descend) {
if (node instanceof UglifyJS.AST_Var && node.definitions[0].name.name === "libjass") {
nodesToRemove.push({ node: node, parent: this.parent().body });
}
}));
// 2. All but the first top-level "var __extends = ..."
var firstVarExtendsFound = false;
root.walk(new UglifyJS.TreeWalker(function (node, descend) {
if (node instanceof UglifyJS.AST_Var && node.definitions[0].name.name === "__extends") {
if (firstVarExtendsFound === false) {
firstVarExtendsFound = true;
// Remove the check for this.__extends
node.definitions[0].value = node.definitions[0].value.right;
}
else {
// 1. All the top-level "var libjass;"
root.walk(new UglifyJS.TreeWalker(function (node, descend) {
if (node instanceof UglifyJS.AST_Var && node.definitions[0].name.name === "libjass") {
nodesToRemove.push({ node: node, parent: this.parent().body });
}
}
}));
}));
// Repeat because removing some declarations may make others unreferenced
for (;;) {
// 3. Unreferenced variable and function declarations, and unreferenced terminal function arguments
// 2. All but the first top-level "var __extends = ..."
var firstVarExtendsFound = false;
root.walk(new UglifyJS.TreeWalker(function (node, descend) {
if (node instanceof UglifyJS.AST_SymbolDeclaration && node.unreferenced()) {
if (node instanceof UglifyJS.AST_SymbolFunarg) {
if (this.parent().argnames.indexOf(node) === this.parent().argnames.length - 1) {
nodesToRemove.push({ node: node, parent: this.parent().argnames });
}
if (node instanceof UglifyJS.AST_Var && node.definitions[0].name.name === "__extends") {
if (firstVarExtendsFound === false) {
firstVarExtendsFound = true;
// Remove the check for this.__extends
node.definitions[0].value = node.definitions[0].value.right;
}
else if (node instanceof UglifyJS.AST_SymbolVar) {
nodesToRemove.push({ node: this.parent(), parent: this.parent(1).definitions });
if (this.parent(1).definitions.length === 1) {
nodesToRemove.push({ node: this.parent(1), parent: this.parent(2).body });
}
}
else if (node instanceof UglifyJS.AST_SymbolDefun) {
nodesToRemove.push({ node: this.parent(), parent: this.parent(1).body });
else {
nodesToRemove.push({ node: node, parent: this.parent().body });
}
}
}));
if (nodesToRemove.length === 0) {
break;
}
nodesToRemove.forEach(function (tuple) {
tuple.parent.splice(tuple.parent.indexOf(tuple.node), 1);
});
nodesToRemove = [];
root.figure_out_scope({ screw_ie8: true });
}
// 4. 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).
root.walk(new UglifyJS.TreeWalker(function (node, descend) {
if (
node instanceof UglifyJS.AST_SymbolFunarg &&
node.thedef.name[0] === "_" &&
node.thedef.name !== "_super" // Don't rename _super (used in TypeScript's inheritance shim) to super. super is a reserved word.
) {
node.thedef.name = node.thedef.name.slice(1);
}
}));
// Output
var firstLicenseHeaderFound = false; // To detect and preserve the first license header
var output = {
source_map: UglifyJS.SourceMap({
file: path.basename(sourceMapFile.path),
orig: sourceMapFile.contents.toString(),
root: ""
}),
beautify: true,
comments: function (node, comment) {
// If this is a license header
if (comment.value.indexOf("Copyright") !== -1) {
if (!firstLicenseHeaderFound) {
firstLicenseHeaderFound = true;
// Repeat because removing some declarations may make others unreferenced
for (;;) {
// 3. Unreferenced variable and function declarations, and unreferenced terminal function arguments
root.walk(new UglifyJS.TreeWalker(function (node, descend) {
if (node instanceof UglifyJS.AST_SymbolDeclaration && node.unreferenced()) {
if (node instanceof UglifyJS.AST_SymbolFunarg) {
if (this.parent().argnames.indexOf(node) === this.parent().argnames.length - 1) {
nodesToRemove.push({ node: node, parent: this.parent().argnames });
}
}
else if (node instanceof UglifyJS.AST_SymbolVar) {
nodesToRemove.push({ node: this.parent(), parent: this.parent(1).definitions });
if (this.parent(1).definitions.length === 1) {
nodesToRemove.push({ node: this.parent(1), parent: this.parent(2).body });
}
}
else if (node instanceof UglifyJS.AST_SymbolDefun) {
nodesToRemove.push({ node: this.parent(), parent: this.parent(1).body });
}
}
else {
// This isn't the first license header. Strip it.
}));
if (nodesToRemove.length === 0) {
break;
}
nodesToRemove.forEach(function (tuple) {
tuple.parent.splice(tuple.parent.indexOf(tuple.node), 1);
});
nodesToRemove = [];
root.figure_out_scope({ screw_ie8: true });
}
// 4. 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).
root.walk(new UglifyJS.TreeWalker(function (node, descend) {
if (
node instanceof UglifyJS.AST_SymbolFunarg &&
node.thedef.name[0] === "_" &&
node.thedef.name !== "_super" // Don't rename _super (used in TypeScript's inheritance shim) to super. super is a reserved word.
) {
node.thedef.name = node.thedef.name.slice(1);
}
}));
// Output
var firstLicenseHeaderFound = false; // To detect and preserve the first license header
var output = {
source_map: UglifyJS.SourceMap({
file: path.basename(sourceMapFile.path),
orig: sourceMapFile.contents.toString()
}),
beautify: true,
comments: function (node, comment) {
// If this is a license header
if (comment.value.indexOf("Copyright") !== -1) {
if (!firstLicenseHeaderFound) {
firstLicenseHeaderFound = true;
}
else {
// This isn't the first license header. Strip it.
return false;
}
}
// If this is a TypeScript reference comment, strip it.
if (comment.value.substr(0, "/<reference path=".length) === "/<reference path=") {
return false;
}
/* Align multi-line comments correctly.
* TypeScript shifts them one space left, and UJS shifts them four spaces left,
* so shift each line except the first five spaces right.
*/
if (comment.type === "comment2") {
var lines = comment.value.split("\n");
lines = [lines[0]].concat(lines.slice(1).map(function (line) { return ' ' + line; }));
comment.value = lines.join('\n');
}
return true;
}
};
// If this is a TypeScript reference comment, strip it.
if (comment.value.substr(0, "/<reference path=".length) === "/<reference path=") {
return false;
}
var stream = UglifyJS.OutputStream(output);
root.print(stream);
/* Align multi-line comments correctly.
* TypeScript shifts them one space left, and UJS shifts them four spaces left,
* so shift each line except the first five spaces right.
*/
if (comment.type === "comment2") {
var lines = comment.value.split("\n");
lines = [lines[0]].concat(lines.slice(1).map(function (line) { return ' ' + line; }));
comment.value = lines.join('\n');
}
codeFile.contents = Buffer.concat([new Buffer(stream.toString()), new Buffer("\n//# sourceMappingURL="), new Buffer(sourceMapFile.relative)]);
this.push(codeFile);
return true;
}
};
output.source_map.get()._sources.toArray().forEach(function (filename) {
output.source_map.get().setSourceContent(filename, fs.readFileSync(filename, { encoding: "utf8"}));
});
var stream = UglifyJS.OutputStream(output);
root.print(stream);
sourceMapFile.contents = new Buffer(output.source_map.toString());
this.push(sourceMapFile);
codeFile.contents = Buffer.concat([new Buffer(stream.toString()), new Buffer("\n//# sourceMappingURL="), new Buffer(sourceMapFile.relative)]);
this.push(codeFile);
output.source_map.get()._sources.toArray().forEach(function (filename) {
output.source_map.get().setSourceContent(filename, fs.readFileSync(filename, { encoding: "utf8"}));
});
sourceMapFile.contents = new Buffer(output.source_map.toString());
this.push(sourceMapFile);
codeFile = null;
sourceMapFile = null;
}
});
},
@@ -198,142 +202,146 @@ module.exports = {
sourceMapFile = file;
break;
}
}, function () {
UglifyJS.base54.reset();
if (codeFile !== null && sourceMapFile !== null) {
UglifyJS.base54.reset();
// Parse
var root = null;
root = UglifyJS.parse(codeFile.contents.toString(), {
filename: path.basename(codeFile.path),
toplevel: root
});
// Parse
var root = null;
root = UglifyJS.parse(codeFile.contents.toString(), {
filename: path.basename(codeFile.path),
toplevel: root
});
root.figure_out_scope({ screw_ie8: true });
root.figure_out_scope({ screw_ie8: true });
// Suppress some warnings
var originalWarn = UglifyJS.AST_Node.warn;
UglifyJS.AST_Node.warn = function (text, properties) {
if (
(text === "{type} {name} is declared but not referenced [{file}:{line},{col}]" && properties.type === "Symbol" && properties.name === "substring")
) {
return;
}
originalWarn.call(UglifyJS.AST_Node, text, properties);
};
// Warnings
root.scope_warnings({
func_arguments: false
});
// Warnings
root.scope_warnings({
func_arguments: false
});
// Compress
var compressor = UglifyJS.Compressor({
warnings: true,
screw_ie8: true
});
root = root.transform(compressor);
// Compress
var compressor = UglifyJS.Compressor({
warnings: true,
screw_ie8: true
});
root = root.transform(compressor);
// Mangle
root.figure_out_scope({ screw_ie8: true });
root.compute_char_frequency();
root.mangle_names({ screw_ie8: true });
// Mangle
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);
// 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] !== "_"
) {
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.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
var output = {
source_map: UglifyJS.SourceMap({
file: path.basename(sourceMapFile.path),
orig: sourceMapFile.contents.toString(),
root: ""
}),
comments: {
test: function (comment) {
if (!firstLicenseHeaderFound && comment.indexOf("Copyright") !== -1) {
firstLicenseHeaderFound = true;
return true;
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]++;
}
return false;
}
},
screw_ie8: true
};
}));
var stream = UglifyJS.OutputStream(output);
root.print(stream);
var identifiers = Object.keys(occurrences);
identifiers.sort(function (first, second) { return occurrences[second] - occurrences[first]; });
codeFile.path = codeFile.path.replace(/\.js$/, ".min.js");
sourceMapFile.path = sourceMapFile.path.replace(/\.js\.map$/, ".min.js.map");
var generatedIdentifiers = occurrences;
codeFile.contents = Buffer.concat([new Buffer(stream.toString()), new Buffer("\n//# sourceMappingURL="), new Buffer(sourceMapFile.relative)]);
this.push(codeFile);
var validIdentifierCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_0123456789";
var toIdentifier = function (index) {
var result = validIdentifierCharacters[(index % validIdentifierCharacters.length)];
index = (index / validIdentifierCharacters.length) | 0;
output.source_map.get()._sources.toArray().forEach(function (filename) {
output.source_map.get().setSourceContent(filename, fs.readFileSync(filename, { encoding: "utf8"}));
});
while (index > 0) {
index--;
result = validIdentifierCharacters[index % validIdentifierCharacters.length] + result;
index = (index / validIdentifierCharacters.length) | 0;
}
sourceMapFile.contents = new Buffer(output.source_map.toString());
this.push(sourceMapFile);
return "_" + result;
};
identifiers.forEach(function (identifier, index) {
generatedIdentifiers[identifier] = toIdentifier(index);
});
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
var output = {
source_map: UglifyJS.SourceMap({
file: path.basename(sourceMapFile.path),
orig: sourceMapFile.contents.toString()
}),
comments: {
test: function (comment) {
if (!firstLicenseHeaderFound && comment.indexOf("Copyright") !== -1) {
firstLicenseHeaderFound = true;
return true;
}
return false;
}
},
screw_ie8: true
};
var stream = UglifyJS.OutputStream(output);
root.print(stream);
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)]);
this.push(codeFile);
output.source_map.get()._sources.toArray().forEach(function (filename) {
output.source_map.get().setSourceContent(filename, fs.readFileSync(filename, { encoding: "utf8"}));
});
sourceMapFile.contents = new Buffer(output.source_map.toString());
this.push(sourceMapFile);
codeFile = null;
sourceMapFile = null;
}
});
}
};
var originalSymbolUnreferenced = UglifyJS.AST_Symbol.prototype.unreferenced;
UglifyJS.AST_Symbol.prototype.unreferenced = function () {
if (this.start.comments_before.length > 0 && this.start.comments_before[0].value.trim() === "ujs:unreferenced") {
return false;
}
return originalSymbolUnreferenced.call(this);
};
+106 -79
View File
@@ -36,102 +36,129 @@ addEventListener("DOMContentLoaded", function () {
var ass = null;
var testVideoAndASSLoaded = function () {
if (videoMetadataLoaded && ass) {
var renderer = new libjass.renderers.DefaultRenderer(video, ass, {
fontMap: libjass.renderers.RendererSettings.makeFontMapFromStyleElement(document.querySelector("#font-map"))
});
if (videoMetadataLoaded === false || ass === null) {
return;
}
if (libjass.debugMode) {
window.renderer = renderer;
// Both video metadata and ASS are loaded
// Create a DefaultRenderer using the video element, the ASS object, and the font map
var renderer = new libjass.renderers.DefaultRenderer(video, ass, {
fontMap: libjass.renderers.RendererSettings.makeFontMapFromStyleElement(document.querySelector("#font-map"))
});
if (libjass.debugMode) {
// Export the renderer for debugging
window.renderer = renderer;
}
// Autoplay the video when the renderer is ready
renderer.addEventListener("ready", function () {
debug("All fonts have been preloaded. Beginning autoplay.");
video.play();
});
document.querySelector("#enable-disable-subs").addEventListener("change", function (event) {
renderer.setEnabled(event.target.checked);
}, false);
// Run when the video/script resolution needs to be changed, either because the user clicked one of the resolution options,
// or because they exited full screen and their selection needs to be restored
var changeVideoSizeSelection = function (id) {
if (typeof id === "undefined") {
// Find which option is selected
[].slice.call(videoSizeSelector.querySelectorAll("input[name='video-size']")).some(function (option) {
if (option.checked) {
id = option.id;
return true;
}
return false;
});
}
renderer.addEventListener("ready", function () {
debug("All fonts have been preloaded. Beginning autoplay.");
video.play();
if (id === "video-size-video-radio") {
// Resize to video resolution
subToggleSpanTextNode.textContent = renderer.enabled ? "off" : "on";
});
video.style.width = video.videoWidth + "px";
video.style.height = video.videoHeight + "px";
renderer.resize(video.videoWidth, video.videoHeight);
}
var subToggleSpanTextNode = document.createTextNode("");
document.querySelector("#enable-disable-subs-span").appendChild(subToggleSpanTextNode);
else if (id === "video-size-script-radio") {
// Resize to script resolution
document.querySelector("#enable-disable-button").addEventListener("click", function (event) {
renderer.toggle();
subToggleSpanTextNode.textContent = renderer.enabled ? "off" : "on";
}, false);
var changeVideoSizeSelection = function (id) {
if (typeof id === "undefined") {
[].some.call(videoSizeSelector.querySelectorAll("input[name='video-size']"), function (option) {
if (option.checked) {
id = option.id;
return true;
}
return false;
});
}
if (id === "video-size-video-radio") {
video.style.width = video.videoWidth + "px";
video.style.height = video.videoHeight + "px";
renderer.resize(video.videoWidth, video.videoHeight);
}
else if (id === "video-size-script-radio") {
video.style.width = ass.properties.resolutionX + "px";
video.style.height = ass.properties.resolutionY + "px";
renderer.resize(ass.properties.resolutionX, ass.properties.resolutionY);
}
};
var videoSizeSelector = document.querySelector("#video-size-selector");
videoSizeSelector.addEventListener("change", function (event) { changeVideoSizeSelection(event.target.id); }, false);
renderer.addEventListener("fullScreenChange", function (newState) {
if (newState === false) {
changeVideoSizeSelection();
}
});
changeVideoSizeSelection();
video.style.width = ass.properties.resolutionX + "px";
video.style.height = ass.properties.resolutionY + "px";
renderer.resize(ass.properties.resolutionX, ass.properties.resolutionY);
}
};
}
if (video.readyState < HTMLMediaElement.HAVE_METADATA) {
video.addEventListener("loadedmetadata", function () {
debug("Video metadata loaded.");
videoMetadataLoaded = true;
var videoSizeSelector = document.querySelector("#video-size-selector");
videoSizeSelector.addEventListener("change", function (event) { changeVideoSizeSelection(event.target.id); }, false);
document.querySelector("#video-resolution-label-width").appendChild(document.createTextNode(video.videoWidth));
document.querySelector("#video-resolution-label-height").appendChild(document.createTextNode(video.videoHeight));
// When video exits full screen, restore the size according to the resolution option that's selected
renderer.addEventListener("fullScreenChange", function (newState) {
if (newState === false) {
changeVideoSizeSelection();
}
});
testVideoAndASSLoaded();
}, false);
}
else {
// Set the resolution to whatever's selected by default
changeVideoSizeSelection();
};
(function (onVideoMetadataLoaded) {
if (video.readyState < HTMLMediaElement.HAVE_METADATA) {
// Video metadata isn't available yet. Register an event handler for it.
video.addEventListener("loadedmetadata", onVideoMetadataLoaded, false);
}
else {
// Video metadata is already available.
onVideoMetadataLoaded();
}
})(function () {
debug("Video metadata loaded.");
videoMetadataLoaded = true;
testVideoAndASSLoaded();
}
videoMetadataLoaded = true;
// Prepare the "Video resolution" option label
document.querySelector("#video-resolution-label-width").appendChild(document.createTextNode(video.videoWidth));
document.querySelector("#video-resolution-label-height").appendChild(document.createTextNode(video.videoHeight));
// Test if everything is loaded
testVideoAndASSLoaded();
});
// Find the ASS or SRT track and load it
var track = document.querySelector("#video > track[data-format='ass'], #video > track[data-format='srt']");
var subsRequest = new XMLHttpRequest();
subsRequest.open("GET", track.src || track.getAttribute("src"), true);
subsRequest.addEventListener("readystatechange", function () {
if (subsRequest.readyState === XMLHttpRequest.DONE) {
debug("Script received.");
subsRequest.addEventListener("load", function () {
debug("Script received.");
ass = libjass.ASS.fromString(subsRequest.responseText, libjass.Format[track.getAttribute("data-format").toUpperCase()]);
if (libjass.debugMode) {
window.ass = ass;
}
document.querySelector("#script-resolution-label-width").appendChild(document.createTextNode(ass.properties.resolutionX));
document.querySelector("#script-resolution-label-height").appendChild(document.createTextNode(ass.properties.resolutionY));
testVideoAndASSLoaded();
if (libjass.debugMode) {
console.time("Parsing ASS took");
}
// Parse the response string into an ASS object
ass = libjass.ASS.fromString(subsRequest.responseText, libjass.Format[track.getAttribute("data-format").toUpperCase()]);
if (libjass.debugMode) {
console.timeEnd("Parsing ASS took");
// Export the ASS object for debugging
window.ass = ass;
}
// Prepare the "Script resolution" option label
document.querySelector("#script-resolution-label-width").appendChild(document.createTextNode(ass.properties.resolutionX));
document.querySelector("#script-resolution-label-height").appendChild(document.createTextNode(ass.properties.resolutionY));
// Test if everything is loaded
testVideoAndASSLoaded();
}, false);
subsRequest.send(null);
}, false);
+4 -3
View File
@@ -42,9 +42,6 @@
<track src="/video/gc-01.ass" kind="metadata" data-format="ass" />
</video>
<form class="settings-form">
<div>
<button type="button" id="enable-disable-button">Turn subs <span id="enable-disable-subs-span" /></button>
</div>
<fieldset>
<legend>Video size</legend>
<div id="video-size-selector">
@@ -52,6 +49,10 @@
<input type="radio" name="video-size" id="video-size-script-radio" /><label for="video-size-script-radio">Script resolution <span id="script-resolution-label-width" />x<span id="script-resolution-label-height" /></label>
</div>
</fieldset>
<fieldset>
<legend>Subtitles</legend>
<input type="checkbox" id="enable-disable-subs" checked="checked" /><label for="enable-disable-subs">Subtitles</label>
</fieldset>
</form>
</body>
</html>
+1 -1
View File
@@ -81,7 +81,7 @@
}
/* Filter wrapper span */
.libjass-subs div[data-dialogue-id] > span {
.libjass-subs div[data-dialogue-id] {
-webkit-perspective-origin: center;
-webkit-perspective: 200px;
perspective-origin: center;
+1
View File
@@ -19,6 +19,7 @@
*/
///<reference path="utility.ts" />
///<reference path="web-worker.ts" />
///<reference path="parts.ts" />
///<reference path="parser.ts" />
///<reference path="renderers-clocks.ts" />
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "libjass",
"version": "0.7.0",
"version": "0.9.0",
"description": "A library to render ASS subtitles on HTML5 video in the browser.",
"keywords": ["browser", "html5", "subtitles"],
"homepage": "https://github.com/Arnavion/libjass",
@@ -21,13 +21,13 @@
},
"scripts": {
"postinstall": "gulp clean default",
"test": "gulp test"
"test": "node ./gulplib/test-runner.js"
},
"dependencies": {
"gulp": "latest",
"mocha": "1.18.x",
"mocha": "1.21.x",
"typescript": "1.0.1",
"uglify-js": "2.x >=2.4.13",
"uglify-js": "2.x >=2.4.14",
"vinyl": "latest"
}
}
+180 -78
View File
@@ -24,12 +24,13 @@ module libjass.parser {
/**
* Parses a given string with the specified rule.
*
* @param {string} input
* @param {string} rule
* @return {*}
* @param {string} input The string to be parsed.
* @param {string} rule The rule to parse the string with
* @param {libjass.parser.ParseNode=null} customParseTree If provided, this is used as the root of the parse tree instead of a new object.
* @return {*} The value returned depends on the rule used.
*/
export function parse(input: string, rule: string): any {
var run = new ParserRun(input, rule);
export function parse(input: string, rule: string, customParseTree: ParseNode = null): any {
var run = new ParserRun(input, rule, customParseTree);
if (run.result === null || run.result.end !== input.length) {
if (libjass.debugMode) {
@@ -49,10 +50,17 @@ module libjass.parser {
* @param {string} rule
*/
class ParserRun {
private _parseTree: ParseNode = new ParseNode(null);
private _parseTree: ParseNode;
private _result: ParseNode;
constructor(private _input: string, rule: string) {
constructor(private _input: string, rule: string, customParseTree: ParseNode) {
if (customParseTree === null) {
this._parseTree = new ParseNode(null);
}
else {
this._parseTree = customParseTree;
}
this._result = rules.get(rule).call(this, this._parseTree);
}
@@ -70,13 +78,14 @@ module libjass.parser {
parse_assScript(parent: ParseNode): ParseNode {
var current = new ParseNode(parent);
current.value = Object.create(null);
var sections = new Map<string, any>();
current.value = sections;
while (this._haveMore()) {
var scriptSectionNode = this.parse_assScriptSection(current);
if (scriptSectionNode !== null) {
current.value[scriptSectionNode.value.name] = scriptSectionNode.value.contents;
sections.set(scriptSectionNode.value.name, scriptSectionNode.value.contents);
}
else if (this.read(current, "\n") === null) {
parent.pop();
@@ -94,20 +103,25 @@ module libjass.parser {
parse_assScriptSection(parent: ParseNode): ParseNode {
var current = new ParseNode(parent);
current.value = Object.create(null);
current.value.contents = null;
var sectionHeaderNode = this.parse_assScriptSectionHeader(current);
if (sectionHeaderNode === null) {
parent.pop();
return null;
}
current.value = Object.create(null);
current.value.name = sectionHeaderNode.value;
current.value.contents = null;
var formatSpecifier: string[] = null;
var sectionTemplate: Map<string, string> = null;
var sectionTemplateArray: TypedTemplateArray = null;
while (this._haveMore() && this._peek() !== "[") {
if (this.read(current, "\n") !== null) {
continue;
}
if (this.parse_assScriptComment(current) !== null) {
continue;
}
@@ -115,44 +129,46 @@ module libjass.parser {
var propertyNode = this.parse_assScriptProperty(current);
if (propertyNode !== null) {
var property = propertyNode.value;
if (sectionTemplateArray === null) {
var property: { key: string; value: string } = propertyNode.value;
if (property.key === "Format") {
formatSpecifier = property.value.split(",").map((formatPart: string) => formatPart.trim());
}
if (sectionTemplateArray === null && property.key === "Format") {
// The first line of this section is a format specifier. This section will be an array of templates.
sectionTemplateArray = <TypedTemplateArray>[];
sectionTemplateArray.formatSpecifier = property.value.split(",").map(formatPart => formatPart.trim());
else if (formatSpecifier !== null) {
if (current.value.contents === null) {
current.value.contents = <any[]>[];
current.value.contents = sectionTemplateArray;
}
var template = Object.create(null);
var value = property.value.split(",");
else {
// This section is a single template. Each line is a property of this template.
if (sectionTemplate === null) {
sectionTemplate = new Map<string, string>();
current.value.contents = sectionTemplate;
}
if (value.length > formatSpecifier.length) {
value[formatSpecifier.length - 1] = value.slice(formatSpecifier.length - 1).join(",");
sectionTemplate.set(property.key, property.value);
}
formatSpecifier.forEach((formatKey, index) => {
template[formatKey] = value[index];
});
current.value.contents.push({ type: property.key, template: template });
}
else {
if (current.value.contents === null) {
current.value.contents = Object.create(null);
}
// Each line in this section is a template of a particular type, and the section is an array of these templates.
current.value.contents[property.key] = property.value;
sectionTemplateArray.push(<TypedTemplate>propertyNode.value);
}
}
// Empty line
else if (this.read(current, "\n") === null) {
parent.pop();
return null;
else {
// Not a property line. Ignore.
if (this._haveMore() && this.read(current, "\n") === null) {
var garbageNode = new ParseNode(current, "");
for (var next = this._peek(); this._haveMore() && next !== "\n"; next = this._peek()) {
garbageNode.value += next;
}
this.read(current, "\n");
}
}
}
@@ -189,6 +205,8 @@ module libjass.parser {
return null;
}
this.read(current, "\n");
return current;
}
@@ -199,8 +217,6 @@ module libjass.parser {
parse_assScriptProperty(parent: ParseNode): ParseNode {
var current = new ParseNode(parent);
current.value = Object.create(null);
var keyNode = new ParseNode(current, "");
var next: string;
@@ -231,8 +247,36 @@ module libjass.parser {
valueNode.value += next;
}
current.value.key = keyNode.value;
current.value.value = valueNode.value;
this.read(current, "\n");
var property: { key: string; value: string } = Object.create(null);
property.key = keyNode.value;
property.value = valueNode.value;
current.value = property;
if (parent.value !== null) {
var scriptSectionContents = parent.value.contents;
if (scriptSectionContents !== undefined && scriptSectionContents !== null) {
var formatSpecifier = (<TypedTemplateArray>scriptSectionContents).formatSpecifier;
if (formatSpecifier !== undefined) {
// This line is a template of a particular type, and its parent section is an array of these templates.
var template = new Map<string, string>();
var value = property.value.split(",");
if (value.length > formatSpecifier.length) {
value[formatSpecifier.length - 1] = value.slice(formatSpecifier.length - 1).join(",");
}
formatSpecifier.forEach((formatKey, index) => {
template.set(formatKey, value[index]);
});
current.value = <TypedTemplate>{ type: property.key, template: template };
}
}
}
return current;
}
@@ -254,6 +298,8 @@ module libjass.parser {
valueNode.value += next;
}
this.read(current, "\n");
current.value = valueNode.value;
return current;
@@ -276,30 +322,20 @@ module libjass.parser {
}
else {
var newLineNode = this.parse_newline(current);
var whiteSpaceOrTextNode = this.parse_newline(current) || this.parse_hardspace(current) || this.parse_text(current);
if (newLineNode !== null) {
current.value.push(newLineNode.value);
if (whiteSpaceOrTextNode === null) {
parent.pop();
return null;
}
if (whiteSpaceOrTextNode.value instanceof parts.Text && current.value[current.value.length - 1] instanceof parts.Text) {
// Merge consecutive text parts into one part
var previousTextPart = <parts.Text>current.value[current.value.length - 1];
current.value[current.value.length - 1] = new parts.Text(previousTextPart.value + (<parts.Text>whiteSpaceOrTextNode.value).value);
}
else {
var textNode = this.parse_newline(current) || this.parse_hardspace(current) || this.parse_text(current);
if (textNode !== null) {
if (current.value[current.value.length - 1] instanceof parts.Text) {
// Merge consecutive text parts into one part
var previousTextPart = <parts.Text>current.value[current.value.length - 1];
current.value[current.value.length - 1] = new parts.Text(previousTextPart.value + (<parts.Text>textNode.value).value);
}
else {
current.value.push(textNode.value);
}
}
else {
parent.pop();
return null;
}
current.value.push(whiteSpaceOrTextNode.value);
}
}
}
@@ -361,6 +397,8 @@ module libjass.parser {
this.parse_tag_fry(current) ||
this.parse_tag_frz(current) ||
this.parse_tag_fsp(current) ||
this.parse_tag_fsplus(current) ||
this.parse_tag_fsminus(current) ||
this.parse_tag_org(current) ||
this.parse_tag_pbo(current) ||
this.parse_tag_pos(current) ||
@@ -923,6 +961,54 @@ module libjass.parser {
throw new Error("Method not implemented.");
}
/**
* @param {!ParseNode} parent
* @return {ParseNode}
*/
parse_tag_fsplus(parent: ParseNode): ParseNode {
var current = new ParseNode(parent);
if (this.read(current, "fs+") === null) {
parent.pop();
return null;
}
var valueNode = this.parse_decimal(current);
if (valueNode === null) {
parent.pop();
return null;
}
current.value = new parts.FontSizePlus(valueNode.value);
return current;
}
/**
* @param {!ParseNode} parent
* @return {ParseNode}
*/
parse_tag_fsminus(parent: ParseNode): ParseNode {
var current = new ParseNode(parent);
if (this.read(current, "fs-") === null) {
parent.pop();
return null;
}
var valueNode = this.parse_decimal(current);
if (valueNode === null) {
parent.pop();
return null;
}
current.value = new parts.FontSizeMinus(valueNode.value);
return current;
}
/**
* @param {!ParseNode} parent
* @return {ParseNode}
@@ -1377,6 +1463,8 @@ module libjass.parser {
this.parse_tag_fry(current) ||
this.parse_tag_frz(current) ||
this.parse_tag_fsp(current) ||
this.parse_tag_fsplus(current) ||
this.parse_tag_fsminus(current) ||
this.parse_tag_be(current) ||
this.parse_tag_fr(current) ||
@@ -2073,7 +2161,7 @@ module libjass.parser {
var minuteDigitNodes = new Array<ParseNode>(2);
for (var i = 0; i < minuteDigitNodes.length; i++) {
for (i = 0; i < minuteDigitNodes.length; i++) {
if (!this._haveMore()) {
parent.pop();
return null;
@@ -2096,7 +2184,7 @@ module libjass.parser {
var secondDigitNodes = new Array<ParseNode>(2);
for (var i = 0; i < secondDigitNodes.length; i++) {
for (i = 0; i < secondDigitNodes.length; i++) {
if (!this._haveMore()) {
parent.pop();
return null;
@@ -2119,7 +2207,7 @@ module libjass.parser {
var millisecondDigitNodes = new Array<ParseNode>(3);
for (var i = 0; i < millisecondDigitNodes.length; i++) {
for (i = 0; i < millisecondDigitNodes.length; i++) {
if (!this._haveMore()) {
parent.pop();
return null;
@@ -2161,6 +2249,9 @@ module libjass.parser {
* @param {number=1} count
*/
private _peek(count: number = 1): string {
// Fastpath for count === 1. http://jsperf.com/substr-vs-indexer
if (count === 1) { return this._input[this._parseTree.end]; }
return this._input.substr(this._parseTree.end, count);
}
@@ -2271,7 +2362,7 @@ module libjass.parser {
if (valueNode !== null) {
current.value = new tagConstructor(valueNode.value);
}
else if (required) {
else if (!required) {
current.value = new tagConstructor(null);
}
else {
@@ -2329,29 +2420,25 @@ module libjass.parser {
/**
* This class represents a single parse node. It has a start and end position, and an optional value object.
*
* @param {ParseNode} parent The parent of this parse node. The parent's end position will be updated to the end position of this node whenever the latter changes.
* @param {?string=null} value A shortcut to assign a string to the value property.
* @param {ParseNode} parent The parent of this parse node.
* @param {*=null} value If provided, it is assigned as the value of the node.
*/
class ParseNode {
export class ParseNode {
private _children: ParseNode[] = [];
private _start: number;
private _end: number;
private _value: any;
constructor(private _parent: ParseNode, value: string = null) {
constructor(private _parent: ParseNode, value: any = null) {
if (_parent !== null) {
_parent._children.push(this);
_parent.children.push(this);
}
this._start = ((_parent !== null) ? _parent.end : 0);
this._end = this._start;
if (value !== null) {
this.value = value;
}
else {
this._setEnd(this._start);
}
this.value = value;
}
/**
@@ -2405,7 +2492,7 @@ module libjass.parser {
set value(newValue: any) {
this._value = newValue;
if (this._value.constructor === String && this._children.length === 0) {
if (this._value !== null && this._value.constructor === String && this._children.length === 0) {
this._setEnd(this._start + this._value.length);
}
}
@@ -2438,3 +2525,18 @@ module libjass.parser {
}
}
}
module libjass.webworker {
export enum WorkerCommands {
Parse = 1
}
libjass.webworker._registerWorkerCommand(WorkerCommands.Parse, (parameters: any, response: WorkerResultCallback): void => {
try {
response(null, libjass.parser.parse(parameters.input, parameters.rule));
}
catch (ex) {
response(ex, null);
}
});
}
+38
View File
@@ -393,6 +393,42 @@ module libjass.parts {
}
}
/**
* A font size increase tag {\fs+}
*
* @param {?number} value {\fs+###} -> difference (number)
*/
export class FontSizePlus {
constructor(private _value: number) { }
/**
* The value of this font size increase tag.
*
* @type {?number}
*/
get value(): number {
return this._value;
}
}
/**
* A font size decrease tag {\fs-}
*
* @param {?number} value {\fs-###} -> difference (number)
*/
export class FontSizeMinus {
constructor(private _value: number) { }
/**
* The value of this font size decrease tag.
*
* @type {?number}
*/
get value(): number {
return this._value;
}
}
/**
* A horizontal font scaling tag {\fscx}
*
@@ -1388,6 +1424,7 @@ module libjass.parts {
var value = (<any>libjass.parts)[key];
if (value instanceof Function) {
addToString(value, key);
libjass.webworker._registerClassPrototype(value.prototype);
}
});
@@ -1395,6 +1432,7 @@ module libjass.parts {
var value = (<any>libjass.parts.drawing)[key];
if (value instanceof Function) {
addToString(value, "Drawing" + key);
libjass.webworker._registerClassPrototype(value.prototype);
}
});
}
+228 -115
View File
@@ -61,8 +61,9 @@ module libjass.renderers {
export enum ClockEvent {
Play,
Tick,
Pause,
TimeUpdate,
Stop,
}
export interface Clock extends EventSource<ClockEvent> {
@@ -78,22 +79,33 @@ module libjass.renderers {
/**
* Enable the clock.
*
* @return {boolean} True if the clock is now enabled, false if it was already enabled.
*/
enable(): void;
enable(): boolean;
/**
* Disable the clock.
*
* @return {boolean} True if the clock is now disabled, false if it was already disabled.
*/
disable(): void;
disable(): boolean;
/**
* Toggle the clock.
*/
toggle(): void;
/**
* Enable or disable the clock.
*
* @param {boolean} enabled If true, the clock is enabled, otherwise it's disabled.
* @return {boolean} True if the clock is now in the given state, false if it was already in that state.
*/
setEnabled(enabled: boolean): boolean;
// EventSource members
addEventListener: (type: ClockEvent, listener: Function) => void;
_dispatchEvent: (type: ClockEvent, args: Object[]) => void;
}
/**
@@ -101,91 +113,40 @@ module libjass.renderers {
*/
export class ManualClock implements Clock {
private _currentTime: number = -1;
private _enabled: boolean = true;
/**
* Trigger a play event.
* Trigger a ClockEvent.Play
*/
play(): void {
this._dispatchEvent(ClockEvent.Play, []);
}
/**
* Trigger a pause event.
* Trigger a ClockEvent.Tick
*
* @param {number} currentTime
*/
tick(currentTime: number): void {
this._currentTime = currentTime;
this._dispatchEvent(ClockEvent.Tick, []);
}
/**
* Trigger a ClockEvent.Pause
*/
pause(): void {
this._dispatchEvent(ClockEvent.Pause, []);
}
/**
* Trigger a timeUpdate event with the given current time.
*
* @param {number} currentTime
* Trigger a ClockEvent.Stop
*/
timeUpdate(currentTime: number): void {
this._currentTime = currentTime;
this._dispatchEvent(ClockEvent.TimeUpdate, []);
stop(): void {
this._dispatchEvent(ClockEvent.Stop, []);
}
/**
* @type {number}
*/
get currentTime(): number {
return this._currentTime;
}
/**
* @type {boolean}
*/
get enabled(): boolean {
return true;
}
/**
* Enable the clock. This is a no-op for this type.
*/
enable(): void { }
/**
* Disable the clock. This is a no-op for this type.
*/
disable(): void { }
/**
* Toggle the clock. This is a no-op for this type.
*/
toggle(): void { }
// EventSource members
_eventListeners: Map<ClockEvent, Function[]> = new Map<ClockEvent, Function[]>();
addEventListener: (type: ClockEvent, listener: Function) => void;
_dispatchEvent: (type: ClockEvent, args: Object[]) => void;
}
mixin(ManualClock, [EventSource]);
enum VideoClockState {
Playing = 0,
Paused = 1,
}
/**
* An implementation of libjass.renderers.Clock that generates play, pause and timeUpdate events according to the state of a <video> element.
*
* @param {!HTMLVideoElement} video
*/
export class VideoClock implements Clock {
private _currentTime: number;
private _state: VideoClockState;
private _enabled: boolean = true;
private _nextAnimationFrameRequestId: number = null;
constructor(private _video: HTMLVideoElement) {
this._video.addEventListener("playing", () => this._onVideoPlaying(), false);
this._video.addEventListener("pause", () => this._onVideoPause(), false);
this._video.addEventListener("seeking", () => this._onVideoSeeking(), false);
}
// Clock members
/**
* @type {number}
@@ -203,28 +164,32 @@ module libjass.renderers {
/**
* Enable the clock.
*
* @return {boolean} True if the clock is now enabled, false if it was already enabled.
*/
enable(): void {
enable(): boolean {
if (this._enabled) {
return;
return false;
}
this._enabled = true;
this._onVideoPlaying();
return true;
}
/**
* Disable the clock.
*
* @return {boolean} True if the clock is now disabled, false if it was already disabled.
*/
disable(): void {
disable(): boolean {
if (!this._enabled) {
return;
return false;
}
this._onVideoPause();
this._enabled = false;
return true;
}
/**
@@ -239,22 +204,153 @@ module libjass.renderers {
}
}
/**
* Enable or disable the clock.
*
* @param {boolean} enabled If true, the clock is enabled, otherwise it's disabled.
* @return {boolean} True if the clock is now in the given state, false if it was already in that state.
*/
setEnabled(enabled: boolean): boolean {
if (enabled) {
return this.enable();
}
else {
return this.disable();
}
}
// EventSource members
_eventListeners: Map<ClockEvent, Function[]> = new Map<ClockEvent, Function[]>();
addEventListener: (type: ClockEvent, listener: Function) => void;
_dispatchEvent: (type: ClockEvent, args: Object[]) => void;
}
mixin(ManualClock, [EventSource]);
enum VideoState {
Playing = 0,
Paused = 1,
}
/**
* An implementation of libjass.renderers.Clock that generates play, pause and timeUpdate events according to the state of a <video> element.
*
* @param {!HTMLVideoElement} video
*/
export class VideoClock implements Clock {
private _currentTime: number = -1;
private _enabled: boolean = true;
private _videoState: VideoState;
private _nextAnimationFrameRequestId: number = null;
constructor(private _video: HTMLVideoElement) {
this._video.addEventListener("playing", () => this._onVideoPlaying(), false);
this._video.addEventListener("pause", () => this._onVideoPause(), false);
this._video.addEventListener("seeking", () => this._onVideoSeeking(), false);
}
// Clock members
/**
* @type {number}
*/
get currentTime(): number {
return this._currentTime;
}
/**
* @type {boolean}
*/
get enabled(): boolean {
return this._enabled;
}
/**
* Enable the clock.
*
* @return {boolean} True if the clock is now enabled, false if it was already enabled.
*/
enable(): boolean {
if (this._enabled) {
return false;
}
if (this._videoState !== VideoState.Paused) {
if (libjass.debugMode) {
console.warn("VideoClock.enable: Abnormal state detected. VideoClock._videoState should have been VideoState.Paused");
}
}
this._enabled = true;
this._startTicking();
return true;
}
/**
* Disable the clock.
*
* @return {boolean} True if the clock is now disabled, false if it was already disabled.
*/
disable(): boolean {
if (!this._enabled) {
return false;
}
if (this._videoState === VideoState.Playing) {
this._videoState = VideoState.Paused;
this._dispatchEvent(ClockEvent.Pause, []);
}
this._enabled = false;
this._dispatchEvent(ClockEvent.Stop, []);
this._stopTicking();
return true;
}
/**
* Toggle the clock.
*/
toggle(): void {
if (this._enabled) {
this.disable();
}
else {
this.enable();
}
}
/**
* Enable or disable the clock.
*
* @param {boolean} enabled If true, the clock is enabled, otherwise it's disabled.
* @return {boolean} True if the clock is now in the given state, false if it was already in that state.
*/
setEnabled(enabled: boolean): boolean {
if (enabled) {
return this.enable();
}
else {
return this.disable();
}
}
private _onVideoPlaying(): void {
if (!this._enabled) {
return;
}
if (this._state === VideoClockState.Playing) {
if (this._videoState === VideoState.Playing) {
return;
}
this._state = VideoClockState.Playing;
this._dispatchEvent(ClockEvent.Play, []);
if (this._nextAnimationFrameRequestId === null) {
this._timerTick();
}
this._startTicking();
}
private _onVideoPause(): void {
@@ -262,7 +358,11 @@ module libjass.renderers {
return;
}
this._state = VideoClockState.Paused;
if (this._videoState === VideoState.Paused) {
return;
}
this._videoState = VideoState.Paused;
this._dispatchEvent(ClockEvent.Pause, []);
@@ -270,14 +370,11 @@ module libjass.renderers {
if (libjass.debugMode) {
console.warn("VideoClock._onVideoPause: Abnormal state detected. VideoClock._nextAnimationFrameRequestId should not have been null");
}
return;
}
cancelAnimationFrame(this._nextAnimationFrameRequestId);
this._nextAnimationFrameRequestId = null;
if (libjass.verboseMode) {
console.log("VideoClock._onVideoPause: Cancelled VideoClock._nextAnimationFrameRequestId");
}
this._stopTicking();
}
private _onVideoSeeking(): void {
@@ -285,42 +382,58 @@ module libjass.renderers {
return;
}
if (this._currentTime === this._video.currentTime) {
return;
if (this._videoState === VideoState.Playing) {
this._videoState = VideoState.Paused;
this._dispatchEvent(ClockEvent.Pause, []);
}
if (this._state !== VideoClockState.Paused) {
if (this._currentTime === this._video.currentTime) {
return;
}
this._currentTime = this._video.currentTime;
this._dispatchEvent(ClockEvent.Play, []);
this._dispatchEvent(ClockEvent.TimeUpdate, []);
this._dispatchEvent(ClockEvent.Pause, []);
this._dispatchEvent(ClockEvent.Tick, []);
}
private _timerTick(): void {
if (this._currentTime !== this._video.currentTime) {
this._currentTime = this._video.currentTime;
if (this._state !== VideoClockState.Playing) {
this._state = VideoClockState.Playing;
this._dispatchEvent(ClockEvent.Play, []);
}
this._dispatchEvent(ClockEvent.TimeUpdate, []);
private _onTimerTick(): void {
if (!this._enabled) {
return;
}
else {
if (this._state !== VideoClockState.Paused) {
this._state = VideoClockState.Paused;
if (this._videoState === VideoState.Playing) {
if (this._currentTime !== this._video.currentTime) {
this._currentTime = this._video.currentTime;
this._dispatchEvent(ClockEvent.Tick, []);
}
else {
this._videoState = VideoState.Paused;
this._dispatchEvent(ClockEvent.Pause, []);
}
}
else {
if (this._currentTime !== this._video.currentTime) {
this._currentTime = this._video.currentTime;
this._videoState = VideoState.Playing;
this._dispatchEvent(ClockEvent.Play, []);
this._dispatchEvent(ClockEvent.Tick, []);
}
}
this._nextAnimationFrameRequestId = requestAnimationFrame(() => this._timerTick());
this._nextAnimationFrameRequestId = requestAnimationFrame(() => this._onTimerTick());
}
private _startTicking(): void {
if (this._nextAnimationFrameRequestId === null) {
this._nextAnimationFrameRequestId = requestAnimationFrame(() => this._onTimerTick());
}
}
private _stopTicking(): void {
if (this._nextAnimationFrameRequestId !== null) {
cancelAnimationFrame(this._nextAnimationFrameRequestId);
this._nextAnimationFrameRequestId = null;
}
}
// EventSource members
+81 -17
View File
@@ -41,8 +41,9 @@ module libjass.renderers {
this._settings = RendererSettings.from(settings);
this._clock.addEventListener(ClockEvent.Play, () => this._onClockPlay());
this._clock.addEventListener(ClockEvent.Tick, () => this._onClockTick());
this._clock.addEventListener(ClockEvent.Pause, () => this._onClockPause());
this._clock.addEventListener(ClockEvent.TimeUpdate, () => this._onClockTimeUpdate());
this._clock.addEventListener(ClockEvent.Stop, () => this._onClockStop());
}
/**
@@ -90,7 +91,49 @@ module libjass.renderers {
draw(dialogue: Dialogue): void { }
/**
* Runs when the clock starts playing, or is resumed from pause.
* Enable the renderer.
*
* @return {boolean} True if the renderer is now enabled, false if it was already enabled.
*/
enable(): boolean {
return this._clock.enable();
}
/**
* Disable the renderer.
*
* @return {boolean} True if the renderer is now disabled, false if it was already disabled.
*/
disable(): boolean {
return this._clock.disable();
}
/**
* Toggle the renderer.
*/
toggle(): void {
this._clock.toggle();
}
/**
* Enable or disable the renderer.
*
* @param {boolean} enabled If true, the renderer is enabled, otherwise it's disabled.
* @return {boolean} True if the renderer is now in the given state, false if it was already in that state.
*/
setEnabled(enabled: boolean): boolean {
return this._clock.setEnabled(enabled);
}
/**
* @type {boolean}
*/
get enabled(): boolean {
return this._clock.enabled;
}
/**
* Runs when the clock is enabled, or starts playing, or is resumed from pause.
*/
_onClockPlay(): void {
if (libjass.verboseMode) {
@@ -98,23 +141,14 @@ module libjass.renderers {
}
}
/**
* Runs when the clock is paused.
*/
_onClockPause(): void {
if (libjass.verboseMode) {
console.log("NullRenderer._onClockPause");
}
}
/**
* Runs when the clock's current time changed. This might be a result of either regular playback or seeking.
*/
_onClockTimeUpdate(): void {
var currentTime = this.clock.currentTime;
_onClockTick(): void {
var currentTime = this._clock.currentTime;
if (libjass.verboseMode) {
console.log("NullRenderer._onClockTimeUpdate: currentTime = " + currentTime);
console.log("NullRenderer._onClockTick: currentTime = " + currentTime);
}
for (var i = 0; i < this._ass.dialogues.length; i++) {
@@ -132,6 +166,24 @@ module libjass.renderers {
}
}
}
/**
* Runs when the clock is paused.
*/
_onClockPause(): void {
if (libjass.verboseMode) {
console.log("NullRenderer._onClockPause");
}
}
/**
* Runs when the clock is disabled.
*/
_onClockStop(): void {
if (libjass.verboseMode) {
console.log("NullRenderer._onClockStop");
}
}
}
/**
@@ -148,12 +200,23 @@ module libjass.renderers {
fontMap: Map<string, string[]>;
/**
* Subtitles will be pre-rendered for this amount of time (seconds)
* Subtitles will be pre-rendered for this amount of time (seconds).
*
* Defaults to 5.
*
* @type {number}
*/
preRenderTime: number;
/**
* Subtitle outlines will be rendered in full detail. When false, the value of blur is used to draw less outlines for better performance and (hopefully) similar output.
*
* Defaults to false.
*
* @type {boolean}
*/
preciseOutlines: boolean;
/**
* A convenience method to create a font map from a <style> or <link> element that contains @font-face rules.
*
@@ -199,13 +262,14 @@ module libjass.renderers {
* @return {!libjass.renderers.RendererSettings}
*/
static from(object: any): RendererSettings {
return RendererSettings._from(object.fontMap, object.preRenderTime);
return RendererSettings._from(object.fontMap, object.preRenderTime, object.preciseOutlines);
}
private static _from(fontMap: Map<string, string[]> = null, preRenderTime: number = 5): RendererSettings {
private static _from(fontMap: Map<string, string[]> = null, preRenderTime: number = 5, preciseOutlines: boolean = false): RendererSettings {
var result = new RendererSettings();
result.fontMap = fontMap;
result.preRenderTime = preRenderTime;
result.preciseOutlines = preciseOutlines;
return result;
}
+232 -178
View File
@@ -46,13 +46,11 @@ module libjass.renderers {
private _svgDefsElement: SVGDefsElement = null;
private _currentSubs: Map<Dialogue, HTMLDivElement> = new Map<Dialogue, HTMLDivElement>();
private _preRenderedSubs: Map<number, HTMLDivElement> = new Map<number, HTMLDivElement>();
private _preRenderedSubs: Map<number, { sub: HTMLDivElement; animationDelays: number[] }> = new Map<number, { sub: HTMLDivElement; animationDelays: number[] }>();
private _scaleX: number;
private _scaleY: number;
private _enabled: boolean = true;
constructor(ass: ASS, clock: Clock, settings: RendererSettings, private _libjassSubsWrapper: HTMLDivElement) {
super(ass, clock, settings);
@@ -84,44 +82,41 @@ module libjass.renderers {
else {
// Preload fonts
var urlsToPreload: string[] = [];
this.settings.fontMap.forEach((src: string[], name: string) => {
urlsToPreload.unshift.apply(urlsToPreload, src);
var urlsToPreload = new Set<string>();
this.settings.fontMap.forEach(srcs => {
srcs.forEach(src => urlsToPreload.add(src));
});
var urlsLeftToPreload = urlsToPreload.length;
var urlsLeftToPreload = urlsToPreload.size;
if (libjass.debugMode) {
console.log("Preloading fonts...");
}
urlsToPreload.forEach((url: string) => {
urlsToPreload.forEach(url => {
var xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.addEventListener("readystatechange", () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
xhr.addEventListener("load", () => {
if (libjass.debugMode) {
console.log("Preloaded " + url + ".");
}
--urlsLeftToPreload;
if (libjass.debugMode) {
console.log(urlsLeftToPreload + " fonts left to preload.");
}
if (urlsLeftToPreload === 0) {
if (libjass.debugMode) {
console.log("Preloaded " + url + ".");
console.log("All fonts have been preloaded.");
}
--urlsLeftToPreload;
if (libjass.debugMode) {
console.log(urlsLeftToPreload + " fonts left to preload.");
}
if (urlsLeftToPreload === 0) {
if (libjass.debugMode) {
console.log("All fonts have been preloaded.");
}
this._ready();
}
this._ready();
}
}, false);
xhr.send(null);
return xhr;
});
if (libjass.debugMode) {
@@ -144,13 +139,6 @@ module libjass.renderers {
return this._libjassSubsWrapper;
}
/**
* @type {boolean}
*/
get enabled(): boolean {
return this._enabled;
}
/**
* Resize the subtitles to the given new dimensions.
*
@@ -186,32 +174,7 @@ module libjass.renderers {
// 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._onClockTimeUpdate();
}
}
enable(): void {
this.clock.enable();
this._subsWrapper.style.display = "";
this._enabled = true;
}
disable(): void {
this.clock.disable();
this._subsWrapper.style.display = "none";
this._enabled = false;
}
toggle(): void {
if (this._enabled) {
this.disable();
}
else {
this.enable();
this._onClockTick();
}
}
@@ -230,20 +193,41 @@ module libjass.renderers {
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 - 2 * (this._scaleX * dialogue.style.marginLeft)).toFixed(3) + "px";
sub.style.minWidth = (this._subsWrapper.offsetWidth - this._scaleX * (dialogue.style.marginLeft + dialogue.style.marginRight)).toFixed(3) + "px";
var animationCollection = new AnimationCollection(this, dialogue);
var currentSpan: HTMLSpanElement = null;
var currentSpanStyles = new SpanStyles(this, dialogue, this._scaleX, this._scaleY, this._fontSizeElement, this._svgDefsElement);
var currentSpanStyles = new SpanStyles(this, dialogue, this._scaleX, this._scaleY, this.settings, this._fontSizeElement, this._svgDefsElement);
var nextSpanParent: HTMLElement = sub;
var rotationX = 0;
var rotationY = 0;
var rotationZ = 0;
var startNewSpan = (addNewLine: boolean): void => {
if (currentSpan !== null && currentSpan.textContent !== "") {
sub.appendChild(currentSpanStyles.setStylesOnSpan(currentSpan));
var newSpan = currentSpanStyles.setStylesOnSpan(currentSpan, rotationX, rotationY, rotationZ);
if (currentSpanStyles.rotationX !== rotationX || currentSpanStyles.rotationY !== rotationY || currentSpanStyles.rotationZ !== rotationZ) {
nextSpanParent.appendChild(newSpan);
nextSpanParent = newSpan;
rotationX = currentSpanStyles.rotationX;
rotationY = currentSpanStyles.rotationY;
rotationZ = currentSpanStyles.rotationZ;
}
else {
nextSpanParent.appendChild(newSpan.firstElementChild);
}
}
if (addNewLine) {
sub.appendChild(document.createElement("br"));
nextSpanParent = sub;
nextSpanParent.appendChild(document.createElement("br"));
rotationX = 0;
rotationY = 0;
rotationZ = 0;
}
currentSpan = document.createElement("span");
@@ -313,6 +297,14 @@ module libjass.renderers {
currentSpanStyles.fontSize = (<parts.FontSize>part).value;
}
else if (part instanceof parts.FontSizePlus) {
currentSpanStyles.fontSize += (<parts.FontSizePlus>part).value;
}
else if (part instanceof parts.FontSizeMinus) {
currentSpanStyles.fontSize -= (<parts.FontSizeMinus>part).value;
}
else if (part instanceof parts.FontScaleX) {
currentSpanStyles.fontScaleX = (<parts.FontScaleX>part).value;
}
@@ -396,7 +388,7 @@ module libjass.renderers {
var newStyleName = (<parts.Reset>part).value;
var newStyle: Style = null;
if (newStyleName !== null) {
newStyle = this.ass.styles[newStyleName];
newStyle = this.ass.styles.get(newStyleName);
}
currentSpanStyles.reset(newStyle);
}
@@ -413,51 +405,51 @@ module libjass.renderers {
var movePart = <parts.Move>part;
sub.style.position = "absolute";
animationCollection.add("linear", [new Keyframe(0, {
left: (this._scaleX * movePart.x1).toFixed(3) + "px",
top: (this._scaleY * movePart.y1).toFixed(3) + "px"
}), new Keyframe(movePart.t1, {
left: (this._scaleX * movePart.x1).toFixed(3) + "px",
top: (this._scaleY * movePart.y1).toFixed(3) + "px"
}), new Keyframe(movePart.t2, {
left: (this._scaleX * movePart.x2).toFixed(3) + "px",
top: (this._scaleY * movePart.y2).toFixed(3) + "px"
}), new Keyframe(dialogue.end - dialogue.start, {
left: (this._scaleX * movePart.x2).toFixed(3) + "px",
top: (this._scaleY * movePart.y2).toFixed(3) + "px"
})]);
animationCollection.add("linear", [new Keyframe(0, new Map<string, string>()
.set("left", (this._scaleX * movePart.x1).toFixed(3) + "px")
.set("top", (this._scaleY * movePart.y1).toFixed(3) + "px")
), new Keyframe(movePart.t1, new Map<string, string>()
.set("left", (this._scaleX * movePart.x1).toFixed(3) + "px")
.set("top", (this._scaleY * movePart.y1).toFixed(3) + "px")
), new Keyframe(movePart.t2, new Map<string, string>()
.set("left", (this._scaleX * movePart.x2).toFixed(3) + "px")
.set("top", (this._scaleY * movePart.y2).toFixed(3) + "px")
), new Keyframe(dialogue.end - dialogue.start, new Map<string, string>()
.set("left", (this._scaleX * movePart.x2).toFixed(3) + "px")
.set("top", (this._scaleY * movePart.y2).toFixed(3) + "px")
)]);
}
else if (part instanceof parts.Fade) {
var fadePart = <parts.Fade>part;
animationCollection.add("linear", [new Keyframe(0, {
opacity: "0"
}), new Keyframe(fadePart.start, {
opacity: "1"
}), new Keyframe(dialogue.end - dialogue.start - fadePart.end, {
opacity: String("1")
}), new Keyframe(dialogue.end - dialogue.start, {
opacity: "0"
})]);
animationCollection.add("linear", [new Keyframe(0, new Map<string, string>()
.set("opacity", "0")
), new Keyframe(fadePart.start, new Map<string, string>()
.set("opacity", "1")
), new Keyframe(dialogue.end - dialogue.start - fadePart.end, new Map<string, string>()
.set("opacity", "1")
), new Keyframe(dialogue.end - dialogue.start, new Map<string, string>()
.set("opacity", "0")
)]);
}
else if (part instanceof parts.ComplexFade) {
var complexFadePart = <parts.ComplexFade>part;
animationCollection.add("linear", [new Keyframe(0, {
opacity: String(complexFadePart.a1)
}), new Keyframe(complexFadePart.t1, {
opacity: String(complexFadePart.a1)
}), new Keyframe(complexFadePart.t2, {
opacity: String(complexFadePart.a2)
}), new Keyframe(complexFadePart.t3, {
opacity: String(complexFadePart.a2)
}), new Keyframe(complexFadePart.t4, {
opacity: String(complexFadePart.a3)
}), new Keyframe(dialogue.end - dialogue.start, {
opacity: String(complexFadePart.a3)
})]);
animationCollection.add("linear", [new Keyframe(0, new Map<string, string>()
.set("opacity", complexFadePart.a1.toFixed(3))
), new Keyframe(complexFadePart.t1, new Map<string, string>()
.set("opacity", complexFadePart.a1.toFixed(3))
), new Keyframe(complexFadePart.t2, new Map<string, string>()
.set("opacity", complexFadePart.a2.toFixed(3))
), new Keyframe(complexFadePart.t3, new Map<string, string>()
.set("opacity", complexFadePart.a2.toFixed(3))
), new Keyframe(complexFadePart.t4, new Map<string, string>()
.set("opacity", complexFadePart.a3.toFixed(3))
), new Keyframe(dialogue.end - dialogue.start, new Map<string, string>()
.set("opacity", complexFadePart.a3.toFixed(3))
)]);
}
else if (part instanceof parts.DrawingMode) {
@@ -493,7 +485,7 @@ module libjass.renderers {
dialogue.parts.some(part => {
if (part instanceof parts.Position || part instanceof parts.Move) {
var transformOrigin = DefaultRenderer._transformOrigins[dialogue.alignment];
var 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] + "%";
@@ -522,6 +514,15 @@ module libjass.renderers {
break;
}
if (sub.style.position !== "") {
// Explicitly set text alignment on absolutely-positioned subs because they'll go in a .an0 <div> and so won't get alignment CSS text-align.
switch (dialogue.alignment) {
case 1: case 4: case 7: sub.style.textAlign = "left"; break;
case 2: case 5: case 8: sub.style.textAlign = "center"; break;
case 3: case 6: case 9: sub.style.textAlign = "right"; break;
}
}
if (this._animationStyleElement === null) {
this._animationStyleElement = document.createElement("style");
this._animationStyleElement.id = "libjass-animation-styles-" + this.id;
@@ -536,7 +537,7 @@ module libjass.renderers {
sub.setAttribute("data-dialogue-id", this.id + "-" + dialogue.id);
this._preRenderedSubs.set(dialogue.id, sub);
this._preRenderedSubs.set(dialogue.id, { sub: sub, animationDelays: animationCollection.animationDelays });
}
/**
@@ -569,22 +570,11 @@ module libjass.renderers {
}
}
var result = <HTMLDivElement>preRenderedSub.cloneNode(true);
var result = <HTMLDivElement>preRenderedSub.sub.cloneNode(true);
var defaultAnimationDelay = result.style.webkitAnimationDelay;
if (defaultAnimationDelay === undefined) {
defaultAnimationDelay = result.style.animationDelay;
}
if (defaultAnimationDelay !== "") {
var animationDelay =
defaultAnimationDelay
.split(",")
.map(delay => (parseFloat(delay) + dialogue.start - this.clock.currentTime).toFixed(3) + "s")
.join(",");
result.style.webkitAnimationDelay = animationDelay;
result.style.animationDelay = animationDelay;
}
var animationDelay = preRenderedSub.animationDelays.map(delay => (delay + dialogue.start - this.clock.currentTime).toFixed(3) + "s").join(", ");
result.style.webkitAnimationDelay = animationDelay;
result.style.animationDelay = animationDelay;
var layer = dialogue.layer;
var alignment = (result.style.position === "absolute") ? 0 : dialogue.alignment; // Alignment 0 is for absolutely-positioned subs
@@ -636,26 +626,36 @@ module libjass.renderers {
this._removeAllSubs();
this._subsWrapper.style.display = "";
this._subsWrapper.classList.remove("paused");
}
_onClockTick(): void {
// Remove dialogues that should be removed before adding new ones via super._onClockTick()
var currentTime = this.clock.currentTime;
this._currentSubs.forEach((sub: HTMLDivElement, dialogue: Dialogue) => {
if (dialogue.start > currentTime || dialogue.end < currentTime) {
this._currentSubs.delete(dialogue);
this._removeSub(sub);
}
});
super._onClockTick();
}
_onClockPause(): void {
super._onClockPause();
this._subsWrapper.classList.add("paused");
}
_onClockTimeUpdate(): void {
var currentTime = this.clock.currentTime;
_onClockStop(): void {
super._onClockStop();
super._onClockTimeUpdate();
this._currentSubs.forEach((sub: HTMLDivElement, dialogue: Dialogue) => {
if (dialogue.start > currentTime || dialogue.end < currentTime) {
this._currentSubs.delete(dialogue);
this._removeSub(sub);
}
});
this._subsWrapper.style.display = "none";
}
_ready(): void {
@@ -685,10 +685,6 @@ module libjass.renderers {
}
mixin(WebRenderer, [EventSource]);
interface KeyframePropertiesMap {
[key: string]: string;
}
/**
* This class represents a single keyframe. It has a list of CSS properties (names and values) associated with a point in time. Multiple keyframes make up an animation.
*
@@ -696,7 +692,7 @@ module libjass.renderers {
* @param {!Object.<string, string>} properties
*/
class Keyframe {
constructor(private _time: number, private _properties: KeyframePropertiesMap) { }
constructor(private _time: number, private _properties: Map<string, string>) { }
/**
* @type {number}
@@ -708,7 +704,7 @@ module libjass.renderers {
/**
* @type {!Object.<string, string>}
*/
get properties(): KeyframePropertiesMap {
get properties(): Map<string, string> {
return this._properties;
}
}
@@ -727,6 +723,7 @@ module libjass.renderers {
private _cssText: string = "";
private _animationStyle: string = "";
private _animationDelays: number[] = [];
private _numAnimations: number = 0;
constructor(renderer: NullRenderer, dialogue: Dialogue) {
@@ -753,6 +750,13 @@ module libjass.renderers {
return this._animationStyle;
}
/**
* This array should be used to set the "animation-delay" CSS property of the target element.
*/
get animationDelays(): number[] {
return this._animationDelays;
}
/**
* Add an animation to this collection. The given keyframes together make one animation.
*
@@ -774,8 +778,8 @@ module libjass.renderers {
ruleCssText += "\t" + (100 * keyframe.time / (this._end - this._start)).toFixed(3) + "% {\n";
Object.keys(keyframe.properties).forEach(propertyName => {
ruleCssText += "\t\t" + propertyName + ": " + keyframe.properties[propertyName] + ";\n";
keyframe.properties.forEach((value, name) => {
ruleCssText += "\t\t" + name + ": " + value + ";\n";
});
ruleCssText += "\t}\n";
@@ -791,7 +795,8 @@ module libjass.renderers {
this._animationStyle += ",";
}
this._animationStyle += animationName + " " + (endTime - startTime).toFixed(3) + "s " + timingFunction + " " + startTime.toFixed(3) + "s";
this._animationStyle += animationName + " " + (endTime - startTime).toFixed(3) + "s " + timingFunction;
this._animationDelays.push(startTime);
}
}
@@ -803,6 +808,7 @@ module libjass.renderers {
* @param {!libjass.Dialogue} dialogue The Dialogue that this set of styles is associated with
* @param {number} scaleX The horizontal scaling of the subtitles
* @param {number} scaleY The vertical scaling of the subtitles
* @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
*/
@@ -853,7 +859,7 @@ module libjass.renderers {
private _nextFilterId = 0;
constructor(renderer: NullRenderer, dialogue: Dialogue, private _scaleX: number, private _scaleY: number, 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) {
this._id = renderer.id + "-" + dialogue.id;
this._defaultStyle = dialogue.style;
@@ -889,8 +895,8 @@ module libjass.renderers {
this.letterSpacing = newStyle.letterSpacing;
this._rotationX = null;
this._rotationY = null;
this._rotationX = 0;
this._rotationY = 0;
this._rotationZ = newStyle.rotationZ;
this._skewX = null;
@@ -916,7 +922,7 @@ module libjass.renderers {
* @param {!HTMLSpanElement} span
* @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): HTMLSpanElement {
setStylesOnSpan(span: HTMLSpanElement, previousRotationX: number, previousRotationY: number, previousRotationZ: number): HTMLSpanElement {
var isTextOnlySpan = span.childNodes[0] instanceof Text;
var fontStyleOrWeight = "";
@@ -962,15 +968,6 @@ module libjass.renderers {
transform += "scaleY(" + this._fontScaleY + ") ";
}
}
if (this._rotationY !== null) {
transform += "rotateY(" + this._rotationY + "deg) ";
}
if (this._rotationX !== null) {
transform += "rotateX(" + this._rotationX + "deg) ";
}
if (this._rotationZ !== 0) {
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);
@@ -991,53 +988,66 @@ module libjass.renderers {
var outlineColor = this._outlineColor.withAlpha(this._outlineAlpha);
var outlineWidth = (this._scaleX * this._outlineWidth);
var outlineHeight = (this._scaleY * this._outlineHeight);
var outlineWidth = this._scaleX * this._outlineWidth;
var outlineHeight = this._scaleY * this._outlineHeight;
var filterId = "svg-filter-" + this._id + "-" + this._nextFilterId++;
var outlineFilter = '';
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 radiiPairs: number[][] = [];
var outlineNumber = 0;
if (outlineWidth >= outlineHeight) {
if (outlineHeight > 0) {
for (var y = 0; y <= outlineHeight; y++) {
radiiPairs.push([outlineWidth / outlineHeight * Math.sqrt(outlineHeight * outlineHeight - y * y), y]);
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 {
radiiPairs.push([outlineWidth, 0]);
}
}
else {
if (outlineWidth > 0) {
for (var x = 0; x <= outlineWidth; x++) {
radiiPairs.push([x, outlineHeight / outlineWidth * Math.sqrt(outlineWidth * outlineWidth - x * x)]);
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);
}
}
else {
radiiPairs.push([0, outlineHeight]);
}
}
radiiPairs.forEach((radii, index) => {
})((x: number, y: number): void => {
outlineFilter +=
'\t<feMorphology in="SourceAlpha" operator="dilate" radius="' + radii[0].toFixed(3) + ' ' + radii[1].toFixed(3) + '" result="outline' + index + '" />\n';
'\t<feMorphology in="SourceAlpha" operator="dilate" radius="' + x.toFixed(3) + ' ' + y.toFixed(3) + '" result="outline' + outlineNumber + '" />\n';
mergeOutlinesFilter +=
'\t\t<feMergeNode in="outline' + index + '" />\n';
'\t\t<feMergeNode in="outline' + outlineNumber + '" />\n';
outlineNumber++;
});
outlineFilter =
'\t<feFlood flood-color="' + outlineColor.toString() + '" result="outlineColor" />' +
outlineFilter +
'\t<feMerge>\n' +
outlineFilter +=
'\t<feMerge result="outline">\n' +
mergeOutlinesFilter +
'\t</feMerge>\n' +
'\t<feComposite operator="in" in="outlineColor" />';
'\t<feFlood flood-color="' + outlineColor.toString() + '" />' +
'\t<feComposite operator="in" in2="outline" />';
}
var blurFilter = '';
@@ -1055,7 +1065,7 @@ module libjass.renderers {
if (outlineFilter !== '' || blurFilter !== '') {
var filterString =
'<filter xmlns="http://www.w3.org/2000/svg" id="' + filterId + '">\n' +
'<filter xmlns="http://www.w3.org/2000/svg" id="' + filterId + '" x="-50%" width="200%" y="-50%" height="200%">\n' +
outlineFilter +
blurFilter +
'\t<feMerge>\n' +
@@ -1082,7 +1092,39 @@ module libjass.renderers {
filterWrapperSpan.style.display = "inline-block";
}
return filterWrapperSpan;
var rotationWrapperSpan = document.createElement("span");
rotationWrapperSpan.appendChild(filterWrapperSpan);
var rotationTransform = "";
if (this._rotationX !== previousRotationX || this._rotationY !== previousRotationY || this._rotationZ !== previousRotationZ) {
if (previousRotationZ !== 0) {
rotationTransform += "rotateZ(" + (previousRotationZ) + "deg) ";
}
if (previousRotationX !== 0) {
rotationTransform += "rotateX(" + (-previousRotationX) + "deg) ";
}
if (previousRotationY !== 0) {
rotationTransform += "rotateY(" + (-previousRotationY) + "deg) ";
}
if (this._rotationY !== 0) {
rotationTransform += "rotateY(" + this._rotationY + "deg) ";
}
if (this._rotationX !== 0) {
rotationTransform += "rotateX(" + this._rotationX + "deg) ";
}
if (this._rotationZ !== 0) {
rotationTransform += "rotateZ(" + (-this._rotationZ) + "deg) ";
}
}
if (rotationTransform !== "") {
rotationWrapperSpan.style.webkitTransform = rotationTransform;
rotationWrapperSpan.style.webkitTransformOrigin = "50% 50%";
rotationWrapperSpan.style.transform = rotationTransform;
rotationWrapperSpan.style.transformOrigin = "50% 50%";
rotationWrapperSpan.style.display = "inline-block";
}
return rotationWrapperSpan;
}
/**
@@ -1220,13 +1262,21 @@ module libjass.renderers {
this._letterSpacing = SpanStyles._valueOrDefault(value, this._defaultStyle.letterSpacing);
}
get rotationX() {
return this._rotationX;
}
/**
* Sets the X-axis rotation property.
*
* @type {?number}
*/
set rotationX(value: number) {
this._rotationX = value;
this._rotationX = SpanStyles._valueOrDefault(value, 0);
}
get rotationY() {
return this._rotationY;
}
/**
@@ -1235,7 +1285,11 @@ module libjass.renderers {
* @type {?number}
*/
set rotationY(value: number) {
this._rotationY = value;
this._rotationY = SpanStyles._valueOrDefault(value, 0);
}
get rotationZ() {
return this._rotationZ;
}
/**
+70 -15
View File
@@ -48,6 +48,8 @@ var require = function (name) {
var Suite = function (name, parent) {
this.name = name;
this.setup = function () { };
this.children = [];
this.parent = parent;
@@ -98,12 +100,25 @@ var require = function (name) {
}, enumerable: true },
});
Suite.prototype.run = function (logger) {
Suite.prototype.run = function (logger, done) {
var _this = this;
logger.beginSuite(this);
this.children.forEach(function (child) { child.run(logger); });
this.setup();
logger.endSuite(this);
var remainingChildren = this.children.slice();
var runChild = function () {
if (remainingChildren.length > 0) {
var nextChild = remainingChildren.shift();
nextChild.run(logger, runChild);
}
else {
logger.endSuite(_this);
done();
}
};
runChild();
};
var Test = function (description, body, parent) {
@@ -118,21 +133,49 @@ var require = function (name) {
this.exception = null;
};
Test.prototype.run = function (logger) {
Test.prototype.run = function (logger, done) {
if (this.body) {
this.passed = false;
try {
this.body();
if (this.body.length === 0) {
try {
this.body();
this.passed = true;
this.passed = true;
}
catch (testException) {
this.exception = testException;
}
logger.writeTest(this);
done();
}
catch (testException) {
this.exception = testException;
else {
try {
var _this = this;
this.body(function (testException) {
if (testException !== undefined && testException !== null) {
_this.exception = testException;
}
else {
_this.passed = true;
}
logger.writeTest(_this);
done();
});
}
catch (testException) {
this.exception = testException;
logger.writeTest(this);
done();
}
}
}
logger.writeTest(this);
};
var currentSuite = null;
@@ -147,6 +190,10 @@ var require = function (name) {
return newSuite;
};
setup = function (body) {
currentSuite.setup = body;
}
test = function (description, body) {
return new Test(description, body, currentSuite);
};
@@ -284,7 +331,7 @@ var Logger = function (outputDiv) {
this.writeTest = function (test) {
if (test.passed === true) {
var message = "Test " + test.number + " - \"" + test.description + "\" - " + test.customProperties.rule + " [ " + test.customProperties.input + " ] ";
var message = "Test " + test.number + " - \"" + test.description + "\" - " + test.customProperties.rule + " [ " + test.customProperties.input + " ]";
console.log(message);
append(testDiv, message).className += " passed";
}
@@ -345,7 +392,15 @@ var Logger = function (outputDiv) {
addEventListener("DOMContentLoaded", function () {
var logger = new Logger(document.querySelector("#output"));
rootSuites.forEach(function (suite) { suite.run(logger); });
logger.writeTotal(rootSuites);
var remainingSuites = rootSuites.slice();
var runSuite = function () {
if (remainingSuites.length > 0) {
var nextSuite = remainingSuites.shift();
nextSuite.run(logger, runSuite);
}
else {
logger.writeTotal(rootSuites);
}
};
runSuite();
}, false);
+1
View File
@@ -39,6 +39,7 @@
<script src="test-primitives.js" />
<script src="test-tags.js" />
<script src="test-miscellaneous.js" />
<script src="test-webworker.js" />
</head>
<body>
<div id="output" />
+22
View File
@@ -56,6 +56,14 @@ suite("Tags", function () {
parserTest("Enclosed tag", "{\\b150}", "enclosedTags", [new libjass.parts.Bold(true), new libjass.parts.Comment("50")]);
});
suite("Italics tag - \\i", function () {
parserTest("True", "i1", "tag_i", new libjass.parts.Italic(true));
parserTest("False", "i0", "tag_i", new libjass.parts.Italic(false));
parserTest("null", "i", "tag_i", new libjass.parts.Italic(null));
});
suite("Primary color tags - \\c and \\1c", function () {
parserTest("Just the tag", "1c&H3F171F&", "tag_1c", new libjass.parts.PrimaryColor(new libjass.parts.Color(31, 23, 63, 1)));
@@ -72,6 +80,20 @@ suite("Tags", function () {
parserTest("Just the tag", "alpha00", "tag_alpha", new libjass.parts.Alpha(1));
});
suite("Font size tag - \\fs", function () {
parserTest("5", "fs5", "tag_fs", new libjass.parts.FontSize(5));
parserTest("null", "fs", "tag_fs", new libjass.parts.FontSize(null));
parserTest("Plus 5", "fs+5", "tag_fsplus", new libjass.parts.FontSizePlus(5));
parserTest("Plus null", "fs+", "tag_fsplus", null);
parserTest("Minus 5", "fs-5", "tag_fsminus", new libjass.parts.FontSizeMinus(5));
parserTest("Minus null", "fs-", "tag_fsminus", null);
});
suite("Transform tag - \\t", function () {
parserTest("No tags", "{\\t(100,200)}", "enclosedTags", [new libjass.parts.Comment("\\t(100,200)")]);
});
+65
View File
@@ -0,0 +1,65 @@
/**
* 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 libjass = require("../libjass.js");
var assert = require("assert");
suite("Web worker", function () {
var workerChannel = null;
setup(function () {
workerChannel = libjass.webworker.createWorker();
});
suite("Parse", function () {
if (libjass.webworker.supported) {
test("Parse", function (done) {
this.customProperties = {
rule: "dialogueParts",
input: "{\\an8}Are {\\i1}you{\\i0} the one who stole the clock?!"
};
workerChannel.request(libjass.webworker.WorkerCommands.Parse, {
input: this.customProperties.input,
rule: this.customProperties.rule
}).then(function (promise) {
try {
assert.deepEqual(promise.result, [
new libjass.parts.Alignment(8),
new libjass.parts.Text("Are "),
new libjass.parts.Italic(true),
new libjass.parts.Text("you"),
new libjass.parts.Italic(false),
new libjass.parts.Text(" the one who stole the clock?!")
]);
done();
}
catch (ex) {
done(ex);
}
});
});
}
else {
test("Parse: Web workers not supported in this environment.");
}
});
});
+109 -87
View File
@@ -21,17 +21,14 @@
///<reference path="libjass.ts" />
module libjass {
export interface StyleMap {
[name: string]: Style;
}
/**
* This class represents an ASS script. It contains the script properties, an array of Styles, and an array of Dialogues.
*/
export class ASS {
private _properties: ScriptProperties = new ScriptProperties();
private _styles: StyleMap = Object.create(null);
private _styles: Map<string, Style> = new Map<string, Style>();
private _dialogues: Dialogue[] = [];
private _dialoguesFormatSpecifier: string[];
/**
* The properties of this script.
@@ -45,9 +42,9 @@ module libjass {
/**
* The styles in this script.
*
* @type {!Object.<string, !libjass.Style>}
* @type {!Map.<string, !libjass.Style>}
*/
get styles(): StyleMap {
get styles(): Map<string, Style> {
return this._styles;
}
@@ -67,6 +64,32 @@ module libjass {
}
}
/**
* Add an event to this ASS script.
*
* @param {string} line The line from the script that contains the new event.
*/
addEvent(line: string): void {
var typedTemplateArray = <TypedTemplateArray>[];
typedTemplateArray.formatSpecifier = this._dialoguesFormatSpecifier;
var typedTemplate = <TypedTemplate>parser.parse(line, "assScriptProperty", new parser.ParseNode(null, { contents: typedTemplateArray }));
this._addEvent(typedTemplate);
}
private _addEvent(line: TypedTemplate) {
if (line.type === "Dialogue") {
var dialogueTemplate = line.template;
if (libjass.verboseMode) {
console.log("Read dialogue: " + JSON.stringify(dialogueTemplate), dialogueTemplate);
}
// Create the dialogue and add it to the dialogues array
this.dialogues.push(new Dialogue(dialogueTemplate, this));
}
}
/**
* Creates an ASS object from the raw text of an ASS script.
*
@@ -88,27 +111,27 @@ module libjass {
}
private static _fromASSString(rawASS: string): ASS {
var script = parser.parse(rawASS, "assScript");
var script = <Map<string, any>>parser.parse(rawASS, "assScript");
var result = new ASS();
// Get the script info template
var infoTemplate: Template = script["Script Info"];
var infoTemplate: Map<string, string> = script.get("Script Info");
if (libjass.verboseMode) {
console.log("Read script info: " + JSON.stringify(infoTemplate), infoTemplate);
}
// Parse the script properties
result.properties.resolutionX = parseInt(infoTemplate["PlayResX"]);
result.properties.resolutionY = parseInt(infoTemplate["PlayResY"]);
result.properties.wrappingStyle = parseInt(infoTemplate["WrapStyle"]);
result.properties.scaleBorderAndShadow = (infoTemplate["ScaledBorderAndShadow"] === "yes");
result.properties.resolutionX = parseInt(infoTemplate.get("PlayResX"));
result.properties.resolutionY = parseInt(infoTemplate.get("PlayResY"));
result.properties.wrappingStyle = parseInt(infoTemplate.get("WrapStyle"));
result.properties.scaleBorderAndShadow = (infoTemplate.get("ScaledBorderAndShadow") === "yes");
// Get styles from the styles section
script["V4+ Styles"].forEach((line: any) => {
(<TypedTemplateArray>script.get("V4+ Styles")).forEach(line => {
if (line.type === "Style") {
var styleTemplate: Template = line.template;
var styleTemplate = line.template;
if (libjass.verboseMode) {
console.log("Read style: " + JSON.stringify(styleTemplate), styleTemplate);
@@ -116,23 +139,14 @@ module libjass {
// Create the style and add it to the styles map
var newStyle = new Style(styleTemplate);
result.styles[newStyle.name] = newStyle;
result.styles.set(newStyle.name, newStyle);
}
});
// Get dialogues from the events section
script["Events"].forEach((line: any) => {
if (line.type === "Dialogue") {
var dialogueTemplate: Template = line.template;
if (libjass.verboseMode) {
console.log("Read dialogue: " + JSON.stringify(dialogueTemplate), dialogueTemplate);
}
// Create the dialogue and add it to the dialogues array
result.dialogues.push(new Dialogue(dialogueTemplate, result));
}
});
var events = <TypedTemplateArray>script.get("Events");
result._dialoguesFormatSpecifier = events.formatSpecifier;
events.forEach(line => result._addEvent(line));
return result;
}
@@ -148,36 +162,36 @@ module libjass {
result.properties.wrappingStyle = 1;
result.properties.scaleBorderAndShadow = true;
var newStyle = new Style({
"Name": "Default",
"Italic": "0", "Bold": "0", "Underline": "0", "StrikeOut": "0",
"Fontname": "", "Fontsize": "50",
"ScaleX": "100", "ScaleY": "100",
"Spacing": "0",
"Angle": "0",
"PrimaryColour": "&H0000FFFF", "SecondaryColour": "&H00000000", "OutlineColour": "&H00000000", "BackColour": "&H00000000",
"Outline": "1", "BorderStyle": "1",
"Shadow": "1",
"Alignment": "2",
"MarginL": "80", "MarginR": "80", "MarginV": "35"
});
result.styles[newStyle.name] = newStyle;
var newStyle = new Style(new Map<string, string>()
.set("Name", "Default")
.set("Italic", "0").set("Bold", "0").set("Underline", "0").set("StrikeOut", "0")
.set("Fontname", "").set("Fontsize", "50")
.set("ScaleX", "100").set("ScaleY", "100")
.set("Spacing", "0")
.set("Angle", "0")
.set("PrimaryColour", "&H0000FFFF").set("SecondaryColour", "&H00000000").set("OutlineColour", "&H00000000").set("BackColour", "&H00000000")
.set("Outline", "1").set("BorderStyle", "1")
.set("Shadow", "1")
.set("Alignment", "2")
.set("MarginL", "80").set("MarginR", "80").set("MarginV", "35")
);
result.styles.set(newStyle.name, newStyle);
script.sort((line1, line2) => line1["number"] - line2["number"]).forEach(line => {
result.dialogues.push(new Dialogue({
"Style": "Default",
"Start": line.start, "End": line.end,
"Layer": "0",
"Text":
result.dialogues.push(new Dialogue(new Map<string, string>()
.set("Style", "Default")
.set("Start", line.start).set("End", line.end)
.set("Layer", "0")
.set("Text",
(<string>line.text)
.replace("<b>", "{\\b1}").replace("</b>", "{\\b0}")
.replace("<i>", "{\\i1}").replace("</i>", "{\\i0}")
.replace("<u>", "{\\u1}").replace("</u>", "{\\u0}")
.replace(/<b>/g, "{\\b1}").replace(/<\/b>/g, "{\\b0}")
.replace(/<i>/g, "{\\i1}").replace(/<\/i>/g, "{\\i0}")
.replace(/<u>/g, "{\\u1}").replace(/<\/u>/g, "{\\u0}")
.replace(
/<font color="#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})">/g,
(substring: string, red: string, green: string, blue: string) => "{\c&H" + blue + green + red + "&}"
).replace("</font>", "{\\c}")
}, result));
(/* ujs:unreferenced */ substring: string, red: string, green: string, blue: string) => "{\c&H" + blue + green + red + "&}"
).replace(/<\/font>/g, "{\\c}")
), result));
});
return result;
@@ -286,7 +300,7 @@ module libjass {
/**
* This class represents a single global style declaration in an ASS script. The styles can be obtained via the ASS.styles property.
*
* @param {!Object} template The template object that contains the style's properties. It is a map of the string values read from the ASS file.
* @param {!Map.<string, string>} template The template object that contains the style's properties. It is a map of the string values read from the ASS file.
* @param {string} template["Name"] The name of the style
* @param {string} template["Italic"] -1 if the style is italicized
* @param {string} template["Bold"] -1 if the style is bold
@@ -341,39 +355,39 @@ module libjass {
private _marginRight: number;
private _marginVertical: number;
constructor(template: Template) {
this._name = template["Name"];
constructor(template: Map<string, string>) {
this._name = template.get("Name");
this._italic = template["Italic"] === "-1";
this._bold = template["Bold"] === "-1";
this._underline = template["Underline"] === "-1";
this._strikeThrough = template["StrikeOut"] === "-1";
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._fontName = template["Fontname"];
this._fontSize = parseFloat(template["Fontsize"]);
this._fontName = template.get("Fontname");
this._fontSize = parseFloat(template.get("Fontsize"));
this._fontScaleX = parseFloat(template["ScaleX"]) / 100;
this._fontScaleY = parseFloat(template["ScaleY"]) / 100;
this._fontScaleX = parseFloat(template.get("ScaleX")) / 100;
this._fontScaleY = parseFloat(template.get("ScaleY")) / 100;
this._letterSpacing = parseFloat(template["Spacing"]);
this._letterSpacing = parseFloat(template.get("Spacing"));
this._rotationZ = parseFloat(template["Angle"]);
this._rotationZ = parseFloat(template.get("Angle"));
this._primaryColor = <parts.Color>parser.parse(template["PrimaryColour"], "colorWithAlpha");
this._secondaryColor = <parts.Color>parser.parse(template["SecondaryColour"], "colorWithAlpha");
this._outlineColor = <parts.Color>parser.parse(template["OutlineColour"], "colorWithAlpha");
this._shadowColor = <parts.Color>parser.parse(template["BackColour"], "colorWithAlpha");
this._primaryColor = <parts.Color>parser.parse(template.get("PrimaryColour"), "colorWithAlpha");
this._secondaryColor = <parts.Color>parser.parse(template.get("SecondaryColour"), "colorWithAlpha");
this._outlineColor = <parts.Color>parser.parse(template.get("OutlineColour"), "colorWithAlpha");
this._shadowColor = <parts.Color>parser.parse(template.get("BackColour"), "colorWithAlpha");
this._outlineThickness = parseFloat(template["Outline"]);
this._borderStyle = parseInt(template["BorderStyle"]);
this._outlineThickness = parseFloat(template.get("Outline"));
this._borderStyle = parseInt(template.get("BorderStyle"));
this._shadowDepth = parseFloat(template["Shadow"]);
this._shadowDepth = parseFloat(template.get("Shadow"));
this._alignment = parseInt(template["Alignment"]);
this._alignment = parseInt(template.get("Alignment"));
this._marginLeft = parseFloat(template["MarginL"]);
this._marginRight = parseFloat(template["MarginR"]);
this._marginVertical = parseFloat(template["MarginV"]);
this._marginLeft = parseFloat(template.get("MarginL"));
this._marginRight = parseFloat(template.get("MarginR"));
this._marginVertical = parseFloat(template.get("MarginV"));
}
/**
@@ -578,7 +592,7 @@ module libjass {
/**
* This class represents a dialogue in an ASS script.
*
* @param {!Object} template The template object that contains the dialogue's properties. It is a map of the string values read from the ASS file.
* @param {!Map.<string, string>} template The template object that contains the dialogue's properties. It is a map of the string values read from the ASS file.
* @param {string} template["Style"] The name of the default style of this dialogue
* @param {string} template["Start"] The start time
* @param {string} template["End"] The end time
@@ -604,17 +618,17 @@ module libjass {
private _sub: HTMLDivElement = null;
constructor(template: Template, ass: ASS) {
constructor(template: Map<string, string>, ass: ASS) {
this._id = ++Dialogue._lastDialogueId;
this._style = ass.styles[template["Style"]];
this._style = ass.styles.get(template.get("Style"));
this._start = Dialogue._toTime(template["Start"]);
this._end = Dialogue._toTime(template["End"]);
this._start = Dialogue._toTime(template.get("Start"));
this._end = Dialogue._toTime(template.get("End"));
this._layer = Math.max(parseInt(template["Layer"]), 0);
this._layer = Math.max(parseInt(template.get("Layer")), 0);
this._rawPartsString = template["Text"];
this._rawPartsString = template.get("Text");
}
/**
@@ -755,10 +769,18 @@ module libjass {
}
/**
* A template object. It is a map of string keys and string values.
* A template object with a particular type.
*/
export interface Template {
[key: string]: string;
export interface TypedTemplate {
type: string;
template: Map<string, string>;
}
/**
* An array of TypedTemplate objects with the format specifier that was used to parse them.
*/
export interface TypedTemplateArray extends Array<TypedTemplate> {
formatSpecifier: string[];
}
/**
+33 -23
View File
@@ -22,6 +22,13 @@
"use strict";
interface Global {
Set: { new <T>(): Set<T>; prototype: Set<any> }
Map: { new <K, V>(): Map<K, V>; prototype: Map<any, any> }
}
declare var global: Global; // Defined as a parameter of the anonymous function wrapper
module libjass {
/**
* Set implementation for browsers that don't support it. Only supports Number and String elements.
@@ -32,6 +39,7 @@ module libjass {
*/
class SimpleSet<T> implements Set<T> {
private _elements: { [key: string]: T };
private _size: number;
constructor() {
this.clear();
@@ -48,6 +56,10 @@ module libjass {
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;
@@ -57,6 +69,7 @@ module libjass {
*/
clear(): void {
this._elements = Object.create(null);
this._size = 0;
}
/**
@@ -87,8 +100,11 @@ module libjass {
throw new Error("This Set implementation doesn't support delete().");
}
/**
* @type {number}
*/
get size(): number {
throw new Error("This Set implementation doesn't support size.");
return this._size;
}
private _toProperty(value: T): string {
@@ -109,16 +125,9 @@ module libjass {
*
* @type {function(new:Set)}
*/
export var Set: {
new <T>(): Set<T>;
} = null;
// Use this browser's implementation of Set if it has one
if (global.Set !== undefined && typeof global.Set.prototype.forEach === "function") {
Set = global.Set;
}
else {
Set = SimpleSet;
export var Set = global.Set;
if (Set === undefined || typeof Set.prototype.forEach !== "function") {
Set = <any>SimpleSet;
}
/**
@@ -131,6 +140,7 @@ module libjass {
class SimpleMap<K, V> implements Map<K, V> {
private _keys: { [key: string]: K };
private _values: { [key: string]: V };
private _size: number;
constructor() {
this.clear();
@@ -176,6 +186,10 @@ module libjass {
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;
@@ -198,6 +212,7 @@ module libjass {
if (result) {
delete this._keys[property];
delete this._values[property];
this._size--;
}
return result;
@@ -208,6 +223,7 @@ module libjass {
clear(): void {
this._keys = Object.create(null);
this._values = Object.create(null);
this._size = 0;
}
/**
@@ -221,8 +237,11 @@ module libjass {
}
}
/**
* @type {number}
*/
get size(): number {
throw new Error("This Map implementation doesn't support size.");
return this._size;
}
private _keyToProperty(key: K): string {
@@ -247,17 +266,8 @@ module libjass {
*
* @type {function(new:Map)}
*/
export var Map: {
new <K, V>(): Map<K, V>;
} = null;
declare var global: any; // Defined as a parameter of the anonymous function wrapper
// Use this browser's implementation of Map if it has one
if (global.Map !== undefined && typeof global.Map.prototype.forEach === "function") {
Map = global.Map;
}
else {
export var Map = global.Map;
if (Map === undefined || typeof Map.prototype.forEach !== "function") {
Map = <any>SimpleMap;
}
+298
View File
@@ -0,0 +1,298 @@
/**
* 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.
*/
///<reference path="libjass.ts" />
interface Document {
currentScript: HTMLScriptElement;
}
interface WorkerGlobalScope {
postMessage(message: any): void;
addEventListener(type: string, listener: (message: any) => void, useCapture: boolean): void;
}
declare var WorkerGlobalScope: {
prototype: WorkerGlobalScope;
new (): WorkerGlobalScope;
};
module libjass.webworker {
Object.defineProperty(webworker, "supported", {
value: typeof Worker !== "undefined",
configurable: true,
enumerable: true
});
/**
* Represents a communication channel between the host and the web worker. An instance of this class is created by calling libjass.webworker.setup()
*/
export interface WorkerChannel {
/**
* Sends a request to the other side to execute the given command with the given parameters.
*
* @param {number} command
* @param {*} parameters
* @return {!libjass.webworker.WorkerPromise} A promise that will get resolved when the other side computes the result
*/
request(command: WorkerCommands, parameters: any): WorkerPromise;
}
/**
* Create a new web worker.
*
* @return {!libjass.webworker.WorkerChannel} A communication channel to the new web worker.
*/
export function createWorker(): WorkerChannel {
return new WorkerChannelImpl(new Worker(_scriptNode.src));
}
/**
* A promise returned by libjass.webworker.WorkerChannel.request()
*/
export interface WorkerPromise {
resolved: boolean;
result: any;
/**
* Registers a callback to run when this promise is resolved. If the promise is already resolved, the callback is called immediately.
*
* @param {function(*) } callback A function of the form (promise: *): void
*/
then(callback: WorkerPromiseCallback): void;
cancel(): void;
}
/**
* The signature of a callback called by WorkerPromise when it is resolved.
*/
export interface WorkerPromiseCallback {
(promise: WorkerPromise): void;
}
/**
* The commands that can be sent to or from a web worker.
*/
export enum WorkerCommands {
Response = 0
}
/**
* The signature of a handler registered to handle a particular WorkerCommand.
*/
export interface WorkerCommandHandler {
(parameters: any, response: WorkerResultCallback): void;
}
/**
* The signature of a callback called by a WorkerCommandHandler to report its result back to the caller.
*/
export interface WorkerResultCallback {
(error: any, result: any): void;
}
/**
* Registers a handler for the given worker command.
*
* @param {number} command The command that this handler will handle. Valid values are the values of libjass.webworker.WorkerCommands
* @param {function(*, function(*, *))} handler The handler. A function of the form (parameters: *, response: function(error: *, result: *): void): void
*/
export function _registerWorkerCommand(command: WorkerCommands, handler: WorkerCommandHandler) {
workerCommands.set(command, handler);
}
export function _registerClassPrototype(prototype: any): void {
prototype._classTag = classPrototypes.size;
classPrototypes.set(prototype._classTag, prototype);
}
var _scriptNode: HTMLScriptElement = null;
if (typeof document !== "undefined" && document.currentScript !== undefined) {
_scriptNode = document.currentScript;
}
var workerCommands = new Map<WorkerCommands, WorkerCommandHandler>();
var classPrototypes = new Map<number, any>();
interface WorkerCommunication {
addEventListener(type: "message", listener: (ev: MessageEvent) => any, useCapture?: boolean): void;
addEventListener(type: string, listener: EventListener, useCapture?: boolean): void;
postMessage(message: any): void;
}
interface WorkerRequestMessage {
requestId: number;
command: WorkerCommands;
parameters: any;
}
interface WorkerResponseMessage {
requestId: number;
error: any;
result: any;
}
/**
* Internal implementation of libjass.webworker.WorkerChannel
*
* @param {!*} comm The other side of the channel. When created by the host, this is the web worker. When created by the web worker, this is its global object.
*/
class WorkerChannelImpl implements WorkerChannel {
private _pendingRequests = new Map<number, WorkerPromiseImpl>();
constructor(private _comm: WorkerCommunication) {
this._comm.addEventListener("message", ev => this._onMessage(<string>ev.data), false);
}
request(command: WorkerCommands, parameters: any): WorkerPromise {
var promise = new WorkerPromiseImpl(this);
var requestId = promise.id;
this._pendingRequests.set(requestId, promise);
var requestMessage: WorkerRequestMessage = { requestId: requestId, command: command, parameters: parameters };
this._comm.postMessage(WorkerChannelImpl._toJSON(requestMessage));
return promise;
}
cancelRequest(requestId: number): void {
this._pendingRequests.delete(requestId);
}
private _respond(message: WorkerResponseMessage): void {
this._comm.postMessage(WorkerChannelImpl._toJSON({ command: WorkerCommands.Response, requestId: message.requestId, error: message.error, result: message.result }));
}
private _onMessage(rawMessage: string): void {
var message = <{ command: WorkerCommands }>WorkerChannelImpl._fromJSON(rawMessage);
if (message.command === WorkerCommands.Response) {
var responseMessage = <WorkerResponseMessage><any>message;
var promise = this._pendingRequests.get(responseMessage.requestId);
if (promise !== undefined) {
this._pendingRequests.delete(responseMessage.requestId);
promise.resolve(responseMessage.error, responseMessage.result);
}
return;
}
var requestMessage = <WorkerRequestMessage>message;
var commandCallback = workerCommands.get(requestMessage.command);
if (commandCallback === undefined) {
this._respond({ requestId: requestMessage.requestId, error: new Error("Unrecognized command: " + requestMessage.command), result: null });
return;
}
commandCallback(requestMessage.parameters, (error: any, result: any) => this._respond({ requestId: requestMessage.requestId, error: error, result: result }));
}
private static _toJSON(obj: any): string {
return JSON.stringify(obj, (/* ujs:unreferenced */ key: string, value: any) => {
if (value && value._classTag !== undefined) {
value._classTag = value._classTag;
}
return value;
});
}
private static _fromJSON(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));
Object.keys(value).forEach(key => {
if (key !== "_classTag") {
hydratedValue[key] = value[key];
}
});
value = hydratedValue;
}
return value;
});
}
}
var inWorker = (typeof WorkerGlobalScope !== "undefined" && global instanceof WorkerGlobalScope);
if (inWorker) {
new WorkerChannelImpl(<WorkerGlobalScope><any>global);
}
class WorkerPromiseImpl implements WorkerPromise {
private static _lastPromiseId: number = -1;
private _id: number = ++WorkerPromiseImpl._lastPromiseId;
private _resolved: boolean = false;
private _result: any = null;
private _error: any = null;
private _callback: WorkerPromiseCallback = null;
constructor(private _channel: WorkerChannelImpl) { }
get resolved(): boolean {
return this._resolved;
}
get result(): any {
if (!this._resolved) {
throw new Error("Unresolved promise.");
}
if (this._error !== null) {
throw this._error;
}
return this._result;
}
then(callback: WorkerPromiseCallback): void {
this._callback = callback;
if (this._resolved) {
setTimeout(() => this._callback(this), 0);
}
}
cancel(): void {
if (this._resolved) {
return;
}
this._channel.cancelRequest(this._id);
}
get id(): number {
return this._id;
}
resolve(error: any, result: any): void {
this._resolved = true;
this._error = error;
this._result = result;
if (this._callback !== null) {
this._callback(this);
}
}
}
}