diff --git a/.gitignore b/.gitignore index 5fd4aa8..773d18d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ .DS_Store .project .settings +lib/tests/pid.txt + +comicbook +node_modules diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index ca3b799..0000000 --- a/.gitmodules +++ /dev/null @@ -1,6 +0,0 @@ -[submodule "lib/pixastic"] - path = lib/pixastic - url = https://github.com/jseidelin/pixastic.git -[submodule "css/Aristo"] - path = css/Aristo - url = git://github.com/taitems/Aristo-jQuery-UI-Theme.git diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6e5919d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,3 @@ +language: node_js +node_js: + - "0.10" diff --git a/bin/closure-complier/COPYING b/LICENSE similarity index 100% rename from bin/closure-complier/COPYING rename to LICENSE diff --git a/Makefile b/Makefile old mode 100644 new mode 100755 index aba517b..bae259b --- a/Makefile +++ b/Makefile @@ -1,20 +1,49 @@ -SOURCES = lib/pixastic/pixastic.js \ - lib/pixastic/pixastic.effects.js \ - lib/pixastic/pixastic.worker.js \ - lib/pixastic/pixastic.worker.control.js \ - lib/ComicBook.js +# +# build package & update examples +# -all: reset lib/ComicBook.combined.js lib/ComicBook.min.js +build: + @echo "Running jshint..." + @./node_modules/.bin/jshint lib/ComicBook.js --config lib/.jshintrc + @echo "Compiling Handlebars templates..." + @./node_modules/.bin/handlebars templates/*.handlebars -f lib/templates.js + @echo "Compiling and minifying javascript..." + @mkdir -p comicbook/js/pixastic + @cat lib/vendor/pixastic/pixastic.js lib/vendor/pixastic/pixastic.effects.js lib/vendor/pixastic/pixastic.worker.js lib/vendor/handlebars.runtime-1.0.rc.1.min.js lib/templates.js lib/ComicBook.js > comicbook/js/comicbook.js + @cp lib/vendor/pixastic/pixastic.js comicbook/js/pixastic + @cp lib/vendor/pixastic/pixastic.effects.js comicbook/js/pixastic + @cp lib/vendor/pixastic/pixastic.worker.js comicbook/js/pixastic + @cp lib/vendor/pixastic/pixastic.worker.control.js comicbook/js/pixastic + @cp lib/vendor/pixastic/license-gpl-3.0.txt comicbook/js/pixastic + @cp lib/vendor/pixastic/license-mpl.txt comicbook/js/pixastic + @./node_modules/.bin/uglifyjs -nc comicbook/js/comicbook.js > comicbook/js/comicbook.min.js + @echo "Compiling CSS..." + @cat fonts/icomoon-toolbar/style.css css/reset.css css/styles.css css/toolbar.css > comicbook/comicbook.css + @echo "Copying assets..." + @cp -r css/img comicbook/img + @cp -r fonts/icomoon-toolbar/fonts comicbook + @cp -r fonts/icomoon-toolbar/license.txt comicbook/fonts + @echo "Updating examples" + @cp -r comicbook examples + @echo "Done" -lib/ComicBook.combined.js: ${SOURCES} - cat > $@ $^ +# +# run jshint & quint tests +# -lib/ComicBook.min.js: lib/ComicBook.combined.js - java -jar bin/closure-complier/compiler.jar --compilation_level SIMPLE_OPTIMIZATIONS --js $< --js_output_file $@ +test: + @./node_modules/.bin/jshint lib/ComicBook.js --config lib/.jshintrc + @./node_modules/.bin/jshint lib/tests/unit/*.js --config lib/.jshintrc + @node lib/tests/server.js & + @./node_modules/.bin/phantomjs lib/tests/phantom.js "http://localhost:3000/lib/tests" + @kill -9 `cat lib/tests/pid.txt` + @rm lib/tests/pid.txt -reset: - rm -f lib/ComicBook.min.js +# +# remove prior builds +# clean: - rm lib/ComicBook.combined.js + @rm -r comicbook + @rm -r examples/comicbook diff --git a/README.md b/README.md new file mode 100755 index 0000000..b1683e6 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +Comic Book Reader +================= + +[![Build Status](https://api.travis-ci.org/balaclark/HTML5-Comic-Book-Reader.png)](https://travis-ci.org/balaclark/HTML5-Comic-Book-Reader) + +A canvas based web application for reading comics. You can also see an implementation +of this as an offline Chrome packaged application CBZ / CBR comic book reader at: +https://github.com/balaclark/chrome-comic-reader. + +Usage +----- +See included examples. + +Development Install +------------------- + +Builds require nodejs and npm. Installs have been tested with nodejs 0.10.0, older +versions may or may not work. + + npm install + make + make test + +Copyright and License +--------------------- + +Copyright 2010 Bala Clark + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this work except in compliance with the License. You may obtain a copy of the +License in the LICENSE file, or 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. diff --git a/bin/closure-complier/README b/bin/closure-complier/README deleted file mode 100644 index e6d12c4..0000000 --- a/bin/closure-complier/README +++ /dev/null @@ -1,292 +0,0 @@ -/* - * Copyright 2009 The Closure Compiler Authors. - * - * 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. - */ - -// -// Contents -// - -The Closure Compiler performs checking, instrumentation, and -optimizations on JavaScript code. The purpose of this README is to -explain how to build and run the Closure Compiler. - -The Closure Compiler requires Java 6 or higher. -http://www.java.com/ - - -// -// Building The Closure Compiler -// - -There are three ways to get a Closure Compiler executable. - -1) Use one we built for you. - -Pre-built Closure binaries can be found at -http://code.google.com/p/closure-compiler/downloads/list - - -2) Check out the source and build it with Apache Ant. - -First, check out the full source tree of the Closure Compiler. There -are instructions on how to do this at the project site. -http://code.google.com/p/closure-compiler/source/checkout - -Apache Ant is a cross-platform build tool. -http://ant.apache.org/ - -At the root of the source tree, there is an Ant file named -build.xml. To use it, navigate to the same directory and type the -command - -ant jar - -This will produce a jar file called "build/compiler.jar". - - -3) Check out the source and build it with Eclipse. - -Eclipse is a cross-platform IDE. -http://www.eclipse.org/ - -Under Eclipse's File menu, click "New > Project ..." and create a -"Java Project." You will see an options screen. Give the project a -name, select "Create project from existing source," and choose the -root of the checked-out source tree as the existing directory. Verify -that you are using JRE version 6 or higher. - -Eclipse can use the build.xml file to discover rules. When you -navigate to the build.xml file, you will see all the build rules in -the "Outline" pane. Run the "jar" rule to build the compiler in -build/compiler.jar. - - -// -// Running The Closure Compiler -// - -Once you have the jar binary, running the Closure Compiler is straightforward. - -On the command line, type - -java -jar compiler.jar - -This starts the compiler in interactive mode. Type - -var x = 17 + 25; - -then hit "Enter", then hit "Ctrl-Z" (on Windows) or "Ctrl-D" (on Mac or Linux) -and "Enter" again. The Compiler will respond: - -var x=42; - -The Closure Compiler has many options for reading input from a file, -writing output to a file, checking your code, and running -optimizations. To learn more, type - -java -jar compiler.jar --help - -You can read more detailed documentation about the many flags at -http://code.google.com/closure/compiler/docs/gettingstarted_app.html - - -// -// Compiling Multiple Scripts -// - -If you have multiple scripts, you should compile them all together with -one compile command. - -java -jar compiler.jar --js=in1.js --js=in2.js ... --js_output_file=out.js - -The Closure Compiler will concatenate the files in the order they're -passed at the command line. - -If you need to compile many, many scripts together, you may start to -run into problems with managing dependencies between scripts. You -should check out the Closure Library. It contains functions for -enforcing dependencies between scripts, and a tool called calcdeps.py -that knows how to give scripts to the Closure Compiler in the right -order. - -http://code.google.com/p/closure-library/ - -// -// Licensing -// - -Unless otherwise stated, all source files are licensed under -the Apache License, Version 2.0. - - ------ -Code under: -src/com/google/javascript/rhino -test/com/google/javascript/rhino - -URL: http://www.mozilla.org/rhino -Version: 1.5R3, with heavy modifications -License: Netscape Public License and MPL / GPL dual license - -Description: A partial copy of Mozilla Rhino. Mozilla Rhino is an -implementation of JavaScript for the JVM. The JavaScript parser and -the parse tree data structures were extracted and modified -significantly for use by Google's JavaScript compiler. - -Local Modifications: The packages have been renamespaced. All code not -relavant to parsing has been removed. A JSDoc parser and static typing -system have been added. - - ------ -Code in: -lib/rhino - -Rhino -URL: http://www.mozilla.org/rhino -Version: Trunk -License: Netscape Public License and MPL / GPL dual license - -Description: Mozilla Rhino is an implementation of JavaScript for the JVM. - -Local Modifications: Minor changes to parsing JSDoc that usually get pushed -up-stream to Rhino trunk. - - ------ -Code in: -lib/args4j.jar - -Args4j -URL: https://args4j.dev.java.net/ -Version: 2.0.12 -License: MIT - -Description: -args4j is a small Java class library that makes it easy to parse command line -options/arguments in your CUI application. - -Local Modifications: None. - - ------ -Code in: -lib/guava.jar - -Guava Libraries -URL: http://code.google.com/p/guava-libraries/ -Version: r08 -License: Apache License 2.0 - -Description: Google's core Java libraries. - -Local Modifications: None. - - ------ -Code in: -lib/jsr305.jar - -Annotations for software defect detection -URL: http://code.google.com/p/jsr-305/ -Version: svn revision 47 -License: BSD License - -Description: Annotations for software defect detection. - -Local Modifications: None. - - ------ -Code in: -lib/jarjar.jar - -Jar Jar Links -URL: http://jarjar.googlecode.com/ -Version: 1.1 -License: Apache License 2.0 - -Description: -A utility for repackaging Java libraries. - -Local Modifications: None. - - ----- -Code in: -lib/junit.jar - -JUnit -URL: http://sourceforge.net/projects/junit/ -Version: 4.8.2 -License: Common Public License 1.0 - -Description: A framework for writing and running automated tests in Java. - -Local Modifications: None. - - ---- -Code in: -lib/protobuf-java.jar - -Protocol Buffers -URL: http://code.google.com/p/protobuf/ -Version: 2.3.0 -License: New BSD License - -Description: Supporting libraries for protocol buffers, -an encoding of structured data. - -Local Modifications: None - - ---- -Code in: -lib/ant.jar -lib/ant-launcher.jar - -URL: http://ant.apache.org/bindownload.cgi -Version: 1.8.1 -License: Apache License 2.0 -Description: - Ant is a Java based build tool. In theory it is kind of like "make" - without make's wrinkles and with the full portability of pure java code. - -Local Modifications: None - - ---- -Code in: -lib/json.jar -URL: http://json.org/java/index.html -Version: JSON version 20090211 -License: MIT license -Description: -JSON is a set of java files for use in transmitting data in JSON format. - -Local Modifications: None - ---- -Code in: -tools/maven-ant-tasks-2.1.1.jar -URL: http://maven.apache.org -Version 2.1.1 -License: Apache License 2.0 -Description: - Maven Ant tasks are used to manage dependencies and to install/deploy to - maven repositories. - -Local Modifications: None diff --git a/bin/closure-complier/compiler.jar b/bin/closure-complier/compiler.jar deleted file mode 100644 index 62cfdd0..0000000 Binary files a/bin/closure-complier/compiler.jar and /dev/null differ diff --git a/css/Aristo b/css/Aristo deleted file mode 160000 index 6f16099..0000000 --- a/css/Aristo +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6f16099bc3a43689ecd86dc7d34fa01941fbb92a diff --git a/img/loading.gif b/css/img/loading.gif similarity index 100% rename from img/loading.gif rename to css/img/loading.gif diff --git a/css/styles.css b/css/styles.css old mode 100644 new mode 100755 index 6febb75..864767f --- a/css/styles.css +++ b/css/styles.css @@ -7,75 +7,47 @@ .cb-control { color: #fff; background-color: #111; - background-image: linear-gradient(bottom, rgb(17,17,17) 20%, rgb(41,41,41) 72%); - background-image: -o-linear-gradient(bottom, rgb(17,17,17) 20%, rgb(41,41,41) 72%); - background-image: -moz-linear-gradient(bottom, rgb(17,17,17) 20%, rgb(41,41,41) 72%); - background-image: -webkit-linear-gradient(bottom, rgb(17,17,17) 20%, rgb(41,41,41) 72%); - background-image: -ms-linear-gradient(bottom, rgb(17,17,17) 20%, rgb(41,41,41) 72%); - - background-image: -webkit-gradient( - linear, - left bottom, - left top, - color-stop(0.2, rgb(17,17,17)), - color-stop(0.72, rgb(41,41,41)) - ); padding: 10px; position: fixed !important; - -webkit-box-shadow: 0 0 4px #000; - -moz-box-shadow: 0 0 4px #000; box-shadow: 0 0 4px #000; } -.cb-control label { - display: inline-block; - margin: 0 0 4px; -} - -.cb-control.cb-navigate { +.navigate { top: 0; margin: 0; cursor: pointer; width: 128px; opacity: 0; background: center no-repeat; - -webkit-box-shadow: none; - -moz-box-shadow: none; box-shadow: none; + padding: 0 3em; } -.cb-control.cb-navigate:hover { +.navigate > span { + color: #000; + font-size: 10em; + background-color: rgba(255, 255, 255, 0.8); + border-radius: 1em; + top: 35%; + position: relative; +} + +body:not(.mobile) .navigate:hover { opacity: 1; } -.cb-control.cb-navigate.left { +.navigate-left { left: 0; - background-image: url(../img/left.png); } -.cb-control.cb-navigate.right { +.navigate-right { right: 0; - background-image: url(../img/right.png); -} - -.cb-option { - margin-bottom: 16px; -} - -.cb-option:last-child { - margin-bottom: 0; -} - -.ui-draggable { - cursor: move; } #cb-loading-overlay { z-index: 100; opacity: 0.8; - background: #000 url("../img/loading.gif") no-repeat center; - -webkit-box-shadow: none; - -moz-box-shadow: none; + background: #000 url("img/loading.gif") no-repeat center; box-shadow: none; } @@ -85,105 +57,26 @@ right: 0; bottom: 0; margin: 8px; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; border-radius: 4px; } #cb-progress-bar { width: 200px; +} + +#cb-progress-bar, +#cb-progress-bar .progressbar-value { height: 3px; } -#cb-progress-bar .ui-widget-header { +#cb-progress-bar .progressbar-value { + width: 0; background: #86C441; border-color: #3E7600; } -#cb-toolbar #cb-comic-info { - float: right; - line-height: 24px; -} - -#cb-toolbar { - top: 0; - border-bottom: 1px solid #888; -} - -#cb-toolbar button { - height: 24px; - width: 24px; - color: transparent; - border: none; - background-image: url("../img/iconic/sprite.png"); - background-color: transparent; - background-repeat: no-repeat; - margin: 0 16px; - cursor: pointer; -} - -#cb-toolbar button.cb-close { background-position: 0 -120px } -#cb-toolbar button.cb-close:hover { background-position: -24px -120px } - -#cb-toolbar button.cb-color { background-position: 0 0 } -#cb-toolbar button.cb-color:hover, -#cb-toolbar button.cb-color.active { background-position: -24px 0 } - -#cb-toolbar button.cb-layout { background-position: 0 -24px } -#cb-toolbar button.cb-layout:hover { background-position: -24px -24px } - -#cb-toolbar button.cb-zoom-out { background-position: 0 -72px } -#cb-toolbar button.cb-zoom-out:hover { background-position: -24px -72px } - -#cb-toolbar button.cb-zoom-in { background-position: 0 -96px } -#cb-toolbar button.cb-zoom-in:hover { background-position: -24px -96px } - -#cb-toolbar button.cb-fit-width { background-position: 0 -48px } -#cb-toolbar button.cb-fit-width:hover { background-position: -24px -48px } -#cb-toolbar button.cb-fit-width[disabled=disabled] { background-position: -48px -48px } - -#cb-toolbar button.cb-fit-best { background-position: 0 -192px } -#cb-toolbar button.cb-fit-best:hover { background-position: -24px -192px } -#cb-toolbar button.cb-fit-best[disabled=disabled] { background-position: -48px -192px } - -#cb-toolbar button.cb-read-direction#toleft{ background-position: 0 -144px } -#cb-toolbar button.cb-read-direction:hover#toleft{ background-position: -24px -144px } - -#cb-toolbar button.cb-read-direction#toright{ background-position: 0 -168px } -#cb-toolbar button.cb-read-direction:hover#toright { background-position: -24px -168px } - -#cb-color { - width: 246px; - top: 44px; - left: 120px; - z-index: 1; - border: 2px solid #444; - border-top: none; - -moz-border-radius-bottomleft: 16px; - -moz-border-radius-bottomright: 16px; - -webkit-border-bottom-left-radius: 16px; - -webkit-border-bottom-right-radius: 16px; - border-bottom-left-radius: 16px; - border-bottom-right-radius: 16px; -} - -label[for="cb-desaturate"] { - padding: 4px; -} - -#cb-desaturate { - float: left; -} - -#cb-reset { - float: right; -} - -#cb-reset { - background: transparent url(../img/iconic/reload_12x14.png) no-repeat left center; - padding-left: 16px; - height: 14px; - border: none; - color: #fff; - cursor: pointer; +* { + -webkit-user-select: none; + -webkit-touch-callout: none; + -webkit-tap-highlight-color: rgba(0,0,0,0); } diff --git a/css/toolbar.css b/css/toolbar.css new file mode 100644 index 0000000..26b655b --- /dev/null +++ b/css/toolbar.css @@ -0,0 +1,130 @@ + +body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 12px; + line-height: 20px; + color: #333; +} + +button, input, label { + cursor: pointer; +} + +.pull-left { + float: left; +} + +.pull-right { + float: right; +} + +.toolbar { + color: white; + background-color: black; + background-image: linear-gradient(to bottom, rgb(80, 80, 80), rgb(17, 17, 17)); + overflow: visible; + padding: 0.75em; + position: fixed; + left: 0; + right: 0; + z-index: 99; + margin-bottom: 0; + box-shadow: 0 1px 10px rgba(0, 0, 0, 0.4); + opacity: 0; + transition: opacity 0.2s ease-in-out; +} + +.mobile .toolbar { + opacity: 1; +} + +.toolbar:hover { + opacity: 1; +} + +.toolbar li { + display: inline-block; + position: relative; +} + +.toolbar .separator { + border: solid 1px; + height: 1em; +} + +.toolbar button { + color: white; + border: none; + background-color: transparent; + padding: 0; +} + +.toolbar li > button { + font-size: 1.5em; + padding: 0 12px; +} + +.mobile .toolbar li > button { + padding: 0 9px; +} + +.toolbar li > button:hover { + color: #8CC746; +} + +.toolbar button[data-action=close]:hover { + color: #FF6464; +} + +.toolbar .dropdown { + font-size: 1em; + position: absolute; + width: 212px; + background-color: white; + color: #111; + border-radius: 4px; + box-shadow: 0 1px 10px rgba(0, 0, 0, 0.4); + top: 2em; + padding: 4px 0; + display: none; +} + +body:not(.mobile) .toolbar li:hover > .dropdown { + display: block; +} + +/* dropdown arrow code taken from Twitter Bootstrap 2.3.1 */ +.toolbar .dropdown:after { + position: absolute; + top: -4px; + left: 15px; + display: inline-block; + border-right: 6px solid transparent; + border-bottom: 6px solid #ffffff; + border-left: 6px solid transparent; + content: ''; +} + +.toolbar .close { + display: none; +} + +.dropdown .control-group { + padding: 8px; +} + +.dropdown .sliders { + font-size: 1.5em; +} + +.dropdown .control-group span { + float: left; + margin: 0 2px; + clear: both; +} + +.dropdown .control-group input[type=range] { + width: 171px; + float: right; + margin: 0; +} diff --git a/examples/basic.html b/examples/basic.html index 782bf63..2d60f27 100755 --- a/examples/basic.html +++ b/examples/basic.html @@ -2,19 +2,15 @@ + + + + Basic - - - - - - - - - - - + + + @@ -48,10 +44,13 @@ 'goldenboy/goldenboy_23.jpg', 'goldenboy/goldenboy_24.jpg', 'goldenboy/goldenboy_25.jpg' - ]); + ], { + libPath: "/reader/examples/comicbook/js/" + }); + book.draw(); - $(window).resize(function(event) { + $(window).on('resize', function(event) { book.draw(); }); diff --git a/examples/bitjs/README.txt b/examples/bitjs/README.txt new file mode 100755 index 0000000..9de777a --- /dev/null +++ b/examples/bitjs/README.txt @@ -0,0 +1 @@ +bitjs: Binary Tools for JavaScript diff --git a/examples/bitjs/archive.js b/examples/bitjs/archive.js new file mode 100755 index 0000000..ae6f1e5 --- /dev/null +++ b/examples/bitjs/archive.js @@ -0,0 +1,340 @@ +/** + * archive.js + * + * Provides base functionality for unarchiving. + * + * Licensed under the MIT License + * + * Copyright(c) 2011 Google Inc. + */ + +var bitjs = bitjs || {}; +bitjs.archive = bitjs.archive || {}; + +(function() { + +// =========================================================================== +// Stolen from Closure because it's the best way to do Java-like inheritance. +bitjs.base = function(me, opt_methodName, var_args) { + var caller = arguments.callee.caller; + if (caller.superClass_) { + // This is a constructor. Call the superclass constructor. + return caller.superClass_.constructor.apply( + me, Array.prototype.slice.call(arguments, 1)); + } + + var args = Array.prototype.slice.call(arguments, 2); + var foundCaller = false; + for (var ctor = me.constructor; + ctor; ctor = ctor.superClass_ && ctor.superClass_.constructor) { + if (ctor.prototype[opt_methodName] === caller) { + foundCaller = true; + } else if (foundCaller) { + return ctor.prototype[opt_methodName].apply(me, args); + } + } + + // If we did not find the caller in the prototype chain, + // then one of two things happened: + // 1) The caller is an instance method. + // 2) This method was not called by the right caller. + if (me[opt_methodName] === caller) { + return me.constructor.prototype[opt_methodName].apply(me, args); + } else { + throw Error( + 'goog.base called from a method of one name ' + + 'to a method of a different name'); + } +}; +bitjs.inherits = function(childCtor, parentCtor) { + /** @constructor */ + function tempCtor() {}; + tempCtor.prototype = parentCtor.prototype; + childCtor.superClass_ = parentCtor.prototype; + childCtor.prototype = new tempCtor(); + childCtor.prototype.constructor = childCtor; +}; +// =========================================================================== + +/** + * An unarchive event. + * + * @param {string} type The event type. + * @constructor + */ +bitjs.archive.UnarchiveEvent = function(type) { + /** + * The event type. + * + * @type {string} + */ + this.type = type; +}; + +/** + * The UnarchiveEvent types. + */ +bitjs.archive.UnarchiveEvent.Type = { + START: 'start', + PROGRESS: 'progress', + EXTRACT: 'extract', + FINISH: 'finish', + INFO: 'info', + ERROR: 'error' +}; + +/** + * Useful for passing info up to the client (for debugging). + * + * @param {string} msg The info message. + */ +bitjs.archive.UnarchiveInfoEvent = function(msg) { + bitjs.base(this, bitjs.archive.UnarchiveEvent.Type.INFO); + + /** + * The information message. + * + * @type {string} + */ + this.msg = msg; +}; +bitjs.inherits(bitjs.archive.UnarchiveInfoEvent, bitjs.archive.UnarchiveEvent); + +/** + * An unrecoverable error has occured. + * + * @param {string} msg The error message. + */ +bitjs.archive.UnarchiveErrorEvent = function(msg) { + bitjs.base(this, bitjs.archive.UnarchiveEvent.Type.ERROR); + + /** + * The information message. + * + * @type {string} + */ + this.msg = msg; +}; +bitjs.inherits(bitjs.archive.UnarchiveErrorEvent, bitjs.archive.UnarchiveEvent); + +/** + * Start event. + * + * @param {string} msg The info message. + */ +bitjs.archive.UnarchiveStartEvent = function() { + bitjs.base(this, bitjs.archive.UnarchiveEvent.Type.START); +}; +bitjs.inherits(bitjs.archive.UnarchiveStartEvent, bitjs.archive.UnarchiveEvent); + +/** + * Finish event. + * + * @param {string} msg The info message. + */ +bitjs.archive.UnarchiveFinishEvent = function() { + bitjs.base(this, bitjs.archive.UnarchiveEvent.Type.FINISH); +}; +bitjs.inherits(bitjs.archive.UnarchiveFinishEvent, bitjs.archive.UnarchiveEvent); + +/** + * Progress event. + */ +bitjs.archive.UnarchiveProgressEvent = function( + currentFilename, + currentFileNumber, + currentBytesUnarchivedInFile, + currentBytesUnarchived, + totalUncompressedBytesInArchive, + totalFilesInArchive) { + bitjs.base(this, bitjs.archive.UnarchiveEvent.Type.PROGRESS); + + this.currentFilename = currentFilename; + this.currentFileNumber = currentFileNumber; + this.currentBytesUnarchivedInFile = currentBytesUnarchivedInFile; + this.totalFilesInArchive = totalFilesInArchive; + this.currentBytesUnarchived = currentBytesUnarchived; + this.totalUncompressedBytesInArchive = totalUncompressedBytesInArchive; +}; +bitjs.inherits(bitjs.archive.UnarchiveProgressEvent, bitjs.archive.UnarchiveEvent); + +/** + * All extracted files returned by an Unarchiver will implement + * the following interface: + * + * interface UnarchivedFile { + * string filename + * TypedArray fileData + * } + * + */ + +/** + * Extract event. + */ +bitjs.archive.UnarchiveExtractEvent = function(unarchivedFile) { + bitjs.base(this, bitjs.archive.UnarchiveEvent.Type.EXTRACT); + + /** + * @type {UnarchivedFile} + */ + this.unarchivedFile = unarchivedFile; +}; +bitjs.inherits(bitjs.archive.UnarchiveExtractEvent, bitjs.archive.UnarchiveEvent); + + +/** + * Base class for all Unarchivers. + * + * @param {ArrayBuffer} arrayBuffer The Array Buffer. + * @param {string} opt_pathToBitJS Optional string for where the BitJS files are located. + * @constructor + */ +bitjs.archive.Unarchiver = function(arrayBuffer, opt_pathToBitJS) { + /** + * The ArrayBuffer object. + * @type {ArrayBuffer} + * @protected + */ + this.ab = arrayBuffer; + + /** + * The path to the BitJS files. + * @type {string} + * @private + */ + this.pathToBitJS_ = opt_pathToBitJS || ''; + + /** + * A map from event type to an array of listeners. + * @type {Map.} + */ + this.listeners_ = {}; + for (var type in bitjs.archive.UnarchiveEvent.Type) { + this.listeners_[bitjs.archive.UnarchiveEvent.Type[type]] = []; + } +}; + +/** + * Private web worker initialized during start(). + * @type {Worker} + * @private + */ +bitjs.archive.Unarchiver.prototype.worker_ = null; + +/** + * This method must be overridden by the subclass to return the script filename. + * @return {string} The script filename. + * @protected. + */ +bitjs.archive.Unarchiver.prototype.getScriptFileName = function() { + throw 'Subclasses of AbstractUnarchiver must overload getScriptFileName()'; +}; + +/** + * Adds an event listener for UnarchiveEvents. + * + * @param {string} Event type. + * @param {function} An event handler function. + */ +bitjs.archive.Unarchiver.prototype.addEventListener = function(type, listener) { + if (type in this.listeners_) { + if (this.listeners_[type].indexOf(listener) == -1) { + this.listeners_[type].push(listener); + } + } +}; + +/** + * Removes an event listener. + * + * @param {string} Event type. + * @param {EventListener|function} An event listener or handler function. + */ +bitjs.archive.Unarchiver.prototype.removeEventListener = function(type, listener) { + if (type in this.listeners_) { + var index = this.listeners_[type].indexOf(listener); + if (index != -1) { + this.listeners_[type].splice(index, 1); + } + } +}; + +/** + * Receive an event and pass it to the listener functions. + * + * @param {bitjs.archive.UnarchiveEvent} e + * @private + */ +bitjs.archive.Unarchiver.prototype.handleWorkerEvent_ = function(e) { + if ((e instanceof bitjs.archive.UnarchiveEvent || e.type) && + this.listeners_[e.type] instanceof Array) { + this.listeners_[e.type].forEach(function (listener) { listener(e) }); + } else { + console.log(e); + } +}; + +/** + * Starts the unarchive in a separate Web Worker thread and returns immediately. + */ + bitjs.archive.Unarchiver.prototype.start = function() { + var me = this; + var scriptFileName = this.pathToBitJS_ + this.getScriptFileName(); + if (scriptFileName) { + this.worker_ = new Worker(scriptFileName); + + this.worker_.onerror = function(e) { + console.log('Worker error: message = ' + e.message); + throw e; + }; + + this.worker_.onmessage = function(e) { + if (typeof e.data == 'string') { + // Just log any strings the workers pump our way. + console.log(e.data); + } else { + // Assume that it is an UnarchiveEvent. Some browsers preserve the 'type' + // so that instanceof UnarchiveEvent returns true, but others do not. + me.handleWorkerEvent_(e.data); + } + }; + + this.worker_.postMessage({file: this.ab}); + } +} + +/** + * Unzipper + * @extends {bitjs.archive.Unarchiver} + * @constructor + */ +bitjs.archive.Unzipper = function(arrayBuffer, opt_pathToBitJS) { + bitjs.base(this, arrayBuffer, opt_pathToBitJS); +}; +bitjs.inherits(bitjs.archive.Unzipper, bitjs.archive.Unarchiver); +bitjs.archive.Unzipper.prototype.getScriptFileName = function() { return 'unzip.js' }; + +/** + * Unrarrer + * @extends {bitjs.archive.Unarchiver} + * @constructor + */ +bitjs.archive.Unrarrer = function(arrayBuffer, opt_pathToBitJS) { + bitjs.base(this, arrayBuffer, opt_pathToBitJS); +}; +bitjs.inherits(bitjs.archive.Unrarrer, bitjs.archive.Unarchiver); +bitjs.archive.Unrarrer.prototype.getScriptFileName = function() { return 'unrar.js' }; + +/** + * Untarrer + * @extends {bitjs.archive.Unarchiver} + * @constructor + */ +bitjs.archive.Untarrer = function(arrayBuffer, opt_pathToBitJS) { + bitjs.base(this, arrayBuffer, opt_pathToBitJS); +}; +bitjs.inherits(bitjs.archive.Untarrer, bitjs.archive.Unarchiver); +bitjs.archive.Untarrer.prototype.getScriptFileName = function() { return 'untar.js' }; + +})(); \ No newline at end of file diff --git a/examples/bitjs/drive.html b/examples/bitjs/drive.html new file mode 100755 index 0000000..1ad7b7d --- /dev/null +++ b/examples/bitjs/drive.html @@ -0,0 +1,79 @@ + + + + + + + + + + diff --git a/examples/bitjs/io.js b/examples/bitjs/io.js new file mode 100755 index 0000000..0abd637 --- /dev/null +++ b/examples/bitjs/io.js @@ -0,0 +1,310 @@ +/* + * io.js + * + * Provides readers for bit/byte streams (reading) and a byte buffer (writing). + * + * Licensed under the MIT License + * + * Copyright(c) 2011 Google Inc. + * Copyright(c) 2011 antimatter15 + */ + +var bitjs = bitjs || {}; +bitjs.io = bitjs.io || {}; + +(function() { + +// mask for getting the Nth bit (zero-based) +bitjs.BIT = [ 0x01, 0x02, 0x04, 0x08, + 0x10, 0x20, 0x40, 0x80, + 0x100, 0x200, 0x400, 0x800, + 0x1000, 0x2000, 0x4000, 0x8000]; + +// mask for getting N number of bits (0-8) +var BITMASK = [0, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F, 0xFF ]; + + +/** + * This bit stream peeks and consumes bits out of a binary stream. + * + * {ArrayBuffer} ab An ArrayBuffer object or a Uint8Array. + * {boolean} rtl Whether the stream reads bits from the byte starting + * from bit 7 to 0 (true) or bit 0 to 7 (false). + * {Number} opt_offset The offset into the ArrayBuffer + * {Number} opt_length The length of this BitStream + */ +bitjs.io.BitStream = function(ab, rtl, opt_offset, opt_length) { + if (!ab || !ab.toString || ab.toString() !== "[object ArrayBuffer]") { + throw "Error! BitArray constructed with an invalid ArrayBuffer object"; + } + + var offset = opt_offset || 0; + var length = opt_length || ab.byteLength; + this.bytes = new Uint8Array(ab, offset, length); + this.bytePtr = 0; // tracks which byte we are on + this.bitPtr = 0; // tracks which bit we are on (can have values 0 through 7) + this.peekBits = rtl ? this.peekBits_rtl : this.peekBits_ltr; +}; + +// byte0 byte1 byte2 byte3 +// 7......0 | 7......0 | 7......0 | 7......0 +// +// The bit pointer starts at bit0 of byte0 and moves left until it reaches +// bit7 of byte0, then jumps to bit0 of byte1, etc. +bitjs.io.BitStream.prototype.peekBits_ltr = function(n, movePointers) { + if (n <= 0 || typeof n != typeof 1) { + return 0; + } + + var movePointers = movePointers || false, + bytePtr = this.bytePtr, + bitPtr = this.bitPtr, + result = 0, + bitsIn = 0, + bytes = this.bytes; + + // keep going until we have no more bits left to peek at + // TODO: Consider putting all bits from bytes we will need into a variable and then + // shifting/masking it to just extract the bits we want. + // This could be considerably faster when reading more than 3 or 4 bits at a time. + while (n > 0) { + if (bytePtr >= bytes.length) { + throw "Error! Overflowed the bit stream! n=" + n + ", bytePtr=" + bytePtr + ", bytes.length=" + + bytes.length + ", bitPtr=" + bitPtr; + return -1; + } + + var numBitsLeftInThisByte = (8 - bitPtr); + if (n >= numBitsLeftInThisByte) { + var mask = (BITMASK[numBitsLeftInThisByte] << bitPtr); + result |= (((bytes[bytePtr] & mask) >> bitPtr) << bitsIn); + + bytePtr++; + bitPtr = 0; + bitsIn += numBitsLeftInThisByte; + n -= numBitsLeftInThisByte; + } + else { + var mask = (BITMASK[n] << bitPtr); + result |= (((bytes[bytePtr] & mask) >> bitPtr) << bitsIn); + + bitPtr += n; + bitsIn += n; + n = 0; + } + } + + if (movePointers) { + this.bitPtr = bitPtr; + this.bytePtr = bytePtr; + } + + return result; +}; + +// byte0 byte1 byte2 byte3 +// 7......0 | 7......0 | 7......0 | 7......0 +// +// The bit pointer starts at bit7 of byte0 and moves right until it reaches +// bit0 of byte0, then goes to bit7 of byte1, etc. +bitjs.io.BitStream.prototype.peekBits_rtl = function(n, movePointers) { + if (n <= 0 || typeof n != typeof 1) { + return 0; + } + + var movePointers = movePointers || false, + bytePtr = this.bytePtr, + bitPtr = this.bitPtr, + result = 0, + bytes = this.bytes; + + // keep going until we have no more bits left to peek at + // TODO: Consider putting all bits from bytes we will need into a variable and then + // shifting/masking it to just extract the bits we want. + // This could be considerably faster when reading more than 3 or 4 bits at a time. + while (n > 0) { + + if (bytePtr >= bytes.length) { + throw "Error! Overflowed the bit stream! n=" + n + ", bytePtr=" + bytePtr + ", bytes.length=" + + bytes.length + ", bitPtr=" + bitPtr; + return -1; + } + + var numBitsLeftInThisByte = (8 - bitPtr); + if (n >= numBitsLeftInThisByte) { + result <<= numBitsLeftInThisByte; + result |= (BITMASK[numBitsLeftInThisByte] & bytes[bytePtr]); + bytePtr++; + bitPtr = 0; + n -= numBitsLeftInThisByte; + } + else { + result <<= n; + result |= ((bytes[bytePtr] & (BITMASK[n] << (8 - n - bitPtr))) >> (8 - n - bitPtr)); + + bitPtr += n; + n = 0; + } + } + + if (movePointers) { + this.bitPtr = bitPtr; + this.bytePtr = bytePtr; + } + + return result; +}; + +//some voodoo magic +bitjs.io.BitStream.prototype.getBits = function() { + return (((((this.bytes[this.bytePtr] & 0xff) << 16) + + ((this.bytes[this.bytePtr+1] & 0xff) << 8) + + ((this.bytes[this.bytePtr+2] & 0xff))) >>> (8-this.bitPtr)) & 0xffff); +}; + +bitjs.io.BitStream.prototype.readBits = function(n) { + return this.peekBits(n, true); +}; + +// This returns n bytes as a sub-array, advancing the pointer if movePointers +// is true. +// Only use this for uncompressed blocks as this throws away remaining bits in +// the current byte. +bitjs.io.BitStream.prototype.peekBytes = function(n, movePointers) { + if (n <= 0 || typeof n != typeof 1) { + return 0; + } + + // from http://tools.ietf.org/html/rfc1951#page-11 + // "Any bits of input up to the next byte boundary are ignored." + while (this.bitPtr != 0) { + this.readBits(1); + } + + var movePointers = movePointers || false; + var bytePtr = this.bytePtr, + bitPtr = this.bitPtr; + + var result = this.bytes.subarray(bytePtr, bytePtr + n); + + if (movePointers) { + this.bytePtr += n; + } + + return result; +}; + +bitjs.io.BitStream.prototype.readBytes = function( n ) { + return this.peekBytes(n, true); +}; + + +/** + * This object allows you to peek and consume bytes as numbers and strings + * out of an ArrayBuffer. + * + * This object is much easier to write than the above BitStream since + * everything is byte-aligned. + * + * {ArrayBuffer} ab The ArrayBuffer object. + * {Number} opt_offset The offset into the ArrayBuffer + * {Number} opt_length The length of this BitStream + */ +bitjs.io.ByteStream = function(ab, opt_offset, opt_length) { + var offset = opt_offset || 0; + var length = opt_length || ab.byteLength; + this.bytes = new Uint8Array(ab, offset, length); + this.ptr = 0; +}; + +// peeks at the next n bytes as an unsigned number but does not advance the pointer +// TODO: This apparently cannot read more than 4 bytes as a number? +bitjs.io.ByteStream.prototype.peekNumber = function( n ) { + // TODO: return error if n would go past the end of the stream? + if (n <= 0 || typeof n != typeof 1) + return -1; + + var result = 0; + // read from last byte to first byte and roll them in + var curByte = this.ptr + n - 1; + while (curByte >= this.ptr) { + result <<= 8; + result |= this.bytes[curByte]; + --curByte; + } + return result; +}; + +// returns the next n bytes as an unsigned number (or -1 on error) +// and advances the stream pointer n bytes +bitjs.io.ByteStream.prototype.readNumber = function( n ) { + var num = this.peekNumber( n ); + this.ptr += n; + return num; +}; + +// This returns n bytes as a sub-array, advancing the pointer if movePointers +// is true. +bitjs.io.ByteStream.prototype.peekBytes = function(n, movePointers) { + if (n <= 0 || typeof n != typeof 1) { + return 0; + } + + var result = this.bytes.subarray(this.ptr, this.ptr + n); + + if (movePointers) { + this.ptr += n; + } + + return result; +}; + +bitjs.io.ByteStream.prototype.readBytes = function( n ) { + return this.peekBytes(n, true); +}; + +// peeks at the next n bytes as a string but does not advance the pointer +bitjs.io.ByteStream.prototype.peekString = function( n ) { + if (n <= 0 || typeof n != typeof 1) { + return 0; + } + + var result = ""; + for (var p = this.ptr, end = this.ptr + n; p < end; ++p) { + result += String.fromCharCode(this.bytes[p]); + } + return result; +}; + +// returns the next n bytes as a string +// and advances the stream pointer n bytes +bitjs.io.ByteStream.prototype.readString = function(n) { + var strToReturn = this.peekString(n); + this.ptr += n; + return strToReturn; +}; + + +/** + * A write-only Byte buffer which uses a Uint8 Typed Array as a backing store. + */ +bitjs.io.ByteBuffer = function(numBytes) { + if (typeof numBytes != typeof 1 || numBytes <= 0) { + throw "Error! ByteBuffer initialized with '" + numBytes + "'"; + } + this.data = new Uint8Array(numBytes); + this.ptr = 0; +}; + +bitjs.io.ByteBuffer.prototype.insertByte = function(b) { + // TODO: throw if byte is invalid? + this.data[this.ptr++] = b; +}; + +bitjs.io.ByteBuffer.prototype.insertBytes = function(bytes) { + // TODO: throw if bytes is invalid? + this.data.set(bytes, this.ptr); + this.ptr += bytes.length; +}; + +})(); diff --git a/examples/bitjs/unrar.js b/examples/bitjs/unrar.js new file mode 100755 index 0000000..e7133dc --- /dev/null +++ b/examples/bitjs/unrar.js @@ -0,0 +1,906 @@ +/** + * unrar.js + * + * Copyright(c) 2011 Google Inc. + * Copyright(c) 2011 antimatter15 + * + * Reference Documentation: + * + * http://kthoom.googlecode.com/hg/docs/unrar.html + */ + +// This file expects to be invoked as a Worker (see onmessage below). +importScripts('io.js'); +importScripts('archive.js'); + +// Progress variables. +var currentFilename = ""; +var currentFileNumber = 0; +var currentBytesUnarchivedInFile = 0; +var currentBytesUnarchived = 0; +var totalUncompressedBytesInArchive = 0; +var totalFilesInArchive = 0; + +// Helper functions. +var info = function(str) { + postMessage(new bitjs.archive.UnarchiveInfoEvent(str)); +}; +var err = function(str) { + postMessage(new bitjs.archive.UnarchiveErrorEvent(str)); +}; +var postProgress = function() { + postMessage(new bitjs.archive.UnarchiveProgressEvent( + currentFilename, + currentFileNumber, + currentBytesUnarchivedInFile, + currentBytesUnarchived, + totalUncompressedBytesInArchive, + totalFilesInArchive)); +}; + +// shows a byte value as its hex representation +var nibble = "0123456789ABCDEF"; +var byteValueToHexString = function(num) { + return nibble[num>>4] + nibble[num&0xF]; +}; +var twoByteValueToHexString = function(num) { + return nibble[(num>>12)&0xF] + nibble[(num>>8)&0xF] + nibble[(num>>4)&0xF] + nibble[num&0xF]; +}; + + +// Volume Types +var MARK_HEAD = 0x72, + MAIN_HEAD = 0x73, + FILE_HEAD = 0x74, + COMM_HEAD = 0x75, + AV_HEAD = 0x76, + SUB_HEAD = 0x77, + PROTECT_HEAD = 0x78, + SIGN_HEAD = 0x79, + NEWSUB_HEAD = 0x7a, + ENDARC_HEAD = 0x7b; + +// bstream is a bit stream +var RarVolumeHeader = function(bstream) { + + var headPos = bstream.bytePtr; + // byte 1,2 + info("Rar Volume Header @"+bstream.bytePtr); + + this.crc = bstream.readBits(16); + info(" crc=" + this.crc); + + // byte 3 + this.headType = bstream.readBits(8); + info(" headType=" + this.headType); + + // Get flags + // bytes 4,5 + this.flags = {}; + this.flags.value = bstream.peekBits(16); + + info(" flags=" + twoByteValueToHexString(this.flags.value)); + switch (this.headType) { + case MAIN_HEAD: + this.flags.MHD_VOLUME = !!bstream.readBits(1); + this.flags.MHD_COMMENT = !!bstream.readBits(1); + this.flags.MHD_LOCK = !!bstream.readBits(1); + this.flags.MHD_SOLID = !!bstream.readBits(1); + this.flags.MHD_PACK_COMMENT = !!bstream.readBits(1); + this.flags.MHD_NEWNUMBERING = this.flags.MHD_PACK_COMMENT; + this.flags.MHD_AV = !!bstream.readBits(1); + this.flags.MHD_PROTECT = !!bstream.readBits(1); + this.flags.MHD_PASSWORD = !!bstream.readBits(1); + this.flags.MHD_FIRSTVOLUME = !!bstream.readBits(1); + this.flags.MHD_ENCRYPTVER = !!bstream.readBits(1); + bstream.readBits(6); // unused + break; + case FILE_HEAD: + this.flags.LHD_SPLIT_BEFORE = !!bstream.readBits(1); // 0x0001 + this.flags.LHD_SPLIT_AFTER = !!bstream.readBits(1); // 0x0002 + this.flags.LHD_PASSWORD = !!bstream.readBits(1); // 0x0004 + this.flags.LHD_COMMENT = !!bstream.readBits(1); // 0x0008 + this.flags.LHD_SOLID = !!bstream.readBits(1); // 0x0010 + bstream.readBits(3); // unused + this.flags.LHD_LARGE = !!bstream.readBits(1); // 0x0100 + this.flags.LHD_UNICODE = !!bstream.readBits(1); // 0x0200 + this.flags.LHD_SALT = !!bstream.readBits(1); // 0x0400 + this.flags.LHD_VERSION = !!bstream.readBits(1); // 0x0800 + this.flags.LHD_EXTTIME = !!bstream.readBits(1); // 0x1000 + this.flags.LHD_EXTFLAGS = !!bstream.readBits(1); // 0x2000 + bstream.readBits(2); // unused + info(" LHD_SPLIT_BEFORE = " + this.flags.LHD_SPLIT_BEFORE); + break; + default: + bstream.readBits(16); + } + + // byte 6,7 + this.headSize = bstream.readBits(16); + info(" headSize=" + this.headSize); + switch (this.headType) { + case MAIN_HEAD: + this.highPosAv = bstream.readBits(16); + this.posAv = bstream.readBits(32); + if (this.flags.MHD_ENCRYPTVER) { + this.encryptVer = bstream.readBits(8); + } + info("Found MAIN_HEAD with highPosAv=" + this.highPosAv + ", posAv=" + this.posAv); + break; + case FILE_HEAD: + this.packSize = bstream.readBits(32); + this.unpackedSize = bstream.readBits(32); + this.hostOS = bstream.readBits(8); + this.fileCRC = bstream.readBits(32); + this.fileTime = bstream.readBits(32); + this.unpVer = bstream.readBits(8); + this.method = bstream.readBits(8); + this.nameSize = bstream.readBits(16); + this.fileAttr = bstream.readBits(32); + + if (this.flags.LHD_LARGE) { + info("Warning: Reading in LHD_LARGE 64-bit size values"); + this.HighPackSize = bstream.readBits(32); + this.HighUnpSize = bstream.readBits(32); + } else { + this.HighPackSize = 0; + this.HighUnpSize = 0; + if (this.unpackedSize == 0xffffffff) { + this.HighUnpSize = 0x7fffffff + this.unpackedSize = 0xffffffff; + } + } + this.fullPackSize = 0; + this.fullUnpackSize = 0; + this.fullPackSize |= this.HighPackSize; + this.fullPackSize <<= 32; + this.fullPackSize |= this.packSize; + + // read in filename + + this.filename = bstream.readBytes(this.nameSize); + for (var _i = 0, _s = ''; _i < this.filename.length; _i++) { + _s += String.fromCharCode(this.filename[_i]); + } + + this.filename = _s; + + if (this.flags.LHD_SALT) { + info("Warning: Reading in 64-bit salt value"); + this.salt = bstream.readBits(64); // 8 bytes + } + + if (this.flags.LHD_EXTTIME) { + // 16-bit flags + var extTimeFlags = bstream.readBits(16); + + // this is adapted straight out of arcread.cpp, Archive::ReadHeader() + for (var I = 0; I < 4; ++I) { + var rmode = extTimeFlags >> ((3-I)*4); + if ((rmode & 8)==0) + continue; + if (I!=0) + bstream.readBits(16); + var count = (rmode&3); + for (var J = 0; J < count; ++J) + bstream.readBits(8); + } + } + + if (this.flags.LHD_COMMENT) { + info("Found a LHD_COMMENT"); + } + + + while(headPos + this.headSize > bstream.bytePtr) bstream.readBits(1); + + info("Found FILE_HEAD with packSize=" + this.packSize + ", unpackedSize= " + this.unpackedSize + ", hostOS=" + this.hostOS + ", unpVer=" + this.unpVer + ", method=" + this.method + ", filename=" + this.filename); + + break; + default: + info("Found a header of type 0x" + byteValueToHexString(this.headType)); + // skip the rest of the header bytes (for now) + bstream.readBytes( this.headSize - 7 ); + break; + } +}; + +var BLOCK_LZ = 0, + BLOCK_PPM = 1; + +var rLDecode = [0,1,2,3,4,5,6,7,8,10,12,14,16,20,24,28,32,40,48,56,64,80,96,112,128,160,192,224], + rLBits = [0,0,0,0,0,0,0,0,1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5], + rDBitLengthCounts = [4,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,14,0,12], + rSDDecode = [0,4,8,16,32,64,128,192], + rSDBits = [2,2,3, 4, 5, 6, 6, 6]; + +var rDDecode = [0, 1, 2, 3, 4, 6, 8, 12, 16, 24, 32, + 48, 64, 96, 128, 192, 256, 384, 512, 768, 1024, 1536, 2048, 3072, + 4096, 6144, 8192, 12288, 16384, 24576, 32768, 49152, 65536, 98304, + 131072, 196608, 262144, 327680, 393216, 458752, 524288, 589824, + 655360, 720896, 786432, 851968, 917504, 983040]; + +var rDBits = [0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, + 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, + 15, 15, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16]; + +var rLOW_DIST_REP_COUNT = 16; + +var rNC = 299, + rDC = 60, + rLDC = 17, + rRC = 28, + rBC = 20, + rHUFF_TABLE_SIZE = (rNC+rDC+rRC+rLDC); + +var UnpBlockType = BLOCK_LZ; +var UnpOldTable = new Array(rHUFF_TABLE_SIZE); + +var BD = { //bitdecode + DecodeLen: new Array(16), + DecodePos: new Array(16), + DecodeNum: new Array(rBC) +}; +var LD = { //litdecode + DecodeLen: new Array(16), + DecodePos: new Array(16), + DecodeNum: new Array(rNC) +}; +var DD = { //distdecode + DecodeLen: new Array(16), + DecodePos: new Array(16), + DecodeNum: new Array(rDC) +}; +var LDD = { //low dist decode + DecodeLen: new Array(16), + DecodePos: new Array(16), + DecodeNum: new Array(rLDC) +}; +var RD = { //rep decode + DecodeLen: new Array(16), + DecodePos: new Array(16), + DecodeNum: new Array(rRC) +}; + +var rBuffer; + +// read in Huffman tables for RAR +function RarReadTables(bstream) { + var BitLength = new Array(rBC), + Table = new Array(rHUFF_TABLE_SIZE); + + // before we start anything we need to get byte-aligned + bstream.readBits( (8 - bstream.bitPtr) & 0x7 ); + + if (bstream.readBits(1)) { + info("Error! PPM not implemented yet"); + return; + } + + if (!bstream.readBits(1)) { //discard old table + for (var i = UnpOldTable.length; i--;) UnpOldTable[i] = 0; + } + + // read in bit lengths + for (var I = 0; I < rBC; ++I) { + + var Length = bstream.readBits(4); + if (Length == 15) { + var ZeroCount = bstream.readBits(4); + if (ZeroCount == 0) { + BitLength[I] = 15; + } + else { + ZeroCount += 2; + while (ZeroCount-- > 0 && I < rBC) + BitLength[I++] = 0; + --I; + } + } + else { + BitLength[I] = Length; + } + } + + // now all 20 bit lengths are obtained, we construct the Huffman Table: + + RarMakeDecodeTables(BitLength, 0, BD, rBC); + + var TableSize = rHUFF_TABLE_SIZE; + //console.log(DecodeLen, DecodePos, DecodeNum); + for (var i = 0; i < TableSize;) { + var num = RarDecodeNumber(bstream, BD); + if (num < 16) { + Table[i] = (num + UnpOldTable[i]) & 0xf; + i++; + } else if(num < 18) { + var N = (num == 16) ? (bstream.readBits(3) + 3) : (bstream.readBits(7) + 11); + + while (N-- > 0 && i < TableSize) { + Table[i] = Table[i - 1]; + i++; + } + } else { + var N = (num == 18) ? (bstream.readBits(3) + 3) : (bstream.readBits(7) + 11); + + while (N-- > 0 && i < TableSize) { + Table[i++] = 0; + } + } + } + + RarMakeDecodeTables(Table, 0, LD, rNC); + RarMakeDecodeTables(Table, rNC, DD, rDC); + RarMakeDecodeTables(Table, rNC + rDC, LDD, rLDC); + RarMakeDecodeTables(Table, rNC + rDC + rLDC, RD, rRC); + + for (var i = UnpOldTable.length; i--;) { + UnpOldTable[i] = Table[i]; + } + return true; +} + + +function RarDecodeNumber(bstream, dec) { + var DecodeLen = dec.DecodeLen, DecodePos = dec.DecodePos, DecodeNum = dec.DecodeNum; + var bitField = bstream.getBits() & 0xfffe; + //some sort of rolled out binary search + var bits = ((bitField < DecodeLen[8])? + ((bitField < DecodeLen[4])? + ((bitField < DecodeLen[2])? + ((bitField < DecodeLen[1])?1:2) + :((bitField < DecodeLen[3])?3:4)) + :(bitField < DecodeLen[6])? + ((bitField < DecodeLen[5])?5:6) + :((bitField < DecodeLen[7])?7:8)) + :((bitField < DecodeLen[12])? + ((bitField < DecodeLen[10])? + ((bitField < DecodeLen[9])?9:10) + :((bitField < DecodeLen[11])?11:12)) + :(bitField < DecodeLen[14])? + ((bitField < DecodeLen[13])?13:14) + :15)); + bstream.readBits(bits); + var N = DecodePos[bits] + ((bitField - DecodeLen[bits -1]) >>> (16 - bits)); + + return DecodeNum[N]; +} + + + +function RarMakeDecodeTables(BitLength, offset, dec, size) { + var DecodeLen = dec.DecodeLen, DecodePos = dec.DecodePos, DecodeNum = dec.DecodeNum; + var LenCount = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], + TmpPos = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], + N = 0, M = 0; + for (var i = DecodeNum.length; i--;) DecodeNum[i] = 0; + for (var i = 0; i < size; i++) { + LenCount[BitLength[i + offset] & 0xF]++; + } + LenCount[0] = 0; + TmpPos[0] = 0; + DecodePos[0] = 0; + DecodeLen[0] = 0; + + for (var I = 1; I < 16; ++I) { + N = 2 * (N+LenCount[I]); + M = (N << (15-I)); + if (M > 0xFFFF) + M = 0xFFFF; + DecodeLen[I] = M; + DecodePos[I] = DecodePos[I-1] + LenCount[I-1]; + TmpPos[I] = DecodePos[I]; + } + for (I = 0; I < size; ++I) + if (BitLength[I + offset] != 0) + DecodeNum[ TmpPos[ BitLength[offset + I] & 0xF ]++] = I; + +} + +// TODO: implement +function Unpack15(bstream, Solid) { + info("ERROR! RAR 1.5 compression not supported"); +} + +function Unpack20(bstream, Solid) { + var destUnpSize = rBuffer.data.length; + var oldDistPtr = 0; + + RarReadTables20(bstream); + while (destUnpSize > rBuffer.ptr) { + var num = RarDecodeNumber(bstream, LD); + if (num < 256) { + rBuffer.insertByte(num); + continue; + } + if (num > 269) { + var Length = rLDecode[num -= 270] + 3; + if ((Bits = rLBits[num]) > 0) { + Length += bstream.readBits(Bits); + } + var DistNumber = RarDecodeNumber(bstream, DD); + var Distance = rDDecode[DistNumber] + 1; + if ((Bits = rDBits[DistNumber]) > 0) { + Distance += bstream.readBits(Bits); + } + if (Distance >= 0x2000) { + Length++; + if(Distance >= 0x40000) Length++; + } + lastLength = Length; + lastDist = rOldDist[oldDistPtr++ & 3] = Distance; + RarCopyString(Length, Distance); + continue; + } + if (num == 269) { + RarReadTables20(bstream); + + RarUpdateProgress() + + continue; + } + if (num == 256) { + lastDist = rOldDist[oldDistPtr++ & 3] = lastDist; + RarCopyString(lastLength, lastDist); + continue; + } + if (num < 261) { + var Distance = rOldDist[(oldDistPtr - (num - 256)) & 3]; + var LengthNumber = RarDecodeNumber(bstream, RD); + var Length = rLDecode[LengthNumber] +2; + if ((Bits = rLBits[LengthNumber]) > 0) { + Length += bstream.readBits(Bits); + } + if (Distance >= 0x101) { + Length++; + if (Distance >= 0x2000) { + Length++ + if (Distance >= 0x40000) Length++; + } + } + lastLength = Length; + lastDist = rOldDist[oldDistPtr++ & 3] = Distance; + RarCopyString(Length, Distance); + continue; + } + if (num < 270) { + var Distance = rSDDecode[num -= 261] + 1; + if ((Bits = rSDBits[num]) > 0) { + Distance += bstream.readBits(Bits); + } + lastLength = 2; + lastDist = rOldDist[oldDistPtr++ & 3] = Distance; + RarCopyString(2, Distance); + continue; + } + + } + RarUpdateProgress() +} + +function RarUpdateProgress() { + var change = rBuffer.ptr - currentBytesUnarchivedInFile; + currentBytesUnarchivedInFile = rBuffer.ptr; + currentBytesUnarchived += change; + postProgress(); +} + + +var rNC20 = 298, + rDC20 = 48, + rRC20 = 28, + rBC20 = 19, + rMC20 = 257; + +var UnpOldTable20 = new Array(rMC20 * 4); + +function RarReadTables20(bstream) { + var BitLength = new Array(rBC20); + var Table = new Array(rMC20 * 4); + var TableSize, N, I; + var AudioBlock = bstream.readBits(1); + if (!bstream.readBits(1)) + for (var i = UnpOldTable20.length; i--;) UnpOldTable20[i] = 0; + TableSize = rNC20 + rDC20 + rRC20; + for (var I = 0; I < rBC20; I++) + BitLength[I] = bstream.readBits(4); + RarMakeDecodeTables(BitLength, 0, BD, rBC20); + I = 0; + while (I < TableSize) { + var num = RarDecodeNumber(bstream, BD); + if (num < 16) { + Table[I] = num + UnpOldTable20[I] & 0xf; + I++; + } else if(num == 16) { + N = bstream.readBits(2) + 3; + while (N-- > 0 && I < TableSize) { + Table[I] = Table[I - 1]; + I++; + } + } else { + if (num == 17) { + N = bstream.readBits(3) + 3; + } else { + N = bstream.readBits(7) + 11; + } + while (N-- > 0 && I < TableSize) { + Table[I++] = 0; + } + } + } + RarMakeDecodeTables(Table, 0, LD, rNC20); + RarMakeDecodeTables(Table, rNC20, DD, rDC20); + RarMakeDecodeTables(Table, rNC20 + rDC20, RD, rRC20); + for (var i = UnpOldTable20.length; i--;) UnpOldTable20[i] = Table[i]; +} + +var lowDistRepCount = 0, prevLowDist = 0; + +var rOldDist = [0,0,0,0]; +var lastDist; +var lastLength; + + +function Unpack29(bstream, Solid) { + // lazy initialize rDDecode and rDBits + + var DDecode = new Array(rDC); + var DBits = new Array(rDC); + + var Dist=0,BitLength=0,Slot=0; + + for (var I = 0; I < rDBitLengthCounts.length; I++,BitLength++) { + for (var J = 0; J < rDBitLengthCounts[I]; J++,Slot++,Dist+=(1<= 271) { + var Length = rLDecode[num -= 271] + 3; + if ((Bits = rLBits[num]) > 0) { + Length += bstream.readBits(Bits); + } + var DistNumber = RarDecodeNumber(bstream, DD); + var Distance = DDecode[DistNumber]+1; + if ((Bits = DBits[DistNumber]) > 0) { + if (DistNumber > 9) { + if (Bits > 4) { + Distance += ((bstream.getBits() >>> (20 - Bits)) << 4); + bstream.readBits(Bits - 4); + //todo: check this + } + if (lowDistRepCount > 0) { + lowDistRepCount--; + Distance += prevLowDist; + } else { + var LowDist = RarDecodeNumber(bstream, LDD); + if (LowDist == 16) { + lowDistRepCount = rLOW_DIST_REP_COUNT - 1; + Distance += prevLowDist; + } else { + Distance += LowDist; + prevLowDist = LowDist; + } + } + } else { + Distance += bstream.readBits(Bits); + } + } + if (Distance >= 0x2000) { + Length++; + if (Distance >= 0x40000) { + Length++; + } + } + RarInsertOldDist(Distance); + RarInsertLastMatch(Length, Distance); + RarCopyString(Length, Distance); + continue; + } + if (num == 256) { + if (!RarReadEndOfBlock(bstream)) break; + + continue; + } + if (num == 257) { + //console.log("READVMCODE"); + if (!RarReadVMCode(bstream)) break; + continue; + } + if (num == 258) { + if (lastLength != 0) { + RarCopyString(lastLength, lastDist); + } + continue; + } + if (num < 263) { + var DistNum = num - 259; + var Distance = rOldDist[DistNum]; + + for (var I = DistNum; I > 0; I--) { + rOldDist[I] = rOldDist[I-1]; + } + rOldDist[0] = Distance; + + var LengthNumber = RarDecodeNumber(bstream, RD); + var Length = rLDecode[LengthNumber] + 2; + if ((Bits = rLBits[LengthNumber]) > 0) { + Length += bstream.readBits(Bits); + } + RarInsertLastMatch(Length, Distance); + RarCopyString(Length, Distance); + continue; + } + if (num < 272) { + var Distance = rSDDecode[num -= 263] + 1; + if ((Bits = rSDBits[num]) > 0) { + Distance += bstream.readBits(Bits); + } + RarInsertOldDist(Distance); + RarInsertLastMatch(2, Distance); + RarCopyString(2, Distance); + continue; + } + + } + RarUpdateProgress() +} + +function RarReadEndOfBlock(bstream) { + + RarUpdateProgress() + + + var NewTable = false, NewFile = false; + if (bstream.readBits(1)) { + NewTable = true; + } else { + NewFile = true; + NewTable = !!bstream.readBits(1); + } + //tablesRead = !NewTable; + return !(NewFile || NewTable && !RarReadTables(bstream)); +} + + +function RarReadVMCode(bstream) { + var FirstByte = bstream.readBits(8); + var Length = (FirstByte & 7) + 1; + if (Length == 7) { + Length = bstream.readBits(8) + 7; + } else if(Length == 8) { + Length = bstream.readBits(16); + } + var vmCode = []; + for(var I = 0; I < Length; I++) { + //do something here with cheking readbuf + vmCode.push(bstream.readBits(8)); + } + return RarAddVMCode(FirstByte, vmCode, Length); +} + +function RarAddVMCode(firstByte, vmCode, length) { + //console.log(vmCode); + if (vmCode.length > 0) { + info("Error! RarVM not supported yet!"); + } + return true; +} + +function RarInsertLastMatch(length, distance) { + lastDist = distance; + lastLength = length; +} + +function RarInsertOldDist(distance) { + rOldDist.splice(3,1); + rOldDist.splice(0,0,distance); +} + +//this is the real function, the other one is for debugging +function RarCopyString(length, distance) { + var destPtr = rBuffer.ptr - distance; + if(destPtr < 0){ + var l = rOldBuffers.length; + while(destPtr < 0){ + destPtr = rOldBuffers[--l].data.length + destPtr + } + //TODO: lets hope that it never needs to read beyond file boundaries + while(length--) rBuffer.insertByte(rOldBuffers[l].data[destPtr++]); + + } + if (length > distance) { + while(length--) rBuffer.insertByte(rBuffer.data[destPtr++]); + } else { + rBuffer.insertBytes(rBuffer.data.subarray(destPtr, destPtr + length)); + } + +} + +var rOldBuffers = [] +// v must be a valid RarVolume +function unpack(v) { + + // TODO: implement what happens when unpVer is < 15 + var Ver = v.header.unpVer <= 15 ? 15 : v.header.unpVer, + Solid = v.header.LHD_SOLID, + bstream = new bitjs.io.BitStream(v.fileData.buffer, true /* rtl */, v.fileData.byteOffset, v.fileData.byteLength ); + + rBuffer = new bitjs.io.ByteBuffer(v.header.unpackedSize); + + info("Unpacking "+v.filename+" RAR v"+Ver); + + switch(Ver) { + case 15: // rar 1.5 compression + Unpack15(bstream, Solid); + break; + case 20: // rar 2.x compression + case 26: // files larger than 2GB + Unpack20(bstream, Solid); + break; + case 29: // rar 3.x compression + case 36: // alternative hash + Unpack29(bstream, Solid); + break; + } // switch(method) + + rOldBuffers.push(rBuffer); + //TODO: clear these old buffers when there's over 4MB of history + return rBuffer.data; +} + +// bstream is a bit stream +var RarLocalFile = function(bstream) { + + this.header = new RarVolumeHeader(bstream); + this.filename = this.header.filename; + + if (this.header.headType != FILE_HEAD && this.header.headType != ENDARC_HEAD) { + this.isValid = false; + info("Error! RAR Volume did not include a FILE_HEAD header "); + } + else { + // read in the compressed data + this.fileData = null; + if (this.header.packSize > 0) { + this.fileData = bstream.readBytes(this.header.packSize); + this.isValid = true; + } + } +}; + +RarLocalFile.prototype.unrar = function() { + + if (!this.header.flags.LHD_SPLIT_BEFORE) { + // unstore file + if (this.header.method == 0x30) { + info("Unstore "+this.filename); + this.isValid = true; + + currentBytesUnarchivedInFile += this.fileData.length; + currentBytesUnarchived += this.fileData.length; + } else { + this.isValid = true; + this.fileData = unpack(this); + } + } +} + +var unrar = function(arrayBuffer) { + currentFilename = ""; + currentFileNumber = 0; + currentBytesUnarchivedInFile = 0; + currentBytesUnarchived = 0; + totalUncompressedBytesInArchive = 0; + totalFilesInArchive = 0; + + postMessage(new bitjs.archive.UnarchiveStartEvent()); + var bstream = new bitjs.io.BitStream(arrayBuffer, false /* rtl */); + + var header = new RarVolumeHeader(bstream); + if (header.crc == 0x6152 && + header.headType == 0x72 && + header.flags.value == 0x1A21 && + header.headSize == 7) { + info("Found RAR signature"); + + var mhead = new RarVolumeHeader(bstream); + if (mhead.headType != MAIN_HEAD) { + info("Error! RAR did not include a MAIN_HEAD header"); + } + else { + var localFiles = [], + localFile = null; + do { + try { + localFile = new RarLocalFile(bstream); + info("RAR localFile isValid=" + localFile.isValid + ", volume packSize=" + localFile.header.packSize); + if (localFile && localFile.isValid && localFile.header.packSize > 0) { + totalUncompressedBytesInArchive += localFile.header.unpackedSize; + localFiles.push(localFile); + } else if (localFile.header.packSize == 0 && localFile.header.unpackedSize == 0) { + localFile.isValid = true; + } + } catch(err) { + break; + } + //info("bstream" + bstream.bytePtr+"/"+bstream.bytes.length); + } while( localFile.isValid ); + totalFilesInArchive = localFiles.length; + + // now we have all information but things are unpacked + // TODO: unpack + localFiles = localFiles.sort(function(a,b) { + // extract the number at the end of both filenames + var aname = a.filename; + var bname = b.filename; + return aname > bname ? 1 : -1; + /* + var aindex = aname.length, bindex = bname.length; + + // Find the last number character from the back of the filename. + while (aname[aindex-1] < '0' || aname[aindex-1] > '9') --aindex; + while (bname[bindex-1] < '0' || bname[bindex-1] > '9') --bindex; + + // Find the first number character from the back of the filename + while (aname[aindex-1] >= '0' && aname[aindex-1] <= '9') --aindex; + while (bname[bindex-1] >= '0' && bname[bindex-1] <= '9') --bindex; + + // parse them into numbers and return comparison + var anum = parseInt(aname.substr(aindex), 10), + bnum = parseInt(bname.substr(bindex), 10); + return bnum - anum;*/ + }); + + info(localFiles.map(function(a){return a.filename}).join(', ')); + for (var i = 0; i < localFiles.length; ++i) { + var localfile = localFiles[i]; + + // update progress + currentFilename = localfile.header.filename; + currentBytesUnarchivedInFile = 0; + + // actually do the unzipping + localfile.unrar(); + + if (localfile.isValid) { + postMessage(new bitjs.archive.UnarchiveExtractEvent(localfile)); + postProgress(); + } + } + + postProgress(); + } + } + else { + err("Invalid RAR file"); + } + postMessage(new bitjs.archive.UnarchiveFinishEvent()); +}; + +// event.data.file has the ArrayBuffer. +onmessage = function(event) { + var ab = event.data.file; + unrar(ab, true); +}; diff --git a/examples/bitjs/untar.js b/examples/bitjs/untar.js new file mode 100755 index 0000000..3b16764 --- /dev/null +++ b/examples/bitjs/untar.js @@ -0,0 +1,182 @@ +/** + * untar.js + * + * Copyright(c) 2011 Google Inc. + * + * Reference Documentation: + * + * TAR format: http://www.gnu.org/software/automake/manual/tar/Standard.html + */ + +// This file expects to be invoked as a Worker (see onmessage below). +importScripts('io.js'); +importScripts('archive.js'); + +// Progress variables. +var currentFilename = ""; +var currentFileNumber = 0; +var currentBytesUnarchivedInFile = 0; +var currentBytesUnarchived = 0; +var totalUncompressedBytesInArchive = 0; +var totalFilesInArchive = 0; + +// Helper functions. +var info = function(str) { + postMessage(new bitjs.archive.UnarchiveInfoEvent(str)); +}; +var err = function(str) { + postMessage(new bitjs.archive.UnarchiveErrorEvent(str)); +}; +var postProgress = function() { + postMessage(new bitjs.archive.UnarchiveProgressEvent( + currentFilename, + currentFileNumber, + currentBytesUnarchivedInFile, + currentBytesUnarchived, + totalUncompressedBytesInArchive, + totalFilesInArchive)); +}; + +// Removes all characters from the first zero-byte in the string onwards. +var readCleanString = function(bstr, numBytes) { + var str = bstr.readString(numBytes); + var zIndex = str.indexOf(String.fromCharCode(0)); + return zIndex != -1 ? str.substr(0, zIndex) : str; +}; + +// takes a ByteStream and parses out the local file information +var TarLocalFile = function(bstream) { + this.isValid = false; + + // Read in the header block + this.name = readCleanString(bstream, 100); + this.mode = readCleanString(bstream, 8); + this.uid = readCleanString(bstream, 8); + this.gid = readCleanString(bstream, 8); + this.size = parseInt(readCleanString(bstream, 12), 8); + this.mtime = readCleanString(bstream, 12); + this.chksum = readCleanString(bstream, 8); + this.typeflag = readCleanString(bstream, 1); + this.linkname = readCleanString(bstream, 100); + this.maybeMagic = readCleanString(bstream, 6); + + if (this.maybeMagic == "ustar") { + this.version = readCleanString(bstream, 2); + this.uname = readCleanString(bstream, 32); + this.gname = readCleanString(bstream, 32); + this.devmajor = readCleanString(bstream, 8); + this.devminor = readCleanString(bstream, 8); + this.prefix = readCleanString(bstream, 155); + + if (this.prefix.length) { + this.name = this.prefix + this.name; + } + bstream.readBytes(12); // 512 - 500 + } else { + bstream.readBytes(255); // 512 - 257 + } + + // Done header, now rest of blocks are the file contents. + this.filename = this.name; + this.fileData = null; + + info("Untarring file '" + this.filename + "'"); + info(" size = " + this.size); + info(" typeflag = " + this.typeflag); + + // A regular file. + if (this.typeflag == 0) { + info(" This is a regular file."); + var sizeInBytes = parseInt(this.size); + this.fileData = new Uint8Array(bstream.bytes.buffer, bstream.ptr, this.size); + if (this.name.length > 0 && this.size > 0 && this.fileData && this.fileData.buffer) { + this.isValid = true; + } + + bstream.readBytes(this.size); + + // Round up to 512-byte blocks. + var remaining = 512 - this.size % 512; + if (remaining > 0 && remaining < 512) { + bstream.readBytes(remaining); + } + } else if (this.typeflag == 5) { + info(" This is a directory.") + } +}; + +// Takes an ArrayBuffer of a tar file in +// returns null on error +// returns an array of DecompressedFile objects on success +var untar = function(arrayBuffer) { + currentFilename = ""; + currentFileNumber = 0; + currentBytesUnarchivedInFile = 0; + currentBytesUnarchived = 0; + totalUncompressedBytesInArchive = 0; + totalFilesInArchive = 0; + + postMessage(new bitjs.archive.UnarchiveStartEvent()); + var bstream = new bitjs.io.ByteStream(arrayBuffer); + var localFiles = []; + + // While we don't encounter an empty block, keep making TarLocalFiles. + while (bstream.peekNumber(4) != 0) { + var oneLocalFile = new TarLocalFile(bstream); + if (oneLocalFile && oneLocalFile.isValid) { + localFiles.push(oneLocalFile); + totalUncompressedBytesInArchive += oneLocalFile.size; + } + } + totalFilesInArchive = localFiles.length; + + // got all local files, now sort them + localFiles.sort(function(a,b) { + // extract the number at the end of both filenames + var aname = a.filename; + var bname = b.filename; + var aindex = aname.length, bindex = bname.length; + + // Find the last number character from the back of the filename. + while (aname[aindex-1] < '0' || aname[aindex-1] > '9') --aindex; + while (bname[bindex-1] < '0' || bname[bindex-1] > '9') --bindex; + + // Find the first number character from the back of the filename + while (aname[aindex-1] >= '0' && aname[aindex-1] <= '9') --aindex; + while (bname[bindex-1] >= '0' && bname[bindex-1] <= '9') --bindex; + + // parse them into numbers and return comparison + var anum = parseInt(aname.substr(aindex), 10), + bnum = parseInt(bname.substr(bindex), 10); + return anum - bnum; + }); + + // report # files and total length + if (localFiles.length > 0) { + postProgress(); + } + + // now do the shipping of each file + for (var i = 0; i < localFiles.length; ++i) { + var localfile = localFiles[i]; + info("Sending file '" + localfile.filename + "' up"); + + // update progress + currentFilename = localfile.filename; + currentFileNumber = i; + currentBytesUnarchivedInFile = localfile.size; + currentBytesUnarchived += localfile.size; + postMessage(new bitjs.archive.UnarchiveExtractEvent(localfile)); + postProgress(); + } + + postProgress(); + + postMessage(new bitjs.archive.UnarchiveFinishEvent()); +}; + +// event.data.file has the ArrayBuffer. +onmessage = function(event) { + var ab = event.data.file; + untar(ab); +}; diff --git a/examples/bitjs/unzip.js b/examples/bitjs/unzip.js new file mode 100755 index 0000000..99e223d --- /dev/null +++ b/examples/bitjs/unzip.js @@ -0,0 +1,631 @@ +/** + * unzip.js + * + * Copyright(c) 2011 Google Inc. + * Copyright(c) 2011 antimatter15 + * + * Reference Documentation: + * + * ZIP format: http://www.pkware.com/documents/casestudies/APPNOTE.TXT + * DEFLATE format: http://tools.ietf.org/html/rfc1951 + */ + +// This file expects to be invoked as a Worker (see onmessage below). +importScripts('io.js'); +importScripts('archive.js'); + +// Progress variables. +var currentFilename = ""; +var currentFileNumber = 0; +var currentBytesUnarchivedInFile = 0; +var currentBytesUnarchived = 0; +var totalUncompressedBytesInArchive = 0; +var totalFilesInArchive = 0; + +// Helper functions. +var info = function(str) { + postMessage(new bitjs.archive.UnarchiveInfoEvent(str)); +}; +var err = function(str) { + postMessage(new bitjs.archive.UnarchiveErrorEvent(str)); +}; +var postProgress = function() { + postMessage(new bitjs.archive.UnarchiveProgressEvent( + currentFilename, + currentFileNumber, + currentBytesUnarchivedInFile, + currentBytesUnarchived, + totalUncompressedBytesInArchive, + totalFilesInArchive)); +}; + +var zLocalFileHeaderSignature = 0x04034b50; +var zArchiveExtraDataSignature = 0x08064b50; +var zCentralFileHeaderSignature = 0x02014b50; +var zDigitalSignatureSignature = 0x05054b50; +var zEndOfCentralDirSignature = 0x06064b50; +var zEndOfCentralDirLocatorSignature = 0x07064b50; + +// takes a ByteStream and parses out the local file information +var ZipLocalFile = function(bstream) { + if (typeof bstream != typeof {} || !bstream.readNumber || typeof bstream.readNumber != typeof function(){}) { + return null; + } + + bstream.readNumber(4); // swallow signature + this.version = bstream.readNumber(2); + this.generalPurpose = bstream.readNumber(2); + this.compressionMethod = bstream.readNumber(2); + this.lastModFileTime = bstream.readNumber(2); + this.lastModFileDate = bstream.readNumber(2); + this.crc32 = bstream.readNumber(4); + this.compressedSize = bstream.readNumber(4); + this.uncompressedSize = bstream.readNumber(4); + this.fileNameLength = bstream.readNumber(2); + this.extraFieldLength = bstream.readNumber(2); + + this.filename = null; + if (this.fileNameLength > 0) { + this.filename = bstream.readString(this.fileNameLength); + } + + info("Zip Local File Header:"); + info(" version=" + this.version); + info(" general purpose=" + this.generalPurpose); + info(" compression method=" + this.compressionMethod); + info(" last mod file time=" + this.lastModFileTime); + info(" last mod file date=" + this.lastModFileDate); + info(" crc32=" + this.crc32); + info(" compressed size=" + this.compressedSize); + info(" uncompressed size=" + this.uncompressedSize); + info(" file name length=" + this.fileNameLength); + info(" extra field length=" + this.extraFieldLength); + info(" filename = '" + this.filename + "'"); + + this.extraField = null; + if (this.extraFieldLength > 0) { + this.extraField = bstream.readString(this.extraFieldLength); + info(" extra field=" + this.extraField); + } + + // read in the compressed data + this.fileData = null; + if (this.compressedSize > 0) { + this.fileData = new Uint8Array(bstream.bytes.buffer, bstream.ptr, this.compressedSize); + bstream.ptr += this.compressedSize; + } + + // TODO: deal with data descriptor if present (we currently assume no data descriptor!) + // "This descriptor exists only if bit 3 of the general purpose bit flag is set" + // But how do you figure out how big the file data is if you don't know the compressedSize + // from the header?!? + if ((this.generalPurpose & bitjs.BIT[3]) != 0) { + this.crc32 = bstream.readNumber(4); + this.compressedSize = bstream.readNumber(4); + this.uncompressedSize = bstream.readNumber(4); + } +}; + +// determine what kind of compressed data we have and decompress +ZipLocalFile.prototype.unzip = function() { + + // Zip Version 1.0, no compression (store only) + if (this.compressionMethod == 0 ) { + info("ZIP v"+this.version+", store only: " + this.filename + " (" + this.compressedSize + " bytes)"); + currentBytesUnarchivedInFile = this.compressedSize; + currentBytesUnarchived += this.compressedSize; + } + // version == 20, compression method == 8 (DEFLATE) + else if (this.compressionMethod == 8) { + info("ZIP v2.0, DEFLATE: " + this.filename + " (" + this.compressedSize + " bytes)"); + this.fileData = inflate(this.fileData, this.uncompressedSize); + } + else { + err("UNSUPPORTED VERSION/FORMAT: ZIP v" + this.version + ", compression method=" + this.compressionMethod + ": " + this.filename + " (" + this.compressedSize + " bytes)"); + this.fileData = null; + } +}; + + +// Takes an ArrayBuffer of a zip file in +// returns null on error +// returns an array of DecompressedFile objects on success +var unzip = function(arrayBuffer) { + postMessage(new bitjs.archive.UnarchiveStartEvent()); + + currentFilename = ""; + currentFileNumber = 0; + currentBytesUnarchivedInFile = 0; + currentBytesUnarchived = 0; + totalUncompressedBytesInArchive = 0; + totalFilesInArchive = 0; + currentBytesUnarchived = 0; + + var bstream = new bitjs.io.ByteStream(arrayBuffer); + // detect local file header signature or return null + if (bstream.peekNumber(4) == zLocalFileHeaderSignature) { + var localFiles = []; + // loop until we don't see any more local files + while (bstream.peekNumber(4) == zLocalFileHeaderSignature) { + var oneLocalFile = new ZipLocalFile(bstream); + // this should strip out directories/folders + if (oneLocalFile && oneLocalFile.uncompressedSize > 0 && oneLocalFile.fileData) { + localFiles.push(oneLocalFile); + totalUncompressedBytesInArchive += oneLocalFile.uncompressedSize; + } + } + totalFilesInArchive = localFiles.length; + + // got all local files, now sort them + localFiles.sort(function(a,b) { + // extract the number at the end of both filenames + var aname = a.filename; + var bname = b.filename; + var aindex = aname.length, bindex = bname.length; + + // Find the last number character from the back of the filename. + while (aname[aindex-1] < '0' || aname[aindex-1] > '9') --aindex; + while (bname[bindex-1] < '0' || bname[bindex-1] > '9') --bindex; + + // Find the first number character from the back of the filename + while (aname[aindex-1] >= '0' && aname[aindex-1] <= '9') --aindex; + while (bname[bindex-1] >= '0' && bname[bindex-1] <= '9') --bindex; + + // parse them into numbers and return comparison + var anum = parseInt(aname.substr(aindex), 10), + bnum = parseInt(bname.substr(bindex), 10); + return anum - bnum; + }); + + // archive extra data record + if (bstream.peekNumber(4) == zArchiveExtraDataSignature) { + info(" Found an Archive Extra Data Signature"); + + // skipping this record for now + bstream.readNumber(4); + var archiveExtraFieldLength = bstream.readNumber(4); + bstream.readString(archiveExtraFieldLength); + } + + // central directory structure + // TODO: handle the rest of the structures (Zip64 stuff) + if (bstream.peekNumber(4) == zCentralFileHeaderSignature) { + info(" Found a Central File Header"); + + // read all file headers + while (bstream.peekNumber(4) == zCentralFileHeaderSignature) { + bstream.readNumber(4); // signature + bstream.readNumber(2); // version made by + bstream.readNumber(2); // version needed to extract + bstream.readNumber(2); // general purpose bit flag + bstream.readNumber(2); // compression method + bstream.readNumber(2); // last mod file time + bstream.readNumber(2); // last mod file date + bstream.readNumber(4); // crc32 + bstream.readNumber(4); // compressed size + bstream.readNumber(4); // uncompressed size + var fileNameLength = bstream.readNumber(2); // file name length + var extraFieldLength = bstream.readNumber(2); // extra field length + var fileCommentLength = bstream.readNumber(2); // file comment length + bstream.readNumber(2); // disk number start + bstream.readNumber(2); // internal file attributes + bstream.readNumber(4); // external file attributes + bstream.readNumber(4); // relative offset of local header + + bstream.readString(fileNameLength); // file name + bstream.readString(extraFieldLength); // extra field + bstream.readString(fileCommentLength); // file comment + } + } + + // digital signature + if (bstream.peekNumber(4) == zDigitalSignatureSignature) { + info(" Found a Digital Signature"); + + bstream.readNumber(4); + var sizeOfSignature = bstream.readNumber(2); + bstream.readString(sizeOfSignature); // digital signature data + } + + // report # files and total length + if (localFiles.length > 0) { + postProgress(); + } + + // now do the unzipping of each file + for (var i = 0; i < localFiles.length; ++i) { + var localfile = localFiles[i]; + + // update progress + currentFilename = localfile.filename; + currentFileNumber = i; + currentBytesUnarchivedInFile = 0; + + // actually do the unzipping + localfile.unzip(); + + if (localfile.fileData != null) { + postMessage(new bitjs.archive.UnarchiveExtractEvent(localfile)); + postProgress(); + } + } + postProgress(); + postMessage(new bitjs.archive.UnarchiveFinishEvent()); + } +} + +// returns a table of Huffman codes +// each entry's index is its code and its value is a JavaScript object +// containing {length: 6, symbol: X} +function getHuffmanCodes(bitLengths) { + // ensure bitLengths is an array containing at least one element + if (typeof bitLengths != typeof [] || bitLengths.length < 1) { + err("Error! getHuffmanCodes() called with an invalid array"); + return null; + } + + // Reference: http://tools.ietf.org/html/rfc1951#page-8 + var numLengths = bitLengths.length, + bl_count = [], + MAX_BITS = 1; + + // Step 1: count up how many codes of each length we have + for (var i = 0; i < numLengths; ++i) { + var length = bitLengths[i]; + // test to ensure each bit length is a positive, non-zero number + if (typeof length != typeof 1 || length < 0) { + err("bitLengths contained an invalid number in getHuffmanCodes(): " + length + " of type " + (typeof length)); + return null; + } + // increment the appropriate bitlength count + if (bl_count[length] == undefined) bl_count[length] = 0; + // a length of zero means this symbol is not participating in the huffman coding + if (length > 0) bl_count[length]++; + + if (length > MAX_BITS) MAX_BITS = length; + } + + // Step 2: Find the numerical value of the smallest code for each code length + var next_code = [], + code = 0; + for (var bits = 1; bits <= MAX_BITS; ++bits) { + var length = bits-1; + // ensure undefined lengths are zero + if (bl_count[length] == undefined) bl_count[length] = 0; + code = (code + bl_count[bits-1]) << 1; + next_code[bits] = code; + } + + // Step 3: Assign numerical values to all codes + var table = {}, tableLength = 0; + for (var n = 0; n < numLengths; ++n) { + var len = bitLengths[n]; + if (len != 0) { + table[next_code[len]] = { length: len, symbol: n }; //, bitstring: binaryValueToString(next_code[len],len) }; + tableLength++; + next_code[len]++; + } + } + table.maxLength = tableLength; + + return table; +} + +/* + The Huffman codes for the two alphabets are fixed, and are not + represented explicitly in the data. The Huffman code lengths + for the literal/length alphabet are: + + Lit Value Bits Codes + --------- ---- ----- + 0 - 143 8 00110000 through + 10111111 + 144 - 255 9 110010000 through + 111111111 + 256 - 279 7 0000000 through + 0010111 + 280 - 287 8 11000000 through + 11000111 +*/ +// fixed Huffman codes go from 7-9 bits, so we need an array whose index can hold up to 9 bits +var fixedHCtoLiteral = null; +var fixedHCtoDistance = null; +function getFixedLiteralTable() { + // create once + if (!fixedHCtoLiteral) { + var bitlengths = new Array(288); + for (var i = 0; i <= 143; ++i) bitlengths[i] = 8; + for (i = 144; i <= 255; ++i) bitlengths[i] = 9; + for (i = 256; i <= 279; ++i) bitlengths[i] = 7; + for (i = 280; i <= 287; ++i) bitlengths[i] = 8; + + // get huffman code table + fixedHCtoLiteral = getHuffmanCodes(bitlengths); + } + return fixedHCtoLiteral; +} +function getFixedDistanceTable() { + // create once + if (!fixedHCtoDistance) { + var bitlengths = new Array(32); + for (var i = 0; i < 32; ++i) { bitlengths[i] = 5; } + + // get huffman code table + fixedHCtoDistance = getHuffmanCodes(bitlengths); + } + return fixedHCtoDistance; +} + +// extract one bit at a time until we find a matching Huffman Code +// then return that symbol +function decodeSymbol(bstream, hcTable) { + var code = 0, len = 0; + var match = false; + + // loop until we match + for (;;) { + // read in next bit + var bit = bstream.readBits(1); + code = (code<<1) | bit; + ++len; + + // check against Huffman Code table and break if found + if (hcTable.hasOwnProperty(code) && hcTable[code].length == len) { + + break; + } + if (len > hcTable.maxLength) { + err("Bit stream out of sync, didn't find a Huffman Code, length was " + len + + " and table only max code length of " + hcTable.maxLength); + break; + } + } + return hcTable[code].symbol; +} + + +var CodeLengthCodeOrder = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]; + /* + Extra Extra Extra + Code Bits Length(s) Code Bits Lengths Code Bits Length(s) + ---- ---- ------ ---- ---- ------- ---- ---- ------- + 257 0 3 267 1 15,16 277 4 67-82 + 258 0 4 268 1 17,18 278 4 83-98 + 259 0 5 269 2 19-22 279 4 99-114 + 260 0 6 270 2 23-26 280 4 115-130 + 261 0 7 271 2 27-30 281 5 131-162 + 262 0 8 272 2 31-34 282 5 163-194 + 263 0 9 273 3 35-42 283 5 195-226 + 264 0 10 274 3 43-50 284 5 227-257 + 265 1 11,12 275 3 51-58 285 0 258 + 266 1 13,14 276 3 59-66 + + */ +var LengthLookupTable = [ + [0,3], [0,4], [0,5], [0,6], + [0,7], [0,8], [0,9], [0,10], + [1,11], [1,13], [1,15], [1,17], + [2,19], [2,23], [2,27], [2,31], + [3,35], [3,43], [3,51], [3,59], + [4,67], [4,83], [4,99], [4,115], + [5,131], [5,163], [5,195], [5,227], + [0,258] +]; + /* + Extra Extra Extra + Code Bits Dist Code Bits Dist Code Bits Distance + ---- ---- ---- ---- ---- ------ ---- ---- -------- + 0 0 1 10 4 33-48 20 9 1025-1536 + 1 0 2 11 4 49-64 21 9 1537-2048 + 2 0 3 12 5 65-96 22 10 2049-3072 + 3 0 4 13 5 97-128 23 10 3073-4096 + 4 1 5,6 14 6 129-192 24 11 4097-6144 + 5 1 7,8 15 6 193-256 25 11 6145-8192 + 6 2 9-12 16 7 257-384 26 12 8193-12288 + 7 2 13-16 17 7 385-512 27 12 12289-16384 + 8 3 17-24 18 8 513-768 28 13 16385-24576 + 9 3 25-32 19 8 769-1024 29 13 24577-32768 + */ +var DistLookupTable = [ + [0,1], [0,2], [0,3], [0,4], + [1,5], [1,7], + [2,9], [2,13], + [3,17], [3,25], + [4,33], [4,49], + [5,65], [5,97], + [6,129], [6,193], + [7,257], [7,385], + [8,513], [8,769], + [9,1025], [9,1537], + [10,2049], [10,3073], + [11,4097], [11,6145], + [12,8193], [12,12289], + [13,16385], [13,24577] +]; + +function inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer) { + /* + loop (until end of block code recognized) + decode literal/length value from input stream + if value < 256 + copy value (literal byte) to output stream + otherwise + if value = end of block (256) + break from loop + otherwise (value = 257..285) + decode distance from input stream + + move backwards distance bytes in the output + stream, and copy length bytes from this + position to the output stream. + */ + var numSymbols = 0, blockSize = 0; + for (;;) { + var symbol = decodeSymbol(bstream, hcLiteralTable); + ++numSymbols; + if (symbol < 256) { + // copy literal byte to output + buffer.insertByte(symbol); + blockSize++; + } + else { + // end of block reached + if (symbol == 256) { + break; + } + else { + var lengthLookup = LengthLookupTable[symbol-257], + length = lengthLookup[1] + bstream.readBits(lengthLookup[0]), + distLookup = DistLookupTable[decodeSymbol(bstream, hcDistanceTable)], + distance = distLookup[1] + bstream.readBits(distLookup[0]); + + // now apply length and distance appropriately and copy to output + + // TODO: check that backward distance < data.length? + + // http://tools.ietf.org/html/rfc1951#page-11 + // "Note also that the referenced string may overlap the current + // position; for example, if the last 2 bytes decoded have values + // X and Y, a string reference with + // adds X,Y,X,Y,X to the output stream." + // + // loop for each character + var ch = buffer.ptr - distance; + blockSize += length; + if(length > distance) { + var data = buffer.data; + while (length--) { + buffer.insertByte(data[ch++]); + } + } else { + buffer.insertBytes(buffer.data.subarray(ch, ch + length)) + } + + } // length-distance pair + } // length-distance pair or end-of-block + } // loop until we reach end of block + return blockSize; +} + +// {Uint8Array} compressedData A Uint8Array of the compressed file data. +// compression method 8 +// deflate: http://tools.ietf.org/html/rfc1951 +function inflate(compressedData, numDecompressedBytes) { + // Bit stream representing the compressed data. + var bstream = new bitjs.io.BitStream(compressedData.buffer, + false /* rtl */, + compressedData.byteOffset, + compressedData.byteLength); + var buffer = new bitjs.io.ByteBuffer(numDecompressedBytes); + var numBlocks = 0, blockSize = 0; + + // block format: http://tools.ietf.org/html/rfc1951#page-9 + do { + var bFinal = bstream.readBits(1), + bType = bstream.readBits(2); + blockSize = 0; + ++numBlocks; + // no compression + if (bType == 0) { + // skip remaining bits in this byte + while (bstream.bitPtr != 0) bstream.readBits(1); + var len = bstream.readBits(16), + nlen = bstream.readBits(16); + // TODO: check if nlen is the ones-complement of len? + + if(len > 0) buffer.insertBytes(bstream.readBytes(len)); + blockSize = len; + } + // fixed Huffman codes + else if(bType == 1) { + blockSize = inflateBlockData(bstream, getFixedLiteralTable(), getFixedDistanceTable(), buffer); + } + // dynamic Huffman codes + else if(bType == 2) { + var numLiteralLengthCodes = bstream.readBits(5) + 257; + var numDistanceCodes = bstream.readBits(5) + 1, + numCodeLengthCodes = bstream.readBits(4) + 4; + + // populate the array of code length codes (first de-compaction) + var codeLengthsCodeLengths = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; + for (var i = 0; i < numCodeLengthCodes; ++i) { + codeLengthsCodeLengths[ CodeLengthCodeOrder[i] ] = bstream.readBits(3); + } + + // get the Huffman Codes for the code lengths + var codeLengthsCodes = getHuffmanCodes(codeLengthsCodeLengths); + + // now follow this mapping + /* + 0 - 15: Represent code lengths of 0 - 15 + 16: Copy the previous code length 3 - 6 times. + The next 2 bits indicate repeat length + (0 = 3, ... , 3 = 6) + Example: Codes 8, 16 (+2 bits 11), + 16 (+2 bits 10) will expand to + 12 code lengths of 8 (1 + 6 + 5) + 17: Repeat a code length of 0 for 3 - 10 times. + (3 bits of length) + 18: Repeat a code length of 0 for 11 - 138 times + (7 bits of length) + */ + // to generate the true code lengths of the Huffman Codes for the literal + // and distance tables together + var literalCodeLengths = []; + var prevCodeLength = 0; + while (literalCodeLengths.length < numLiteralLengthCodes + numDistanceCodes) { + var symbol = decodeSymbol(bstream, codeLengthsCodes); + if (symbol <= 15) { + literalCodeLengths.push(symbol); + prevCodeLength = symbol; + } + else if (symbol == 16) { + var repeat = bstream.readBits(2) + 3; + while (repeat--) { + literalCodeLengths.push(prevCodeLength); + } + } + else if (symbol == 17) { + var repeat = bstream.readBits(3) + 3; + while (repeat--) { + literalCodeLengths.push(0); + } + } + else if (symbol == 18) { + var repeat = bstream.readBits(7) + 11; + while (repeat--) { + literalCodeLengths.push(0); + } + } + } + + // now split the distance code lengths out of the literal code array + var distanceCodeLengths = literalCodeLengths.splice(numLiteralLengthCodes, numDistanceCodes); + + // now generate the true Huffman Code tables using these code lengths + var hcLiteralTable = getHuffmanCodes(literalCodeLengths), + hcDistanceTable = getHuffmanCodes(distanceCodeLengths); + blockSize = inflateBlockData(bstream, hcLiteralTable, hcDistanceTable, buffer); + } + // error + else { + err("Error! Encountered deflate block of type 3"); + return null; + } + + // update progress + currentBytesUnarchivedInFile += blockSize; + currentBytesUnarchived += blockSize; + postProgress(); + + } while (bFinal != 1); + // we are done reading blocks if the bFinal bit was set for this block + + // return the buffer data bytes + return buffer.data; +} + +// event.data.file has the ArrayBuffer. +onmessage = function(event) { + unzip(event.data.file, true); +}; diff --git a/examples/dev.html b/examples/dev.html new file mode 100755 index 0000000..aaa0109 --- /dev/null +++ b/examples/dev.html @@ -0,0 +1,67 @@ + + + + + + + + + Dev + + + + + + + + + + + + + + + + + + + + diff --git a/examples/file.html b/examples/file.html new file mode 100644 index 0000000..71921de --- /dev/null +++ b/examples/file.html @@ -0,0 +1,207 @@ + + + + + + + + + + App + + + + + + + + + + +
+ + + +
+ + +
+ + + + + + diff --git a/examples/goldenboy.cbz b/examples/goldenboy.cbz new file mode 100644 index 0000000..ec6dba6 Binary files /dev/null and b/examples/goldenboy.cbz differ diff --git a/fonts/icomoon-toolbar/Read Me.txt b/fonts/icomoon-toolbar/Read Me.txt new file mode 100644 index 0000000..9d2b9e5 --- /dev/null +++ b/fonts/icomoon-toolbar/Read Me.txt @@ -0,0 +1,3 @@ +To modify your generated font, use the *dev.svg* file, located in the *fonts* folder in this package. You can import this dev.svg file to the IcoMoon app. All the tags (class names) and the Unicode points of your glyphs are saved in this file. + +See the documentation for more info on how to use this package: http://icomoon.io/#docs/font-face \ No newline at end of file diff --git a/fonts/icomoon-toolbar/fonts/toolbar.dev.svg b/fonts/icomoon-toolbar/fonts/toolbar.dev.svg new file mode 100644 index 0000000..20962b3 --- /dev/null +++ b/fonts/icomoon-toolbar/fonts/toolbar.dev.svg @@ -0,0 +1,110 @@ + + + + +This is a custom SVG font generated by IcoMoon. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fonts/icomoon-toolbar/fonts/toolbar.eot b/fonts/icomoon-toolbar/fonts/toolbar.eot new file mode 100644 index 0000000..be978c4 Binary files /dev/null and b/fonts/icomoon-toolbar/fonts/toolbar.eot differ diff --git a/fonts/icomoon-toolbar/fonts/toolbar.svg b/fonts/icomoon-toolbar/fonts/toolbar.svg new file mode 100644 index 0000000..9fafa5c --- /dev/null +++ b/fonts/icomoon-toolbar/fonts/toolbar.svg @@ -0,0 +1,110 @@ + + + + +This is a custom SVG font generated by IcoMoon. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fonts/icomoon-toolbar/fonts/toolbar.ttf b/fonts/icomoon-toolbar/fonts/toolbar.ttf new file mode 100644 index 0000000..6bf7291 Binary files /dev/null and b/fonts/icomoon-toolbar/fonts/toolbar.ttf differ diff --git a/fonts/icomoon-toolbar/fonts/toolbar.woff b/fonts/icomoon-toolbar/fonts/toolbar.woff new file mode 100644 index 0000000..2722452 Binary files /dev/null and b/fonts/icomoon-toolbar/fonts/toolbar.woff differ diff --git a/fonts/icomoon-toolbar/index.html b/fonts/icomoon-toolbar/index.html new file mode 100644 index 0000000..97bb1a5 --- /dev/null +++ b/fonts/icomoon-toolbar/index.html @@ -0,0 +1,351 @@ + + + +Your Font/Glyphs + + + + + +
+
+
+

