From 8795a4cb7ac69d99fe5d045d03651cb6ab3cf397 Mon Sep 17 00:00:00 2001 From: Artur Nowak Date: Wed, 9 Jul 2014 14:52:13 +0200 Subject: [PATCH] Initial version of the library Summary: This library generates DOCX document based on the supplied HTML file. Test Plan: Use `test/sample.html` to test it. Reviewers: grzegorzp Reviewed By: grzegorzp Differential Revision: https://phabricator.lab.evidenceprime.com/D525 --- bower.json | 31 +++++ coffeelint.json | 69 ++++++++++ gulpfile.coffee | 69 ++++++++++ package.json | 41 ++++++ src/api.coffee | 9 ++ src/assets/content_types.xml | 8 ++ src/assets/document.xml | 7 + src/assets/document.xml.rels | 5 + src/assets/rels.xml | 6 + src/internal.coffee | 22 ++++ test/index.coffee | 61 +++++++++ test/sample.html | 46 +++++++ test/testbed.html | 22 ++++ test/vendor/Blob.js | 179 ++++++++++++++++++++++++++ test/vendor/FileSaver.js | 241 +++++++++++++++++++++++++++++++++++ test/vendor/mocha.css | 1 + test/vendor/mocha.js | 1 + 17 files changed, 818 insertions(+) create mode 100644 bower.json create mode 100644 coffeelint.json create mode 100644 gulpfile.coffee create mode 100644 package.json create mode 100644 src/api.coffee create mode 100644 src/assets/content_types.xml create mode 100644 src/assets/document.xml create mode 100644 src/assets/document.xml.rels create mode 100644 src/assets/rels.xml create mode 100644 src/internal.coffee create mode 100644 test/index.coffee create mode 100644 test/sample.html create mode 100644 test/testbed.html create mode 100644 test/vendor/Blob.js create mode 100644 test/vendor/FileSaver.js create mode 120000 test/vendor/mocha.css create mode 120000 test/vendor/mocha.js diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..a65f167 --- /dev/null +++ b/bower.json @@ -0,0 +1,31 @@ +{ + "name": "html-docx-js", + "version": "0.1.0", + "description": "Converts HTML documents to DOCX in the browser", + "repository": { + "type": "git", + "url": "git://github.com:evidenceprime/html-docx-js.git" + }, + "main": "dist/html-docx.js", + "moduleType": [ + "amd", + "globals", + "node" + ], + "keywords": [ + "docx", + "browser", + "html" + ], + "authors": [ + "Artur Nowak " + ], + "license": "MIT", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ] +} diff --git a/coffeelint.json b/coffeelint.json new file mode 100644 index 0000000..641a671 --- /dev/null +++ b/coffeelint.json @@ -0,0 +1,69 @@ +{ + "no_tabs": { + "level": "ignore", + "comment": "Checked by other linter" + }, + "no_trailing_whitespace": { + "level": "ignore", + "allowed_in_comments": false, + "comment": "Checked by other linter" + }, + "max_line_length": { + "value": 100, + "level": "ignore", + "comment": "Checked by other linter" + }, + "camel_case_classes": { + "level": "error" + }, + "indentation": { + "value": 2, + "level": "error" + }, + "no_implicit_braces": { + "level": "ignore" + }, + "no_trailing_semicolons": { + "level": "error" + }, + "no_plusplus": { + "level": "ignore" + }, + "no_throwing_strings": { + "level": "error" + }, + "cyclomatic_complexity": { + "value": 10, + "level": "warn" + }, + "no_backticks": { + "level": "error" + }, + "line_endings": { + "level": "ignore", + "value": "unix", + "comment": "Checked by other linter" + }, + "no_implicit_parens": { + "level": "ignore" + }, + "no_empty_param_list": { + "level": "error" + }, + "space_operators": { + "level": "warn" + }, + "duplicate_key": { + "level": "error" + }, + "newlines_after_classes": { + "value": 3, + "level": "ignore" + }, + "no_stand_alone_at": { + "level": "ignore" + }, + "coffeescript_error": { + "level": "error" + } +} diff --git a/gulpfile.coffee b/gulpfile.coffee new file mode 100644 index 0000000..6a04b1e --- /dev/null +++ b/gulpfile.coffee @@ -0,0 +1,69 @@ +gulp = require 'gulp' +vinyl = require 'vinyl-source-stream' +browserify = require 'browserify' +watchify = require 'watchify' +gutil = require 'gulp-util' +prettyHrtime = require 'pretty-hrtime' +notify = require 'gulp-notify' +mocha = require 'gulp-mocha' +mochaPhantomJS = require 'gulp-mocha-phantomjs' + +startTime = null +logger = + start: -> + startTime = process.hrtime() + gutil.log 'Running', gutil.colors.green("'bundle'") + '...' + end: -> + taskTime = process.hrtime startTime + prettyTime = prettyHrtime taskTime + gutil.log 'Finished', gutil.colors.green("'bundle'"), 'in', gutil.colors.magenta(prettyTime) + +handleErrors = -> + notify.onError + title: 'Compile error' + message: '<%= error.message %>' + .apply this, arguments + @emit 'end' + +build = (test) -> + [output, entry, options] = if test + ['tests.js', './test/index', debug: true] + else + ['html-docx.js', './src/api', standalone: 'html-docx'] + + bundleMethod = if global.isWatching then watchify else browserify + bundler = bundleMethod + entries: [entry] + extensions: ['.coffee'] + + bundle = -> + logger.start() + bundler + .bundle options + .on 'error', handleErrors + .pipe vinyl(output) + .pipe gulp.dest('./build') + .on 'end', logger.end + + if global.isWatching + bundler.on 'update', bundle + + bundle() + +testsBundle = './test/index.coffee' + +gulp.task 'setWatch', -> global.isWatching = true +gulp.task 'build', -> build() +gulp.task 'watch', ['setWatch', 'build'] + +gulp.task 'test-node', (growl = false) -> + gulp.src(testsBundle, read: false).pipe mocha {reporter: 'spec', growl} +gulp.task 'test-node-watch', -> + sources = ['src/**', 'test/**'] + gulp.watch sources, ['test-node'] + +gulp.task 'build-test-browserify', -> build(true) +gulp.task 'run-phantomjs', -> gulp.src('test/testbed.html').pipe(mochaPhantomJS reporter: 'spec') +gulp.task 'test-phantomjs', ['build-test-browserify', 'run-phantomjs'] + +gulp.task 'default', ['test-node', 'test-node-watch'] diff --git a/package.json b/package.json new file mode 100644 index 0000000..0db6e69 --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "html-docx.js", + "version": "0.1.0", + "description": "Converts HTML documents to DOCX in the browser", + "main": "src/index.coffee", + "repository": { + "type": "git", + "url": "git://github.com:evidenceprime/html-docx-js.git" + }, + "scripts": { + "test": "gulp test" + }, + "browserify": { + "transform": [ + "coffeeify", + "brfs" + ] + }, + "author": "Artur Nowak ", + "license": "MIT", + "devDependencies": { + "brfs": "^1.1.2", + "browserify": "^4.2.0", + "chai": "^1.9.1", + "coffeeify": "^0.6.0", + "gulp": "^3.8.5", + "gulp-mocha": "^0.4.1", + "gulp-mocha-phantomjs": "^0.3.0", + "gulp-notify": "^1.4.0", + "gulp-util": "^2.2.19", + "mocha": "^1.20.1", + "pretty-hrtime": "^0.2.1", + "sinon": "^1.10.2", + "sinon-chai": "^2.5.0", + "vinyl-source-stream": "^0.1.1", + "watchify": "^0.10.2" + }, + "dependencies": { + "jszip": "^2.3.0" + } +} diff --git a/src/api.coffee b/src/api.coffee new file mode 100644 index 0000000..0371312 --- /dev/null +++ b/src/api.coffee @@ -0,0 +1,9 @@ +JSZip = require 'jszip' +internal = require './internal' +fs = require 'fs' + +module.exports = + asBlob: (html) -> + zip = new JSZip() + internal.addFiles(zip, html) + internal.generateDocument(zip) diff --git a/src/assets/content_types.xml b/src/assets/content_types.xml new file mode 100644 index 0000000..38e9b68 --- /dev/null +++ b/src/assets/content_types.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/src/assets/document.xml b/src/assets/document.xml new file mode 100644 index 0000000..dca5b30 --- /dev/null +++ b/src/assets/document.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/src/assets/document.xml.rels b/src/assets/document.xml.rels new file mode 100644 index 0000000..5ad8035 --- /dev/null +++ b/src/assets/document.xml.rels @@ -0,0 +1,5 @@ + + + + diff --git a/src/assets/rels.xml b/src/assets/rels.xml new file mode 100644 index 0000000..8a7e966 --- /dev/null +++ b/src/assets/rels.xml @@ -0,0 +1,6 @@ + + + + diff --git a/src/internal.coffee b/src/internal.coffee new file mode 100644 index 0000000..26fe72c --- /dev/null +++ b/src/internal.coffee @@ -0,0 +1,22 @@ +fs = require 'fs' + +module.exports = + generateDocument: (zip) -> + buffer = zip.generate(type: 'arraybuffer') + if global.Blob + new Blob [buffer], + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + else if global.Buffer + new Buffer new Uint8Array(buffer) + else + throw new Error "Neither Blob nor Buffer are accessible in this environment. " + + "Consider adding Blob.js shim" + + addFiles: (zip, htmlSource) -> + zip.file '[Content_Types].xml', fs.readFileSync __dirname + '/assets/content_types.xml' + zip.folder('_rels').file '.rels', fs.readFileSync __dirname + '/assets/rels.xml' + zip.folder 'word' + .file 'document.xml', fs.readFileSync __dirname + '/assets/document.xml' + .file 'afchunk.htm', htmlSource + .folder '_rels' + .file 'document.xml.rels', fs.readFileSync __dirname + '/assets/document.xml.rels' diff --git a/test/index.coffee b/test/index.coffee new file mode 100644 index 0000000..f900075 --- /dev/null +++ b/test/index.coffee @@ -0,0 +1,61 @@ +chai = require 'chai' +expect = chai.expect +sinon = require 'sinon' +chai.use require 'sinon-chai' +internal = require '../src/internal' + +describe 'Adding files', -> + beforeEach -> + @data = {} + zip = (data) -> + entry = + file: (name, content) -> + data[name] = content + entry + folder: (name) -> + data[name] = {} + zip data[name] + internal.addFiles zip(@data), 'foobar' + + it 'should add file for embedded content types', -> + expect(@data['[Content_Types].xml']).to.be.defined + content = String(@data['[Content_Types].xml']) + expect(content).to.match /Extension="htm"/ + expect(content).to.match /Extension="xml"/ + expect(content).to.match /Extension="rels"/ + + it 'should add manifest for Word document', -> + expect(@data._rels['.rels']).to.be.defined + content = String(@data._rels['.rels']) + expect(content).to.match /Target="\/word\/document.xml"/ + + it 'should add HTML file with given content', -> + expect(@data.word['afchunk.htm']).to.be.defined + expect(String @data.word['afchunk.htm']).to.equal 'foobar' + + it 'should add Word file with altChunk element', -> + expect(@data.word['document.xml']).to.be.defined + expect(String @data.word['document.xml']).to.match /altChunk r:id="htmlChunk"/ + + it 'should add relationship file to link between Word and HTML files', -> + expect(@data.word._rels['document.xml.rels']).to.be.defined + expect(String @data.word._rels['document.xml.rels']).to + .match /Target="\/word\/afchunk.htm" Id="htmlChunk"/ + +describe 'Generating the document', -> + beforeEach -> + @zip = generate: sinon.stub().returns 'DEADBEEF' + + it 'should retrieve ZIP file as arraybuffer', -> + internal.generateDocument @zip + expect(@zip.generate).to.have.been.calledWith type: 'arraybuffer' + + it 'should return Blob with correct content type if it is available', -> + return unless global.Blob + document = internal.generateDocument @zip + expect(document.type).to.be + .equal 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + + it 'should return Buffer in Node.js environment', -> + return unless global.Buffer + expect(internal.generateDocument @zip).to.be.an.instanceOf Buffer diff --git a/test/sample.html b/test/sample.html new file mode 100644 index 0000000..d76ec64 --- /dev/null +++ b/test/sample.html @@ -0,0 +1,46 @@ + + + + + HTML-DOCX test + + + + + +

Enter/paste your document here:

+ + +
+ + + + diff --git a/test/testbed.html b/test/testbed.html new file mode 100644 index 0000000..ad4f527 --- /dev/null +++ b/test/testbed.html @@ -0,0 +1,22 @@ + + + + + Mocha Test Runner + + + +
+ + + + + + + diff --git a/test/vendor/Blob.js b/test/vendor/Blob.js new file mode 100644 index 0000000..c740cd8 --- /dev/null +++ b/test/vendor/Blob.js @@ -0,0 +1,179 @@ +/* Blob.js + * A Blob implementation. + * 2014-07-01 + * + * By Eli Grey, http://eligrey.com + * By Devin Samarin, https://github.com/eboyjr + * License: X11/MIT + * See https://github.com/eligrey/Blob.js/blob/master/LICENSE.md + */ + +/*global self, unescape */ +/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true, + plusplus: true */ + +/*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */ + +(function (view) { + "use strict"; + + view.URL = view.URL || view.webkitURL; + + if (view.Blob && view.URL) { + try { + new Blob; + return; + } catch (e) {} + } + + // Internally we use a BlobBuilder implementation to base Blob off of + // in order to support older browsers that only have BlobBuilder + var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function(view) { + var + get_class = function(object) { + return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1]; + } + , FakeBlobBuilder = function BlobBuilder() { + this.data = []; + } + , FakeBlob = function Blob(data, type, encoding) { + this.data = data; + this.size = data.length; + this.type = type; + this.encoding = encoding; + } + , FBB_proto = FakeBlobBuilder.prototype + , FB_proto = FakeBlob.prototype + , FileReaderSync = view.FileReaderSync + , FileException = function(type) { + this.code = this[this.name = type]; + } + , file_ex_codes = ( + "NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR " + + "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR" + ).split(" ") + , file_ex_code = file_ex_codes.length + , real_URL = view.URL || view.webkitURL || view + , real_create_object_URL = real_URL.createObjectURL + , real_revoke_object_URL = real_URL.revokeObjectURL + , URL = real_URL + , btoa = view.btoa + , atob = view.atob + + , ArrayBuffer = view.ArrayBuffer + , Uint8Array = view.Uint8Array + ; + FakeBlob.fake = FB_proto.fake = true; + while (file_ex_code--) { + FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1; + } + if (!real_URL.createObjectURL) { + URL = view.URL = {}; + } + URL.createObjectURL = function(blob) { + var + type = blob.type + , data_URI_header + ; + if (type === null) { + type = "application/octet-stream"; + } + if (blob instanceof FakeBlob) { + data_URI_header = "data:" + type; + if (blob.encoding === "base64") { + return data_URI_header + ";base64," + blob.data; + } else if (blob.encoding === "URI") { + return data_URI_header + "," + decodeURIComponent(blob.data); + } if (btoa) { + return data_URI_header + ";base64," + btoa(blob.data); + } else { + return data_URI_header + "," + encodeURIComponent(blob.data); + } + } else if (real_create_object_URL) { + return real_create_object_URL.call(real_URL, blob); + } + }; + URL.revokeObjectURL = function(object_URL) { + if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) { + real_revoke_object_URL.call(real_URL, object_URL); + } + }; + FBB_proto.append = function(data/*, endings*/) { + var bb = this.data; + // decode data to a binary string + if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) { + var + str = "" + , buf = new Uint8Array(data) + , i = 0 + , buf_len = buf.length + ; + for (; i < buf_len; i++) { + str += String.fromCharCode(buf[i]); + } + bb.push(str); + } else if (get_class(data) === "Blob" || get_class(data) === "File") { + if (FileReaderSync) { + var fr = new FileReaderSync; + bb.push(fr.readAsBinaryString(data)); + } else { + // async FileReader won't work as BlobBuilder is sync + throw new FileException("NOT_READABLE_ERR"); + } + } else if (data instanceof FakeBlob) { + if (data.encoding === "base64" && atob) { + bb.push(atob(data.data)); + } else if (data.encoding === "URI") { + bb.push(decodeURIComponent(data.data)); + } else if (data.encoding === "raw") { + bb.push(data.data); + } + } else { + if (typeof data !== "string") { + data += ""; // convert unsupported types to strings + } + // decode UTF-16 to binary string + bb.push(unescape(encodeURIComponent(data))); + } + }; + FBB_proto.getBlob = function(type) { + if (!arguments.length) { + type = null; + } + return new FakeBlob(this.data.join(""), type, "raw"); + }; + FBB_proto.toString = function() { + return "[object BlobBuilder]"; + }; + FB_proto.slice = function(start, end, type) { + var args = arguments.length; + if (args < 3) { + type = null; + } + return new FakeBlob( + this.data.slice(start, args > 1 ? end : this.data.length) + , type + , this.encoding + ); + }; + FB_proto.toString = function() { + return "[object Blob]"; + }; + FB_proto.close = function() { + this.size = 0; + delete this.data; + }; + return FakeBlobBuilder; + }(view)); + + view.Blob = function(blobParts, options) { + var type = options ? (options.type || "") : ""; + var builder = new BlobBuilder(); + if (blobParts) { + for (var i = 0, len = blobParts.length; i < len; i++) { + builder.append(blobParts[i]); + } + } + return builder.getBlob(type); + }; +}(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this)); diff --git a/test/vendor/FileSaver.js b/test/vendor/FileSaver.js new file mode 100644 index 0000000..6095017 --- /dev/null +++ b/test/vendor/FileSaver.js @@ -0,0 +1,241 @@ +/* FileSaver.js + * A saveAs() FileSaver implementation. + * 2014-05-27 + * + * By Eli Grey, http://eligrey.com + * License: X11/MIT + * See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md + */ + +/*global self */ +/*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */ + +/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ + +var saveAs = saveAs + // IE 10+ (native saveAs) + || (typeof navigator !== "undefined" && + navigator.msSaveOrOpenBlob && navigator.msSaveOrOpenBlob.bind(navigator)) + // Everyone else + || (function(view) { + "use strict"; + // IE <10 is explicitly unsupported + if (typeof navigator !== "undefined" && + /MSIE [1-9]\./.test(navigator.userAgent)) { + return; + } + var + doc = view.document + // only get URL when necessary in case Blob.js hasn't overridden it yet + , get_URL = function() { + return view.URL || view.webkitURL || view; + } + , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a") + , can_use_save_link = !view.externalHost && "download" in save_link + , click = function(node) { + var event = doc.createEvent("MouseEvents"); + event.initMouseEvent( + "click", true, false, view, 0, 0, 0, 0, 0 + , false, false, false, false, 0, null + ); + node.dispatchEvent(event); + } + , webkit_req_fs = view.webkitRequestFileSystem + , req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem + , throw_outside = function(ex) { + (view.setImmediate || view.setTimeout)(function() { + throw ex; + }, 0); + } + , force_saveable_type = "application/octet-stream" + , fs_min_size = 0 + , deletion_queue = [] + , process_deletion_queue = function() { + var i = deletion_queue.length; + while (i--) { + var file = deletion_queue[i]; + if (typeof file === "string") { // file is an object URL + get_URL().revokeObjectURL(file); + } else { // file is a File + file.remove(); + } + } + deletion_queue.length = 0; // clear queue + } + , dispatch = function(filesaver, event_types, event) { + event_types = [].concat(event_types); + var i = event_types.length; + while (i--) { + var listener = filesaver["on" + event_types[i]]; + if (typeof listener === "function") { + try { + listener.call(filesaver, event || filesaver); + } catch (ex) { + throw_outside(ex); + } + } + } + } + , FileSaver = function(blob, name) { + // First try a.download, then web filesystem, then object URLs + var + filesaver = this + , type = blob.type + , blob_changed = false + , object_url + , target_view + , get_object_url = function() { + var object_url = get_URL().createObjectURL(blob); + deletion_queue.push(object_url); + return object_url; + } + , dispatch_all = function() { + dispatch(filesaver, "writestart progress write writeend".split(" ")); + } + // on any filesys errors revert to saving with object URLs + , fs_error = function() { + // don't create more object URLs than needed + if (blob_changed || !object_url) { + object_url = get_object_url(blob); + } + if (target_view) { + target_view.location.href = object_url; + } else { + window.open(object_url, "_blank"); + } + filesaver.readyState = filesaver.DONE; + dispatch_all(); + } + , abortable = function(func) { + return function() { + if (filesaver.readyState !== filesaver.DONE) { + return func.apply(this, arguments); + } + }; + } + , create_if_not_found = {create: true, exclusive: false} + , slice + ; + filesaver.readyState = filesaver.INIT; + if (!name) { + name = "download"; + } + if (can_use_save_link) { + object_url = get_object_url(blob); + save_link.href = object_url; + save_link.download = name; + click(save_link); + filesaver.readyState = filesaver.DONE; + dispatch_all(); + return; + } + // Object and web filesystem URLs have a problem saving in Google Chrome when + // viewed in a tab, so I force save with application/octet-stream + // http://code.google.com/p/chromium/issues/detail?id=91158 + if (view.chrome && type && type !== force_saveable_type) { + slice = blob.slice || blob.webkitSlice; + blob = slice.call(blob, 0, blob.size, force_saveable_type); + blob_changed = true; + } + // Since I can't be sure that the guessed media type will trigger a download + // in WebKit, I append .download to the filename. + // https://bugs.webkit.org/show_bug.cgi?id=65440 + if (webkit_req_fs && name !== "download") { + name += ".download"; + } + if (type === force_saveable_type || webkit_req_fs) { + target_view = view; + } + if (!req_fs) { + fs_error(); + return; + } + fs_min_size += blob.size; + req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) { + fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) { + var save = function() { + dir.getFile(name, create_if_not_found, abortable(function(file) { + file.createWriter(abortable(function(writer) { + writer.onwriteend = function(event) { + target_view.location.href = file.toURL(); + deletion_queue.push(file); + filesaver.readyState = filesaver.DONE; + dispatch(filesaver, "writeend", event); + }; + writer.onerror = function() { + var error = writer.error; + if (error.code !== error.ABORT_ERR) { + fs_error(); + } + }; + "writestart progress write abort".split(" ").forEach(function(event) { + writer["on" + event] = filesaver["on" + event]; + }); + writer.write(blob); + filesaver.abort = function() { + writer.abort(); + filesaver.readyState = filesaver.DONE; + }; + filesaver.readyState = filesaver.WRITING; + }), fs_error); + }), fs_error); + }; + dir.getFile(name, {create: false}, abortable(function(file) { + // delete file if it already exists + file.remove(); + save(); + }), abortable(function(ex) { + if (ex.code === ex.NOT_FOUND_ERR) { + save(); + } else { + fs_error(); + } + })); + }), fs_error); + }), fs_error); + } + , FS_proto = FileSaver.prototype + , saveAs = function(blob, name) { + return new FileSaver(blob, name); + } + ; + FS_proto.abort = function() { + var filesaver = this; + filesaver.readyState = filesaver.DONE; + dispatch(filesaver, "abort"); + }; + FS_proto.readyState = FS_proto.INIT = 0; + FS_proto.WRITING = 1; + FS_proto.DONE = 2; + + FS_proto.error = + FS_proto.onwritestart = + FS_proto.onprogress = + FS_proto.onwrite = + FS_proto.onabort = + FS_proto.onerror = + FS_proto.onwriteend = + null; + + view.addEventListener("unload", process_deletion_queue, false); + saveAs.unload = function() { + process_deletion_queue(); + view.removeEventListener("unload", process_deletion_queue, false); + }; + return saveAs; +}( + typeof self !== "undefined" && self + || typeof window !== "undefined" && window + || this.content +)); +// `self` is undefined in Firefox for Android content script context +// while `this` is nsIContentFrameMessageManager +// with an attribute `content` that corresponds to the window + +if (typeof module !== "undefined" && module !== null) { + module.exports = saveAs; +} else if ((typeof define !== "undefined" && define !== null) && (define.amd != null)) { + define([], function() { + return saveAs; + }); +} diff --git a/test/vendor/mocha.css b/test/vendor/mocha.css new file mode 120000 index 0000000..bcb345a --- /dev/null +++ b/test/vendor/mocha.css @@ -0,0 +1 @@ +../../node_modules/mocha/mocha.css \ No newline at end of file diff --git a/test/vendor/mocha.js b/test/vendor/mocha.js new file mode 120000 index 0000000..78251a9 --- /dev/null +++ b/test/vendor/mocha.js @@ -0,0 +1 @@ +../../node_modules/mocha/mocha.js \ No newline at end of file