Your font contains the following glyphs

+

The generated SVG font can be imported back to IcoMoon for modification.

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+

Class Names

+
+ + +  icon-file + + + +  icon-image + + + +  icon-zoom-out + + + +  icon-zoom-in + + + +  icon-expand + + + +  icon-expand-2 + + + +  icon-folder-open + + + +  icon-folder + + + +  icon-cog + + + +  icon-menu + + + +  icon-wrench + + + +  icon-settings + + + +  icon-loop + + + +  icon-pin + + + +  icon-first + + + +  icon-last + + + +  icon-arrow-left + + + +  icon-arrow-right + + + +  icon-arrow-left-2 + + + +  icon-arrow-right-2 + + + +  icon-arrow-left-3 + + + +  icon-arrow-right-3 + + + +  icon-previous + + + +  icon-next + + + +  icon-droplet + + + +  icon-adjust + + + +  icon-sun + + + +  icon-remove-sign + + + +  icon-remove + + + +  icon-copy + +
+ +
+ + + \ No newline at end of file diff --git a/fonts/icomoon-toolbar/license.txt b/fonts/icomoon-toolbar/license.txt new file mode 100644 index 0000000..a2dc52a --- /dev/null +++ b/fonts/icomoon-toolbar/license.txt @@ -0,0 +1,11 @@ +Icon Set: IcoMoon - Free -- http://keyamoon.com/icomoon/ +License: CC BY 3.0 -- http://creativecommons.org/licenses/by/3.0/Set: Font Awesome -- http://fortawesome.github.com/Font-Awesome/ +License: CC BY 3.0 -- http://creativecommons.org/licenses/by/3.0/ + + +Icon Set: IcoMoon - Free -- http://keyamoon.com/icomoon/ +License: CC BY 3.0 -- http://creativecommons.org/licenses/by/3.0/ + + +Icon Set: Iconic -- http://somerandomdude.com/work/iconic/ +License: CC BY-SA 3.0 -- http://creativecommons.org/licenses/by-sa/3.0/us/ \ No newline at end of file diff --git a/fonts/icomoon-toolbar/lte-ie7.js b/fonts/icomoon-toolbar/lte-ie7.js new file mode 100644 index 0000000..0f18941 --- /dev/null +++ b/fonts/icomoon-toolbar/lte-ie7.js @@ -0,0 +1,57 @@ +/* Load this script using conditional IE comments if you need to support IE 7 and IE 6. */ + +window.onload = function() { + function addIcon(el, entity) { + var html = el.innerHTML; + el.innerHTML = '' + entity + '' + html; + } + var icons = { + 'icon-file' : '', + 'icon-image' : '', + 'icon-zoom-out' : '', + 'icon-zoom-in' : '', + 'icon-expand' : '', + 'icon-expand-2' : '', + 'icon-folder-open' : '', + 'icon-folder' : '', + 'icon-cog' : '', + 'icon-menu' : '', + 'icon-wrench' : '', + 'icon-settings' : '', + 'icon-loop' : '', + 'icon-pin' : '', + 'icon-first' : '', + 'icon-last' : '', + 'icon-arrow-left' : '', + 'icon-arrow-right' : '', + 'icon-arrow-left-2' : '', + 'icon-arrow-right-2' : '', + 'icon-arrow-left-3' : '', + 'icon-arrow-right-3' : '', + 'icon-previous' : '', + 'icon-next' : '', + 'icon-droplet' : '', + 'icon-adjust' : '', + 'icon-sun' : '', + 'icon-remove-sign' : '', + 'icon-remove' : '', + 'icon-copy' : '' + }, + els = document.getElementsByTagName('*'), + i, attr, html, c, el; + for (i = 0; ; i += 1) { + el = els[i]; + if(!el) { + break; + } + attr = el.getAttribute('data-icon'); + if (attr) { + addIcon(el, attr); + } + c = el.className; + c = c.match(/icon-[^\s'"]+/); + if (c && icons[c[0]]) { + addIcon(el, icons[c[0]]); + } + } +}; \ No newline at end of file diff --git a/fonts/icomoon-toolbar/style.css b/fonts/icomoon-toolbar/style.css new file mode 100644 index 0000000..fe4c57e --- /dev/null +++ b/fonts/icomoon-toolbar/style.css @@ -0,0 +1,129 @@ +@font-face { + font-family: 'toolbar'; + src:url('fonts/toolbar.eot'); + src:url('fonts/toolbar.eot?#iefix') format('embedded-opentype'), + url('fonts/toolbar.woff') format('woff'), + url('fonts/toolbar.ttf') format('truetype'), + url('fonts/toolbar.svg#toolbar') format('svg'); + font-weight: normal; + font-style: normal; +} + +/* Use the following CSS code if you want to use data attributes for inserting your icons */ +[data-icon]:before { + font-family: 'toolbar'; + content: attr(data-icon); + speak: none; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; +} + +/* Use the following CSS code if you want to have a class per icon */ +/* +Instead of a list of all class selectors, +you can use the generic selector below, but it's slower: +[class*="icon-"] { +*/ +.icon-file, .icon-image, .icon-zoom-out, .icon-zoom-in, .icon-expand, .icon-expand-2, .icon-folder-open, .icon-folder, .icon-cog, .icon-menu, .icon-wrench, .icon-settings, .icon-loop, .icon-pin, .icon-first, .icon-last, .icon-arrow-left, .icon-arrow-right, .icon-arrow-left-2, .icon-arrow-right-2, .icon-arrow-left-3, .icon-arrow-right-3, .icon-previous, .icon-next, .icon-droplet, .icon-adjust, .icon-sun, .icon-remove-sign, .icon-remove, .icon-copy { + font-family: 'toolbar'; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; +} +.icon-file:before { + content: "\e000"; +} +.icon-image:before { + content: "\e001"; +} +.icon-zoom-out:before { + content: "\e002"; +} +.icon-zoom-in:before { + content: "\e003"; +} +.icon-expand:before { + content: "\e004"; +} +.icon-expand-2:before { + content: "\e005"; +} +.icon-folder-open:before { + content: "\e006"; +} +.icon-folder:before { + content: "\e007"; +} +.icon-cog:before { + content: "\e008"; +} +.icon-menu:before { + content: "\e009"; +} +.icon-wrench:before { + content: "\e00a"; +} +.icon-settings:before { + content: "\e00b"; +} +.icon-loop:before { + content: "\e00c"; +} +.icon-pin:before { + content: "\e00d"; +} +.icon-first:before { + content: "\e00e"; +} +.icon-last:before { + content: "\e00f"; +} +.icon-arrow-left:before { + content: "\e011"; +} +.icon-arrow-right:before { + content: "\e010"; +} +.icon-arrow-left-2:before { + content: "\e012"; +} +.icon-arrow-right-2:before { + content: "\e013"; +} +.icon-arrow-left-3:before { + content: "\e014"; +} +.icon-arrow-right-3:before { + content: "\e015"; +} +.icon-previous:before { + content: "\e016"; +} +.icon-next:before { + content: "\e017"; +} +.icon-droplet:before { + content: "\e01a"; +} +.icon-adjust:before { + content: "\f042"; +} +.icon-sun:before { + content: "\e018"; +} +.icon-remove-sign:before { + content: "\f057"; +} +.icon-remove:before { + content: "\f00d"; +} +.icon-copy:before { + content: "\e037"; +} diff --git a/img/iconic/LICENSE b/img/iconic/LICENSE deleted file mode 100644 index 403d8c1..0000000 --- a/img/iconic/LICENSE +++ /dev/null @@ -1,7 +0,0 @@ -Iconic Icons by PJ Onori - -@somerandomdude -http://somerandomdude.com/projects/iconic/ - -http://creativecommons.org/licenses/by-sa/3.0/us/ -http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL diff --git a/img/iconic/reload_12x14.png b/img/iconic/reload_12x14.png deleted file mode 100644 index c634491..0000000 Binary files a/img/iconic/reload_12x14.png and /dev/null differ diff --git a/img/iconic/sprite.png b/img/iconic/sprite.png deleted file mode 100644 index b8c010a..0000000 Binary files a/img/iconic/sprite.png and /dev/null differ diff --git a/img/iconic/sprite.psd b/img/iconic/sprite.psd deleted file mode 100644 index 60d00bb..0000000 Binary files a/img/iconic/sprite.psd and /dev/null differ diff --git a/img/left.png b/img/left.png deleted file mode 100644 index 595c8e6..0000000 Binary files a/img/left.png and /dev/null differ diff --git a/img/right.png b/img/right.png deleted file mode 100644 index e98471a..0000000 Binary files a/img/right.png and /dev/null differ diff --git a/lib/.jshintrc b/lib/.jshintrc new file mode 100644 index 0000000..e310d21 --- /dev/null +++ b/lib/.jshintrc @@ -0,0 +1,21 @@ +{ + "predef": [ + "jQuery", + "Handlebars", + "Pixastic" + ], + "forin": true, + "noarg": true, + "noempty": true, + "eqeqeq": true, + "bitwise": true, + "strict": true, + "undef": true, + "unused": true, + "curly": true, + "browser": true, + "maxerr": 50, + "quotmark": "single", + "trailing": false, + "indent": 2 +} diff --git a/lib/ComicBook.js b/lib/ComicBook.js index d7d6095..9fe9b9f 100755 --- a/lib/ComicBook.js +++ b/lib/ComicBook.js @@ -1,973 +1,872 @@ -/*jslint browser: true, on: true, eqeqeq: true, newcap: true, immed: true */ +/* exported ComicBook */ /* - TODOs: + TODOs: - Fo sho: - - trigger preload if requesting valid but not loaded images (can happen if network was interupted) - - loading and generally hackiness of pointer is buggy, fix. - - check for html5 feature support where used: diveintohtml5.org/everything.html or www.modernizr.com - - when applying enhancements reading position gets lost - - full browser test - IE9 / FF3.6+ / Chrome / Safari / Opera - - don't inlcude the closure compiler, expect it (or similar) to be installed instead + Fo sho: + - create onclose callback, show close button only if set + - fix progress bar css + - trigger preload if requesting valid but not loaded images (can happen if network was interupted) + - loading and generally hackiness of pointer is buggy, fix. + - check for html5 feature support where used: diveintohtml5.org/everything.html or www.modernizr.com + - full browser test - IE9 / FF3.6+ / Chrome / Safari / Opera - Nice 2 have: - - lint - - jump to page? - - make page draggable with the cursor - - enable menu items via config, allow for custom items - - split out classes into seperate files - - offline access - - thumbnail browser - - chrome frame / ExplorerCanvas / non canvas version? - - really need to speed up enhancements, try to use webworkers - - refactor so we are not using all these loose shared variables and other nastyness - - use custom event emitters instead of hacky code + Nice 2 have: + - remove image inhancement lag when navigating by pre-applying enhancements to other pages + - jump to page? + - make page draggable with the cursor + - enable menu items via config, allow for custom items + - split out classes into seperate files + - thumbnail browser + - refactor so we are not using all these loose shared variables and other nastyness + - use custom event emitters instead of hacky code + - properly bind 'this' so we don't have to keep using 'self' + - allow toolbar to be hidden on mobile (maybe show a small translucent button that opens the toolbar when clicked) + - enhancement progress bar */ -/** - * Merge two arrays. Any properties in b will replace the same properties in - * a. New properties from b will be added to a. - * - * @param a {Object} - * @param b {Object} - */ -function merge(a, b) { - - var prop; - - if (typeof b === "undefined") { b = {}; } - - for (prop in a) { - if (a.hasOwnProperty(prop)) { - if (prop in b) { continue; } - b[prop] = a[prop]; - } - } - - return b; -} - -/** - * Exception class. Always throw an instance of this when throwing exceptions. - * - * @param {String} type - * @param {Object} object - * @returns {ComicBookException} - */ -var ComicBookException = { - INVALID_PAGE: "invalid page", - INVALID_PAGE_TYPE: "invalid page type", - UNDEFINED_CONTROL: "undefined control", - INVALID_ZOOM_MODE: "invalid zoom mode", - INVALID_NAVIGATION_EVENT: "invalid navigation event" -}; - -function ComicBook(id, srcs, opts) { - - var canvas_id = id; // canvas element id - this.srcs = srcs; // array of image srcs for pages - - var defaults = { - displayMode: "double", // single / double - zoomMode: "fitWidth", // manual / fitWidth - manga: false, // true / false - enhance: {}, - keyboard: { - next: 78, - previous: 80, - toolbar: 84, - toggleLayout: 76 - }, - forward_buffer: 3 - }; - - var options = merge(defaults, opts); // options array for internal use - - var no_pages = srcs.length; - var pages = []; // array of preloaded Image objects - var canvas; // the HTML5 canvas object - var context; // the 2d drawing context - var loaded = []; // the images that have been loaded so far - var scale = 1; // page zoom scale, 1 = 100% - var is_double_page_spread = false; - var controlsRendered = false; // have the user controls been inserted into the dom yet? - var page_requested = false; // used to request non preloaded pages - var shiv = false; - - /** - * Gets the window.innerWidth - scrollbars - */ - function windowWidth() { - - var height = window.innerHeight + 1; - - if (shiv === false) { - shiv = $(document.createElement("div")) - .attr("id", "cb-width-shiv") - .css({ - width: "100%", - position: "absolute", - top: 0, - zIndex: "-1000" - }); - - $("body").append(shiv); - } - - shiv.height(height); - - return shiv.innerWidth(); - } - - /** - * enables the back button - */ - function checkHash() { - - var hash = getHash(); - - if (hash !== pointer && loaded.indexOf(hash) > -1) { - pointer = hash; - ComicBook.prototype.draw(); - } - } - - function getHash() { - var hash = parseInt(location.hash.substring(1),10) - 1 || 0; - if (hash < 0) { - setHash(0); - hash = 0; - } - return hash; - } - - function setHash(pageNo) { - location.hash = pageNo; - } - - // page hash on first load - var hash = getHash(); - - // the current page, can pass a default as a url hash - var pointer = (hash < srcs.length) ? hash : 0; - - /** - * Setup the canvas element for use throughout the class. - * - * @see #ComicBook.prototype.draw - * @see #ComicBook.prototype.enhance - */ - function init() { - // setup canvas - canvas = document.getElementById(canvas_id); - context = canvas.getContext("2d"); - - // render user controls - if (controlsRendered === false) { - ComicBook.prototype.renderControls(); - controlsRendered = true; - } - - // add page controls - canvas.addEventListener("click", ComicBook.prototype.navigation, false); - window.addEventListener("keydown", ComicBook.prototype.navigation, false); - $(window).bind('hashchange', checkHash); - } - - /** - * User controls - * - * TODO: save current values - */ - ComicBook.prototype.control = { - - status: $(document.createElement("div")) - .attr("id", "cb-status") - .addClass("cb-control cb-always-on") - .append( - $(document.createElement("div")) - .attr("id", "cb-progress-bar") - .progressbar() - ), - - toolbar: $(document.createElement("div")) - .attr("id", "cb-toolbar") - .addClass("cb-control") - .append( - $(document.createElement("button")) - .attr("title", "close the toolbar") - .addClass("cb-close") - .click(function(){ - ComicBook.prototype.toggleToolbar(); - }) - ) - .append( - $(document.createElement("button")) - .attr("title", "switch between dual and single page modes") - .addClass("cb-layout " + options.displayMode) - .click(function(){ - ComicBook.prototype.toggleLayout(); - }) - ) - .append( - $(document.createElement("button")) - .attr("title", "tweak the page colors") - .addClass("cb-color cb-menu-button") - .click(function(){ - ComicBook.prototype.toggleControl("color"); - }) - ) - .append( - $(document.createElement("button")) - .attr("title", "zoom out") - .addClass("cb-zoom-out") - .click(function(){ - ComicBook.prototype.zoom(scale - 0.1); - }) - ) - .append( - $(document.createElement("button")) - .attr("title", "zoom in") - .addClass("cb-zoom-in") - .click(function(){ - ComicBook.prototype.zoom(scale + 0.1); - }) - ) - .append( - $(document.createElement("button")) - .attr("title", "fit to page width") - .addClass("cb-fit-width") - .click(function(){ - options.zoomMode = "fitWidth" - ComicBook.prototype.drawPage(); - }) - ) - .append( - $(document.createElement("button")) - .attr("title", "fit to page width/height") - .addClass("cb-fit-best") - .click(function(){ - options.zoomMode = "fitBest" - ComicBook.prototype.drawPage(); - }) - ) - .append( - $(document.createElement("button")) - .attr("title", ((options.manga == true) ? "change reading direction to 'left-to-right'" : "change reading direction to 'right-to-left'")) - .addClass("cb-read-direction") - .click(function(){ - options.manga = !options.manga; - ComicBook.prototype.drawPage(); - }) - .attr("id", ((options.manga == true) ? "toright" : "toleft")) - ) - .append( - $(document.createElement("p")) - .attr("id", "cb-comic-info") - .append(" / " + srcs.length) - ), - - /** - * Image enhancements - */ - color: $(document.createElement("div")) - .attr("id", "cb-color") - .addClass("cb-control") - .append("") - .append( - $("
").slider({ - value: 0, - step: 10, - min: -1000, - max: 1000, - change: function(event, ui) { - ComicBook.prototype.enhance.brightness({ brightness: ui.value }); - } - }) - ) - .append("") - .append( - $("
").slider({ - value: 0, - step: 0.001, - min: 0, - max: 1, - change: function(event, ui) { - ComicBook.prototype.enhance.brightness({ contrast: ui.value }); - } - }) - ) - .append("") - .append( - $("
").slider({ - value: 0, - step: 0.001, - min: 0, - max: 1, - change: function(event, ui) { - ComicBook.prototype.enhance.sharpen({ amount: ui.value }); - } - }) - ) - .append( - $(document.createElement("div")).addClass("cb-option") - .append(" ") - .append("") - ), - - /** - * Page navigation - */ - navigation: { - - left: $(document.createElement("div")) - .addClass("cb-control cb-navigate cb-always-on left") - .click(function(e){ - if(options.manga == false) - { - ComicBook.prototype.drawPrevPage(); - } - else - { - ComicBook.prototype.drawNextPage(); - } - }), - - right: $(document.createElement("div")) - .addClass("cb-control cb-navigate cb-always-on right") - .click(function(e) { - if(options.manga == false) - { - ComicBook.prototype.drawNextPage(); - } - else - { - ComicBook.prototype.drawPrevPage(); - } - }) - }, - - loadingOverlay: $(document.createElement("div")) - .attr("id", "cb-loading-overlay") - .addClass("cb-control") - }; - /*Using left arrow key*/ - $(document).keydown(function(e) { - if (e.keyCode == 37) { - ComicBook.prototype.drawPrevPage(); - } - }); - - /*Using right arrow key*/ - $(document).keydown(function(e) { - if (e.keyCode == 39) { - ComicBook.prototype.drawNextPage(); - } - }); - ComicBook.prototype.renderControls = function() { - - $(canvas) - .before(this.getControl("loadingOverlay")) - .before(this.getControl("status")) - .after(this.getControl("toolbar").hide()) - .after(this.getControl("navigation").left) - .after(this.getControl("navigation").right) - .after(this.getControl("color").hide()); - - $(".cb-menu-button").click(function(e) { - $(this).toggleClass("active"); - }); - - $("#cb-desaturate").click(function(){ - if ($(this).is(":checked")) { - ComicBook.prototype.enhance.desaturate(); - } else { - ComicBook.prototype.enhance.resaturate(); - } - }); - - $("#cb-reset").click(function() { - // TODO: improve performance here. - $("#cb-brightness").slider("value", 0); - $("#cb-contrast").slider("value", 0); - $("#cb-saturation").slider("value", 0); - $("#cb-sharpen").slider("value", 0); - var desaturate = $("#cb-desaturate"); - desaturate.attr("checked", false); - ComicBook.prototype.enhance.reset(); - }); - }; - - ComicBook.prototype.getControl = function(control) { - - if (typeof this.control[control] === "undefined") { - throw ComicBookException.UNDEFINED_CONTROL+' '+control; - } - - return this.control[control]; - }; - - ComicBook.prototype.showControl = function(control) { - this.getControl(control).show().addClass("open"); - }; - - ComicBook.prototype.hideControl = function(control) { - this.getControl(control).removeClass("open").hide(); - }; - - ComicBook.prototype.toggleControl = function(control) { - this.getControl(control).toggle().toggleClass("open"); - }; - - ComicBook.prototype.toggleToolbar = function() { - if ($("#cb-toolbar").is(":visible")) { - $(".cb-control").not(".cb-always-on").hide(); - } else { - $("#cb-toolbar, .cb-control.open").show(); - } - }; - - ComicBook.prototype.toggleLayout = function() { - if (options.displayMode === "double") { - $("#cb-toolbar .cb-layout").removeClass("double"); - options.displayMode = "single"; - } else { - $("#cb-toolbar .cb-layout").removeClass("single"); - options.displayMode = "double"; - } - $("#cb-toolbar .cb-layout").addClass(options.displayMode); - ComicBook.prototype.drawPage(); - }; - - /** - * Get the image for a given page. - * - * @return Image - */ - ComicBook.prototype.getPage = function(i) { - - if (i < 0 || i > srcs.length-1) { - throw ComicBookException.INVALID_PAGE+' '+i; - } - - if (typeof pages[i] === "object") { - return pages[i]; - } else { - page_requested = i; - this.showControl("loadingOverlay"); - } - }; - - /** - * @see #preload - */ - ComicBook.prototype.draw = function () { - - init(); - - // resize navigation controls - $(".cb-control.cb-navigate").outerHeight(window.innerHeight); - $("#cb-toolbar").outerWidth(windowWidth()); - $("#cb-loading-overlay").outerWidth(windowWidth()).height(window.innerHeight); - - // preload images if needed - if (pages.length !== no_pages) { - this.preload(); - } else { - this.drawPage(); - } - }; - - /** - * Zoom the canvas - * - * @param new_scale {Number} Scale the canvas to this ratio - */ - ComicBook.prototype.zoom = function (new_scale) { - options.zoomMode = "manual"; - scale = new_scale; - if (typeof this.getPage(pointer) === "object") { this.drawPage(); } - }; - - /** - * Preload all images, draw the page only after a given number have been loaded. - * - * @see #drawPage - */ - ComicBook.prototype.preload = function () { - - var i = pointer; // the current page counter for this method - var rendered = false; - var queue = []; - - this.showControl("loadingOverlay"); - - function loadImage(i) { - - var page = new Image(); - page.src = srcs[i]; - - page.onload = function () { - - pages[i] = this; - loaded.push(i); - - $("#cb-progress-bar").progressbar("value", Math.floor((loaded.length / no_pages) * 100)); - - // double page mode needs an extra page added - var buffer = (options.displayMode === "double" && pointer < srcs.length-1) ? 1 : 0; - - // start rendering the comic when the requested page is ready - if ((rendered === false && ($.inArray(pointer + buffer, loaded) !== -1) - || - (typeof page_requested === "number" && $.inArray(page_requested, loaded) !== -1)) - ) { - // if the user is waiting for a page to be loaded, render that one instead of the default pointer - if (typeof page_requested === "number") { - pointer = page_requested-1; - page_requested = false; - } - - ComicBook.prototype.drawPage(); - ComicBook.prototype.hideControl("loadingOverlay"); - rendered = true; - } - - if (queue.length) { - loadImage(queue[0]); - queue.splice(0,1); - } else { - $("#cb-status").delay(500).fadeOut(); - } - }; - } - - // loads pages in both directions so you don't have to wait for all pages - // to be loaded before you can scroll backwards - function preload(start, stop) { - var j = 0; - var count = 1; - var forward = start; - var backward = start-1; - - while (forward <= stop) { - - if (count > options.forward_buffer && backward > -1) { - queue.push(backward); - backward--; - count = 0; - } else { - queue.push(forward); - forward++; - } - count++; - } - - while (backward > -1) { - queue.push(backward); - backward--; - } - - loadImage(queue[j]); - } - - preload(i, srcs.length-1); - }; - - ComicBook.prototype.pageLoaded = function (page_no) { - return (typeof loaded[page_no-1] !== "undefined"); - }; - - /** - * Draw the current page in the canvas - */ - ComicBook.prototype.drawPage = function(page_no) { - - // if a specific page is given try to render it, if not bail and wait for preload() to render it - if (typeof page_no === "number" && page_no < srcs.length && page_no > 0) { - pointer = page_no-1; - if (!this.pageLoaded(page_no)) { - this.showControl("loadingOverlay"); - return; - } - } - - if (pointer < 0) { pointer = 0; } - - var zoom_scale; - var offsetW = 0, offsetH = 0; - - var page = ComicBook.prototype.getPage(pointer); - var page2 = false; - - if (options.displayMode === "double" && pointer < srcs.length-1) { - page2 = ComicBook.prototype.getPage(pointer + 1); - } - - if (typeof page !== "object") { - throw ComicBookException.INVALID_PAGE_TYPE+' '+typeof page; - } - - var width = page.width; - var height = page.height; - - // reset the canvas to stop duplicate pages showing - canvas.width = 0; - canvas.height = 0; - - // show double page spreads on a single page - is_double_page_spread = ( - typeof page2 === "object" - && (page.width > page.height || page2.width > page2.height) - && options.displayMode === "double" - ); - if (is_double_page_spread) { options.displayMode = "single"; } - - if (options.displayMode === "double") { - - // for double page spreads, factor in the width of both pages - if (typeof page2 === "object") { width += page2.width; } - - // if this is the last page and there is no page2, still keep the canvas wide - else { width += width; } - } - - // update the page scale if a non manual mode has been chosen - switch(options.zoomMode) { - - case "manual": - document.body.style.overflowX = "auto"; - zoom_scale = (options.displayMode === "double") ? scale * 2 : scale; - break; - - case "fitWidth": - document.body.style.overflowX = "hidden"; - - zoom_scale = (windowWidth() > width) - ? ((windowWidth() - width) / windowWidth()) + 1 // scale up if the window is wider than the page - : windowWidth() / width; // scale down if the window is narrower than the page - - // update the interal scale var so switching zoomModes while zooming will be smooth - scale = zoom_scale - break; - - case "fitBest": - document.body.style.overflowX = "hidden"; - - var width_scale = (windowWidth() > width) - ? ((windowWidth() - width) / windowWidth()) + 1 // scale up if the window is wider than the page - : windowWidth() / width; // scale down if the window is narrower than the page - var windowHeight = window.innerHeight; - var height_scale = (windowHeight > height) - ? ((windowHeight - height) / windowHeight) + 1 // scale up if the window is wider than the page - : windowHeight / height; // scale down if the window is narrower than the page - - zoom_scale = (width_scale > height_scale)? height_scale : width_scale; - scale = zoom_scale; - break; - - default: - throw ComicBookException.INVALID_ZOOM_MODE+' '+options.zoomMode; - } - - var canvas_width = page.width * zoom_scale; - var canvas_height = page.height * zoom_scale; - - var page_width = (options.zoomMode === "manual") ? page.width * scale : canvas_width; - var page_height = (options.zoomMode === "manual") ? page.height * scale : canvas_height; - - canvas_height = page_height; - - // make sure the canvas is always at least full screen, even if the page is more narrow than the screen - canvas.width = (canvas_width < windowWidth()) ? windowWidth() : canvas_width; - canvas.height = (canvas_height < window.innerHeight) ? window.innerHeight : canvas_height; - - // work out a horizontal position that will keep the pages always centred - if (canvas_width < windowWidth() && options.zoomMode === "manual") { - offsetW = (windowWidth() - page_width) / 2; - if (options.displayMode === "double") { offsetW = offsetW - page_width / 2; } - } - - // work out a vertical position that will keep the pages always centred - if (canvas_height < window.innerHeight && options.zoomMode === "manual") { - offsetH = (window.innerHeight - page_height) / 2; - } - - // in manga double page mode reverse the page(s) - if (options.manga && options.displayMode === "double" && typeof page2 === "object") { - var tmpPage = page; - var tmpPage2 = page2; - page = tmpPage2; - page2 = tmpPage; - } - - // draw the page(s) - context.drawImage(page, offsetW, offsetH, page_width, page_height); - if (options.displayMode === "double" && typeof page2 === "object") { - context.drawImage(page2, page_width + offsetW, offsetH, page_width, page_height); - } - - // apply any image enhancements previously defined - $.each(options.enhance, function(action, options) { - ComicBook.prototype.enhance[action](options); - }); - - var current_page = (options.displayMode === "double" && pointer+2 <= srcs.length) - ? (pointer+1) + "-" + (pointer+2) : pointer+1 - $("#cb-current-page").text(current_page); - - // revert page mode back to double if it was auto switched for a double page spread - if (is_double_page_spread) { options.displayMode = "double"; } - - // disable the fit width button if needed - $("button.cb-fit-width").attr("disabled", (options.zoomMode === "fitWidth")); - $("button.cb-fit-best").attr("disabled", (options.zoomMode === "fitBest")); - - //Change the icon on the read direction - if(options.manga == true) - { - $("button.cb-read-direction").attr("id", "toright"); - } - else - { - $("button.cb-read-direction").attr("id", "toleft"); - } - - // disable prev/next buttons if not needed - $(".cb-navigate").show(); - if ((pointer === 0) && (options.manga == false)) { - $(".cb-navigate.left").hide(); - $(".cb-navigate.right").show(); - } - else if ((pointer === 0) && (options.manga == true)) - { - $(".cb-navigate.left").show(); - $(".cb-navigate.right").hide(); - } - - if ((pointer === srcs.length-1 || (typeof page2 === "object" && pointer === srcs.length-2)) && (options.manga == false)) { - $(".cb-navigate.left").show(); - $(".cb-navigate.right").hide(); - } - else if ((pointer === srcs.length-1 || (typeof page2 === "object" && pointer === srcs.length-2)) && (options.manga == true)) - { - $(".cb-navigate.left").hide(); - $(".cb-navigate.right").show(); - } - - // user callback - if (typeof options.afterDrawPage === "function") { - options.afterDrawPage(pointer + 1); - } - - // update hash location - if (getHash() !== pointer) { - setHash(pointer + 1); - } - - // make sure the top of the page is in view - window.scroll(0, 0); - }; - - /** - * Increment the counter and draw the page in the canvas - * - * @see #drawPage - */ - ComicBook.prototype.drawNextPage = function () { - - var page; - - try { - page = this.getPage(pointer+1); - } catch (e) {} - - if (!page) { return false; } - - if (pointer + 1 < pages.length) { - pointer += (options.displayMode === "single" || is_double_page_spread) ? 1 : 2; - try { - this.drawPage(); - } catch (e) {} - } - }; - - /** - * Decrement the counter and draw the page in the canvas - * - * @see #drawPage - */ - ComicBook.prototype.drawPrevPage = function () { - - var page; - - try { - page = this.getPage(pointer-1); - } catch (e) {} - - if (!page) { return false; } - - is_double_page_spread = (page.width > page.height); // need to run double page check again here as we are going backwards - - if (pointer > 0) { - pointer -= (options.displayMode === "single" || is_double_page_spread) ? 1 : 2; - this.drawPage(); - } - }; - - /** - * Apply image enhancements to the canvas. - * - * Powered by the awesome Pixastic: http://www.pixastic.com/ - * - * TODO: reset & apply all image enhancements each time before applying new one - * TODO: abstract this into an "Enhance" object, separate from ComicBook? - */ - ComicBook.prototype.enhance = { - - /** - * Reset enhancements. - * This can reset a specific enhancement if the method name is passed, or - * it will reset all. - * - * @param method {string} the specific enhancement to reset - */ - reset: function (method) { - if (!method) { - options.enhance = {}; - } else { - delete options.enhance[method]; - } - ComicBook.prototype.drawPage(); - }, - - /** - * Adjust brightness / contrast - * - * params - * brightness (int) -150 to 150 - * contrast: (float) -1 to infinity - * - * @param {Object} params Brightness & contrast levels - * @param {Boolean} reset Reset before applying more enhancements? - */ - brightness: function (params, reset) { - - if (reset !== false) { this.reset("brightness"); } - - // merge user options with defaults - var opts = merge({ brightness: 0, contrast: 0 }, params); - - // remember options for later - options.enhance.brightness = opts; - - // run the enhancement - var P = new Pixastic(canvas.getContext("2d")); - P["brightness"]({ - brightness: opts.brightness, - contrast: opts.contrast, - legacy: true - }).done(); - - init(); - }, - - /** - * Force black and white - */ - desaturate: function () { - - options.enhance.desaturate = {}; - - Pixastic.process(canvas, "desaturate", { average : false }); - - init(); - }, - - /** - * Undo desaturate - */ - resaturate: function() { - delete options.enhance.desaturate; - ComicBook.prototype.drawPage(); - }, - - /** - * Sharpen - * - * options: - * amount: number (-1 to infinity) - * - * @param {Object} options - */ - sharpen: function (params) { - - this.desharpen(); - - var opts = merge({ amount: 0 }, params); - - options.enhance.sharpen = opts; - - Pixastic.process(canvas, "sharpen", { - amount: opts.amount - }); - - init(); - }, - - desharpen: function() { - delete options.enhance.sharpen; - ComicBook.prototype.drawPage(); - } - }; - - ComicBook.prototype.navigation = function (e) { - - // disable navigation when the overlay is showing - if ($("#cb-loading-overlay").is(":visible")) { return false; } - - var side = false; - - switch (e.type) { - case "click": - ComicBook.prototype.toggleToolbar(); - break; - case "keydown": - - // navigation - if (e.keyCode === options.keyboard.previous) { side = "left"; } - if (e.keyCode === options.keyboard.next) { side = "right"; } - - // display controls - if (e.keyCode === options.keyboard.toolbar) { - ComicBook.prototype.toggleToolbar(); - } - if (e.keyCode === options.keyboard.toggleLayout) { - ComicBook.prototype.toggleLayout(); - } - break; - default: - throw ComicBookException.INVALID_NAVIGATION_EVENT+' '+e.type; - } - - if (side) { - - e.stopPropagation(); - - // western style (left to right) - if (!options.manga) { - if (side === "left") { ComicBook.prototype.drawPrevPage(); } - if (side === "right") { ComicBook.prototype.drawNextPage(); } - } - // manga style (right to left) - else { - if (side === "left") { ComicBook.prototype.drawNextPage(); } - if (side === "right") { ComicBook.prototype.drawPrevPage(); } - } - - return false; - } - }; - -} +var ComicBook = (function ($) { + + 'use strict'; + + /** + * Merge two arrays. Any properties in b will replace the same properties in + * a. New properties from b will be added to a. + * + * @param a {Object} + * @param b {Object} + */ + function merge(a, b) { + + var prop; + + if (typeof b === 'undefined') { b = {}; } + + for (prop in a) { + if (a.hasOwnProperty(prop)) { + if (prop in b) { continue; } + b[prop] = a[prop]; + } + } + + return b; + } + + /** + * Exception class. Always throw an instance of this when throwing exceptions. + * + * @param {String} type + * @param {Object} object + * @returns {ComicBookException} + */ + var ComicBookException = { + INVALID_ACTION: 'invalid action', + INVALID_PAGE: 'invalid page', + INVALID_PAGE_TYPE: 'invalid page type', + UNDEFINED_CONTROL: 'undefined control', + INVALID_ZOOM_MODE: 'invalid zoom mode', + INVALID_NAVIGATION_EVENT: 'invalid navigation event' + }; + + function ComicBook(id, srcs, opts) { + + var self = this; + var canvas_id = id; // canvas element id + this.srcs = srcs; // array of image srcs for pages + + var defaults = { + displayMode: 'double', // single / double + zoomMode: 'fitWidth', // manual / fitWidth + manga: false, // true / false + enhance: {}, + keyboard: { + next: 78, + previous: 80, + toolbar: 84, + toggleLayout: 76 + }, + libPath: '/lib/', + forward_buffer: 3 + }; + + this.isMobile = false; + + // mobile enhancements + if (navigator.userAgent.match(/mobile/i)) { + this.isMobile = true; + document.body.classList.add('mobile'); + defaults.displayMode = 'single'; + + window.addEventListener('load', function () { + setTimeout(function () { + window.scrollTo(0, 1); + }, 0); + }); + } + + var options = merge(defaults, opts); // options array for internal use + + var no_pages = srcs.length; + var pages = []; // array of preloaded Image objects + var canvas; // the HTML5 canvas object + var context; // the 2d drawing context + var loaded = []; // the images that have been loaded so far + var scale = 1; // page zoom scale, 1 = 100% + var is_double_page_spread = false; + var controlsRendered = false; // have the user controls been inserted into the dom yet? + var page_requested = false; // used to request non preloaded pages + var shiv = false; + + /** + * Gets the window.innerWidth - scrollbars + */ + function windowWidth() { + + var height = window.innerHeight + 1; + + if (shiv === false) { + shiv = $(document.createElement('div')) + .attr('id', 'cb-width-shiv') + .css({ + width: '100%', + position: 'absolute', + top: 0, + zIndex: '-1000' + }); + + $('body').append(shiv); + } + + shiv.height(height); + + return shiv.innerWidth(); + } + + /** + * enables the back button + */ + function checkHash() { + + var hash = getHash(); + + if (hash !== pointer && loaded.indexOf(hash) > -1) { + pointer = hash; + self.draw(); + } + } + + function getHash() { + var hash = parseInt(location.hash.substring(1),10) - 1 || 0; + if (hash < 0) { + setHash(0); + hash = 0; + } + return hash; + } + + function setHash(pageNo) { + location.hash = pageNo; + } + + // page hash on first load + var hash = getHash(); + + // the current page, can pass a default as a url hash + var pointer = (hash < srcs.length) ? hash : 0; + + /** + * Setup the canvas element for use throughout the class. + * + * @see #ComicBook.prototype.draw + * @see #ComicBook.prototype.enhance + */ + function init() { + + // setup canvas + canvas = document.getElementById(canvas_id); + context = canvas.getContext('2d'); + + // render user controls + if (controlsRendered === false) { + self.renderControls(); + controlsRendered = true; + } + + // add page controls + window.addEventListener('keydown', self.navigation, false); + window.addEventListener('hashchange', checkHash, false); + } + + window.addEventListener('touchstart', function (e) { + var $el = $(e.target); + if ($el.data('toggle') === 'dropdown' ) { + $el.siblings('.dropdown').toggle(); + } + }, false); + + /** + * Render Handlebars templates. Templates with data-trigger & data-action will + * have the specified events bound. + */ + ComicBook.prototype.renderControls = function () { + + var controls = {}, $toolbar; + + $.each(Handlebars.templates, function (name, template) { + + var $template = $(template().trim()); + controls[name] = $template; + + // add event listeners to controls that specify callbacks + $template.find('*').andSelf().filter('[data-action][data-trigger]').each(function () { + + var $this = $(this); + var trigger = $this.data('trigger'); + var action = $this.data('action'); + + // trigger a direct method if exists + if (typeof self[$this.data('action')] === 'function') { + $this.on(trigger, self[action]); + } + + // throw an event to be caught outside if the app code + $this.on(trigger, function (e) { + $(self).trigger(trigger, e); + }); + }); + + $(canvas).before($template); + }); + + this.controls = controls; + + $toolbar = this.getControl('toolbar'); + $toolbar + .find('.manga-' + options.manga).show().end() + .find('.manga-' + !options.manga).hide().end() + .find('.layout').hide().end().find('.layout-' + options.displayMode).show(); + + }; + + ComicBook.prototype.getControl = function (control) { + if (typeof this.controls[control] !== 'object') { + throw ComicBookException.UNDEFINED_CONTROL + ' ' + control; + } + return this.controls[control]; + }; + + ComicBook.prototype.showControl = function (control) { + this.getControl(control).show().addClass('open'); + }; + + ComicBook.prototype.hideControl = function (control) { + this.getControl(control).removeClass('open').hide(); + }; + + ComicBook.prototype.toggleControl = function (control) { + this.getControl(control).toggle().toggleClass('open'); + }; + + ComicBook.prototype.toggleLayout = function() { + + var $toolbar = self.getControl('toolbar'); + var displayMode = (options.displayMode === 'single') ? 'double' : 'single'; + + options.displayMode = displayMode; + + $toolbar.find('.layout').hide().end().find('.layout-' + options.displayMode).show(); + + self.drawPage(); + }; + + /** + * Get the image for a given page. + * + * @return Image + */ + ComicBook.prototype.getPage = function (i) { + + if (i < 0 || i > srcs.length-1) { + throw ComicBookException.INVALID_PAGE + ' ' + i; + } + + if (typeof pages[i] === 'object') { + return pages[i]; + } else { + page_requested = i; + this.showControl('loadingOverlay'); + } + }; + + /** + * @see #preload + */ + ComicBook.prototype.draw = function () { + + init(); + + // resize navigation controls + $('.navigate').outerHeight(window.innerHeight); + $('#cb-loading-overlay').outerWidth(windowWidth()).height(window.innerHeight); + + // preload images if needed + if (pages.length !== no_pages) { + this.preload(); + } else { + this.drawPage(); + } + }; + + /** + * Zoom the canvas + * + * @param new_scale {Number} Scale the canvas to this ratio + */ + ComicBook.prototype.zoom = function (new_scale) { + options.zoomMode = 'manual'; + scale = new_scale; + if (typeof this.getPage(pointer) === 'object') { this.drawPage(); } + }; + + ComicBook.prototype.zoomIn = function () { + self.zoom(scale + 0.1); + }; + + ComicBook.prototype.zoomOut = function () { + self.zoom(scale - 0.1); + }; + + ComicBook.prototype.fitWidth = function () { + options.zoomMode = 'fitWidth'; + ComicBook.prototype.drawPage(); + }; + + /** + * Preload all images, draw the page only after a given number have been loaded. + * + * @see #drawPage + */ + ComicBook.prototype.preload = function () { + + var i = pointer; // the current page counter for this method + var rendered = false; + var queue = []; + + this.showControl('loadingOverlay'); + + function loadImage(i) { + + var page = new Image(); + page.src = srcs[i]; + + page.onload = function () { + + pages[i] = this; + loaded.push(i); + + $('#cb-progress-bar .progressbar-value').css('width', Math.floor((loaded.length / no_pages) * 100) + '%'); + + // double page mode needs an extra page added + var buffer = (options.displayMode === 'double' && pointer < srcs.length-1) ? 1 : 0; + + // start rendering the comic when the requested page is ready + if ((rendered === false && ($.inArray(pointer + buffer, loaded) !== -1) || + (typeof page_requested === 'number' && $.inArray(page_requested, loaded) !== -1)) + ) { + // if the user is waiting for a page to be loaded, render that one instead of the default pointer + if (typeof page_requested === 'number') { + pointer = page_requested-1; + page_requested = false; + } + + self.drawPage(); + self.hideControl('loadingOverlay'); + rendered = true; + } + + if (queue.length) { + loadImage(queue[0]); + queue.splice(0,1); + } else { + $('#cb-status').delay(500).fadeOut(); + } + }; + } + + // loads pages in both directions so you don't have to wait for all pages + // to be loaded before you can scroll backwards + function preload(start, stop) { + + var j = 0; + var count = 1; + var forward = start; + var backward = start-1; + + while (forward <= stop) { + + if (count > options.forward_buffer && backward > -1) { + queue.push(backward); + backward--; + count = 0; + } else { + queue.push(forward); + forward++; + } + count++; + } + + while (backward > -1) { + queue.push(backward); + backward--; + } + + loadImage(queue[j]); + } + + preload(i, srcs.length-1); + }; + + ComicBook.prototype.pageLoaded = function (page_no) { + return (typeof loaded[page_no-1] !== 'undefined'); + }; + + /** + * Draw the current page in the canvas + */ + ComicBook.prototype.drawPage = function(page_no, reset_scroll) { + + var scrollY; + + reset_scroll = (typeof reset_scroll !== 'undefined') ? reset_scroll : true; + scrollY = reset_scroll ? 0 : window.scrollY; + + // if a specific page is given try to render it, if not bail and wait for preload() to render it + if (typeof page_no === 'number' && page_no < srcs.length && page_no > 0) { + pointer = page_no-1; + if (!this.pageLoaded(page_no)) { + this.showControl('loadingOverlay'); + return; + } + } + + if (pointer < 0) { pointer = 0; } + + var zoom_scale; + var offsetW = 0, offsetH = 0; + + var page = self.getPage(pointer); + var page2 = false; + + if (options.displayMode === 'double' && pointer < srcs.length-1) { + page2 = self.getPage(pointer + 1); + } + + if (typeof page !== 'object') { + throw ComicBookException.INVALID_PAGE_TYPE + ' ' + typeof page; + } + + var width = page.width; + + // reset the canvas to stop duplicate pages showing + canvas.width = 0; + canvas.height = 0; + + // show double page spreads on a single page + is_double_page_spread = ( + typeof page2 === 'object' && + (page.width > page.height || page2.width > page2.height) && + options.displayMode === 'double' + ); + if (is_double_page_spread) { options.displayMode = 'single'; } + + if (options.displayMode === 'double') { + + // for double page spreads, factor in the width of both pages + if (typeof page2 === 'object') { width += page2.width; } + + // if this is the last page and there is no page2, still keep the canvas wide + else { width += width; } + } + + // update the page scale if a non manual mode has been chosen + switch (options.zoomMode) { + + case 'manual': + document.body.style.overflowX = 'auto'; + zoom_scale = (options.displayMode === 'double') ? scale * 2 : scale; + break; + + case 'fitWidth': + document.body.style.overflowX = 'hidden'; + + // scale up if the window is wider than the page, scale down if the window + // is narrower than the page + zoom_scale = (windowWidth() > width) ? ((windowWidth() - width) / windowWidth()) + 1 : windowWidth() / width; + + // update the interal scale var so switching zoomModes while zooming will be smooth + scale = zoom_scale; + break; + + default: + throw ComicBookException.INVALID_ZOOM_MODE + ' ' + options.zoomMode; + } + + var canvas_width = page.width * zoom_scale; + var canvas_height = page.height * zoom_scale; + + var page_width = (options.zoomMode === 'manual') ? page.width * scale : canvas_width; + var page_height = (options.zoomMode === 'manual') ? page.height * scale : canvas_height; + + canvas_height = page_height; + + // make sure the canvas is always at least full screen, even if the page is more narrow than the screen + canvas.width = (canvas_width < windowWidth()) ? windowWidth() : canvas_width; + canvas.height = (canvas_height < window.innerHeight) ? window.innerHeight : canvas_height; + + // work out a horizontal position that will keep the pages always centred + if (canvas_width < windowWidth() && options.zoomMode === 'manual') { + offsetW = (windowWidth() - page_width) / 2; + if (options.displayMode === 'double') { offsetW = offsetW - page_width / 2; } + } + + // work out a vertical position that will keep the pages always centred + if (canvas_height < window.innerHeight && options.zoomMode === 'manual') { + offsetH = (window.innerHeight - page_height) / 2; + } + + // in manga double page mode reverse the page(s) + if (options.manga && options.displayMode === 'double' && typeof page2 === 'object') { + var tmpPage = page; + var tmpPage2 = page2; + page = tmpPage2; + page2 = tmpPage; + } + + // draw the page(s) + context.drawImage(page, offsetW, offsetH, page_width, page_height); + if (options.displayMode === 'double' && typeof page2 === 'object') { + context.drawImage(page2, page_width + offsetW, offsetH, page_width, page_height); + } + + this.pixastic = new Pixastic(context, options.libPath + 'pixastic/'); + + // apply any image enhancements previously defined + $.each(options.enhance, function(action, options) { + self.enhance[action](options); + }); + + var current_page = + (options.displayMode === 'double' && + pointer + 2 <= srcs.length) ? (pointer + 1) + '-' + (pointer + 2) : pointer + 1; + + this.getControl('toolbar') + .find('#current-page').text(current_page) + .end() + .find('#page-count').text(srcs.length); + + // revert page mode back to double if it was auto switched for a double page spread + if (is_double_page_spread) { options.displayMode = 'double'; } + + // disable the fit width button if needed + $('button.cb-fit-width').attr('disabled', (options.zoomMode === 'fitWidth')); + + // disable prev/next buttons if not needed + $('.navigate').show(); + if (pointer === 0) { + if (options.manga) { + $('.navigate-left').show(); + $('.navigate-right').hide(); + } else { + $('.navigate-left').hide(); + $('.navigate-right').show(); + } + } + + if (pointer === srcs.length-1 || (typeof page2 === 'object' && pointer === srcs.length-2)) { + if (options.manga) { + $('.navigate-left').hide(); + $('.navigate-right').show(); + } else { + $('.navigate-left').show(); + $('.navigate-right').hide(); + } + } + + if (pointer !== getHash()){ + $(this).trigger('navigate'); + } + + // update hash location + if (getHash() !== pointer) { + setHash(pointer + 1); + } + }; + + /** + * Increment the counter and draw the page in the canvas + * + * @see #drawPage + */ + ComicBook.prototype.drawNextPage = function () { + + var page; + + try { + page = self.getPage(pointer+1); + } catch (e) {} + + if (!page) { return false; } + + if (pointer + 1 < pages.length) { + pointer += (options.displayMode === 'single' || is_double_page_spread) ? 1 : 2; + try { + self.drawPage(); + } catch (e) {} + } + + // make sure the top of the page is in view + window.scroll(0, 0); + }; + + /** + * Decrement the counter and draw the page in the canvas + * + * @see #drawPage + */ + ComicBook.prototype.drawPrevPage = function () { + + var page; + + try { + page = self.getPage(pointer-1); + } catch (e) {} + + if (!page) { return false; } + + is_double_page_spread = (page.width > page.height); // need to run double page check again here as we are going backwards + + if (pointer > 0) { + pointer -= (options.displayMode === 'single' || is_double_page_spread) ? 1 : 2; + self.drawPage(); + } + + // make sure the top of the page is in view + window.scroll(0, 0); + }; + + ComicBook.prototype.brightness = function () { + self.enhance.brightness({ brightness: $(this).val() }); + }; + + ComicBook.prototype.contrast = function () { + self.enhance.brightness({ contrast: $(this).val() }); + }; + + ComicBook.prototype.sharpen = function () { + self.enhance.sharpen({ strength: $(this).val() }); + }; + + ComicBook.prototype.desaturate = function () { + if ($(this).is(':checked')) { + self.enhance.desaturate(); + } else { + self.enhance.resaturate(); + } + }; + + ComicBook.prototype.resetEnhancements = function () { + self.enhance.reset(); + }; + + /** + * Apply image enhancements to the canvas. + * + * Powered by the awesome Pixastic: http://www.pixastic.com/ + * + * TODO: reset & apply all image enhancements each time before applying new one + * TODO: abstract this into an 'Enhance' object, separate from ComicBook? + */ + ComicBook.prototype.enhance = { + + /** + * Reset enhancements. + * This can reset a specific enhancement if the method name is passed, or + * it will reset all. + * + * @param method {string} the specific enhancement to reset + */ + reset: function (method) { + if (!method) { + options.enhance = {}; + } else { + delete options.enhance[method]; + } + self.drawPage(null, false); + }, + + /** + * Pixastic progress callback + * @param {float} progress + */ + // progress: function (progress) { + progress: function () { + // console.info(Math.floor(progress * 100)); + }, + + /** + * Pixastic on complete callback + */ + done: function () { + + }, + + /** + * Adjust brightness / contrast + * + * params + * brightness (int) -150 to 150 + * contrast: (float) -1 to infinity + * + * @param {Object} params Brightness & contrast levels + * @param {Boolean} reset Reset before applying more enhancements? + */ + brightness: function (params, reset) { + + if (reset !== false) { this.reset('brightness'); } + + // merge user options with defaults + var opts = merge({ brightness: 0, contrast: 0 }, params); + + // remember options for later + options.enhance.brightness = opts; + + // run the enhancement + self.pixastic.brightness({ + brightness: opts.brightness, + contrast: opts.contrast + }).done(this.done, this.progress); + }, + + /** + * Force black and white + */ + desaturate: function () { + options.enhance.desaturate = {}; + self.pixastic.desaturate().done(this.done, this.progress); + }, + + /** + * Undo desaturate + */ + resaturate: function() { + delete options.enhance.desaturate; + self.drawPage(null, false); + }, + + /** + * Sharpen + * + * options: + * strength: number (-1 to infinity) + * + * @param {Object} options + */ + sharpen: function (params) { + + this.desharpen(); + + var opts = merge({ strength: 0 }, params); + + options.enhance.sharpen = opts; + + self.pixastic.sharpen3x3({ + strength: opts.strength + }).done(this.done, this.progress); + }, + + desharpen: function() { + delete options.enhance.sharpen; + self.drawPage(null, false); + } + }; + + ComicBook.prototype.navigation = function (e) { + + // disable navigation when the overlay is showing + if ($('#cb-loading-overlay').is(':visible')) { return false; } + + var side = false; + + switch (e.type) { + + case 'click': + side = e.currentTarget.getAttribute('data-navigate-side'); + break; + + case 'keydown': + + // navigation + if (e.keyCode === options.keyboard.previous) { side = 'left'; } + if (e.keyCode === options.keyboard.next) { side = 'right'; } + + // display controls + if (e.keyCode === options.keyboard.toolbar) { + self.toggleToolbar(); + } + if (e.keyCode === options.keyboard.toggleLayout) { + self.toggleLayout(); + } + break; + + default: + throw ComicBookException.INVALID_NAVIGATION_EVENT + ' ' + e.type; + } + + if (side) { + + e.stopPropagation(); + + // western style (left to right) + if (!options.manga) { + if (side === 'left') { self.drawPrevPage(); } + if (side === 'right') { self.drawNextPage(); } + } + // manga style (right to left) + else { + if (side === 'left') { self.drawNextPage(); } + if (side === 'right') { self.drawPrevPage(); } + } + + return false; + } + }; + + ComicBook.prototype.toggleReadingMode = function () { + options.manga = !options.manga; + self.getControl('toolbar') + .find('.manga-' + options.manga).show().end() + .find('.manga-' + !options.manga).hide(); + }; + + ComicBook.prototype.destroy = function () { + + $.each(this.controls, function (name, $control) { + $control.remove(); + }); + + canvas.width = 0; + canvas.height = 0; + + window.removeEventListener('keydown', this.navigation, false); + window.removeEventListener('hashchange', checkHash, false); + + setHash(''); + + // $(this).trigger('destroy'); + }; + + } + + return ComicBook; + +})(jQuery); diff --git a/lib/ComicBook.min.js b/lib/ComicBook.min.js deleted file mode 100644 index f9af946..0000000 --- a/lib/ComicBook.min.js +++ /dev/null @@ -1,23 +0,0 @@ -function merge(j,d){var i;typeof d==="undefined"&&(d={});for(i in j)j.hasOwnProperty(i)&&!(i in d)&&(d[i]=j[i]);return d}var ComicBookException={INVALID_PAGE:"invalid page",INVALID_PAGE_TYPE:"invalid page type",UNDEFINED_CONTROL:"undefined control",INVALID_ZOOM_MODE:"invalid zoom mode",INVALID_NAVIGATION_EVENT:"invalid navigation event"}; -function ComicBook(j,d,i){function m(){var a=window.innerHeight+1;p===!1&&(p=$(document.createElement("div")).attr("id","cb-width-shiv").css({width:"100%",position:"absolute",top:0,zIndex:"-1000"}),$("body").append(p));p.height(a);return p.innerWidth()}function x(){var a=t();a!==c&&l.indexOf(a)>-1&&(c=a,ComicBook.prototype.draw())}function t(){var a=parseInt(location.hash.substring(1),10)-1||0;if(a<0)a=location.hash=0;return a}function s(){f=document.getElementById(y);u=f.getContext("2d");v===!1&& -(ComicBook.prototype.renderControls(),v=!0);f.addEventListener("click",ComicBook.prototype.navigation,!1);window.addEventListener("keydown",ComicBook.prototype.navigation,!1);window.addEventListener("hashchange",x,!1)}var y=j;this.srcs=d;var b=merge({displayMode:"double",zoomMode:"fitWidth",manga:!1,enhance:{},keyboard:{next:78,previous:80,toolbar:84,toggleLayout:76}},i),w=d.length,q=[],f,u,l=[],n=1,r=!1,v=!1,o=!1,p=!1,j=t(),c=j / "+d.length)),color:$(document.createElement("div")).attr("id", -"cb-color").addClass("cb-control").append("").append($("
").slider({value:0,step:10,min:-1E3,max:1E3,change:function(a,b){ComicBook.prototype.enhance.brightness({brightness:b.value})}})).append("").append($("
").slider({value:0,step:0.001,min:0,max:1,change:function(a,b){ComicBook.prototype.enhance.brightness({contrast:b.value})}})).append("").append($("
").slider({value:0, -step:0.001,min:0,max:1,change:function(a,b){ComicBook.prototype.enhance.sharpen({amount:b.value})}})).append($(document.createElement("div")).addClass("cb-option").append(" ").append("")),navigation:{left:$(document.createElement("div")).addClass("cb-control cb-navigate cb-always-on left").click(function(){b.manga==!1?ComicBook.prototype.drawPrevPage():ComicBook.prototype.drawNextPage()}), -right:$(document.createElement("div")).addClass("cb-control cb-navigate cb-always-on right").click(function(){b.manga==!1?ComicBook.prototype.drawNextPage():ComicBook.prototype.drawPrevPage()})},loadingOverlay:$(document.createElement("div")).attr("id","cb-loading-overlay").addClass("cb-control")};ComicBook.prototype.renderControls=function(){$(f).before(this.getControl("loadingOverlay")).before(this.getControl("status")).after(this.getControl("toolbar").hide()).after(this.getControl("navigation").left).after(this.getControl("navigation").right).after(this.getControl("color").hide()); -$(".cb-menu-button").click(function(){$(this).toggleClass("active")});$("#cb-desaturate").click(function(){$(this).is(":checked")?ComicBook.prototype.enhance.desaturate():ComicBook.prototype.enhance.resaturate()});$("#cb-reset").click(function(){$("#cb-brightness").slider("value",0);$("#cb-contrast").slider("value",0);$("#cb-saturation").slider("value",0);$("#cb-sharpen").slider("value",0);$("#cb-desaturate").attr("checked",!1);ComicBook.prototype.enhance.reset()})};ComicBook.prototype.getControl= -function(a){if(typeof this.control[a]==="undefined")throw ComicBookException.UNDEFINED_CONTROL+" "+a;return this.control[a]};ComicBook.prototype.showControl=function(a){this.getControl(a).show().addClass("open")};ComicBook.prototype.hideControl=function(a){this.getControl(a).removeClass("open").hide()};ComicBook.prototype.toggleControl=function(a){this.getControl(a).toggle().toggleClass("open")};ComicBook.prototype.toggleToolbar=function(){$("#cb-toolbar").is(":visible")?$(".cb-control").not(".cb-always-on").hide(): -$("#cb-toolbar, .cb-control.open").show()};ComicBook.prototype.toggleLayout=function(){b.displayMode==="double"?($("#cb-toolbar .cb-layout").removeClass("double"),b.displayMode="single"):($("#cb-toolbar .cb-layout").removeClass("single"),b.displayMode="double");$("#cb-toolbar .cb-layout").addClass(b.displayMode);ComicBook.prototype.drawPage()};ComicBook.prototype.getPage=function(a){if(a<0||a>d.length-1)throw ComicBookException.INVALID_PAGE+" "+a;if(typeof q[a]==="object")return q[a];else o=a,this.showControl("loadingOverlay")}; -ComicBook.prototype.draw=function(){s();$(".cb-control.cb-navigate").outerHeight(window.innerHeight);$("#cb-toolbar").outerWidth(m());$("#cb-loading-overlay").outerWidth(m()).height(window.innerHeight);q.length!==w?this.preload():this.drawPage()};ComicBook.prototype.zoom=function(a){b.zoomMode="manual";n=a;typeof this.getPage(c)==="object"&&this.drawPage()};ComicBook.prototype.preload=function(){function a(e){var g=new Image;g.src=d[e];g.onload=function(){q[e]=this;l.push(e);$("#cb-progress-bar").progressbar("value", -Math.floor(l.length/w*100));var g=b.displayMode==="double"&&c3&&h>-1?(f.push(h),h--,e=0):(f.push(d),d++),e++;for(;h>-1;)f.push(h), -h--;a(f[0])})(e,d.length-1)};ComicBook.prototype.pageLoaded=function(a){return typeof l[a-1]!=="undefined"};ComicBook.prototype.drawPage=function(a){if(typeof a==="number"&&a0&&(c=a-1,!this.pageLoaded(a))){this.showControl("loadingOverlay");return}c<0&&(c=0);var e,h=0,j=0,k=ComicBook.prototype.getPage(c),a=!1;b.displayMode==="double"&&ck.height||a.width>a.height)&&b.displayMode==="double")b.displayMode="single";b.displayMode==="double"&&(g+=typeof a==="object"?a.width:g);switch(b.zoomMode){case "manual":document.body.style.overflowX="auto";e=b.displayMode==="double"?n*2:n;break;case "fitWidth":document.body.style.overflowX="hidden";n=e=m()>g?(m()-g)/m()+1:m()/g;break;default:throw ComicBookException.INVALID_ZOOM_MODE+" "+b.zoomMode;}var g=k.width*e,i=k.height*e;e=b.zoomMode==="manual"? -k.width*n:g;var l=b.zoomMode==="manual"?k.height*n:i,i=l;f.width=ga.height;c>0&&(c-=b.displayMode==="single"||r?1:2,this.drawPage())};ComicBook.prototype.enhance={reset:function(a){a?delete b.enhance[a]:b.enhance={};ComicBook.prototype.drawPage()},brightness:function(a,c){c!==!1&&this.reset("brightness");var d=merge({brightness:0,contrast:0},a);b.enhance.brightness=d;Pixastic.process(f,"brightness",{brightness:d.brightness, -contrast:d.contrast,legacy:!0});s()},desaturate:function(){b.enhance.desaturate={};Pixastic.process(f,"desaturate",{average:!1});s()},resaturate:function(){delete b.enhance.desaturate;ComicBook.prototype.drawPage()},sharpen:function(a){this.desharpen();a=merge({amount:0},a);b.enhance.sharpen=a;Pixastic.process(f,"sharpen",{amount:a.amount});s()},desharpen:function(){delete b.enhance.sharpen;ComicBook.prototype.drawPage()}};ComicBook.prototype.navigation=function(a){if($("#cb-loading-overlay").is(":visible"))return!1; -var c=!1;switch(a.type){case "click":ComicBook.prototype.toggleToolbar();break;case "keydown":a.keyCode===b.keyboard.previous&&(c="left");a.keyCode===b.keyboard.next&&(c="right");a.keyCode===b.keyboard.toolbar&&ComicBook.prototype.toggleToolbar();a.keyCode===b.keyboard.toggleLayout&&ComicBook.prototype.toggleLayout();break;default:throw ComicBookException.INVALID_NAVIGATION_EVENT+" "+a.type;}if(c)return a.stopPropagation(),b.manga?(c==="left"&&ComicBook.prototype.drawNextPage(),c==="right"&&ComicBook.prototype.drawPrevPage()): -(c==="left"&&ComicBook.prototype.drawPrevPage(),c==="right"&&ComicBook.prototype.drawNextPage()),!1}}; diff --git a/lib/pixastic b/lib/pixastic deleted file mode 160000 index 5de8438..0000000 --- a/lib/pixastic +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5de84389685e3124137f6b45daa64811cc6a8220 diff --git a/lib/templates.js b/lib/templates.js new file mode 100644 index 0000000..00b1f84 --- /dev/null +++ b/lib/templates.js @@ -0,0 +1,43 @@ +(function() { + var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {}; +templates['loadingOverlay'] = template(function (Handlebars,depth0,helpers,partials,data) { + this.compilerInfo = [2,'>= 1.0.0-rc.3']; +helpers = helpers || Handlebars.helpers; data = data || {}; + + + + return "\n
\n"; + }); +templates['navigateLeft'] = template(function (Handlebars,depth0,helpers,partials,data) { + this.compilerInfo = [2,'>= 1.0.0-rc.3']; +helpers = helpers || Handlebars.helpers; data = data || {}; + + + + return "\n
\n \n
\n"; + }); +templates['navigateRight'] = template(function (Handlebars,depth0,helpers,partials,data) { + this.compilerInfo = [2,'>= 1.0.0-rc.3']; +helpers = helpers || Handlebars.helpers; data = data || {}; + + + + return "\n
\n \n
\n"; + }); +templates['progressbar'] = template(function (Handlebars,depth0,helpers,partials,data) { + this.compilerInfo = [2,'>= 1.0.0-rc.3']; +helpers = helpers || Handlebars.helpers; data = data || {}; + + + + return "
\n
\n
\n
\n
\n"; + }); +templates['toolbar'] = template(function (Handlebars,depth0,helpers,partials,data) { + this.compilerInfo = [2,'>= 1.0.0-rc.3']; +helpers = helpers || Handlebars.helpers; data = data || {}; + + + + return "\n
\n\n
    \n
  • \n \n
  • \n
  • \n
  • \n \n
    \n
    \n
    \n
    \n \n \n
    \n
    \n \n \n
    \n
    \n \n \n
    \n
    \n
    \n \n \n
    \n
    \n \n
    \n
    \n
    \n
  • \n
  • \n \n \n
  • \n
  • \n \n
  • \n
  • \n \n
  • \n
  • \n \n
  • \n
  • \n \n \n
  • \n
\n\n
    \n
  • /
  • \n
\n\n
\n"; + }); +})(); \ No newline at end of file diff --git a/lib/tests/img/1.png b/lib/tests/img/1.png new file mode 100755 index 0000000..a8d2c9a Binary files /dev/null and b/lib/tests/img/1.png differ diff --git a/lib/tests/img/2.png b/lib/tests/img/2.png new file mode 100755 index 0000000..c41255b Binary files /dev/null and b/lib/tests/img/2.png differ diff --git a/lib/tests/img/3.png b/lib/tests/img/3.png new file mode 100755 index 0000000..c8e349a Binary files /dev/null and b/lib/tests/img/3.png differ diff --git a/lib/tests/img/4.png b/lib/tests/img/4.png new file mode 100755 index 0000000..0a334c2 Binary files /dev/null and b/lib/tests/img/4.png differ diff --git a/lib/tests/img/5.png b/lib/tests/img/5.png new file mode 100755 index 0000000..fa53250 Binary files /dev/null and b/lib/tests/img/5.png differ diff --git a/lib/tests/img/6.png b/lib/tests/img/6.png new file mode 100755 index 0000000..f18c20c Binary files /dev/null and b/lib/tests/img/6.png differ diff --git a/lib/tests/index.html b/lib/tests/index.html new file mode 100644 index 0000000..fc2ecef --- /dev/null +++ b/lib/tests/index.html @@ -0,0 +1,27 @@ + + + + Comic Book Reader Test Suite + + + + + + + + + + + + + + + + + +
+
+
+
+ + diff --git a/lib/tests/phantom.js b/lib/tests/phantom.js new file mode 100644 index 0000000..3a5de31 --- /dev/null +++ b/lib/tests/phantom.js @@ -0,0 +1,63 @@ +// Simple phantom.js integration script +// Taken from Twitter Bootstrap + +function waitFor(testFx, onReady, timeOutMillis) { + var maxtimeOutMillis = timeOutMillis ? timeOutMillis : 5001 //< Default Max Timout is 5s + , start = new Date().getTime() + , condition = false + , interval = setInterval(function () { + if ((new Date().getTime() - start < maxtimeOutMillis) && !condition) { + // If not time-out yet and condition not yet fulfilled + condition = (typeof(testFx) === "string" ? eval(testFx) : testFx()) //< defensive code + } else { + if (!condition) { + // If condition still not fulfilled (timeout but condition is 'false') + console.log("'waitFor()' timeout") + phantom.exit(1) + } else { + // Condition fulfilled (timeout and/or condition is 'true') + typeof(onReady) === "string" ? eval(onReady) : onReady() //< Do what it's supposed to do once the condition is fulfilled + clearInterval(interval) //< Stop this interval + } + } + }, 100) //< repeat check every 100ms +} + + +if (phantom.args.length === 0 || phantom.args.length > 2) { + console.log('Usage: phantom.js URL') + phantom.exit() +} + +var page = new WebPage() + +// Route "console.log()" calls from within the Page context to the main Phantom context (i.e. current "this") +page.onConsoleMessage = function(msg) { + console.log(msg) +}; + +page.open(phantom.args[0], function(status){ + if (status !== "success") { + console.log("Unable to access network") + phantom.exit() + } else { + waitFor(function(){ + return page.evaluate(function(){ + var el = document.getElementById('qunit-testresult') + if (el && el.innerText.match('completed')) { + return true + } + return false + }) + }, function(){ + var failedNum = page.evaluate(function(){ + var el = document.getElementById('qunit-testresult') + try { + return el.getElementsByClassName('failed')[0].innerHTML + } catch (e) { } + return 10000 + }); + phantom.exit((parseInt(failedNum, 10) > 0) ? 1 : 0) + }) + } +}) diff --git a/lib/tests/server.js b/lib/tests/server.js new file mode 100755 index 0000000..3964d86 --- /dev/null +++ b/lib/tests/server.js @@ -0,0 +1,25 @@ +/* + * Simple connect server for phantom.js + * Adapted from Twitter Bootstrap + */ + +var connect = require('connect'), + http = require('http'), + fs = require('fs'), + app = connect(), + pid_path = __dirname + '/pid.txt'; + +// clean up after failed test runs +if (fs.existsSync(pid_path)) { + try { + var pid = fs.readFileSync(pid_path, { encoding: 'utf-8' }); + process.kill(pid, 'SIGHUP'); + } catch (e) {} + fs.unlinkSync(pid_path); +} + +app.use(connect.static(__dirname + '/../../')); + +http.createServer(app).listen(3000); + +fs.writeFileSync(pid_path, process.pid, 'utf-8'); diff --git a/lib/tests/unit/ComicBook.js b/lib/tests/unit/ComicBook.js new file mode 100644 index 0000000..fc93ddd --- /dev/null +++ b/lib/tests/unit/ComicBook.js @@ -0,0 +1,62 @@ +/* global $: false, module: false, test: false, equal: false, ComicBook: false console: false */ + +$(function () { + + 'use strict'; + + var $fixture; + var book; + + function initBook() { + + $fixture = $('#qunit-fixture'); + $fixture.append(''); + + book = new ComicBook( + 'comic', + ['img/1.png','img/2.png','img/3.png','img/4.png','img/5.png','img/6.png'], + { libPath: '../vendor/' } + ); + } + + module('not yet rendered comic', { + setup: initBook + }); + + test('controls shouldn\'t be renderd yet', function () { + equal($('.cb-control, .toolbar').length, 0, 'book not drawn yet, nothing should be rendered'); + }); + + module('rendered comic', { + + setup: function () { + initBook(); + book.draw(); + }, + + teardown: function () { + } + }); + + test('all controls should be rendered', function () { + equal($('.cb-control, .toolbar').length, 5, 'All toolbar elements should have rendered after book.draw'); + }); + + // navigate on keyboard + // don't navigate if nothing left + // show current page + // customise keyboard control + // dropdown menus + // apply effects + // maximise + // minimise + // fit width + // single page / double page + // single page should allow double page spreads + // preloading + // update hash + // resume based on hash + // load from middle of page + // emit custom events based on data-attributes + // destroy +}); diff --git a/lib/tests/unit/logger.js b/lib/tests/unit/logger.js new file mode 100644 index 0000000..96514d5 --- /dev/null +++ b/lib/tests/unit/logger.js @@ -0,0 +1,24 @@ +/* jshint strict: false */ +/* global console: false, QUnit: false */ + +// Logging setup for phantom integration +// Taken from Twitter Bootstrap + +QUnit.begin = function () { + console.log('Starting test suite'); + console.log('================================================\n'); +}; + +QUnit.moduleDone = function (opts) { + if (opts.failed === 0) { + console.log('\u2714 All tests passed in "' + opts.name + '" module'); + } else { + console.log('\u2716 ' + opts.failed + ' tests failed in "' + opts.name + '" module'); + } +}; + +QUnit.done = function (opts) { + console.log('\n================================================'); + console.log('Tests completed in ' + opts.runtime + ' milliseconds'); + console.log(opts.passed + ' tests of ' + opts.total + ' passed, ' + opts.failed + ' failed.'); +}; diff --git a/lib/tests/vendor/qunit-1.11.0.css b/lib/tests/vendor/qunit-1.11.0.css new file mode 100644 index 0000000..d7fc0c8 --- /dev/null +++ b/lib/tests/vendor/qunit-1.11.0.css @@ -0,0 +1,244 @@ +/** + * QUnit v1.11.0 - A JavaScript Unit Testing Framework + * + * http://qunitjs.com + * + * Copyright 2012 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +/** Font Family and Sizes */ + +#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { + font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; +} + +#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } +#qunit-tests { font-size: smaller; } + + +/** Resets */ + +#qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { + margin: 0; + padding: 0; +} + + +/** Header */ + +#qunit-header { + padding: 0.5em 0 0.5em 1em; + + color: #8699a4; + background-color: #0d3349; + + font-size: 1.5em; + line-height: 1em; + font-weight: normal; + + border-radius: 5px 5px 0 0; + -moz-border-radius: 5px 5px 0 0; + -webkit-border-top-right-radius: 5px; + -webkit-border-top-left-radius: 5px; +} + +#qunit-header a { + text-decoration: none; + color: #c2ccd1; +} + +#qunit-header a:hover, +#qunit-header a:focus { + color: #fff; +} + +#qunit-testrunner-toolbar label { + display: inline-block; + padding: 0 .5em 0 .1em; +} + +#qunit-banner { + height: 5px; +} + +#qunit-testrunner-toolbar { + padding: 0.5em 0 0.5em 2em; + color: #5E740B; + background-color: #eee; + overflow: hidden; +} + +#qunit-userAgent { + padding: 0.5em 0 0.5em 2.5em; + background-color: #2b81af; + color: #fff; + text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; +} + +#qunit-modulefilter-container { + float: right; +} + +/** Tests: Pass/Fail */ + +#qunit-tests { + list-style-position: inside; +} + +#qunit-tests li { + padding: 0.4em 0.5em 0.4em 2.5em; + border-bottom: 1px solid #fff; + list-style-position: inside; +} + +#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { + display: none; +} + +#qunit-tests li strong { + cursor: pointer; +} + +#qunit-tests li a { + padding: 0.5em; + color: #c2ccd1; + text-decoration: none; +} +#qunit-tests li a:hover, +#qunit-tests li a:focus { + color: #000; +} + +#qunit-tests li .runtime { + float: right; + font-size: smaller; +} + +.qunit-assert-list { + margin-top: 0.5em; + padding: 0.5em; + + background-color: #fff; + + border-radius: 5px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; +} + +.qunit-collapsed { + display: none; +} + +#qunit-tests table { + border-collapse: collapse; + margin-top: .2em; +} + +#qunit-tests th { + text-align: right; + vertical-align: top; + padding: 0 .5em 0 0; +} + +#qunit-tests td { + vertical-align: top; +} + +#qunit-tests pre { + margin: 0; + white-space: pre-wrap; + word-wrap: break-word; +} + +#qunit-tests del { + background-color: #e0f2be; + color: #374e0c; + text-decoration: none; +} + +#qunit-tests ins { + background-color: #ffcaca; + color: #500; + text-decoration: none; +} + +/*** Test Counts */ + +#qunit-tests b.counts { color: black; } +#qunit-tests b.passed { color: #5E740B; } +#qunit-tests b.failed { color: #710909; } + +#qunit-tests li li { + padding: 5px; + background-color: #fff; + border-bottom: none; + list-style-position: inside; +} + +/*** Passing Styles */ + +#qunit-tests li li.pass { + color: #3c510c; + background-color: #fff; + border-left: 10px solid #C6E746; +} + +#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } +#qunit-tests .pass .test-name { color: #366097; } + +#qunit-tests .pass .test-actual, +#qunit-tests .pass .test-expected { color: #999999; } + +#qunit-banner.qunit-pass { background-color: #C6E746; } + +/*** Failing Styles */ + +#qunit-tests li li.fail { + color: #710909; + background-color: #fff; + border-left: 10px solid #EE5757; + white-space: pre; +} + +#qunit-tests > li:last-child { + border-radius: 0 0 5px 5px; + -moz-border-radius: 0 0 5px 5px; + -webkit-border-bottom-right-radius: 5px; + -webkit-border-bottom-left-radius: 5px; +} + +#qunit-tests .fail { color: #000000; background-color: #EE5757; } +#qunit-tests .fail .test-name, +#qunit-tests .fail .module-name { color: #000000; } + +#qunit-tests .fail .test-actual { color: #EE5757; } +#qunit-tests .fail .test-expected { color: green; } + +#qunit-banner.qunit-fail { background-color: #EE5757; } + + +/** Result */ + +#qunit-testresult { + padding: 0.5em 0.5em 0.5em 2.5em; + + color: #2b81af; + background-color: #D2E0E6; + + border-bottom: 1px solid white; +} +#qunit-testresult .module-name { + font-weight: bold; +} + +/** Fixture */ + +#qunit-fixture { + position: absolute; + top: -10000px; + left: -10000px; + width: 1000px; + height: 1000px; +} diff --git a/lib/tests/vendor/qunit-1.11.0.js b/lib/tests/vendor/qunit-1.11.0.js new file mode 100644 index 0000000..302545f --- /dev/null +++ b/lib/tests/vendor/qunit-1.11.0.js @@ -0,0 +1,2152 @@ +/** + * QUnit v1.11.0 - A JavaScript Unit Testing Framework + * + * http://qunitjs.com + * + * Copyright 2012 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +(function( window ) { + +var QUnit, + assert, + config, + onErrorFnPrev, + testId = 0, + fileName = (sourceFromStacktrace( 0 ) || "" ).replace(/(:\d+)+\)?/, "").replace(/.+\//, ""), + toString = Object.prototype.toString, + hasOwn = Object.prototype.hasOwnProperty, + // Keep a local reference to Date (GH-283) + Date = window.Date, + defined = { + setTimeout: typeof window.setTimeout !== "undefined", + sessionStorage: (function() { + var x = "qunit-test-string"; + try { + sessionStorage.setItem( x, x ); + sessionStorage.removeItem( x ); + return true; + } catch( e ) { + return false; + } + }()) + }, + /** + * Provides a normalized error string, correcting an issue + * with IE 7 (and prior) where Error.prototype.toString is + * not properly implemented + * + * Based on http://es5.github.com/#x15.11.4.4 + * + * @param {String|Error} error + * @return {String} error message + */ + errorString = function( error ) { + var name, message, + errorString = error.toString(); + if ( errorString.substring( 0, 7 ) === "[object" ) { + name = error.name ? error.name.toString() : "Error"; + message = error.message ? error.message.toString() : ""; + if ( name && message ) { + return name + ": " + message; + } else if ( name ) { + return name; + } else if ( message ) { + return message; + } else { + return "Error"; + } + } else { + return errorString; + } + }, + /** + * Makes a clone of an object using only Array or Object as base, + * and copies over the own enumerable properties. + * + * @param {Object} obj + * @return {Object} New object with only the own properties (recursively). + */ + objectValues = function( obj ) { + // Grunt 0.3.x uses an older version of jshint that still has jshint/jshint#392. + /*jshint newcap: false */ + var key, val, + vals = QUnit.is( "array", obj ) ? [] : {}; + for ( key in obj ) { + if ( hasOwn.call( obj, key ) ) { + val = obj[key]; + vals[key] = val === Object(val) ? objectValues(val) : val; + } + } + return vals; + }; + +function Test( settings ) { + extend( this, settings ); + this.assertions = []; + this.testNumber = ++Test.count; +} + +Test.count = 0; + +Test.prototype = { + init: function() { + var a, b, li, + tests = id( "qunit-tests" ); + + if ( tests ) { + b = document.createElement( "strong" ); + b.innerHTML = this.nameHtml; + + // `a` initialized at top of scope + a = document.createElement( "a" ); + a.innerHTML = "Rerun"; + a.href = QUnit.url({ testNumber: this.testNumber }); + + li = document.createElement( "li" ); + li.appendChild( b ); + li.appendChild( a ); + li.className = "running"; + li.id = this.id = "qunit-test-output" + testId++; + + tests.appendChild( li ); + } + }, + setup: function() { + if ( this.module !== config.previousModule ) { + if ( config.previousModule ) { + runLoggingCallbacks( "moduleDone", QUnit, { + name: config.previousModule, + failed: config.moduleStats.bad, + passed: config.moduleStats.all - config.moduleStats.bad, + total: config.moduleStats.all + }); + } + config.previousModule = this.module; + config.moduleStats = { all: 0, bad: 0 }; + runLoggingCallbacks( "moduleStart", QUnit, { + name: this.module + }); + } else if ( config.autorun ) { + runLoggingCallbacks( "moduleStart", QUnit, { + name: this.module + }); + } + + config.current = this; + + this.testEnvironment = extend({ + setup: function() {}, + teardown: function() {} + }, this.moduleTestEnvironment ); + + this.started = +new Date(); + runLoggingCallbacks( "testStart", QUnit, { + name: this.testName, + module: this.module + }); + + // allow utility functions to access the current test environment + // TODO why?? + QUnit.current_testEnvironment = this.testEnvironment; + + if ( !config.pollution ) { + saveGlobal(); + } + if ( config.notrycatch ) { + this.testEnvironment.setup.call( this.testEnvironment ); + return; + } + try { + this.testEnvironment.setup.call( this.testEnvironment ); + } catch( e ) { + QUnit.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); + } + }, + run: function() { + config.current = this; + + var running = id( "qunit-testresult" ); + + if ( running ) { + running.innerHTML = "Running:
" + this.nameHtml; + } + + if ( this.async ) { + QUnit.stop(); + } + + this.callbackStarted = +new Date(); + + if ( config.notrycatch ) { + this.callback.call( this.testEnvironment, QUnit.assert ); + this.callbackRuntime = +new Date() - this.callbackStarted; + return; + } + + try { + this.callback.call( this.testEnvironment, QUnit.assert ); + this.callbackRuntime = +new Date() - this.callbackStarted; + } catch( e ) { + this.callbackRuntime = +new Date() - this.callbackStarted; + + QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); + // else next test will carry the responsibility + saveGlobal(); + + // Restart the tests if they're blocking + if ( config.blocking ) { + QUnit.start(); + } + } + }, + teardown: function() { + config.current = this; + if ( config.notrycatch ) { + if ( typeof this.callbackRuntime === "undefined" ) { + this.callbackRuntime = +new Date() - this.callbackStarted; + } + this.testEnvironment.teardown.call( this.testEnvironment ); + return; + } else { + try { + this.testEnvironment.teardown.call( this.testEnvironment ); + } catch( e ) { + QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); + } + } + checkPollution(); + }, + finish: function() { + config.current = this; + if ( config.requireExpects && this.expected === null ) { + QUnit.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack ); + } else if ( this.expected !== null && this.expected !== this.assertions.length ) { + QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack ); + } else if ( this.expected === null && !this.assertions.length ) { + QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack ); + } + + var i, assertion, a, b, time, li, ol, + test = this, + good = 0, + bad = 0, + tests = id( "qunit-tests" ); + + this.runtime = +new Date() - this.started; + config.stats.all += this.assertions.length; + config.moduleStats.all += this.assertions.length; + + if ( tests ) { + ol = document.createElement( "ol" ); + ol.className = "qunit-assert-list"; + + for ( i = 0; i < this.assertions.length; i++ ) { + assertion = this.assertions[i]; + + li = document.createElement( "li" ); + li.className = assertion.result ? "pass" : "fail"; + li.innerHTML = assertion.message || ( assertion.result ? "okay" : "failed" ); + ol.appendChild( li ); + + if ( assertion.result ) { + good++; + } else { + bad++; + config.stats.bad++; + config.moduleStats.bad++; + } + } + + // store result when possible + if ( QUnit.config.reorder && defined.sessionStorage ) { + if ( bad ) { + sessionStorage.setItem( "qunit-test-" + this.module + "-" + this.testName, bad ); + } else { + sessionStorage.removeItem( "qunit-test-" + this.module + "-" + this.testName ); + } + } + + if ( bad === 0 ) { + addClass( ol, "qunit-collapsed" ); + } + + // `b` initialized at top of scope + b = document.createElement( "strong" ); + b.innerHTML = this.nameHtml + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; + + addEvent(b, "click", function() { + var next = b.parentNode.lastChild, + collapsed = hasClass( next, "qunit-collapsed" ); + ( collapsed ? removeClass : addClass )( next, "qunit-collapsed" ); + }); + + addEvent(b, "dblclick", function( e ) { + var target = e && e.target ? e.target : window.event.srcElement; + if ( target.nodeName.toLowerCase() === "span" || target.nodeName.toLowerCase() === "b" ) { + target = target.parentNode; + } + if ( window.location && target.nodeName.toLowerCase() === "strong" ) { + window.location = QUnit.url({ testNumber: test.testNumber }); + } + }); + + // `time` initialized at top of scope + time = document.createElement( "span" ); + time.className = "runtime"; + time.innerHTML = this.runtime + " ms"; + + // `li` initialized at top of scope + li = id( this.id ); + li.className = bad ? "fail" : "pass"; + li.removeChild( li.firstChild ); + a = li.firstChild; + li.appendChild( b ); + li.appendChild( a ); + li.appendChild( time ); + li.appendChild( ol ); + + } else { + for ( i = 0; i < this.assertions.length; i++ ) { + if ( !this.assertions[i].result ) { + bad++; + config.stats.bad++; + config.moduleStats.bad++; + } + } + } + + runLoggingCallbacks( "testDone", QUnit, { + name: this.testName, + module: this.module, + failed: bad, + passed: this.assertions.length - bad, + total: this.assertions.length, + duration: this.runtime + }); + + QUnit.reset(); + + config.current = undefined; + }, + + queue: function() { + var bad, + test = this; + + synchronize(function() { + test.init(); + }); + function run() { + // each of these can by async + synchronize(function() { + test.setup(); + }); + synchronize(function() { + test.run(); + }); + synchronize(function() { + test.teardown(); + }); + synchronize(function() { + test.finish(); + }); + } + + // `bad` initialized at top of scope + // defer when previous test run passed, if storage is available + bad = QUnit.config.reorder && defined.sessionStorage && + +sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName ); + + if ( bad ) { + run(); + } else { + synchronize( run, true ); + } + } +}; + +// Root QUnit object. +// `QUnit` initialized at top of scope +QUnit = { + + // call on start of module test to prepend name to all tests + module: function( name, testEnvironment ) { + config.currentModule = name; + config.currentModuleTestEnvironment = testEnvironment; + config.modules[name] = true; + }, + + asyncTest: function( testName, expected, callback ) { + if ( arguments.length === 2 ) { + callback = expected; + expected = null; + } + + QUnit.test( testName, expected, callback, true ); + }, + + test: function( testName, expected, callback, async ) { + var test, + nameHtml = "" + escapeText( testName ) + ""; + + if ( arguments.length === 2 ) { + callback = expected; + expected = null; + } + + if ( config.currentModule ) { + nameHtml = "" + escapeText( config.currentModule ) + ": " + nameHtml; + } + + test = new Test({ + nameHtml: nameHtml, + testName: testName, + expected: expected, + async: async, + callback: callback, + module: config.currentModule, + moduleTestEnvironment: config.currentModuleTestEnvironment, + stack: sourceFromStacktrace( 2 ) + }); + + if ( !validTest( test ) ) { + return; + } + + test.queue(); + }, + + // Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. + expect: function( asserts ) { + if (arguments.length === 1) { + config.current.expected = asserts; + } else { + return config.current.expected; + } + }, + + start: function( count ) { + // QUnit hasn't been initialized yet. + // Note: RequireJS (et al) may delay onLoad + if ( config.semaphore === undefined ) { + QUnit.begin(function() { + // This is triggered at the top of QUnit.load, push start() to the event loop, to allow QUnit.load to finish first + setTimeout(function() { + QUnit.start( count ); + }); + }); + return; + } + + config.semaphore -= count || 1; + // don't start until equal number of stop-calls + if ( config.semaphore > 0 ) { + return; + } + // ignore if start is called more often then stop + if ( config.semaphore < 0 ) { + config.semaphore = 0; + QUnit.pushFailure( "Called start() while already started (QUnit.config.semaphore was 0 already)", null, sourceFromStacktrace(2) ); + return; + } + // A slight delay, to avoid any current callbacks + if ( defined.setTimeout ) { + window.setTimeout(function() { + if ( config.semaphore > 0 ) { + return; + } + if ( config.timeout ) { + clearTimeout( config.timeout ); + } + + config.blocking = false; + process( true ); + }, 13); + } else { + config.blocking = false; + process( true ); + } + }, + + stop: function( count ) { + config.semaphore += count || 1; + config.blocking = true; + + if ( config.testTimeout && defined.setTimeout ) { + clearTimeout( config.timeout ); + config.timeout = window.setTimeout(function() { + QUnit.ok( false, "Test timed out" ); + config.semaphore = 1; + QUnit.start(); + }, config.testTimeout ); + } + } +}; + +// `assert` initialized at top of scope +// Asssert helpers +// All of these must either call QUnit.push() or manually do: +// - runLoggingCallbacks( "log", .. ); +// - config.current.assertions.push({ .. }); +// We attach it to the QUnit object *after* we expose the public API, +// otherwise `assert` will become a global variable in browsers (#341). +assert = { + /** + * Asserts rough true-ish result. + * @name ok + * @function + * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); + */ + ok: function( result, msg ) { + if ( !config.current ) { + throw new Error( "ok() assertion outside test context, was " + sourceFromStacktrace(2) ); + } + result = !!result; + + var source, + details = { + module: config.current.module, + name: config.current.testName, + result: result, + message: msg + }; + + msg = escapeText( msg || (result ? "okay" : "failed" ) ); + msg = "" + msg + ""; + + if ( !result ) { + source = sourceFromStacktrace( 2 ); + if ( source ) { + details.source = source; + msg += "
Source:
" + escapeText( source ) + "
"; + } + } + runLoggingCallbacks( "log", QUnit, details ); + config.current.assertions.push({ + result: result, + message: msg + }); + }, + + /** + * Assert that the first two arguments are equal, with an optional message. + * Prints out both actual and expected values. + * @name equal + * @function + * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" ); + */ + equal: function( actual, expected, message ) { + /*jshint eqeqeq:false */ + QUnit.push( expected == actual, actual, expected, message ); + }, + + /** + * @name notEqual + * @function + */ + notEqual: function( actual, expected, message ) { + /*jshint eqeqeq:false */ + QUnit.push( expected != actual, actual, expected, message ); + }, + + /** + * @name propEqual + * @function + */ + propEqual: function( actual, expected, message ) { + actual = objectValues(actual); + expected = objectValues(expected); + QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); + }, + + /** + * @name notPropEqual + * @function + */ + notPropEqual: function( actual, expected, message ) { + actual = objectValues(actual); + expected = objectValues(expected); + QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); + }, + + /** + * @name deepEqual + * @function + */ + deepEqual: function( actual, expected, message ) { + QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); + }, + + /** + * @name notDeepEqual + * @function + */ + notDeepEqual: function( actual, expected, message ) { + QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); + }, + + /** + * @name strictEqual + * @function + */ + strictEqual: function( actual, expected, message ) { + QUnit.push( expected === actual, actual, expected, message ); + }, + + /** + * @name notStrictEqual + * @function + */ + notStrictEqual: function( actual, expected, message ) { + QUnit.push( expected !== actual, actual, expected, message ); + }, + + "throws": function( block, expected, message ) { + var actual, + expectedOutput = expected, + ok = false; + + // 'expected' is optional + if ( typeof expected === "string" ) { + message = expected; + expected = null; + } + + config.current.ignoreGlobalErrors = true; + try { + block.call( config.current.testEnvironment ); + } catch (e) { + actual = e; + } + config.current.ignoreGlobalErrors = false; + + if ( actual ) { + // we don't want to validate thrown error + if ( !expected ) { + ok = true; + expectedOutput = null; + // expected is a regexp + } else if ( QUnit.objectType( expected ) === "regexp" ) { + ok = expected.test( errorString( actual ) ); + // expected is a constructor + } else if ( actual instanceof expected ) { + ok = true; + // expected is a validation function which returns true is validation passed + } else if ( expected.call( {}, actual ) === true ) { + expectedOutput = null; + ok = true; + } + + QUnit.push( ok, actual, expectedOutput, message ); + } else { + QUnit.pushFailure( message, null, 'No exception was thrown.' ); + } + } +}; + +/** + * @deprecate since 1.8.0 + * Kept assertion helpers in root for backwards compatibility. + */ +extend( QUnit, assert ); + +/** + * @deprecated since 1.9.0 + * Kept root "raises()" for backwards compatibility. + * (Note that we don't introduce assert.raises). + */ +QUnit.raises = assert[ "throws" ]; + +/** + * @deprecated since 1.0.0, replaced with error pushes since 1.3.0 + * Kept to avoid TypeErrors for undefined methods. + */ +QUnit.equals = function() { + QUnit.push( false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead" ); +}; +QUnit.same = function() { + QUnit.push( false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead" ); +}; + +// We want access to the constructor's prototype +(function() { + function F() {} + F.prototype = QUnit; + QUnit = new F(); + // Make F QUnit's constructor so that we can add to the prototype later + QUnit.constructor = F; +}()); + +/** + * Config object: Maintain internal state + * Later exposed as QUnit.config + * `config` initialized at top of scope + */ +config = { + // The queue of tests to run + queue: [], + + // block until document ready + blocking: true, + + // when enabled, show only failing tests + // gets persisted through sessionStorage and can be changed in UI via checkbox + hidepassed: false, + + // by default, run previously failed tests first + // very useful in combination with "Hide passed tests" checked + reorder: true, + + // by default, modify document.title when suite is done + altertitle: true, + + // when enabled, all tests must call expect() + requireExpects: false, + + // add checkboxes that are persisted in the query-string + // when enabled, the id is set to `true` as a `QUnit.config` property + urlConfig: [ + { + id: "noglobals", + label: "Check for Globals", + tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings." + }, + { + id: "notrycatch", + label: "No try-catch", + tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings." + } + ], + + // Set of all modules. + modules: {}, + + // logging callback queues + begin: [], + done: [], + log: [], + testStart: [], + testDone: [], + moduleStart: [], + moduleDone: [] +}; + +// Export global variables, unless an 'exports' object exists, +// in that case we assume we're in CommonJS (dealt with on the bottom of the script) +if ( typeof exports === "undefined" ) { + extend( window, QUnit ); + + // Expose QUnit object + window.QUnit = QUnit; +} + +// Initialize more QUnit.config and QUnit.urlParams +(function() { + var i, + location = window.location || { search: "", protocol: "file:" }, + params = location.search.slice( 1 ).split( "&" ), + length = params.length, + urlParams = {}, + current; + + if ( params[ 0 ] ) { + for ( i = 0; i < length; i++ ) { + current = params[ i ].split( "=" ); + current[ 0 ] = decodeURIComponent( current[ 0 ] ); + // allow just a key to turn on a flag, e.g., test.html?noglobals + current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; + urlParams[ current[ 0 ] ] = current[ 1 ]; + } + } + + QUnit.urlParams = urlParams; + + // String search anywhere in moduleName+testName + config.filter = urlParams.filter; + + // Exact match of the module name + config.module = urlParams.module; + + config.testNumber = parseInt( urlParams.testNumber, 10 ) || null; + + // Figure out if we're running the tests from a server or not + QUnit.isLocal = location.protocol === "file:"; +}()); + +// Extend QUnit object, +// these after set here because they should not be exposed as global functions +extend( QUnit, { + assert: assert, + + config: config, + + // Initialize the configuration options + init: function() { + extend( config, { + stats: { all: 0, bad: 0 }, + moduleStats: { all: 0, bad: 0 }, + started: +new Date(), + updateRate: 1000, + blocking: false, + autostart: true, + autorun: false, + filter: "", + queue: [], + semaphore: 1 + }); + + var tests, banner, result, + qunit = id( "qunit" ); + + if ( qunit ) { + qunit.innerHTML = + "

" + escapeText( document.title ) + "

" + + "

" + + "
" + + "

" + + "
    "; + } + + tests = id( "qunit-tests" ); + banner = id( "qunit-banner" ); + result = id( "qunit-testresult" ); + + if ( tests ) { + tests.innerHTML = ""; + } + + if ( banner ) { + banner.className = ""; + } + + if ( result ) { + result.parentNode.removeChild( result ); + } + + if ( tests ) { + result = document.createElement( "p" ); + result.id = "qunit-testresult"; + result.className = "result"; + tests.parentNode.insertBefore( result, tests ); + result.innerHTML = "Running...
     "; + } + }, + + // Resets the test setup. Useful for tests that modify the DOM. + reset: function() { + var fixture = id( "qunit-fixture" ); + if ( fixture ) { + fixture.innerHTML = config.fixture; + } + }, + + // Trigger an event on an element. + // @example triggerEvent( document.body, "click" ); + triggerEvent: function( elem, type, event ) { + if ( document.createEvent ) { + event = document.createEvent( "MouseEvents" ); + event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, + 0, 0, 0, 0, 0, false, false, false, false, 0, null); + + elem.dispatchEvent( event ); + } else if ( elem.fireEvent ) { + elem.fireEvent( "on" + type ); + } + }, + + // Safe object type checking + is: function( type, obj ) { + return QUnit.objectType( obj ) === type; + }, + + objectType: function( obj ) { + if ( typeof obj === "undefined" ) { + return "undefined"; + // consider: typeof null === object + } + if ( obj === null ) { + return "null"; + } + + var match = toString.call( obj ).match(/^\[object\s(.*)\]$/), + type = match && match[1] || ""; + + switch ( type ) { + case "Number": + if ( isNaN(obj) ) { + return "nan"; + } + return "number"; + case "String": + case "Boolean": + case "Array": + case "Date": + case "RegExp": + case "Function": + return type.toLowerCase(); + } + if ( typeof obj === "object" ) { + return "object"; + } + return undefined; + }, + + push: function( result, actual, expected, message ) { + if ( !config.current ) { + throw new Error( "assertion outside test context, was " + sourceFromStacktrace() ); + } + + var output, source, + details = { + module: config.current.module, + name: config.current.testName, + result: result, + message: message, + actual: actual, + expected: expected + }; + + message = escapeText( message ) || ( result ? "okay" : "failed" ); + message = "" + message + ""; + output = message; + + if ( !result ) { + expected = escapeText( QUnit.jsDump.parse(expected) ); + actual = escapeText( QUnit.jsDump.parse(actual) ); + output += ""; + + if ( actual !== expected ) { + output += ""; + output += ""; + } + + source = sourceFromStacktrace(); + + if ( source ) { + details.source = source; + output += ""; + } + + output += "
    Expected:
    " + expected + "
    Result:
    " + actual + "
    Diff:
    " + QUnit.diff( expected, actual ) + "
    Source:
    " + escapeText( source ) + "
    "; + } + + runLoggingCallbacks( "log", QUnit, details ); + + config.current.assertions.push({ + result: !!result, + message: output + }); + }, + + pushFailure: function( message, source, actual ) { + if ( !config.current ) { + throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace(2) ); + } + + var output, + details = { + module: config.current.module, + name: config.current.testName, + result: false, + message: message + }; + + message = escapeText( message ) || "error"; + message = "" + message + ""; + output = message; + + output += ""; + + if ( actual ) { + output += ""; + } + + if ( source ) { + details.source = source; + output += ""; + } + + output += "
    Result:
    " + escapeText( actual ) + "
    Source:
    " + escapeText( source ) + "
    "; + + runLoggingCallbacks( "log", QUnit, details ); + + config.current.assertions.push({ + result: false, + message: output + }); + }, + + url: function( params ) { + params = extend( extend( {}, QUnit.urlParams ), params ); + var key, + querystring = "?"; + + for ( key in params ) { + if ( !hasOwn.call( params, key ) ) { + continue; + } + querystring += encodeURIComponent( key ) + "=" + + encodeURIComponent( params[ key ] ) + "&"; + } + return window.location.protocol + "//" + window.location.host + + window.location.pathname + querystring.slice( 0, -1 ); + }, + + extend: extend, + id: id, + addEvent: addEvent + // load, equiv, jsDump, diff: Attached later +}); + +/** + * @deprecated: Created for backwards compatibility with test runner that set the hook function + * into QUnit.{hook}, instead of invoking it and passing the hook function. + * QUnit.constructor is set to the empty F() above so that we can add to it's prototype here. + * Doing this allows us to tell if the following methods have been overwritten on the actual + * QUnit object. + */ +extend( QUnit.constructor.prototype, { + + // Logging callbacks; all receive a single argument with the listed properties + // run test/logs.html for any related changes + begin: registerLoggingCallback( "begin" ), + + // done: { failed, passed, total, runtime } + done: registerLoggingCallback( "done" ), + + // log: { result, actual, expected, message } + log: registerLoggingCallback( "log" ), + + // testStart: { name } + testStart: registerLoggingCallback( "testStart" ), + + // testDone: { name, failed, passed, total, duration } + testDone: registerLoggingCallback( "testDone" ), + + // moduleStart: { name } + moduleStart: registerLoggingCallback( "moduleStart" ), + + // moduleDone: { name, failed, passed, total } + moduleDone: registerLoggingCallback( "moduleDone" ) +}); + +if ( typeof document === "undefined" || document.readyState === "complete" ) { + config.autorun = true; +} + +QUnit.load = function() { + runLoggingCallbacks( "begin", QUnit, {} ); + + // Initialize the config, saving the execution queue + var banner, filter, i, label, len, main, ol, toolbar, userAgent, val, + urlConfigCheckboxesContainer, urlConfigCheckboxes, moduleFilter, + numModules = 0, + moduleFilterHtml = "", + urlConfigHtml = "", + oldconfig = extend( {}, config ); + + QUnit.init(); + extend(config, oldconfig); + + config.blocking = false; + + len = config.urlConfig.length; + + for ( i = 0; i < len; i++ ) { + val = config.urlConfig[i]; + if ( typeof val === "string" ) { + val = { + id: val, + label: val, + tooltip: "[no tooltip available]" + }; + } + config[ val.id ] = QUnit.urlParams[ val.id ]; + urlConfigHtml += ""; + } + + moduleFilterHtml += ""; + + // `userAgent` initialized at top of scope + userAgent = id( "qunit-userAgent" ); + if ( userAgent ) { + userAgent.innerHTML = navigator.userAgent; + } + + // `banner` initialized at top of scope + banner = id( "qunit-header" ); + if ( banner ) { + banner.innerHTML = "" + banner.innerHTML + " "; + } + + // `toolbar` initialized at top of scope + toolbar = id( "qunit-testrunner-toolbar" ); + if ( toolbar ) { + // `filter` initialized at top of scope + filter = document.createElement( "input" ); + filter.type = "checkbox"; + filter.id = "qunit-filter-pass"; + + addEvent( filter, "click", function() { + var tmp, + ol = document.getElementById( "qunit-tests" ); + + if ( filter.checked ) { + ol.className = ol.className + " hidepass"; + } else { + tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; + ol.className = tmp.replace( / hidepass /, " " ); + } + if ( defined.sessionStorage ) { + if (filter.checked) { + sessionStorage.setItem( "qunit-filter-passed-tests", "true" ); + } else { + sessionStorage.removeItem( "qunit-filter-passed-tests" ); + } + } + }); + + if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem( "qunit-filter-passed-tests" ) ) { + filter.checked = true; + // `ol` initialized at top of scope + ol = document.getElementById( "qunit-tests" ); + ol.className = ol.className + " hidepass"; + } + toolbar.appendChild( filter ); + + // `label` initialized at top of scope + label = document.createElement( "label" ); + label.setAttribute( "for", "qunit-filter-pass" ); + label.setAttribute( "title", "Only show tests and assertons that fail. Stored in sessionStorage." ); + label.innerHTML = "Hide passed tests"; + toolbar.appendChild( label ); + + urlConfigCheckboxesContainer = document.createElement("span"); + urlConfigCheckboxesContainer.innerHTML = urlConfigHtml; + urlConfigCheckboxes = urlConfigCheckboxesContainer.getElementsByTagName("input"); + // For oldIE support: + // * Add handlers to the individual elements instead of the container + // * Use "click" instead of "change" + // * Fallback from event.target to event.srcElement + addEvents( urlConfigCheckboxes, "click", function( event ) { + var params = {}, + target = event.target || event.srcElement; + params[ target.name ] = target.checked ? true : undefined; + window.location = QUnit.url( params ); + }); + toolbar.appendChild( urlConfigCheckboxesContainer ); + + if (numModules > 1) { + moduleFilter = document.createElement( 'span' ); + moduleFilter.setAttribute( 'id', 'qunit-modulefilter-container' ); + moduleFilter.innerHTML = moduleFilterHtml; + addEvent( moduleFilter.lastChild, "change", function() { + var selectBox = moduleFilter.getElementsByTagName("select")[0], + selectedModule = decodeURIComponent(selectBox.options[selectBox.selectedIndex].value); + + window.location = QUnit.url( { module: ( selectedModule === "" ) ? undefined : selectedModule } ); + }); + toolbar.appendChild(moduleFilter); + } + } + + // `main` initialized at top of scope + main = id( "qunit-fixture" ); + if ( main ) { + config.fixture = main.innerHTML; + } + + if ( config.autostart ) { + QUnit.start(); + } +}; + +addEvent( window, "load", QUnit.load ); + +// `onErrorFnPrev` initialized at top of scope +// Preserve other handlers +onErrorFnPrev = window.onerror; + +// Cover uncaught exceptions +// Returning true will surpress the default browser handler, +// returning false will let it run. +window.onerror = function ( error, filePath, linerNr ) { + var ret = false; + if ( onErrorFnPrev ) { + ret = onErrorFnPrev( error, filePath, linerNr ); + } + + // Treat return value as window.onerror itself does, + // Only do our handling if not surpressed. + if ( ret !== true ) { + if ( QUnit.config.current ) { + if ( QUnit.config.current.ignoreGlobalErrors ) { + return true; + } + QUnit.pushFailure( error, filePath + ":" + linerNr ); + } else { + QUnit.test( "global failure", extend( function() { + QUnit.pushFailure( error, filePath + ":" + linerNr ); + }, { validTest: validTest } ) ); + } + return false; + } + + return ret; +}; + +function done() { + config.autorun = true; + + // Log the last module results + if ( config.currentModule ) { + runLoggingCallbacks( "moduleDone", QUnit, { + name: config.currentModule, + failed: config.moduleStats.bad, + passed: config.moduleStats.all - config.moduleStats.bad, + total: config.moduleStats.all + }); + } + + var i, key, + banner = id( "qunit-banner" ), + tests = id( "qunit-tests" ), + runtime = +new Date() - config.started, + passed = config.stats.all - config.stats.bad, + html = [ + "Tests completed in ", + runtime, + " milliseconds.
    ", + "", + passed, + " assertions of ", + config.stats.all, + " passed, ", + config.stats.bad, + " failed." + ].join( "" ); + + if ( banner ) { + banner.className = ( config.stats.bad ? "qunit-fail" : "qunit-pass" ); + } + + if ( tests ) { + id( "qunit-testresult" ).innerHTML = html; + } + + if ( config.altertitle && typeof document !== "undefined" && document.title ) { + // show ✖ for good, ✔ for bad suite result in title + // use escape sequences in case file gets loaded with non-utf-8-charset + document.title = [ + ( config.stats.bad ? "\u2716" : "\u2714" ), + document.title.replace( /^[\u2714\u2716] /i, "" ) + ].join( " " ); + } + + // clear own sessionStorage items if all tests passed + if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) { + // `key` & `i` initialized at top of scope + for ( i = 0; i < sessionStorage.length; i++ ) { + key = sessionStorage.key( i++ ); + if ( key.indexOf( "qunit-test-" ) === 0 ) { + sessionStorage.removeItem( key ); + } + } + } + + // scroll back to top to show results + if ( window.scrollTo ) { + window.scrollTo(0, 0); + } + + runLoggingCallbacks( "done", QUnit, { + failed: config.stats.bad, + passed: passed, + total: config.stats.all, + runtime: runtime + }); +} + +/** @return Boolean: true if this test should be ran */ +function validTest( test ) { + var include, + filter = config.filter && config.filter.toLowerCase(), + module = config.module && config.module.toLowerCase(), + fullName = (test.module + ": " + test.testName).toLowerCase(); + + // Internally-generated tests are always valid + if ( test.callback && test.callback.validTest === validTest ) { + delete test.callback.validTest; + return true; + } + + if ( config.testNumber ) { + return test.testNumber === config.testNumber; + } + + if ( module && ( !test.module || test.module.toLowerCase() !== module ) ) { + return false; + } + + if ( !filter ) { + return true; + } + + include = filter.charAt( 0 ) !== "!"; + if ( !include ) { + filter = filter.slice( 1 ); + } + + // If the filter matches, we need to honour include + if ( fullName.indexOf( filter ) !== -1 ) { + return include; + } + + // Otherwise, do the opposite + return !include; +} + +// so far supports only Firefox, Chrome and Opera (buggy), Safari (for real exceptions) +// Later Safari and IE10 are supposed to support error.stack as well +// See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack +function extractStacktrace( e, offset ) { + offset = offset === undefined ? 3 : offset; + + var stack, include, i; + + if ( e.stacktrace ) { + // Opera + return e.stacktrace.split( "\n" )[ offset + 3 ]; + } else if ( e.stack ) { + // Firefox, Chrome + stack = e.stack.split( "\n" ); + if (/^error$/i.test( stack[0] ) ) { + stack.shift(); + } + if ( fileName ) { + include = []; + for ( i = offset; i < stack.length; i++ ) { + if ( stack[ i ].indexOf( fileName ) !== -1 ) { + break; + } + include.push( stack[ i ] ); + } + if ( include.length ) { + return include.join( "\n" ); + } + } + return stack[ offset ]; + } else if ( e.sourceURL ) { + // Safari, PhantomJS + // hopefully one day Safari provides actual stacktraces + // exclude useless self-reference for generated Error objects + if ( /qunit.js$/.test( e.sourceURL ) ) { + return; + } + // for actual exceptions, this is useful + return e.sourceURL + ":" + e.line; + } +} +function sourceFromStacktrace( offset ) { + try { + throw new Error(); + } catch ( e ) { + return extractStacktrace( e, offset ); + } +} + +/** + * Escape text for attribute or text content. + */ +function escapeText( s ) { + if ( !s ) { + return ""; + } + s = s + ""; + // Both single quotes and double quotes (for attributes) + return s.replace( /['"<>&]/g, function( s ) { + switch( s ) { + case '\'': + return '''; + case '"': + return '"'; + case '<': + return '<'; + case '>': + return '>'; + case '&': + return '&'; + } + }); +} + +function synchronize( callback, last ) { + config.queue.push( callback ); + + if ( config.autorun && !config.blocking ) { + process( last ); + } +} + +function process( last ) { + function next() { + process( last ); + } + var start = new Date().getTime(); + config.depth = config.depth ? config.depth + 1 : 1; + + while ( config.queue.length && !config.blocking ) { + if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) { + config.queue.shift()(); + } else { + window.setTimeout( next, 13 ); + break; + } + } + config.depth--; + if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { + done(); + } +} + +function saveGlobal() { + config.pollution = []; + + if ( config.noglobals ) { + for ( var key in window ) { + // in Opera sometimes DOM element ids show up here, ignore them + if ( !hasOwn.call( window, key ) || /^qunit-test-output/.test( key ) ) { + continue; + } + config.pollution.push( key ); + } + } +} + +function checkPollution() { + var newGlobals, + deletedGlobals, + old = config.pollution; + + saveGlobal(); + + newGlobals = diff( config.pollution, old ); + if ( newGlobals.length > 0 ) { + QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") ); + } + + deletedGlobals = diff( old, config.pollution ); + if ( deletedGlobals.length > 0 ) { + QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") ); + } +} + +// returns a new Array with the elements that are in a but not in b +function diff( a, b ) { + var i, j, + result = a.slice(); + + for ( i = 0; i < result.length; i++ ) { + for ( j = 0; j < b.length; j++ ) { + if ( result[i] === b[j] ) { + result.splice( i, 1 ); + i--; + break; + } + } + } + return result; +} + +function extend( a, b ) { + for ( var prop in b ) { + if ( b[ prop ] === undefined ) { + delete a[ prop ]; + + // Avoid "Member not found" error in IE8 caused by setting window.constructor + } else if ( prop !== "constructor" || a !== window ) { + a[ prop ] = b[ prop ]; + } + } + + return a; +} + +/** + * @param {HTMLElement} elem + * @param {string} type + * @param {Function} fn + */ +function addEvent( elem, type, fn ) { + // Standards-based browsers + if ( elem.addEventListener ) { + elem.addEventListener( type, fn, false ); + // IE + } else { + elem.attachEvent( "on" + type, fn ); + } +} + +/** + * @param {Array|NodeList} elems + * @param {string} type + * @param {Function} fn + */ +function addEvents( elems, type, fn ) { + var i = elems.length; + while ( i-- ) { + addEvent( elems[i], type, fn ); + } +} + +function hasClass( elem, name ) { + return (" " + elem.className + " ").indexOf(" " + name + " ") > -1; +} + +function addClass( elem, name ) { + if ( !hasClass( elem, name ) ) { + elem.className += (elem.className ? " " : "") + name; + } +} + +function removeClass( elem, name ) { + var set = " " + elem.className + " "; + // Class name may appear multiple times + while ( set.indexOf(" " + name + " ") > -1 ) { + set = set.replace(" " + name + " " , " "); + } + // If possible, trim it for prettiness, but not neccecarily + elem.className = window.jQuery ? jQuery.trim( set ) : ( set.trim ? set.trim() : set ); +} + +function id( name ) { + return !!( typeof document !== "undefined" && document && document.getElementById ) && + document.getElementById( name ); +} + +function registerLoggingCallback( key ) { + return function( callback ) { + config[key].push( callback ); + }; +} + +// Supports deprecated method of completely overwriting logging callbacks +function runLoggingCallbacks( key, scope, args ) { + var i, callbacks; + if ( QUnit.hasOwnProperty( key ) ) { + QUnit[ key ].call(scope, args ); + } else { + callbacks = config[ key ]; + for ( i = 0; i < callbacks.length; i++ ) { + callbacks[ i ].call( scope, args ); + } + } +} + +// Test for equality any JavaScript type. +// Author: Philippe Rathé +QUnit.equiv = (function() { + + // Call the o related callback with the given arguments. + function bindCallbacks( o, callbacks, args ) { + var prop = QUnit.objectType( o ); + if ( prop ) { + if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) { + return callbacks[ prop ].apply( callbacks, args ); + } else { + return callbacks[ prop ]; // or undefined + } + } + } + + // the real equiv function + var innerEquiv, + // stack to decide between skip/abort functions + callers = [], + // stack to avoiding loops from circular referencing + parents = [], + + getProto = Object.getPrototypeOf || function ( obj ) { + return obj.__proto__; + }, + callbacks = (function () { + + // for string, boolean, number and null + function useStrictEquality( b, a ) { + /*jshint eqeqeq:false */ + if ( b instanceof a.constructor || a instanceof b.constructor ) { + // to catch short annotaion VS 'new' annotation of a + // declaration + // e.g. var i = 1; + // var j = new Number(1); + return a == b; + } else { + return a === b; + } + } + + return { + "string": useStrictEquality, + "boolean": useStrictEquality, + "number": useStrictEquality, + "null": useStrictEquality, + "undefined": useStrictEquality, + + "nan": function( b ) { + return isNaN( b ); + }, + + "date": function( b, a ) { + return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf(); + }, + + "regexp": function( b, a ) { + return QUnit.objectType( b ) === "regexp" && + // the regex itself + a.source === b.source && + // and its modifers + a.global === b.global && + // (gmi) ... + a.ignoreCase === b.ignoreCase && + a.multiline === b.multiline && + a.sticky === b.sticky; + }, + + // - skip when the property is a method of an instance (OOP) + // - abort otherwise, + // initial === would have catch identical references anyway + "function": function() { + var caller = callers[callers.length - 1]; + return caller !== Object && typeof caller !== "undefined"; + }, + + "array": function( b, a ) { + var i, j, len, loop; + + // b could be an object literal here + if ( QUnit.objectType( b ) !== "array" ) { + return false; + } + + len = a.length; + if ( len !== b.length ) { + // safe and faster + return false; + } + + // track reference to avoid circular references + parents.push( a ); + for ( i = 0; i < len; i++ ) { + loop = false; + for ( j = 0; j < parents.length; j++ ) { + if ( parents[j] === a[i] ) { + loop = true;// dont rewalk array + } + } + if ( !loop && !innerEquiv(a[i], b[i]) ) { + parents.pop(); + return false; + } + } + parents.pop(); + return true; + }, + + "object": function( b, a ) { + var i, j, loop, + // Default to true + eq = true, + aProperties = [], + bProperties = []; + + // comparing constructors is more strict than using + // instanceof + if ( a.constructor !== b.constructor ) { + // Allow objects with no prototype to be equivalent to + // objects with Object as their constructor. + if ( !(( getProto(a) === null && getProto(b) === Object.prototype ) || + ( getProto(b) === null && getProto(a) === Object.prototype ) ) ) { + return false; + } + } + + // stack constructor before traversing properties + callers.push( a.constructor ); + // track reference to avoid circular references + parents.push( a ); + + for ( i in a ) { // be strict: don't ensures hasOwnProperty + // and go deep + loop = false; + for ( j = 0; j < parents.length; j++ ) { + if ( parents[j] === a[i] ) { + // don't go down the same path twice + loop = true; + } + } + aProperties.push(i); // collect a's properties + + if (!loop && !innerEquiv( a[i], b[i] ) ) { + eq = false; + break; + } + } + + callers.pop(); // unstack, we are done + parents.pop(); + + for ( i in b ) { + bProperties.push( i ); // collect b's properties + } + + // Ensures identical properties name + return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); + } + }; + }()); + + innerEquiv = function() { // can take multiple arguments + var args = [].slice.apply( arguments ); + if ( args.length < 2 ) { + return true; // end transition + } + + return (function( a, b ) { + if ( a === b ) { + return true; // catch the most you can + } else if ( a === null || b === null || typeof a === "undefined" || + typeof b === "undefined" || + QUnit.objectType(a) !== QUnit.objectType(b) ) { + return false; // don't lose time with error prone cases + } else { + return bindCallbacks(a, callbacks, [ b, a ]); + } + + // apply transition with (1..n) arguments + }( args[0], args[1] ) && arguments.callee.apply( this, args.splice(1, args.length - 1 )) ); + }; + + return innerEquiv; +}()); + +/** + * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | + * http://flesler.blogspot.com Licensed under BSD + * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008 + * + * @projectDescription Advanced and extensible data dumping for Javascript. + * @version 1.0.0 + * @author Ariel Flesler + * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} + */ +QUnit.jsDump = (function() { + function quote( str ) { + return '"' + str.toString().replace( /"/g, '\\"' ) + '"'; + } + function literal( o ) { + return o + ""; + } + function join( pre, arr, post ) { + var s = jsDump.separator(), + base = jsDump.indent(), + inner = jsDump.indent(1); + if ( arr.join ) { + arr = arr.join( "," + s + inner ); + } + if ( !arr ) { + return pre + post; + } + return [ pre, inner + arr, base + post ].join(s); + } + function array( arr, stack ) { + var i = arr.length, ret = new Array(i); + this.up(); + while ( i-- ) { + ret[i] = this.parse( arr[i] , undefined , stack); + } + this.down(); + return join( "[", ret, "]" ); + } + + var reName = /^function (\w+)/, + jsDump = { + // type is used mostly internally, you can fix a (custom)type in advance + parse: function( obj, type, stack ) { + stack = stack || [ ]; + var inStack, res, + parser = this.parsers[ type || this.typeOf(obj) ]; + + type = typeof parser; + inStack = inArray( obj, stack ); + + if ( inStack !== -1 ) { + return "recursion(" + (inStack - stack.length) + ")"; + } + if ( type === "function" ) { + stack.push( obj ); + res = parser.call( this, obj, stack ); + stack.pop(); + return res; + } + return ( type === "string" ) ? parser : this.parsers.error; + }, + typeOf: function( obj ) { + var type; + if ( obj === null ) { + type = "null"; + } else if ( typeof obj === "undefined" ) { + type = "undefined"; + } else if ( QUnit.is( "regexp", obj) ) { + type = "regexp"; + } else if ( QUnit.is( "date", obj) ) { + type = "date"; + } else if ( QUnit.is( "function", obj) ) { + type = "function"; + } else if ( typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined" ) { + type = "window"; + } else if ( obj.nodeType === 9 ) { + type = "document"; + } else if ( obj.nodeType ) { + type = "node"; + } else if ( + // native arrays + toString.call( obj ) === "[object Array]" || + // NodeList objects + ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) ) + ) { + type = "array"; + } else if ( obj.constructor === Error.prototype.constructor ) { + type = "error"; + } else { + type = typeof obj; + } + return type; + }, + separator: function() { + return this.multiline ? this.HTML ? "
    " : "\n" : this.HTML ? " " : " "; + }, + // extra can be a number, shortcut for increasing-calling-decreasing + indent: function( extra ) { + if ( !this.multiline ) { + return ""; + } + var chr = this.indentChar; + if ( this.HTML ) { + chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); + } + return new Array( this._depth_ + (extra||0) ).join(chr); + }, + up: function( a ) { + this._depth_ += a || 1; + }, + down: function( a ) { + this._depth_ -= a || 1; + }, + setParser: function( name, parser ) { + this.parsers[name] = parser; + }, + // The next 3 are exposed so you can use them + quote: quote, + literal: literal, + join: join, + // + _depth_: 1, + // This is the list of parsers, to modify them, use jsDump.setParser + parsers: { + window: "[Window]", + document: "[Document]", + error: function(error) { + return "Error(\"" + error.message + "\")"; + }, + unknown: "[Unknown]", + "null": "null", + "undefined": "undefined", + "function": function( fn ) { + var ret = "function", + // functions never have name in IE + name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1]; + + if ( name ) { + ret += " " + name; + } + ret += "( "; + + ret = [ ret, QUnit.jsDump.parse( fn, "functionArgs" ), "){" ].join( "" ); + return join( ret, QUnit.jsDump.parse(fn,"functionCode" ), "}" ); + }, + array: array, + nodelist: array, + "arguments": array, + object: function( map, stack ) { + var ret = [ ], keys, key, val, i; + QUnit.jsDump.up(); + keys = []; + for ( key in map ) { + keys.push( key ); + } + keys.sort(); + for ( i = 0; i < keys.length; i++ ) { + key = keys[ i ]; + val = map[ key ]; + ret.push( QUnit.jsDump.parse( key, "key" ) + ": " + QUnit.jsDump.parse( val, undefined, stack ) ); + } + QUnit.jsDump.down(); + return join( "{", ret, "}" ); + }, + node: function( node ) { + var len, i, val, + open = QUnit.jsDump.HTML ? "<" : "<", + close = QUnit.jsDump.HTML ? ">" : ">", + tag = node.nodeName.toLowerCase(), + ret = open + tag, + attrs = node.attributes; + + if ( attrs ) { + for ( i = 0, len = attrs.length; i < len; i++ ) { + val = attrs[i].nodeValue; + // IE6 includes all attributes in .attributes, even ones not explicitly set. + // Those have values like undefined, null, 0, false, "" or "inherit". + if ( val && val !== "inherit" ) { + ret += " " + attrs[i].nodeName + "=" + QUnit.jsDump.parse( val, "attribute" ); + } + } + } + ret += close; + + // Show content of TextNode or CDATASection + if ( node.nodeType === 3 || node.nodeType === 4 ) { + ret += node.nodeValue; + } + + return ret + open + "/" + tag + close; + }, + // function calls it internally, it's the arguments part of the function + functionArgs: function( fn ) { + var args, + l = fn.length; + + if ( !l ) { + return ""; + } + + args = new Array(l); + while ( l-- ) { + // 97 is 'a' + args[l] = String.fromCharCode(97+l); + } + return " " + args.join( ", " ) + " "; + }, + // object calls it internally, the key part of an item in a map + key: quote, + // function calls it internally, it's the content of the function + functionCode: "[code]", + // node calls it internally, it's an html attribute value + attribute: quote, + string: quote, + date: quote, + regexp: literal, + number: literal, + "boolean": literal + }, + // if true, entities are escaped ( <, >, \t, space and \n ) + HTML: false, + // indentation unit + indentChar: " ", + // if true, items in a collection, are separated by a \n, else just a space. + multiline: true + }; + + return jsDump; +}()); + +// from jquery.js +function inArray( elem, array ) { + if ( array.indexOf ) { + return array.indexOf( elem ); + } + + for ( var i = 0, length = array.length; i < length; i++ ) { + if ( array[ i ] === elem ) { + return i; + } + } + + return -1; +} + +/* + * Javascript Diff Algorithm + * By John Resig (http://ejohn.org/) + * Modified by Chu Alan "sprite" + * + * Released under the MIT license. + * + * More Info: + * http://ejohn.org/projects/javascript-diff-algorithm/ + * + * Usage: QUnit.diff(expected, actual) + * + * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick brown fox jumped jumps over" + */ +QUnit.diff = (function() { + /*jshint eqeqeq:false, eqnull:true */ + function diff( o, n ) { + var i, + ns = {}, + os = {}; + + for ( i = 0; i < n.length; i++ ) { + if ( !hasOwn.call( ns, n[i] ) ) { + ns[ n[i] ] = { + rows: [], + o: null + }; + } + ns[ n[i] ].rows.push( i ); + } + + for ( i = 0; i < o.length; i++ ) { + if ( !hasOwn.call( os, o[i] ) ) { + os[ o[i] ] = { + rows: [], + n: null + }; + } + os[ o[i] ].rows.push( i ); + } + + for ( i in ns ) { + if ( !hasOwn.call( ns, i ) ) { + continue; + } + if ( ns[i].rows.length === 1 && hasOwn.call( os, i ) && os[i].rows.length === 1 ) { + n[ ns[i].rows[0] ] = { + text: n[ ns[i].rows[0] ], + row: os[i].rows[0] + }; + o[ os[i].rows[0] ] = { + text: o[ os[i].rows[0] ], + row: ns[i].rows[0] + }; + } + } + + for ( i = 0; i < n.length - 1; i++ ) { + if ( n[i].text != null && n[ i + 1 ].text == null && n[i].row + 1 < o.length && o[ n[i].row + 1 ].text == null && + n[ i + 1 ] == o[ n[i].row + 1 ] ) { + + n[ i + 1 ] = { + text: n[ i + 1 ], + row: n[i].row + 1 + }; + o[ n[i].row + 1 ] = { + text: o[ n[i].row + 1 ], + row: i + 1 + }; + } + } + + for ( i = n.length - 1; i > 0; i-- ) { + if ( n[i].text != null && n[ i - 1 ].text == null && n[i].row > 0 && o[ n[i].row - 1 ].text == null && + n[ i - 1 ] == o[ n[i].row - 1 ]) { + + n[ i - 1 ] = { + text: n[ i - 1 ], + row: n[i].row - 1 + }; + o[ n[i].row - 1 ] = { + text: o[ n[i].row - 1 ], + row: i - 1 + }; + } + } + + return { + o: o, + n: n + }; + } + + return function( o, n ) { + o = o.replace( /\s+$/, "" ); + n = n.replace( /\s+$/, "" ); + + var i, pre, + str = "", + out = diff( o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/) ), + oSpace = o.match(/\s+/g), + nSpace = n.match(/\s+/g); + + if ( oSpace == null ) { + oSpace = [ " " ]; + } + else { + oSpace.push( " " ); + } + + if ( nSpace == null ) { + nSpace = [ " " ]; + } + else { + nSpace.push( " " ); + } + + if ( out.n.length === 0 ) { + for ( i = 0; i < out.o.length; i++ ) { + str += "" + out.o[i] + oSpace[i] + ""; + } + } + else { + if ( out.n[0].text == null ) { + for ( n = 0; n < out.o.length && out.o[n].text == null; n++ ) { + str += "" + out.o[n] + oSpace[n] + ""; + } + } + + for ( i = 0; i < out.n.length; i++ ) { + if (out.n[i].text == null) { + str += "" + out.n[i] + nSpace[i] + ""; + } + else { + // `pre` initialized at top of scope + pre = ""; + + for ( n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) { + pre += "" + out.o[n] + oSpace[n] + ""; + } + str += " " + out.n[i].text + nSpace[i] + pre; + } + } + } + + return str; + }; +}()); + +// for CommonJS enviroments, export everything +if ( typeof exports !== "undefined" ) { + extend( exports, QUnit ); +} + +// get at whatever the global object is, like window in browsers +}( (function() {return this;}.call()) )); diff --git a/lib/vendor/handlebars.runtime-1.0.rc.1.min.js b/lib/vendor/handlebars.runtime-1.0.rc.1.min.js new file mode 100755 index 0000000..cd031ac --- /dev/null +++ b/lib/vendor/handlebars.runtime-1.0.rc.1.min.js @@ -0,0 +1,2 @@ +// lib/handlebars/base.js +/*jshint eqnull:true*/this.Handlebars={},function(e){e.VERSION="1.0.rc.1",e.helpers={},e.partials={},e.registerHelper=function(e,t,n){n&&(t.not=n),this.helpers[e]=t},e.registerPartial=function(e,t){this.partials[e]=t},e.registerHelper("helperMissing",function(e){if(arguments.length===2)return undefined;throw new Error("Could not find property '"+e+"'")});var t=Object.prototype.toString,n="[object Function]";e.registerHelper("blockHelperMissing",function(r,i){var s=i.inverse||function(){},o=i.fn,u="",a=t.call(r);return a===n&&(r=r.call(this)),r===!0?o(this):r===!1||r==null?s(this):a==="[object Array]"?r.length>0?e.helpers.each(r,i):s(this):o(r)}),e.K=function(){},e.createFrame=Object.create||function(t){e.K.prototype=t;var n=new e.K;return e.K.prototype=null,n},e.registerHelper("each",function(t,n){var r=n.fn,i=n.inverse,s=0,o="",u;n.data&&(u=e.createFrame(n.data));if(t&&typeof t=="object")if(t instanceof Array)for(var a=t.length;s":">",'"':""","'":"'","`":"`"},t=/[&<>"'`]/g,n=/[&<>"'`]/,r=function(t){return e[t]||"&"};Handlebars.Utils={escapeExpression:function(e){return e instanceof Handlebars.SafeString?e.toString():e==null||e===!1?"":n.test(e)?e.replace(t,r):e},isEmpty:function(e){return typeof e=="undefined"?!0:e===null?!0:e===!1?!0:Object.prototype.toString.call(e)==="[object Array]"&&e.length===0?!0:!1}}}(),Handlebars.VM={template:function(e){var t={escapeExpression:Handlebars.Utils.escapeExpression,invokePartial:Handlebars.VM.invokePartial,programs:[],program:function(e,t,n){var r=this.programs[e];return n?Handlebars.VM.program(t,n):r?r:(r=this.programs[e]=Handlebars.VM.program(t),r)},programWithDepth:Handlebars.VM.programWithDepth,noop:Handlebars.VM.noop};return function(n,r){return r=r||{},e.call(t,Handlebars,n,r.helpers,r.partials,r.data)}},programWithDepth:function(e,t,n){var r=Array.prototype.slice.call(arguments,2);return function(n,i){return i=i||{},e.apply(this,[n,i.data||t].concat(r))}},program:function(e,t){return function(n,r){return r=r||{},e(n,r.data||t)}},noop:function(){return""},invokePartial:function(e,t,n,r,i,s){var o={helpers:r,partials:i,data:s};if(e===undefined)throw new Handlebars.Exception("The partial "+t+" could not be found");if(e instanceof Function)return e(n,o);if(!Handlebars.compile)throw new Handlebars.Exception("The partial "+t+" could not be compiled when running in runtime-only mode");return i[t]=Handlebars.compile(e,{data:s!==undefined}),i[t](n,o)}},Handlebars.template=Handlebars.VM.template; \ No newline at end of file diff --git a/lib/vendor/jquery-2.0.0.min.js b/lib/vendor/jquery-2.0.0.min.js new file mode 100755 index 0000000..b18e05a --- /dev/null +++ b/lib/vendor/jquery-2.0.0.min.js @@ -0,0 +1,6 @@ +/*! jQuery v2.0.0 | (c) 2005, 2013 jQuery Foundation, Inc. | jquery.org/license +//@ sourceMappingURL=jquery.min.map +*/ +(function(e,undefined){var t,n,r=typeof undefined,i=e.location,o=e.document,s=o.documentElement,a=e.jQuery,u=e.$,l={},c=[],f="2.0.0",p=c.concat,h=c.push,d=c.slice,g=c.indexOf,m=l.toString,y=l.hasOwnProperty,v=f.trim,x=function(e,n){return new x.fn.init(e,n,t)},b=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,w=/\S+/g,T=/^(?:(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,k=/^-ms-/,N=/-([\da-z])/gi,E=function(e,t){return t.toUpperCase()},S=function(){o.removeEventListener("DOMContentLoaded",S,!1),e.removeEventListener("load",S,!1),x.ready()};x.fn=x.prototype={jquery:f,constructor:x,init:function(e,t,n){var r,i;if(!e)return this;if("string"==typeof e){if(r="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:T.exec(e),!r||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof x?t[0]:t,x.merge(this,x.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:o,!0)),C.test(r[1])&&x.isPlainObject(t))for(r in t)x.isFunction(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return i=o.getElementById(r[2]),i&&i.parentNode&&(this.length=1,this[0]=i),this.context=o,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):x.isFunction(e)?n.ready(e):(e.selector!==undefined&&(this.selector=e.selector,this.context=e.context),x.makeArray(e,this))},selector:"",length:0,toArray:function(){return d.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=x.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return x.each(this,e,t)},ready:function(e){return x.ready.promise().done(e),this},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(x.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:h,sort:[].sort,splice:[].splice},x.fn.init.prototype=x.fn,x.extend=x.fn.extend=function(){var e,t,n,r,i,o,s=arguments[0]||{},a=1,u=arguments.length,l=!1;for("boolean"==typeof s&&(l=s,s=arguments[1]||{},a=2),"object"==typeof s||x.isFunction(s)||(s={}),u===a&&(s=this,--a);u>a;a++)if(null!=(e=arguments[a]))for(t in e)n=s[t],r=e[t],s!==r&&(l&&r&&(x.isPlainObject(r)||(i=x.isArray(r)))?(i?(i=!1,o=n&&x.isArray(n)?n:[]):o=n&&x.isPlainObject(n)?n:{},s[t]=x.extend(l,o,r)):r!==undefined&&(s[t]=r));return s},x.extend({expando:"jQuery"+(f+Math.random()).replace(/\D/g,""),noConflict:function(t){return e.$===x&&(e.$=u),t&&e.jQuery===x&&(e.jQuery=a),x},isReady:!1,readyWait:1,holdReady:function(e){e?x.readyWait++:x.ready(!0)},ready:function(e){(e===!0?--x.readyWait:x.isReady)||(x.isReady=!0,e!==!0&&--x.readyWait>0||(n.resolveWith(o,[x]),x.fn.trigger&&x(o).trigger("ready").off("ready")))},isFunction:function(e){return"function"===x.type(e)},isArray:Array.isArray,isWindow:function(e){return null!=e&&e===e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[m.call(e)]||"object":typeof e},isPlainObject:function(e){if("object"!==x.type(e)||e.nodeType||x.isWindow(e))return!1;try{if(e.constructor&&!y.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(t){return!1}return!0},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||o;var r=C.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=x.buildFragment([e],t,i),i&&x(i).remove(),x.merge([],r.childNodes))},parseJSON:JSON.parse,parseXML:function(e){var t,n;if(!e||"string"!=typeof e)return null;try{n=new DOMParser,t=n.parseFromString(e,"text/xml")}catch(r){t=undefined}return(!t||t.getElementsByTagName("parsererror").length)&&x.error("Invalid XML: "+e),t},noop:function(){},globalEval:function(e){var t,n=eval;e=x.trim(e),e&&(1===e.indexOf("use strict")?(t=o.createElement("script"),t.text=e,o.head.appendChild(t).parentNode.removeChild(t)):n(e))},camelCase:function(e){return e.replace(k,"ms-").replace(N,E)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,s=j(e);if(n){if(s){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(s){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:function(e){return null==e?"":v.call(e)},makeArray:function(e,t){var n=t||[];return null!=e&&(j(Object(e))?x.merge(n,"string"==typeof e?[e]:e):h.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:g.call(t,e,n)},merge:function(e,t){var n=t.length,r=e.length,i=0;if("number"==typeof n)for(;n>i;i++)e[r++]=t[i];else while(t[i]!==undefined)e[r++]=t[i++];return e.length=r,e},grep:function(e,t,n){var r,i=[],o=0,s=e.length;for(n=!!n;s>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,s=j(e),a=[];if(s)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(a[a.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(a[a.length]=r);return p.apply([],a)},guid:1,proxy:function(e,t){var n,r,i;return"string"==typeof t&&(n=e[t],t=e,e=n),x.isFunction(e)?(r=d.call(arguments,2),i=function(){return e.apply(t||this,r.concat(d.call(arguments)))},i.guid=e.guid=e.guid||x.guid++,i):undefined},access:function(e,t,n,r,i,o,s){var a=0,u=e.length,l=null==n;if("object"===x.type(n)){i=!0;for(a in n)x.access(e,t,a,n[a],!0,o,s)}else if(r!==undefined&&(i=!0,x.isFunction(r)||(s=!0),l&&(s?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(x(e),n)})),t))for(;u>a;a++)t(e[a],n,s?r:r.call(e[a],a,t(e[a],n)));return i?e:l?t.call(e):u?t(e[0],n):o},now:Date.now,swap:function(e,t,n,r){var i,o,s={};for(o in t)s[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=s[o];return i}}),x.ready.promise=function(t){return n||(n=x.Deferred(),"complete"===o.readyState?setTimeout(x.ready):(o.addEventListener("DOMContentLoaded",S,!1),e.addEventListener("load",S,!1))),n.promise(t)},x.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){l["[object "+t+"]"]=t.toLowerCase()});function j(e){var t=e.length,n=x.type(e);return x.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}t=x(o),function(e,undefined){var t,n,r,i,o,s,a,u,l,c,f,p,h,d,g,m,y="sizzle"+-new Date,v=e.document,b={},w=0,T=0,C=ot(),k=ot(),N=ot(),E=!1,S=function(){return 0},j=typeof undefined,D=1<<31,A=[],L=A.pop,q=A.push,H=A.push,O=A.slice,F=A.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},P="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",R="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",W=M.replace("w","w#"),$="\\["+R+"*("+M+")"+R+"*(?:([*^$|!~]?=)"+R+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+W+")|)|)"+R+"*\\]",B=":("+M+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+$.replace(3,8)+")*)|.*)\\)|)",I=RegExp("^"+R+"+|((?:^|[^\\\\])(?:\\\\.)*)"+R+"+$","g"),z=RegExp("^"+R+"*,"+R+"*"),_=RegExp("^"+R+"*([>+~]|"+R+")"+R+"*"),X=RegExp(R+"*[+~]"),U=RegExp("="+R+"*([^\\]'\"]*)"+R+"*\\]","g"),Y=RegExp(B),V=RegExp("^"+W+"$"),G={ID:RegExp("^#("+M+")"),CLASS:RegExp("^\\.("+M+")"),TAG:RegExp("^("+M.replace("w","w*")+")"),ATTR:RegExp("^"+$),PSEUDO:RegExp("^"+B),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+R+"*(even|odd|(([+-]|)(\\d*)n|)"+R+"*(?:([+-]|)"+R+"*(\\d+)|))"+R+"*\\)|)","i"),"boolean":RegExp("^(?:"+P+")$","i"),needsContext:RegExp("^"+R+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+R+"*((?:-\\d)?\\d*)"+R+"*\\)|)(?=[^-]|$)","i")},J=/^[^{]+\{\s*\[native \w/,Q=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,K=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,et=/'|\\/g,tt=/\\([\da-fA-F]{1,6}[\x20\t\r\n\f]?|.)/g,nt=function(e,t){var n="0x"+t-65536;return n!==n?t:0>n?String.fromCharCode(n+65536):String.fromCharCode(55296|n>>10,56320|1023&n)};try{H.apply(A=O.call(v.childNodes),v.childNodes),A[v.childNodes.length].nodeType}catch(rt){H={apply:A.length?function(e,t){q.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function it(e){return J.test(e+"")}function ot(){var e,t=[];return e=function(n,i){return t.push(n+=" ")>r.cacheLength&&delete e[t.shift()],e[n]=i}}function st(e){return e[y]=!0,e}function at(e){var t=c.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function ut(e,t,n,r){var i,o,s,a,u,f,d,g,x,w;if((t?t.ownerDocument||t:v)!==c&&l(t),t=t||c,n=n||[],!e||"string"!=typeof e)return n;if(1!==(a=t.nodeType)&&9!==a)return[];if(p&&!r){if(i=Q.exec(e))if(s=i[1]){if(9===a){if(o=t.getElementById(s),!o||!o.parentNode)return n;if(o.id===s)return n.push(o),n}else if(t.ownerDocument&&(o=t.ownerDocument.getElementById(s))&&m(t,o)&&o.id===s)return n.push(o),n}else{if(i[2])return H.apply(n,t.getElementsByTagName(e)),n;if((s=i[3])&&b.getElementsByClassName&&t.getElementsByClassName)return H.apply(n,t.getElementsByClassName(s)),n}if(b.qsa&&(!h||!h.test(e))){if(g=d=y,x=t,w=9===a&&e,1===a&&"object"!==t.nodeName.toLowerCase()){f=gt(e),(d=t.getAttribute("id"))?g=d.replace(et,"\\$&"):t.setAttribute("id",g),g="[id='"+g+"'] ",u=f.length;while(u--)f[u]=g+mt(f[u]);x=X.test(e)&&t.parentNode||t,w=f.join(",")}if(w)try{return H.apply(n,x.querySelectorAll(w)),n}catch(T){}finally{d||t.removeAttribute("id")}}}return kt(e.replace(I,"$1"),t,n,r)}o=ut.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},l=ut.setDocument=function(e){var t=e?e.ownerDocument||e:v;return t!==c&&9===t.nodeType&&t.documentElement?(c=t,f=t.documentElement,p=!o(t),b.getElementsByTagName=at(function(e){return e.appendChild(t.createComment("")),!e.getElementsByTagName("*").length}),b.attributes=at(function(e){return e.className="i",!e.getAttribute("className")}),b.getElementsByClassName=at(function(e){return e.innerHTML="
    ",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),b.sortDetached=at(function(e){return 1&e.compareDocumentPosition(c.createElement("div"))}),b.getById=at(function(e){return f.appendChild(e).id=y,!t.getElementsByName||!t.getElementsByName(y).length}),b.getById?(r.find.ID=function(e,t){if(typeof t.getElementById!==j&&p){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},r.filter.ID=function(e){var t=e.replace(tt,nt);return function(e){return e.getAttribute("id")===t}}):(r.find.ID=function(e,t){if(typeof t.getElementById!==j&&p){var n=t.getElementById(e);return n?n.id===e||typeof n.getAttributeNode!==j&&n.getAttributeNode("id").value===e?[n]:undefined:[]}},r.filter.ID=function(e){var t=e.replace(tt,nt);return function(e){var n=typeof e.getAttributeNode!==j&&e.getAttributeNode("id");return n&&n.value===t}}),r.find.TAG=b.getElementsByTagName?function(e,t){return typeof t.getElementsByTagName!==j?t.getElementsByTagName(e):undefined}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=b.getElementsByClassName&&function(e,t){return typeof t.getElementsByClassName!==j&&p?t.getElementsByClassName(e):undefined},d=[],h=[],(b.qsa=it(t.querySelectorAll))&&(at(function(e){e.innerHTML="",e.querySelectorAll("[selected]").length||h.push("\\["+R+"*(?:value|"+P+")"),e.querySelectorAll(":checked").length||h.push(":checked")}),at(function(e){var t=c.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("t",""),e.querySelectorAll("[t^='']").length&&h.push("[*^$]="+R+"*(?:''|\"\")"),e.querySelectorAll(":enabled").length||h.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),h.push(",.*:")})),(b.matchesSelector=it(g=f.webkitMatchesSelector||f.mozMatchesSelector||f.oMatchesSelector||f.msMatchesSelector))&&at(function(e){b.disconnectedMatch=g.call(e,"div"),g.call(e,"[s!='']:x"),d.push("!=",B)}),h=h.length&&RegExp(h.join("|")),d=d.length&&RegExp(d.join("|")),m=it(f.contains)||f.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},S=f.compareDocumentPosition?function(e,n){if(e===n)return E=!0,0;var r=n.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(n);return r?1&r||!b.sortDetached&&n.compareDocumentPosition(e)===r?e===t||m(v,e)?-1:n===t||m(v,n)?1:u?F.call(u,e)-F.call(u,n):0:4&r?-1:1:e.compareDocumentPosition?-1:1}:function(e,n){var r,i=0,o=e.parentNode,s=n.parentNode,a=[e],l=[n];if(e===n)return E=!0,0;if(!o||!s)return e===t?-1:n===t?1:o?-1:s?1:u?F.call(u,e)-F.call(u,n):0;if(o===s)return lt(e,n);r=e;while(r=r.parentNode)a.unshift(r);r=n;while(r=r.parentNode)l.unshift(r);while(a[i]===l[i])i++;return i?lt(a[i],l[i]):a[i]===v?-1:l[i]===v?1:0},c):c},ut.matches=function(e,t){return ut(e,null,null,t)},ut.matchesSelector=function(e,t){if((e.ownerDocument||e)!==c&&l(e),t=t.replace(U,"='$1']"),!(!b.matchesSelector||!p||d&&d.test(t)||h&&h.test(t)))try{var n=g.call(e,t);if(n||b.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(r){}return ut(t,c,null,[e]).length>0},ut.contains=function(e,t){return(e.ownerDocument||e)!==c&&l(e),m(e,t)},ut.attr=function(e,t){(e.ownerDocument||e)!==c&&l(e);var n=r.attrHandle[t.toLowerCase()],i=n&&n(e,t,!p);return i===undefined?b.attributes||!p?e.getAttribute(t):(i=e.getAttributeNode(t))&&i.specified?i.value:null:i},ut.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},ut.uniqueSort=function(e){var t,n=[],r=0,i=0;if(E=!b.detectDuplicates,u=!b.sortStable&&e.slice(0),e.sort(S),E){while(t=e[i++])t===e[i]&&(r=n.push(i));while(r--)e.splice(n[r],1)}return e};function lt(e,t){var n=t&&e,r=n&&(~t.sourceIndex||D)-(~e.sourceIndex||D);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function ct(e,t,n){var r;return n?undefined:(r=e.getAttributeNode(t))&&r.specified?r.value:e[t]===!0?t.toLowerCase():null}function ft(e,t,n){var r;return n?undefined:r=e.getAttribute(t,"type"===t.toLowerCase()?1:2)}function pt(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function ht(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function dt(e){return st(function(t){return t=+t,st(function(n,r){var i,o=e([],n.length,t),s=o.length;while(s--)n[i=o[s]]&&(n[i]=!(r[i]=n[i]))})})}i=ut.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else for(;t=e[r];r++)n+=i(t);return n},r=ut.selectors={cacheLength:50,createPseudo:st,match:G,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(tt,nt),e[3]=(e[4]||e[5]||"").replace(tt,nt),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||ut.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&ut.error(e[0]),e},PSEUDO:function(e){var t,n=!e[5]&&e[2];return G.CHILD.test(e[0])?null:(e[4]?e[2]=e[4]:n&&Y.test(n)&&(t=gt(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(tt,nt).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=C[e+" "];return t||(t=RegExp("(^|"+R+")"+e+"("+R+"|$)"))&&C(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==j&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=ut.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),s="last"!==e.slice(-4),a="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,p,h,d,g=o!==s?"nextSibling":"previousSibling",m=t.parentNode,v=a&&t.nodeName.toLowerCase(),x=!u&&!a;if(m){if(o){while(g){f=t;while(f=f[g])if(a?f.nodeName.toLowerCase()===v:1===f.nodeType)return!1;d=g="only"===e&&!d&&"nextSibling"}return!0}if(d=[s?m.firstChild:m.lastChild],s&&x){c=m[y]||(m[y]={}),l=c[e]||[],h=l[0]===w&&l[1],p=l[0]===w&&l[2],f=h&&m.childNodes[h];while(f=++h&&f&&f[g]||(p=h=0)||d.pop())if(1===f.nodeType&&++p&&f===t){c[e]=[w,h,p];break}}else if(x&&(l=(t[y]||(t[y]={}))[e])&&l[0]===w)p=l[1];else while(f=++h&&f&&f[g]||(p=h=0)||d.pop())if((a?f.nodeName.toLowerCase()===v:1===f.nodeType)&&++p&&(x&&((f[y]||(f[y]={}))[e]=[w,p]),f===t))break;return p-=i,p===r||0===p%r&&p/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||ut.error("unsupported pseudo: "+e);return i[y]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?st(function(e,n){var r,o=i(e,t),s=o.length;while(s--)r=F.call(e,o[s]),e[r]=!(n[r]=o[s])}):function(e){return i(e,0,n)}):i}},pseudos:{not:st(function(e){var t=[],n=[],r=s(e.replace(I,"$1"));return r[y]?st(function(e,t,n,i){var o,s=r(e,null,i,[]),a=e.length;while(a--)(o=s[a])&&(e[a]=!(t[a]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:st(function(e){return function(t){return ut(e,t).length>0}}),contains:st(function(e){return function(t){return(t.textContent||t.innerText||i(t)).indexOf(e)>-1}}),lang:st(function(e){return V.test(e||"")||ut.error("unsupported lang: "+e),e=e.replace(tt,nt).toLowerCase(),function(t){var n;do if(n=p?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===f},focus:function(e){return e===c.activeElement&&(!c.hasFocus||c.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return Z.test(e.nodeName)},input:function(e){return K.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:dt(function(){return[0]}),last:dt(function(e,t){return[t-1]}),eq:dt(function(e,t,n){return[0>n?n+t:n]}),even:dt(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:dt(function(e,t){var n=1;for(;t>n;n+=2)e.push(n);return e}),lt:dt(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:dt(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}};for(t in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})r.pseudos[t]=pt(t);for(t in{submit:!0,reset:!0})r.pseudos[t]=ht(t);function gt(e,t){var n,i,o,s,a,u,l,c=k[e+" "];if(c)return t?0:c.slice(0);a=e,u=[],l=r.preFilter;while(a){(!n||(i=z.exec(a)))&&(i&&(a=a.slice(i[0].length)||a),u.push(o=[])),n=!1,(i=_.exec(a))&&(n=i.shift(),o.push({value:n,type:i[0].replace(I," ")}),a=a.slice(n.length));for(s in r.filter)!(i=G[s].exec(a))||l[s]&&!(i=l[s](i))||(n=i.shift(),o.push({value:n,type:s,matches:i}),a=a.slice(n.length));if(!n)break}return t?a.length:a?ut.error(e):k(e,u).slice(0)}function mt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function yt(e,t,r){var i=t.dir,o=r&&"parentNode"===i,s=T++;return t.first?function(t,n,r){while(t=t[i])if(1===t.nodeType||o)return e(t,n,r)}:function(t,r,a){var u,l,c,f=w+" "+s;if(a){while(t=t[i])if((1===t.nodeType||o)&&e(t,r,a))return!0}else while(t=t[i])if(1===t.nodeType||o)if(c=t[y]||(t[y]={}),(l=c[i])&&l[0]===f){if((u=l[1])===!0||u===n)return u===!0}else if(l=c[i]=[f],l[1]=e(t,r,a)||n,l[1]===!0)return!0}}function vt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function xt(e,t,n,r,i){var o,s=[],a=0,u=e.length,l=null!=t;for(;u>a;a++)(o=e[a])&&(!n||n(o,r,i))&&(s.push(o),l&&t.push(a));return s}function bt(e,t,n,r,i,o){return r&&!r[y]&&(r=bt(r)),i&&!i[y]&&(i=bt(i,o)),st(function(o,s,a,u){var l,c,f,p=[],h=[],d=s.length,g=o||Ct(t||"*",a.nodeType?[a]:a,[]),m=!e||!o&&t?g:xt(g,p,e,a,u),y=n?i||(o?e:d||r)?[]:s:m;if(n&&n(m,y,a,u),r){l=xt(y,h),r(l,[],a,u),c=l.length;while(c--)(f=l[c])&&(y[h[c]]=!(m[h[c]]=f))}if(o){if(i||e){if(i){l=[],c=y.length;while(c--)(f=y[c])&&l.push(m[c]=f);i(null,y=[],l,u)}c=y.length;while(c--)(f=y[c])&&(l=i?F.call(o,f):p[c])>-1&&(o[l]=!(s[l]=f))}}else y=xt(y===s?y.splice(d,y.length):y),i?i(null,s,y,u):H.apply(s,y)})}function wt(e){var t,n,i,o=e.length,s=r.relative[e[0].type],u=s||r.relative[" "],l=s?1:0,c=yt(function(e){return e===t},u,!0),f=yt(function(e){return F.call(t,e)>-1},u,!0),p=[function(e,n,r){return!s&&(r||n!==a)||((t=n).nodeType?c(e,n,r):f(e,n,r))}];for(;o>l;l++)if(n=r.relative[e[l].type])p=[yt(vt(p),n)];else{if(n=r.filter[e[l].type].apply(null,e[l].matches),n[y]){for(i=++l;o>i;i++)if(r.relative[e[i].type])break;return bt(l>1&&vt(p),l>1&&mt(e.slice(0,l-1)).replace(I,"$1"),n,i>l&&wt(e.slice(l,i)),o>i&&wt(e=e.slice(i)),o>i&&mt(e))}p.push(n)}return vt(p)}function Tt(e,t){var i=0,o=t.length>0,s=e.length>0,u=function(u,l,f,p,h){var d,g,m,y=[],v=0,x="0",b=u&&[],T=null!=h,C=a,k=u||s&&r.find.TAG("*",h&&l.parentNode||l),N=w+=null==C?1:Math.random()||.1;for(T&&(a=l!==c&&l,n=i);null!=(d=k[x]);x++){if(s&&d){g=0;while(m=e[g++])if(m(d,l,f)){p.push(d);break}T&&(w=N,n=++i)}o&&((d=!m&&d)&&v--,u&&b.push(d))}if(v+=x,o&&x!==v){g=0;while(m=t[g++])m(b,y,l,f);if(u){if(v>0)while(x--)b[x]||y[x]||(y[x]=L.call(p));y=xt(y)}H.apply(p,y),T&&!u&&y.length>0&&v+t.length>1&&ut.uniqueSort(p)}return T&&(w=N,a=C),b};return o?st(u):u}s=ut.compile=function(e,t){var n,r=[],i=[],o=N[e+" "];if(!o){t||(t=gt(e)),n=t.length;while(n--)o=wt(t[n]),o[y]?r.push(o):i.push(o);o=N(e,Tt(i,r))}return o};function Ct(e,t,n){var r=0,i=t.length;for(;i>r;r++)ut(e,t[r],n);return n}function kt(e,t,n,i){var o,a,u,l,c,f=gt(e);if(!i&&1===f.length){if(a=f[0]=f[0].slice(0),a.length>2&&"ID"===(u=a[0]).type&&9===t.nodeType&&p&&r.relative[a[1].type]){if(t=(r.find.ID(u.matches[0].replace(tt,nt),t)||[])[0],!t)return n;e=e.slice(a.shift().value.length)}o=G.needsContext.test(e)?0:a.length;while(o--){if(u=a[o],r.relative[l=u.type])break;if((c=r.find[l])&&(i=c(u.matches[0].replace(tt,nt),X.test(a[0].type)&&t.parentNode||t))){if(a.splice(o,1),e=i.length&&mt(a),!e)return H.apply(n,i),n;break}}}return s(e,f)(i,t,!p,n,X.test(e)),n}r.pseudos.nth=r.pseudos.eq;function Nt(){}Nt.prototype=r.filters=r.pseudos,r.setFilters=new Nt,b.sortStable=y.split("").sort(S).join("")===y,l(),[0,0].sort(S),b.detectDuplicates=E,at(function(e){if(e.innerHTML="","#"!==e.firstChild.getAttribute("href")){var t="type|href|height|width".split("|"),n=t.length;while(n--)r.attrHandle[t[n]]=ft}}),at(function(e){if(null!=e.getAttribute("disabled")){var t=P.split("|"),n=t.length;while(n--)r.attrHandle[t[n]]=ct}}),x.find=ut,x.expr=ut.selectors,x.expr[":"]=x.expr.pseudos,x.unique=ut.uniqueSort,x.text=ut.getText,x.isXMLDoc=ut.isXML,x.contains=ut.contains}(e);var D={};function A(e){var t=D[e]={};return x.each(e.match(w)||[],function(e,n){t[n]=!0}),t}x.Callbacks=function(e){e="string"==typeof e?D[e]||A(e):x.extend({},e);var t,n,r,i,o,s,a=[],u=!e.once&&[],l=function(f){for(t=e.memory&&f,n=!0,s=i||0,i=0,o=a.length,r=!0;a&&o>s;s++)if(a[s].apply(f[0],f[1])===!1&&e.stopOnFalse){t=!1;break}r=!1,a&&(u?u.length&&l(u.shift()):t?a=[]:c.disable())},c={add:function(){if(a){var n=a.length;(function s(t){x.each(t,function(t,n){var r=x.type(n);"function"===r?e.unique&&c.has(n)||a.push(n):n&&n.length&&"string"!==r&&s(n)})})(arguments),r?o=a.length:t&&(i=n,l(t))}return this},remove:function(){return a&&x.each(arguments,function(e,t){var n;while((n=x.inArray(t,a,n))>-1)a.splice(n,1),r&&(o>=n&&o--,s>=n&&s--)}),this},has:function(e){return e?x.inArray(e,a)>-1:!(!a||!a.length)},empty:function(){return a=[],o=0,this},disable:function(){return a=u=t=undefined,this},disabled:function(){return!a},lock:function(){return u=undefined,t||c.disable(),this},locked:function(){return!u},fireWith:function(e,t){return t=t||[],t=[e,t.slice?t.slice():t],!a||n&&!u||(r?u.push(t):l(t)),this},fire:function(){return c.fireWith(this,arguments),this},fired:function(){return!!n}};return c},x.extend({Deferred:function(e){var t=[["resolve","done",x.Callbacks("once memory"),"resolved"],["reject","fail",x.Callbacks("once memory"),"rejected"],["notify","progress",x.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return x.Deferred(function(n){x.each(t,function(t,o){var s=o[0],a=x.isFunction(e[t])&&e[t];i[o[1]](function(){var e=a&&a.apply(this,arguments);e&&x.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[s+"With"](this===r?n.promise():this,a?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?x.extend(e,r):r}},i={};return r.pipe=r.then,x.each(t,function(e,o){var s=o[2],a=o[3];r[o[1]]=s.add,a&&s.add(function(){n=a},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=s.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=d.call(arguments),r=n.length,i=1!==r||e&&x.isFunction(e.promise)?r:0,o=1===i?e:x.Deferred(),s=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?d.call(arguments):r,n===a?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},a,u,l;if(r>1)for(a=Array(r),u=Array(r),l=Array(r);r>t;t++)n[t]&&x.isFunction(n[t].promise)?n[t].promise().done(s(t,l,n)).fail(o.reject).progress(s(t,u,a)):--i;return i||o.resolveWith(l,n),o.promise()}}),x.support=function(t){var n=o.createElement("input"),r=o.createDocumentFragment(),i=o.createElement("div"),s=o.createElement("select"),a=s.appendChild(o.createElement("option"));return n.type?(n.type="checkbox",t.checkOn=""!==n.value,t.optSelected=a.selected,t.reliableMarginRight=!0,t.boxSizingReliable=!0,t.pixelPosition=!1,n.checked=!0,t.noCloneChecked=n.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!a.disabled,n=o.createElement("input"),n.value="t",n.type="radio",t.radioValue="t"===n.value,n.setAttribute("checked","t"),n.setAttribute("name","t"),r.appendChild(n),t.checkClone=r.cloneNode(!0).cloneNode(!0).lastChild.checked,t.focusinBubbles="onfocusin"in e,i.style.backgroundClip="content-box",i.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===i.style.backgroundClip,x(function(){var n,r,s="padding:0;margin:0;border:0;display:block;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box",a=o.getElementsByTagName("body")[0];a&&(n=o.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",a.appendChild(n).appendChild(i),i.innerHTML="",i.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%",x.swap(a,null!=a.style.zoom?{zoom:1}:{},function(){t.boxSizing=4===i.offsetWidth}),e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(i,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(i,null)||{width:"4px"}).width,r=i.appendChild(o.createElement("div")),r.style.cssText=i.style.cssText=s,r.style.marginRight=r.style.width="0",i.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),a.removeChild(n))}),t):t}({});var L,q,H=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,O=/([A-Z])/g;function F(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=x.expando+Math.random()}F.uid=1,F.accepts=function(e){return e.nodeType?1===e.nodeType||9===e.nodeType:!0},F.prototype={key:function(e){if(!F.accepts(e))return 0;var t={},n=e[this.expando];if(!n){n=F.uid++;try{t[this.expando]={value:n},Object.defineProperties(e,t)}catch(r){t[this.expando]=n,x.extend(e,t)}}return this.cache[n]||(this.cache[n]={}),n},set:function(e,t,n){var r,i=this.key(e),o=this.cache[i];if("string"==typeof t)o[t]=n;else if(x.isEmptyObject(o))this.cache[i]=t;else for(r in t)o[r]=t[r]},get:function(e,t){var n=this.cache[this.key(e)];return t===undefined?n:n[t]},access:function(e,t,n){return t===undefined||t&&"string"==typeof t&&n===undefined?this.get(e,t):(this.set(e,t,n),n!==undefined?n:t)},remove:function(e,t){var n,r,i=this.key(e),o=this.cache[i];if(t===undefined)this.cache[i]={};else{x.isArray(t)?r=t.concat(t.map(x.camelCase)):t in o?r=[t]:(r=x.camelCase(t),r=r in o?[r]:r.match(w)||[]),n=r.length;while(n--)delete o[r[n]]}},hasData:function(e){return!x.isEmptyObject(this.cache[e[this.expando]]||{})},discard:function(e){delete this.cache[this.key(e)]}},L=new F,q=new F,x.extend({acceptData:F.accepts,hasData:function(e){return L.hasData(e)||q.hasData(e)},data:function(e,t,n){return L.access(e,t,n)},removeData:function(e,t){L.remove(e,t)},_data:function(e,t,n){return q.access(e,t,n)},_removeData:function(e,t){q.remove(e,t)}}),x.fn.extend({data:function(e,t){var n,r,i=this[0],o=0,s=null;if(e===undefined){if(this.length&&(s=L.get(i),1===i.nodeType&&!q.get(i,"hasDataAttrs"))){for(n=i.attributes;n.length>o;o++)r=n[o].name,0===r.indexOf("data-")&&(r=x.camelCase(r.substring(5)),P(i,r,s[r]));q.set(i,"hasDataAttrs",!0)}return s}return"object"==typeof e?this.each(function(){L.set(this,e)}):x.access(this,function(t){var n,r=x.camelCase(e);if(i&&t===undefined){if(n=L.get(i,e),n!==undefined)return n;if(n=L.get(i,r),n!==undefined)return n;if(n=P(i,r,undefined),n!==undefined)return n}else this.each(function(){var n=L.get(this,r);L.set(this,r,t),-1!==e.indexOf("-")&&n!==undefined&&L.set(this,e,t)})},null,t,arguments.length>1,null,!0)},removeData:function(e){return this.each(function(){L.remove(this,e)})}});function P(e,t,n){var r;if(n===undefined&&1===e.nodeType)if(r="data-"+t.replace(O,"-$1").toLowerCase(),n=e.getAttribute(r),"string"==typeof n){try{n="true"===n?!0:"false"===n?!1:"null"===n?null:+n+""===n?+n:H.test(n)?JSON.parse(n):n}catch(i){}L.set(e,t,n)}else n=undefined;return n}x.extend({queue:function(e,t,n){var r;return e?(t=(t||"fx")+"queue",r=q.get(e,t),n&&(!r||x.isArray(n)?r=q.access(e,t,x.makeArray(n)):r.push(n)),r||[]):undefined},dequeue:function(e,t){t=t||"fx";var n=x.queue(e,t),r=n.length,i=n.shift(),o=x._queueHooks(e,t),s=function(){x.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),o.cur=i,i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,s,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return q.get(e,n)||q.access(e,n,{empty:x.Callbacks("once memory").add(function(){q.remove(e,[t+"queue",n])})})}}),x.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),n>arguments.length?x.queue(this[0],e):t===undefined?this:this.each(function(){var n=x.queue(this,e,t); +x._queueHooks(this,e),"fx"===e&&"inprogress"!==n[0]&&x.dequeue(this,e)})},dequeue:function(e){return this.each(function(){x.dequeue(this,e)})},delay:function(e,t){return e=x.fx?x.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=x.Deferred(),o=this,s=this.length,a=function(){--r||i.resolveWith(o,[o])};"string"!=typeof e&&(t=e,e=undefined),e=e||"fx";while(s--)n=q.get(o[s],e+"queueHooks"),n&&n.empty&&(r++,n.empty.add(a));return a(),i.promise(t)}});var R,M,W=/[\t\r\n]/g,$=/\r/g,B=/^(?:input|select|textarea|button)$/i;x.fn.extend({attr:function(e,t){return x.access(this,x.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){x.removeAttr(this,e)})},prop:function(e,t){return x.access(this,x.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[x.propFix[e]||e]})},addClass:function(e){var t,n,r,i,o,s=0,a=this.length,u="string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).addClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(w)||[];a>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(W," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=x.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,s=0,a=this.length,u=0===arguments.length||"string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).removeClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(w)||[];a>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(W," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?x.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e,i="boolean"==typeof t;return x.isFunction(e)?this.each(function(n){x(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var o,s=0,a=x(this),u=t,l=e.match(w)||[];while(o=l[s++])u=i?u:!a.hasClass(o),a[u?"addClass":"removeClass"](o)}else(n===r||"boolean"===n)&&(this.className&&q.set(this,"__className__",this.className),this.className=this.className||e===!1?"":q.get(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(W," ").indexOf(t)>=0)return!0;return!1},val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=x.isFunction(e),this.each(function(n){var i,o=x(this);1===this.nodeType&&(i=r?e.call(this,n,o.val()):e,null==i?i="":"number"==typeof i?i+="":x.isArray(i)&&(i=x.map(i,function(e){return null==e?"":e+""})),t=x.valHooks[this.type]||x.valHooks[this.nodeName.toLowerCase()],t&&"set"in t&&t.set(this,i,"value")!==undefined||(this.value=i))});if(i)return t=x.valHooks[i.type]||x.valHooks[i.nodeName.toLowerCase()],t&&"get"in t&&(n=t.get(i,"value"))!==undefined?n:(n=i.value,"string"==typeof n?n.replace($,""):null==n?"":n)}}}),x.extend({valHooks:{option:{get:function(e){var t=e.attributes.value;return!t||t.specified?e.value:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,s=o?null:[],a=o?i+1:r.length,u=0>i?a:o?i:0;for(;a>u;u++)if(n=r[u],!(!n.selected&&u!==i||(x.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&x.nodeName(n.parentNode,"optgroup"))){if(t=x(n).val(),o)return t;s.push(t)}return s},set:function(e,t){var n,r,i=e.options,o=x.makeArray(t),s=i.length;while(s--)r=i[s],(r.selected=x.inArray(x(r).val(),o)>=0)&&(n=!0);return n||(e.selectedIndex=-1),o}}},attr:function(e,t,n){var i,o,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return typeof e.getAttribute===r?x.prop(e,t,n):(1===s&&x.isXMLDoc(e)||(t=t.toLowerCase(),i=x.attrHooks[t]||(x.expr.match.boolean.test(t)?M:R)),n===undefined?i&&"get"in i&&null!==(o=i.get(e,t))?o:(o=x.find.attr(e,t),null==o?undefined:o):null!==n?i&&"set"in i&&(o=i.set(e,n,t))!==undefined?o:(e.setAttribute(t,n+""),n):(x.removeAttr(e,t),undefined))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(w);if(o&&1===e.nodeType)while(n=o[i++])r=x.propFix[n]||n,x.expr.match.boolean.test(n)&&(e[r]=!1),e.removeAttribute(n)},attrHooks:{type:{set:function(e,t){if(!x.support.radioValue&&"radio"===t&&x.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{"for":"htmlFor","class":"className"},prop:function(e,t,n){var r,i,o,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return o=1!==s||!x.isXMLDoc(e),o&&(t=x.propFix[t]||t,i=x.propHooks[t]),n!==undefined?i&&"set"in i&&(r=i.set(e,n,t))!==undefined?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){return e.hasAttribute("tabindex")||B.test(e.nodeName)||e.href?e.tabIndex:-1}}}}),M={set:function(e,t,n){return t===!1?x.removeAttr(e,n):e.setAttribute(n,n),n}},x.each(x.expr.match.boolean.source.match(/\w+/g),function(e,t){var n=x.expr.attrHandle[t]||x.find.attr;x.expr.attrHandle[t]=function(e,t,r){var i=x.expr.attrHandle[t],o=r?undefined:(x.expr.attrHandle[t]=undefined)!=n(e,t,r)?t.toLowerCase():null;return x.expr.attrHandle[t]=i,o}}),x.support.optSelected||(x.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null}}),x.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){x.propFix[this.toLowerCase()]=this}),x.each(["radio","checkbox"],function(){x.valHooks[this]={set:function(e,t){return x.isArray(t)?e.checked=x.inArray(x(e).val(),t)>=0:undefined}},x.support.checkOn||(x.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var I=/^key/,z=/^(?:mouse|contextmenu)|click/,_=/^(?:focusinfocus|focusoutblur)$/,X=/^([^.]*)(?:\.(.+)|)$/;function U(){return!0}function Y(){return!1}function V(){try{return o.activeElement}catch(e){}}x.event={global:{},add:function(e,t,n,i,o){var s,a,u,l,c,f,p,h,d,g,m,y=q.get(e);if(y){n.handler&&(s=n,n=s.handler,o=s.selector),n.guid||(n.guid=x.guid++),(l=y.events)||(l=y.events={}),(a=y.handle)||(a=y.handle=function(e){return typeof x===r||e&&x.event.triggered===e.type?undefined:x.event.dispatch.apply(a.elem,arguments)},a.elem=e),t=(t||"").match(w)||[""],c=t.length;while(c--)u=X.exec(t[c])||[],d=m=u[1],g=(u[2]||"").split(".").sort(),d&&(p=x.event.special[d]||{},d=(o?p.delegateType:p.bindType)||d,p=x.event.special[d]||{},f=x.extend({type:d,origType:m,data:i,handler:n,guid:n.guid,selector:o,needsContext:o&&x.expr.match.needsContext.test(o),namespace:g.join(".")},s),(h=l[d])||(h=l[d]=[],h.delegateCount=0,p.setup&&p.setup.call(e,i,g,a)!==!1||e.addEventListener&&e.addEventListener(d,a,!1)),p.add&&(p.add.call(e,f),f.handler.guid||(f.handler.guid=n.guid)),o?h.splice(h.delegateCount++,0,f):h.push(f),x.event.global[d]=!0);e=null}},remove:function(e,t,n,r,i){var o,s,a,u,l,c,f,p,h,d,g,m=q.hasData(e)&&q.get(e);if(m&&(u=m.events)){t=(t||"").match(w)||[""],l=t.length;while(l--)if(a=X.exec(t[l])||[],h=g=a[1],d=(a[2]||"").split(".").sort(),h){f=x.event.special[h]||{},h=(r?f.delegateType:f.bindType)||h,p=u[h]||[],a=a[2]&&RegExp("(^|\\.)"+d.join("\\.(?:.*\\.|)")+"(\\.|$)"),s=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||a&&!a.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));s&&!p.length&&(f.teardown&&f.teardown.call(e,d,m.handle)!==!1||x.removeEvent(e,h,m.handle),delete u[h])}else for(h in u)x.event.remove(e,h+t[l],n,r,!0);x.isEmptyObject(u)&&(delete m.handle,q.remove(e,"events"))}},trigger:function(t,n,r,i){var s,a,u,l,c,f,p,h=[r||o],d=y.call(t,"type")?t.type:t,g=y.call(t,"namespace")?t.namespace.split("."):[];if(a=u=r=r||o,3!==r.nodeType&&8!==r.nodeType&&!_.test(d+x.event.triggered)&&(d.indexOf(".")>=0&&(g=d.split("."),d=g.shift(),g.sort()),c=0>d.indexOf(":")&&"on"+d,t=t[x.expando]?t:new x.Event(d,"object"==typeof t&&t),t.isTrigger=i?2:3,t.namespace=g.join("."),t.namespace_re=t.namespace?RegExp("(^|\\.)"+g.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=undefined,t.target||(t.target=r),n=null==n?[t]:x.makeArray(n,[t]),p=x.event.special[d]||{},i||!p.trigger||p.trigger.apply(r,n)!==!1)){if(!i&&!p.noBubble&&!x.isWindow(r)){for(l=p.delegateType||d,_.test(l+d)||(a=a.parentNode);a;a=a.parentNode)h.push(a),u=a;u===(r.ownerDocument||o)&&h.push(u.defaultView||u.parentWindow||e)}s=0;while((a=h[s++])&&!t.isPropagationStopped())t.type=s>1?l:p.bindType||d,f=(q.get(a,"events")||{})[t.type]&&q.get(a,"handle"),f&&f.apply(a,n),f=c&&a[c],f&&x.acceptData(a)&&f.apply&&f.apply(a,n)===!1&&t.preventDefault();return t.type=d,i||t.isDefaultPrevented()||p._default&&p._default.apply(h.pop(),n)!==!1||!x.acceptData(r)||c&&x.isFunction(r[d])&&!x.isWindow(r)&&(u=r[c],u&&(r[c]=null),x.event.triggered=d,r[d](),x.event.triggered=undefined,u&&(r[c]=u)),t.result}},dispatch:function(e){e=x.event.fix(e);var t,n,r,i,o,s=[],a=d.call(arguments),u=(q.get(this,"events")||{})[e.type]||[],l=x.event.special[e.type]||{};if(a[0]=e,e.delegateTarget=this,!l.preDispatch||l.preDispatch.call(this,e)!==!1){s=x.event.handlers.call(this,e,u),t=0;while((i=s[t++])&&!e.isPropagationStopped()){e.currentTarget=i.elem,n=0;while((o=i.handlers[n++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(o.namespace))&&(e.handleObj=o,e.data=o.data,r=((x.event.special[o.origType]||{}).handle||o.handler).apply(i.elem,a),r!==undefined&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return l.postDispatch&&l.postDispatch.call(this,e),e.result}},handlers:function(e,t){var n,r,i,o,s=[],a=t.delegateCount,u=e.target;if(a&&u.nodeType&&(!e.button||"click"!==e.type))for(;u!==this;u=u.parentNode||this)if(u.disabled!==!0||"click"!==e.type){for(r=[],n=0;a>n;n++)o=t[n],i=o.selector+" ",r[i]===undefined&&(r[i]=o.needsContext?x(i,this).index(u)>=0:x.find(i,this,null,[u]).length),r[i]&&r.push(o);r.length&&s.push({elem:u,handlers:r})}return t.length>a&&s.push({elem:this,handlers:t.slice(a)}),s},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,t){var n,r,i,s=t.button;return null==e.pageX&&null!=t.clientX&&(n=e.target.ownerDocument||o,r=n.documentElement,i=n.body,e.pageX=t.clientX+(r&&r.scrollLeft||i&&i.scrollLeft||0)-(r&&r.clientLeft||i&&i.clientLeft||0),e.pageY=t.clientY+(r&&r.scrollTop||i&&i.scrollTop||0)-(r&&r.clientTop||i&&i.clientTop||0)),e.which||s===undefined||(e.which=1&s?1:2&s?3:4&s?2:0),e}},fix:function(e){if(e[x.expando])return e;var t,n,r,i=e.type,o=e,s=this.fixHooks[i];s||(this.fixHooks[i]=s=z.test(i)?this.mouseHooks:I.test(i)?this.keyHooks:{}),r=s.props?this.props.concat(s.props):this.props,e=new x.Event(o),t=r.length;while(t--)n=r[t],e[n]=o[n];return 3===e.target.nodeType&&(e.target=e.target.parentNode),s.filter?s.filter(e,o):e},special:{load:{noBubble:!0},focus:{trigger:function(){return this!==V()&&this.focus?(this.focus(),!1):undefined},delegateType:"focusin"},blur:{trigger:function(){return this===V()&&this.blur?(this.blur(),!1):undefined},delegateType:"focusout"},click:{trigger:function(){return"checkbox"===this.type&&this.click&&x.nodeName(this,"input")?(this.click(),!1):undefined},_default:function(e){return x.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){e.result!==undefined&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=x.extend(new x.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?x.event.trigger(i,null,t):x.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},x.removeEvent=function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)},x.Event=function(e,t){return this instanceof x.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.getPreventDefault&&e.getPreventDefault()?U:Y):this.type=e,t&&x.extend(this,t),this.timeStamp=e&&e.timeStamp||x.now(),this[x.expando]=!0,undefined):new x.Event(e,t)},x.Event.prototype={isDefaultPrevented:Y,isPropagationStopped:Y,isImmediatePropagationStopped:Y,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=U,e&&e.preventDefault&&e.preventDefault()},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=U,e&&e.stopPropagation&&e.stopPropagation()},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=U,this.stopPropagation()}},x.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){x.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!x.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),x.support.focusinBubbles||x.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){x.event.simulate(t,e.target,x.event.fix(e),!0)};x.event.special[t]={setup:function(){0===n++&&o.addEventListener(e,r,!0)},teardown:function(){0===--n&&o.removeEventListener(e,r,!0)}}}),x.fn.extend({on:function(e,t,n,r,i){var o,s;if("object"==typeof e){"string"!=typeof t&&(n=n||t,t=undefined);for(s in e)this.on(s,t,n,e[s],i);return this}if(null==n&&null==r?(r=t,n=t=undefined):null==r&&("string"==typeof t?(r=n,n=undefined):(r=n,n=t,t=undefined)),r===!1)r=Y;else if(!r)return this;return 1===i&&(o=r,r=function(e){return x().off(e),o.apply(this,arguments)},r.guid=o.guid||(o.guid=x.guid++)),this.each(function(){x.event.add(this,e,r,n,t)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,x(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return(t===!1||"function"==typeof t)&&(n=t,t=undefined),n===!1&&(n=Y),this.each(function(){x.event.remove(this,e,n,t)})},trigger:function(e,t){return this.each(function(){x.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];return n?x.event.trigger(e,t,n,!0):undefined}});var G=/^.[^:#\[\.,]*$/,J=x.expr.match.needsContext,Q={children:!0,contents:!0,next:!0,prev:!0};x.fn.extend({find:function(e){var t,n,r,i=this.length;if("string"!=typeof e)return t=this,this.pushStack(x(e).filter(function(){for(r=0;i>r;r++)if(x.contains(t[r],this))return!0}));for(n=[],r=0;i>r;r++)x.find(e,this[r],n);return n=this.pushStack(i>1?x.unique(n):n),n.selector=(this.selector?this.selector+" ":"")+e,n},has:function(e){var t=x(e,this),n=t.length;return this.filter(function(){var e=0;for(;n>e;e++)if(x.contains(this,t[e]))return!0})},not:function(e){return this.pushStack(Z(this,e||[],!0))},filter:function(e){return this.pushStack(Z(this,e||[],!1))},is:function(e){return!!e&&("string"==typeof e?J.test(e)?x(e,this.context).index(this[0])>=0:x.filter(e,this).length>0:this.filter(e).length>0)},closest:function(e,t){var n,r=0,i=this.length,o=[],s=J.test(e)||"string"!=typeof e?x(e,t||this.context):0;for(;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(11>n.nodeType&&(s?s.index(n)>-1:1===n.nodeType&&x.find.matchesSelector(n,e))){n=o.push(n);break}return this.pushStack(o.length>1?x.unique(o):o)},index:function(e){return e?"string"==typeof e?g.call(x(e),this[0]):g.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?x(e,t):x.makeArray(e&&e.nodeType?[e]:e),r=x.merge(this.get(),n);return this.pushStack(x.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function K(e,t){while((e=e[t])&&1!==e.nodeType);return e}x.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return x.dir(e,"parentNode")},parentsUntil:function(e,t,n){return x.dir(e,"parentNode",n)},next:function(e){return K(e,"nextSibling")},prev:function(e){return K(e,"previousSibling")},nextAll:function(e){return x.dir(e,"nextSibling")},prevAll:function(e){return x.dir(e,"previousSibling")},nextUntil:function(e,t,n){return x.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return x.dir(e,"previousSibling",n)},siblings:function(e){return x.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return x.sibling(e.firstChild)},contents:function(e){return x.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:x.merge([],e.childNodes)}},function(e,t){x.fn[e]=function(n,r){var i=x.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=x.filter(r,i)),this.length>1&&(Q[e]||x.unique(i),"p"===e[0]&&i.reverse()),this.pushStack(i)}}),x.extend({filter:function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?x.find.matchesSelector(r,e)?[r]:[]:x.find.matches(e,x.grep(t,function(e){return 1===e.nodeType}))},dir:function(e,t,n){var r=[],i=n!==undefined;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&x(e).is(n))break;r.push(e)}return r},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function Z(e,t,n){if(x.isFunction(t))return x.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return x.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(G.test(t))return x.filter(t,e,n);t=x.filter(t,e)}return x.grep(e,function(e){return g.call(t,e)>=0!==n})}var et=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,tt=/<([\w:]+)/,nt=/<|&#?\w+;/,rt=/<(?:script|style|link)/i,it=/^(?:checkbox|radio)$/i,ot=/checked\s*(?:[^=]|=\s*.checked.)/i,st=/^$|\/(?:java|ecma)script/i,at=/^true\/(.*)/,ut=/^\s*\s*$/g,lt={option:[1,""],thead:[1,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};lt.optgroup=lt.option,lt.tbody=lt.tfoot=lt.colgroup=lt.caption=lt.col=lt.thead,lt.th=lt.td,x.fn.extend({text:function(e){return x.access(this,function(e){return e===undefined?x.text(this):this.empty().append((this[0]&&this[0].ownerDocument||o).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=ct(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=ct(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=e?x.filter(e,this):this,i=0;for(;null!=(n=r[i]);i++)t||1!==n.nodeType||x.cleanData(gt(n)),n.parentNode&&(t&&x.contains(n.ownerDocument,n)&&ht(gt(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++)1===e.nodeType&&(x.cleanData(gt(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return x.clone(this,e,t)})},html:function(e){return x.access(this,function(e){var t=this[0]||{},n=0,r=this.length;if(e===undefined&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!rt.test(e)&&!lt[(tt.exec(e)||["",""])[1].toLowerCase()]){e=e.replace(et,"<$1>");try{for(;r>n;n++)t=this[n]||{},1===t.nodeType&&(x.cleanData(gt(t,!1)),t.innerHTML=e);t=0}catch(i){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=x.map(this,function(e){return[e.nextSibling,e.parentNode]}),t=0;return this.domManip(arguments,function(n){var r=e[t++],i=e[t++];i&&(x(this).remove(),i.insertBefore(n,r))},!0),t?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t,n){e=p.apply([],e);var r,i,o,s,a,u,l=0,c=this.length,f=this,h=c-1,d=e[0],g=x.isFunction(d);if(g||!(1>=c||"string"!=typeof d||x.support.checkClone)&&ot.test(d))return this.each(function(r){var i=f.eq(r);g&&(e[0]=d.call(this,r,i.html())),i.domManip(e,t,n)});if(c&&(r=x.buildFragment(e,this[0].ownerDocument,!1,!n&&this),i=r.firstChild,1===r.childNodes.length&&(r=i),i)){for(o=x.map(gt(r,"script"),ft),s=o.length;c>l;l++)a=r,l!==h&&(a=x.clone(a,!0,!0),s&&x.merge(o,gt(a,"script"))),t.call(this[l],a,l);if(s)for(u=o[o.length-1].ownerDocument,x.map(o,pt),l=0;s>l;l++)a=o[l],st.test(a.type||"")&&!q.access(a,"globalEval")&&x.contains(u,a)&&(a.src?x._evalUrl(a.src):x.globalEval(a.textContent.replace(ut,"")))}return this}}),x.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){x.fn[e]=function(e){var n,r=[],i=x(e),o=i.length-1,s=0;for(;o>=s;s++)n=s===o?this:this.clone(!0),x(i[s])[t](n),h.apply(r,n.get());return this.pushStack(r)}}),x.extend({clone:function(e,t,n){var r,i,o,s,a=e.cloneNode(!0),u=x.contains(e.ownerDocument,e);if(!(x.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||x.isXMLDoc(e)))for(s=gt(a),o=gt(e),r=0,i=o.length;i>r;r++)mt(o[r],s[r]);if(t)if(n)for(o=o||gt(e),s=s||gt(a),r=0,i=o.length;i>r;r++)dt(o[r],s[r]);else dt(e,a);return s=gt(a,"script"),s.length>0&&ht(s,!u&>(e,"script")),a},buildFragment:function(e,t,n,r){var i,o,s,a,u,l,c=0,f=e.length,p=t.createDocumentFragment(),h=[];for(;f>c;c++)if(i=e[c],i||0===i)if("object"===x.type(i))x.merge(h,i.nodeType?[i]:i);else if(nt.test(i)){o=o||p.appendChild(t.createElement("div")),s=(tt.exec(i)||["",""])[1].toLowerCase(),a=lt[s]||lt._default,o.innerHTML=a[1]+i.replace(et,"<$1>")+a[2],l=a[0];while(l--)o=o.firstChild;x.merge(h,o.childNodes),o=p.firstChild,o.textContent=""}else h.push(t.createTextNode(i));p.textContent="",c=0;while(i=h[c++])if((!r||-1===x.inArray(i,r))&&(u=x.contains(i.ownerDocument,i),o=gt(p.appendChild(i),"script"),u&&ht(o),n)){l=0;while(i=o[l++])st.test(i.type||"")&&n.push(i)}return p},cleanData:function(e){var t,n,r,i=e.length,o=0,s=x.event.special;for(;i>o;o++){if(n=e[o],x.acceptData(n)&&(t=q.access(n)))for(r in t.events)s[r]?x.event.remove(n,r):x.removeEvent(n,r,t.handle);L.discard(n),q.discard(n)}},_evalUrl:function(e){return x.ajax({url:e,type:"GET",dataType:"text",async:!1,global:!1,success:x.globalEval})}});function ct(e,t){return x.nodeName(e,"table")&&x.nodeName(1===t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function ft(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function pt(e){var t=at.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function ht(e,t){var n=e.length,r=0;for(;n>r;r++)q.set(e[r],"globalEval",!t||q.get(t[r],"globalEval"))}function dt(e,t){var n,r,i,o,s,a,u,l;if(1===t.nodeType){if(q.hasData(e)&&(o=q.access(e),s=x.extend({},o),l=o.events,q.set(t,s),l)){delete s.handle,s.events={};for(i in l)for(n=0,r=l[i].length;r>n;n++)x.event.add(t,i,l[i][n])}L.hasData(e)&&(a=L.access(e),u=x.extend({},a),L.set(t,u))}}function gt(e,t){var n=e.getElementsByTagName?e.getElementsByTagName(t||"*"):e.querySelectorAll?e.querySelectorAll(t||"*"):[];return t===undefined||t&&x.nodeName(e,t)?x.merge([e],n):n}function mt(e,t){var n=t.nodeName.toLowerCase();"input"===n&&it.test(e.type)?t.checked=e.checked:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}x.fn.extend({wrapAll:function(e){var t;return x.isFunction(e)?this.each(function(t){x(this).wrapAll(e.call(this,t))}):(this[0]&&(t=x(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this)},wrapInner:function(e){return x.isFunction(e)?this.each(function(t){x(this).wrapInner(e.call(this,t))}):this.each(function(){var t=x(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=x.isFunction(e);return this.each(function(n){x(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){x.nodeName(this,"body")||x(this).replaceWith(this.childNodes)}).end()}});var yt,vt,xt=/^(none|table(?!-c[ea]).+)/,bt=/^margin/,wt=RegExp("^("+b+")(.*)$","i"),Tt=RegExp("^("+b+")(?!px)[a-z%]+$","i"),Ct=RegExp("^([+-])=("+b+")","i"),kt={BODY:"block"},Nt={position:"absolute",visibility:"hidden",display:"block"},Et={letterSpacing:0,fontWeight:400},St=["Top","Right","Bottom","Left"],jt=["Webkit","O","Moz","ms"];function Dt(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=jt.length;while(i--)if(t=jt[i]+n,t in e)return t;return r}function At(e,t){return e=t||e,"none"===x.css(e,"display")||!x.contains(e.ownerDocument,e)}function Lt(t){return e.getComputedStyle(t,null)}function qt(e,t){var n,r,i,o=[],s=0,a=e.length;for(;a>s;s++)r=e[s],r.style&&(o[s]=q.get(r,"olddisplay"),n=r.style.display,t?(o[s]||"none"!==n||(r.style.display=""),""===r.style.display&&At(r)&&(o[s]=q.access(r,"olddisplay",Pt(r.nodeName)))):o[s]||(i=At(r),(n&&"none"!==n||!i)&&q.set(r,"olddisplay",i?n:x.css(r,"display"))));for(s=0;a>s;s++)r=e[s],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[s]||"":"none"));return e}x.fn.extend({css:function(e,t){return x.access(this,function(e,t,n){var r,i,o={},s=0;if(x.isArray(t)){for(r=Lt(e),i=t.length;i>s;s++)o[t[s]]=x.css(e,t[s],!1,r);return o}return n!==undefined?x.style(e,t,n):x.css(e,t)},e,t,arguments.length>1)},show:function(){return qt(this,!0)},hide:function(){return qt(this)},toggle:function(e){var t="boolean"==typeof e;return this.each(function(){(t?e:At(this))?x(this).show():x(this).hide()})}}),x.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=yt(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,s,a=x.camelCase(t),u=e.style;return t=x.cssProps[a]||(x.cssProps[a]=Dt(u,a)),s=x.cssHooks[t]||x.cssHooks[a],n===undefined?s&&"get"in s&&(i=s.get(e,!1,r))!==undefined?i:u[t]:(o=typeof n,"string"===o&&(i=Ct.exec(n))&&(n=(i[1]+1)*i[2]+parseFloat(x.css(e,t)),o="number"),null==n||"number"===o&&isNaN(n)||("number"!==o||x.cssNumber[a]||(n+="px"),x.support.clearCloneStyle||""!==n||0!==t.indexOf("background")||(u[t]="inherit"),s&&"set"in s&&(n=s.set(e,n,r))===undefined||(u[t]=n)),undefined)}},css:function(e,t,n,r){var i,o,s,a=x.camelCase(t);return t=x.cssProps[a]||(x.cssProps[a]=Dt(e.style,a)),s=x.cssHooks[t]||x.cssHooks[a],s&&"get"in s&&(i=s.get(e,!0,n)),i===undefined&&(i=yt(e,t,r)),"normal"===i&&t in Et&&(i=Et[t]),""===n||n?(o=parseFloat(i),n===!0||x.isNumeric(o)?o||0:i):i}}),yt=function(e,t,n){var r,i,o,s=n||Lt(e),a=s?s.getPropertyValue(t)||s[t]:undefined,u=e.style;return s&&(""!==a||x.contains(e.ownerDocument,e)||(a=x.style(e,t)),Tt.test(a)&&bt.test(t)&&(r=u.width,i=u.minWidth,o=u.maxWidth,u.minWidth=u.maxWidth=u.width=a,a=s.width,u.width=r,u.minWidth=i,u.maxWidth=o)),a};function Ht(e,t,n){var r=wt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function Ot(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:0,s=0;for(;4>o;o+=2)"margin"===n&&(s+=x.css(e,n+St[o],!0,i)),r?("content"===n&&(s-=x.css(e,"padding"+St[o],!0,i)),"margin"!==n&&(s-=x.css(e,"border"+St[o]+"Width",!0,i))):(s+=x.css(e,"padding"+St[o],!0,i),"padding"!==n&&(s+=x.css(e,"border"+St[o]+"Width",!0,i)));return s}function Ft(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=Lt(e),s=x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=yt(e,t,o),(0>i||null==i)&&(i=e.style[t]),Tt.test(i))return i;r=s&&(x.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+Ot(e,t,n||(s?"border":"content"),r,o)+"px"}function Pt(e){var t=o,n=kt[e];return n||(n=Rt(e,t),"none"!==n&&n||(vt=(vt||x("