├── .npmignore ├── .gitignore ├── test ├── vendor │ ├── mocha.js │ ├── mocha.css │ ├── Blob.js │ └── FileSaver.js ├── cat.jpg ├── testbed.html ├── sample.html └── index.coffee ├── src ├── templates │ ├── mht_part.tpl │ ├── mht_document.tpl │ └── document.tpl ├── api.coffee ├── assets │ ├── document.xml.rels │ ├── rels.xml │ └── content_types.xml ├── utils.coffee └── internal.coffee ├── .arcconfig ├── bower.json ├── CHANGELOG.md ├── LICENSE ├── package.json ├── coffeelint.json ├── gulpfile.coffee └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .*.swp 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | .*.swp 4 | -------------------------------------------------------------------------------- /test/vendor/mocha.js: -------------------------------------------------------------------------------- 1 | ../../node_modules/mocha/mocha.js -------------------------------------------------------------------------------- /test/vendor/mocha.css: -------------------------------------------------------------------------------- 1 | ../../node_modules/mocha/mocha.css -------------------------------------------------------------------------------- /test/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evidenceprime/html-docx-js/HEAD/test/cat.jpg -------------------------------------------------------------------------------- /src/templates/mht_part.tpl: -------------------------------------------------------------------------------- 1 | ------=mhtDocumentPart 2 | Content-Type: <%= contentType %> 3 | Content-Transfer-Encoding: <%= contentEncoding %> 4 | Content-Location: <%= contentLocation %> 5 | 6 | <%= encodedContent %> 7 | -------------------------------------------------------------------------------- /src/api.coffee: -------------------------------------------------------------------------------- 1 | JSZip = require 'jszip' 2 | internal = require './internal' 3 | fs = require 'fs' 4 | 5 | module.exports = 6 | asBlob: (html, options) -> 7 | zip = new JSZip() 8 | internal.addFiles(zip, html, options) 9 | internal.generateDocument(zip) 10 | -------------------------------------------------------------------------------- /src/assets/document.xml.rels: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/rels.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /.arcconfig: -------------------------------------------------------------------------------- 1 | { 2 | "project_id": "HTML-DOCX.js", 3 | "conduit_uri": "https://phabricator.lab.evidenceprime.com", 4 | "history.immutable": false, 5 | "arc.land.onto.default": "origin", 6 | "arc.feature.start.default": "origin", 7 | "load": [ 8 | "arcanist-coffee-lib", 9 | "arcanist-gdt-lib" 10 | ], 11 | "lint.engine": "GdtLintEngine", 12 | "lint.coffeelint.config": "coffeelint.json" 13 | } 14 | -------------------------------------------------------------------------------- /src/templates/mht_document.tpl: -------------------------------------------------------------------------------- 1 | MIME-Version: 1.0 2 | Content-Type: multipart/related; 3 | type="text/html"; 4 | boundary="----=mhtDocumentPart" 5 | 6 | 7 | ------=mhtDocumentPart 8 | Content-Type: text/html; 9 | charset="utf-8" 10 | Content-Transfer-Encoding: quoted-printable 11 | Content-Location: file:///C:/fake/document.html 12 | 13 | <%= htmlSource %> 14 | 15 | <%= contentParts %> 16 | 17 | ------=mhtDocumentPart-- 18 | -------------------------------------------------------------------------------- /src/assets/content_types.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /test/testbed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mocha Test Runner 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-docx-js", 3 | "version": "0.3.1", 4 | "description": "Converts HTML documents to DOCX in the browser", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/evidenceprime/html-docx-js.git" 8 | }, 9 | "main": "dist/html-docx.js", 10 | "moduleType": [ 11 | "amd", 12 | "globals", 13 | "node" 14 | ], 15 | "keywords": [ 16 | "docx", 17 | "browser", 18 | "html" 19 | ], 20 | "authors": [ 21 | "Artur Nowak ", 22 | "Ievgen Martynov " 23 | ], 24 | "license": "MIT", 25 | "ignore": [ 26 | "**/.*", 27 | "node_modules", 28 | "bower_components", 29 | "test", 30 | "tests" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.3.1 (May 17, 2016) 2 | 3 | * Fixed support for embedding images on Microsoft Word for Mac 2016 4 | 5 | ## 0.3.0 (Sep 17, 2015) 6 | 7 | * Added support for embedded images. Please see `README.md` for details. 8 | 9 | ## 0.2.2 (May 21, 2015) 10 | 11 | * Corrected publishing as Node.js module (`package.json` now contains correct main entry point) 12 | * Corrected `.npmignore`, so the build artifacts are actually published to npm. Unpublished version 13 | 0.2.1 that was built with incorrect `.npmignore` file. 14 | 15 | ## 0.2.0 (Apr 23, 2015) 16 | 17 | * Added `fullpage` plugin to the sample page to ensure that whole HTML document gets exported 18 | * Added possibility of specyfing document settings, including page orientation 19 | * Added possibility of specyfing page margins 20 | 21 | ## 0.1.0 (Aug 1, 2014) 22 | 23 | * Initial public release 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Evidence Prime 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/utils.coffee: -------------------------------------------------------------------------------- 1 | mhtDocumentTemplate = require './templates/mht_document' 2 | mhtPartTemplate = require './templates/mht_part' 3 | 4 | module.exports = 5 | getMHTdocument: (htmlSource) -> 6 | # take care of images 7 | {htmlSource, imageContentParts} = @_prepareImageParts htmlSource 8 | # for proper MHT parsing all '=' signs in html need to be replaced with '=3D' 9 | htmlSource = htmlSource.replace /\=/g, '=3D' 10 | mhtDocumentTemplate {htmlSource, contentParts: imageContentParts.join '\n'} 11 | 12 | _prepareImageParts: (htmlSource) -> 13 | imageContentParts = [] 14 | inlinedSrcPattern = /"data:(\w+\/\w+);(\w+),(\S+)"/g 15 | # replacer function for images sources via DATA URI 16 | inlinedReplacer = (match, contentType, contentEncoding, encodedContent) -> 17 | index = imageContentParts.length 18 | extension = contentType.split('/')[1] 19 | contentLocation = "file:///C:/fake/image#{index}.#{extension}" 20 | imageContentParts.push mhtPartTemplate {contentType, contentEncoding, contentLocation, encodedContent} 21 | "\"#{contentLocation}\"" 22 | 23 | if typeof htmlSource is 'string' 24 | return {htmlSource, imageContentParts} unless /", 21 | "contributors": [{ 22 | "name": "Ievgen Martynov", 23 | "email": "ievgen.martynov@gmail.com" 24 | }], 25 | "license": "MIT", 26 | "devDependencies": { 27 | "brfs": "^1.1.2", 28 | "browserify": "^4.2.0", 29 | "chai": "^1.9.1", 30 | "coffeeify": "^0.6.0", 31 | "del": "^1.2.0", 32 | "gulp": "^3.8.5", 33 | "gulp-coffee": "^2.3.1", 34 | "gulp-lodash-template": "^0.1.0", 35 | "gulp-mocha": "^0.4.1", 36 | "gulp-mocha-phantomjs": "^0.3.0", 37 | "gulp-notify": "^1.4.0", 38 | "gulp-util": "^2.2.19", 39 | "jstify": "^0.9.0", 40 | "mocha": "^1.20.1", 41 | "pretty-hrtime": "^0.2.1", 42 | "sinon": "^1.10.2", 43 | "sinon-chai": "^2.5.0", 44 | "vinyl-source-stream": "^0.1.1", 45 | "watchify": "^0.10.2" 46 | }, 47 | "dependencies": { 48 | "jszip": "^2.3.0", 49 | "lodash.escape": "^3.0.0", 50 | "lodash.merge": "^3.2.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/internal.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | documentTemplate = require './templates/document' 3 | utils = require './utils' 4 | _ = merge: require 'lodash.merge' 5 | 6 | module.exports = 7 | generateDocument: (zip) -> 8 | buffer = zip.generate(type: 'arraybuffer') 9 | if global.Blob 10 | new Blob [buffer], 11 | type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' 12 | else if global.Buffer 13 | new Buffer new Uint8Array(buffer) 14 | else 15 | throw new Error "Neither Blob nor Buffer are accessible in this environment. " + 16 | "Consider adding Blob.js shim" 17 | 18 | renderDocumentFile: (documentOptions = {}) -> 19 | templateData = _.merge margins: 20 | top: 1440 21 | right: 1440 22 | bottom: 1440 23 | left: 1440 24 | header: 720 25 | footer: 720 26 | gutter: 0 27 | , 28 | switch documentOptions.orientation 29 | when 'landscape' then height: 12240, width: 15840, orient: 'landscape' 30 | else width: 12240, height: 15840, orient: 'portrait' 31 | , 32 | margins: documentOptions.margins 33 | 34 | documentTemplate(templateData) 35 | 36 | addFiles: (zip, htmlSource, documentOptions) -> 37 | zip.file '[Content_Types].xml', fs.readFileSync __dirname + '/assets/content_types.xml' 38 | zip.folder('_rels').file '.rels', fs.readFileSync __dirname + '/assets/rels.xml' 39 | zip.folder 'word' 40 | .file 'document.xml', @renderDocumentFile documentOptions 41 | .file 'afchunk.mht', utils.getMHTdocument htmlSource 42 | .folder '_rels' 43 | .file 'document.xml.rels', fs.readFileSync __dirname + '/assets/document.xml.rels' 44 | -------------------------------------------------------------------------------- /coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "no_tabs": { 3 | "level": "ignore", 4 | "comment": "Checked by other linter" 5 | }, 6 | "no_trailing_whitespace": { 7 | "level": "ignore", 8 | "allowed_in_comments": false, 9 | "comment": "Checked by other linter" 10 | }, 11 | "max_line_length": { 12 | "value": 100, 13 | "level": "ignore", 14 | "comment": "Checked by other linter" 15 | }, 16 | "camel_case_classes": { 17 | "level": "error" 18 | }, 19 | "indentation": { 20 | "value": 2, 21 | "level": "error" 22 | }, 23 | "no_implicit_braces": { 24 | "level": "ignore" 25 | }, 26 | "no_trailing_semicolons": { 27 | "level": "error" 28 | }, 29 | "no_plusplus": { 30 | "level": "ignore" 31 | }, 32 | "no_throwing_strings": { 33 | "level": "error" 34 | }, 35 | "cyclomatic_complexity": { 36 | "value": 10, 37 | "level": "warn" 38 | }, 39 | "no_backticks": { 40 | "level": "error" 41 | }, 42 | "line_endings": { 43 | "level": "ignore", 44 | "value": "unix", 45 | "comment": "Checked by other linter" 46 | }, 47 | "no_implicit_parens": { 48 | "level": "ignore" 49 | }, 50 | "no_empty_param_list": { 51 | "level": "error" 52 | }, 53 | "space_operators": { 54 | "level": "warn" 55 | }, 56 | "duplicate_key": { 57 | "level": "error" 58 | }, 59 | "newlines_after_classes": { 60 | "value": 3, 61 | "level": "ignore" 62 | }, 63 | "no_stand_alone_at": { 64 | "level": "ignore" 65 | }, 66 | "coffeescript_error": { 67 | "level": "error" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/templates/document.tpl: -------------------------------------------------------------------------------- 1 | 2 | 28 | 29 | 30 | 31 | 32 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /gulpfile.coffee: -------------------------------------------------------------------------------- 1 | gulp = require 'gulp' 2 | vinyl = require 'vinyl-source-stream' 3 | browserify = require 'browserify' 4 | watchify = require 'watchify' 5 | gutil = require 'gulp-util' 6 | prettyHrtime = require 'pretty-hrtime' 7 | notify = require 'gulp-notify' 8 | mocha = require 'gulp-mocha' 9 | mochaPhantomJS = require 'gulp-mocha-phantomjs' 10 | template = require 'gulp-lodash-template' 11 | coffee = require 'gulp-coffee' 12 | del = require 'del' 13 | 14 | startTime = null 15 | logger = 16 | start: -> 17 | startTime = process.hrtime() 18 | gutil.log 'Running', gutil.colors.green("'bundle'") + '...' 19 | end: -> 20 | taskTime = process.hrtime startTime 21 | prettyTime = prettyHrtime taskTime 22 | gutil.log 'Finished', gutil.colors.green("'bundle'"), 'in', gutil.colors.magenta(prettyTime) 23 | 24 | handleErrors = -> 25 | notify.onError 26 | title: 'Compile error' 27 | message: '<%= error.message %>' 28 | .apply this, arguments 29 | @emit 'end' 30 | 31 | build = (test) -> 32 | [output, entry, options] = if test 33 | ['tests.js', './test/index', debug: true] 34 | else 35 | ['html-docx.js', './src/api', standalone: 'html-docx'] 36 | 37 | bundleMethod = if global.isWatching then watchify else browserify 38 | bundler = bundleMethod 39 | entries: [entry] 40 | extensions: ['.coffee', '.tpl'] 41 | 42 | bundle = -> 43 | logger.start() 44 | bundler 45 | .transform 'jstify', engine: 'lodash-micro', minifierOpts: false 46 | .bundle options 47 | .on 'error', handleErrors 48 | .pipe vinyl(output) 49 | .pipe gulp.dest('./build') 50 | .on 'end', logger.end 51 | 52 | if global.isWatching 53 | bundler.on 'update', bundle 54 | 55 | bundle() 56 | 57 | testsBundle = './test/index.coffee' 58 | 59 | clean = (cb) -> del 'build', cb 60 | 61 | gulp.task 'clean', clean 62 | gulp.task 'setWatch', -> global.isWatching = true 63 | gulp.task 'build', -> build() 64 | gulp.task 'watch', ['setWatch', 'build'] 65 | 66 | buildNode = (compileCoffee = true) -> 67 | logger.start() 68 | gulp.src('src/**/*', base: 'src').pipe(gulp.dest('build')) 69 | gulp.src('src/templates/*.tpl').pipe(template commonjs: true).pipe(gulp.dest('build/templates')) 70 | if compileCoffee 71 | gulp.src('src/**/*.coffee').pipe(coffee bare: true) 72 | .on('error', handleErrors).on('end', logger.end).pipe(gulp.dest('build')) 73 | else 74 | logger.end() 75 | 76 | gulp.task 'build-node', buildNode 77 | gulp.task 'test-node', (growl = false) -> 78 | buildNode(false) 79 | gulp.src(testsBundle, read: false).pipe mocha {reporter: 'spec', growl} 80 | gulp.task 'test-node-watch', -> 81 | sources = ['src/**', 'test/**'] 82 | gulp.watch sources, ['test-node'] 83 | 84 | gulp.task 'build-test-browserify', -> build(true) 85 | gulp.task 'run-phantomjs', -> gulp.src('test/testbed.html').pipe(mochaPhantomJS reporter: 'spec') 86 | gulp.task 'test-phantomjs', ['build-test-browserify', 'run-phantomjs'] 87 | 88 | gulp.task 'default', ['test-node', 'test-node-watch'] 89 | -------------------------------------------------------------------------------- /test/sample.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HTML-DOCX test 6 | 7 | 8 | 9 | 10 | 11 |

Enter/paste your document here:

12 | 17 |
18 | Page orientation: 19 | 20 | 21 |
22 | 23 |
24 | 25 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | html-docx-js 2 | ============ 3 | 4 | This is a very small library that is capable of converting HTML documents to DOCX format that 5 | is used by Microsoft Word 2007 and onward. It manages to perform the conversion in the browser by 6 | using a feature called 'altchunks'. In a nutshell, it allows embedding content in a different markup 7 | language. We are using MHT document to ship the embedded content to Word as it allows to handle images. 8 | After Word opens such file, it converts the external content to Word Processing ML (this 9 | is how the markup language of DOCX files is called) and replaces the reference. 10 | 11 | Altchunks were not supported by Microsoft Word for Mac 2008 and are not supported by LibreOffice and 12 | Google Docs. 13 | 14 | Compatibility 15 | ------------- 16 | 17 | This library should work on any modern browser that supports `Blobs` (either natively or via 18 | [Blob.js](https://github.com/eligrey/Blob.js/)). It was tested on Google Chrome 36, Safari 7 and 19 | Internet Explorer 10. 20 | 21 | It also works on Node.js (tested on v0.10.12) using `Buffer` instead of `Blob`. 22 | 23 | Images Support 24 | ------------- 25 | 26 | This library supports only inlined base64 images (sourced via DATA URI). But it is easy to convert a 27 | regular image (sourced from static folder) on the fly. If you need an example of such conversion you can [checkout a demo page source](https://github.com/evidenceprime/html-docx-js/blob/master/test/sample.html) (see function `convertImagesToBase64`). 28 | 29 | Usage and demo 30 | -------------- 31 | 32 | Very minimal demo is available as `test/sample.html` in the repository and 33 | [online](http://evidenceprime.github.io/html-docx-js/test/sample.html). Please note that saving 34 | files on Safari is a little bit convoluted and the only reliable method seems to be falling back 35 | to a Flash-based approach (such as [Downloadify](https://github.com/dcneiner/Downloadify)). 36 | Our demo does not include this workaround to keep things simple, so it will not work on Safari at 37 | this point of time. 38 | 39 | You can also find a sample for using it in Node.js environment 40 | [here](https://github.com/evidenceprime/html-docx-js-node-sample). 41 | 42 | To generate DOCX, simply pass a HTML document (as string) to `asBlob` method to receive `Blob` (or `Buffer`) 43 | containing the output file. 44 | 45 | var converted = htmlDocx.asBlob(content); 46 | saveAs(converted, 'test.docx'); 47 | 48 | `asBlob` can take additional options for controlling page setup for the document: 49 | 50 | * `orientation`: `landscape` or `portrait` (default) 51 | * `margins`: map of margin sizes (expressed in twentieths of point, see 52 | [WordprocessingML documentation](http://officeopenxml.com/WPsectionPgMar.php) for details): 53 | - `top`: number (default: 1440, i.e. 2.54 cm) 54 | - `right`: number (default: 1440) 55 | - `bottom`: number (default: 1440) 56 | - `left`: number (default: 1440) 57 | - `header`: number (default: 720) 58 | - `footer`: number (default: 720) 59 | - `gutter`: number (default: 0) 60 | 61 | For example: 62 | 63 | var converted = htmlDocx.asBlob(content, {orientation: 'landscape', margins: {top: 720}}); 64 | saveAs(converted, 'test.docx'); 65 | 66 | **IMPORTANT**: please pass a complete, valid HTML (including DOCTYPE, `html` and `body` tags). 67 | This may be less convenient, but gives you possibility of including CSS rules in `style` tags. 68 | 69 | `html-docx-js` is distributed as 'standalone' Browserify module (UMD). You can `require` it as 70 | `html-docx`. If no module loader is available, it will register itself as `window.htmlDocx`. 71 | See `test/sample.html` for details. 72 | 73 | License 74 | ------- 75 | 76 | Copyright (c) 2015 Evidence Prime, Inc. 77 | See the LICENSE file for license rights and limitations (MIT). 78 | -------------------------------------------------------------------------------- /test/index.coffee: -------------------------------------------------------------------------------- 1 | chai = require 'chai' 2 | expect = chai.expect 3 | sinon = require 'sinon' 4 | chai.use require 'sinon-chai' 5 | internal = require '../build/internal' 6 | utils = require '../build/utils' 7 | 8 | describe 'Adding files', -> 9 | beforeEach -> 10 | @data = {} 11 | zip = (data) -> 12 | entry = 13 | file: (name, content) -> 14 | data[name] = content 15 | entry 16 | folder: (name) -> 17 | data[name] = {} 18 | zip data[name] 19 | sinon.stub(internal, 'renderDocumentFile').returns '' 20 | internal.addFiles zip(@data), 'foobar', someOption: true 21 | afterEach -> 22 | internal.renderDocumentFile.restore() 23 | 24 | it 'should add file for embedded content types', -> 25 | expect(@data['[Content_Types].xml']).to.be.defined 26 | content = String(@data['[Content_Types].xml']) 27 | expect(content).to.match /PartName="\/word\/afchunk.mht"/ 28 | expect(content).to.match /PartName="\/word\/document.xml"/ 29 | expect(content).to.match /Extension="rels"/ 30 | 31 | it 'should add manifest for Word document', -> 32 | expect(@data._rels['.rels']).to.be.defined 33 | content = String(@data._rels['.rels']) 34 | expect(content).to.match /Target="\/word\/document.xml"/ 35 | 36 | it 'should add MHT file with given content', -> 37 | expect(@data.word['afchunk.mht']).to.be.defined 38 | expect(String @data.word['afchunk.mht']).to.match /foobar/ 39 | 40 | it 'should render the Word document and add its contents', -> 41 | expect(internal.renderDocumentFile).to.have.been.calledWith someOption: true 42 | expect(@data.word['document.xml']).to.be.defined 43 | expect(String @data.word['document.xml']).to.match // 44 | 45 | it 'should add relationship file to link between Word and HTML files', -> 46 | expect(@data.word._rels['document.xml.rels']).to.be.defined 47 | expect(String @data.word._rels['document.xml.rels']).to 48 | .match /Target="\/word\/afchunk.mht" Id="htmlChunk"/ 49 | 50 | describe 'Coverting HTML to MHT', -> 51 | it 'should convert HTML source to an MHT document', -> 52 | htmlSource = '' 53 | expect(utils.getMHTdocument(htmlSource)).to.match 54 | /^MIME-Version: 1.0\nContent-Type: multipart\/related;/ 55 | 56 | it 'should fail if HTML source is not a string', -> 57 | htmlSource = {} 58 | expect(utils._prepareImageParts.bind(null, htmlSource)).to.throw /Not a valid source provided!/ 59 | 60 | it 'should detect any embedded image and change its source to ContentPart name', -> 61 | htmlSource = '

' 62 | expect(utils.getMHTdocument(htmlSource)).to.match // 63 | 64 | it 'should produce ContentPart for each embedded image', -> 65 | htmlSource = '

66 | 67 |

' 68 | imageParts = utils._prepareImageParts(htmlSource).imageContentParts 69 | expect(imageParts).to.have.length 3 70 | imageParts.forEach (image, index) -> 71 | expect(image).to.match /Content-Type: image\/(jpeg|png|gif)/ 72 | expect(image).to.match /Content-Transfer-Encoding: base64/ 73 | expect(image).to.have.string "Content-Location: file://fake/image#{index}." 74 | 75 | it 'should replace = signs to 3D=', -> 76 | htmlSource = 'This = 0' 77 | expect(utils.getMHTdocument(htmlSource)).to.match 78 | 'This 3D= 0' 79 | 80 | describe 'Rendering the Word document', -> 81 | it 'should return a Word Processing ML file that embeds the altchunk', -> 82 | expect(internal.renderDocumentFile()).to.match /altChunk r:id="htmlChunk"/ 83 | 84 | it 'should set portrait orientation and letter size if no formatting options are passed', -> 85 | expect(internal.renderDocumentFile()).to 86 | .match // 87 | 88 | it 'should set landscape orientation and letter size if orientation is set to landscape', -> 89 | expect(internal.renderDocumentFile(orientation: 'landscape')).to 90 | .match // 91 | 92 | it 'should set default margins if no options were passed', -> 93 | expect(internal.renderDocumentFile()).to.match / 97 | expect(internal.renderDocumentFile(margins: top: 123)).to.match /]*w:top="123"/ 98 | 99 | it 'should leave default values for margins that are not defined in the options', -> 100 | expect(internal.renderDocumentFile(margins: left: 123)).to.match /]*w:left="123"/ 101 | expect(internal.renderDocumentFile(margins: left: 123)).to.match /]*w:top="1440"/ 102 | 103 | describe 'Generating the document', -> 104 | beforeEach -> 105 | @zip = generate: sinon.stub().returns 'DEADBEEF' 106 | 107 | it 'should retrieve ZIP file as arraybuffer', -> 108 | internal.generateDocument @zip 109 | expect(@zip.generate).to.have.been.calledWith type: 'arraybuffer' 110 | 111 | it 'should return Blob with correct content type if it is available', -> 112 | return unless global.Blob 113 | document = internal.generateDocument @zip 114 | expect(document.type).to.be 115 | .equal 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' 116 | 117 | it 'should return Buffer in Node.js environment', -> 118 | return unless global.Buffer 119 | expect(internal.generateDocument @zip).to.be.an.instanceOf Buffer 120 | -------------------------------------------------------------------------------- /test/vendor/Blob.js: -------------------------------------------------------------------------------- 1 | /* Blob.js 2 | * A Blob implementation. 3 | * 2014-07-01 4 | * 5 | * By Eli Grey, http://eligrey.com 6 | * By Devin Samarin, https://github.com/eboyjr 7 | * License: X11/MIT 8 | * See https://github.com/eligrey/Blob.js/blob/master/LICENSE.md 9 | */ 10 | 11 | /*global self, unescape */ 12 | /*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true, 13 | plusplus: true */ 14 | 15 | /*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */ 16 | 17 | (function (view) { 18 | "use strict"; 19 | 20 | view.URL = view.URL || view.webkitURL; 21 | 22 | if (view.Blob && view.URL) { 23 | try { 24 | new Blob; 25 | return; 26 | } catch (e) {} 27 | } 28 | 29 | // Internally we use a BlobBuilder implementation to base Blob off of 30 | // in order to support older browsers that only have BlobBuilder 31 | var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function(view) { 32 | var 33 | get_class = function(object) { 34 | return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1]; 35 | } 36 | , FakeBlobBuilder = function BlobBuilder() { 37 | this.data = []; 38 | } 39 | , FakeBlob = function Blob(data, type, encoding) { 40 | this.data = data; 41 | this.size = data.length; 42 | this.type = type; 43 | this.encoding = encoding; 44 | } 45 | , FBB_proto = FakeBlobBuilder.prototype 46 | , FB_proto = FakeBlob.prototype 47 | , FileReaderSync = view.FileReaderSync 48 | , FileException = function(type) { 49 | this.code = this[this.name = type]; 50 | } 51 | , file_ex_codes = ( 52 | "NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR " 53 | + "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR" 54 | ).split(" ") 55 | , file_ex_code = file_ex_codes.length 56 | , real_URL = view.URL || view.webkitURL || view 57 | , real_create_object_URL = real_URL.createObjectURL 58 | , real_revoke_object_URL = real_URL.revokeObjectURL 59 | , URL = real_URL 60 | , btoa = view.btoa 61 | , atob = view.atob 62 | 63 | , ArrayBuffer = view.ArrayBuffer 64 | , Uint8Array = view.Uint8Array 65 | ; 66 | FakeBlob.fake = FB_proto.fake = true; 67 | while (file_ex_code--) { 68 | FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1; 69 | } 70 | if (!real_URL.createObjectURL) { 71 | URL = view.URL = {}; 72 | } 73 | URL.createObjectURL = function(blob) { 74 | var 75 | type = blob.type 76 | , data_URI_header 77 | ; 78 | if (type === null) { 79 | type = "application/octet-stream"; 80 | } 81 | if (blob instanceof FakeBlob) { 82 | data_URI_header = "data:" + type; 83 | if (blob.encoding === "base64") { 84 | return data_URI_header + ";base64," + blob.data; 85 | } else if (blob.encoding === "URI") { 86 | return data_URI_header + "," + decodeURIComponent(blob.data); 87 | } if (btoa) { 88 | return data_URI_header + ";base64," + btoa(blob.data); 89 | } else { 90 | return data_URI_header + "," + encodeURIComponent(blob.data); 91 | } 92 | } else if (real_create_object_URL) { 93 | return real_create_object_URL.call(real_URL, blob); 94 | } 95 | }; 96 | URL.revokeObjectURL = function(object_URL) { 97 | if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) { 98 | real_revoke_object_URL.call(real_URL, object_URL); 99 | } 100 | }; 101 | FBB_proto.append = function(data/*, endings*/) { 102 | var bb = this.data; 103 | // decode data to a binary string 104 | if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) { 105 | var 106 | str = "" 107 | , buf = new Uint8Array(data) 108 | , i = 0 109 | , buf_len = buf.length 110 | ; 111 | for (; i < buf_len; i++) { 112 | str += String.fromCharCode(buf[i]); 113 | } 114 | bb.push(str); 115 | } else if (get_class(data) === "Blob" || get_class(data) === "File") { 116 | if (FileReaderSync) { 117 | var fr = new FileReaderSync; 118 | bb.push(fr.readAsBinaryString(data)); 119 | } else { 120 | // async FileReader won't work as BlobBuilder is sync 121 | throw new FileException("NOT_READABLE_ERR"); 122 | } 123 | } else if (data instanceof FakeBlob) { 124 | if (data.encoding === "base64" && atob) { 125 | bb.push(atob(data.data)); 126 | } else if (data.encoding === "URI") { 127 | bb.push(decodeURIComponent(data.data)); 128 | } else if (data.encoding === "raw") { 129 | bb.push(data.data); 130 | } 131 | } else { 132 | if (typeof data !== "string") { 133 | data += ""; // convert unsupported types to strings 134 | } 135 | // decode UTF-16 to binary string 136 | bb.push(unescape(encodeURIComponent(data))); 137 | } 138 | }; 139 | FBB_proto.getBlob = function(type) { 140 | if (!arguments.length) { 141 | type = null; 142 | } 143 | return new FakeBlob(this.data.join(""), type, "raw"); 144 | }; 145 | FBB_proto.toString = function() { 146 | return "[object BlobBuilder]"; 147 | }; 148 | FB_proto.slice = function(start, end, type) { 149 | var args = arguments.length; 150 | if (args < 3) { 151 | type = null; 152 | } 153 | return new FakeBlob( 154 | this.data.slice(start, args > 1 ? end : this.data.length) 155 | , type 156 | , this.encoding 157 | ); 158 | }; 159 | FB_proto.toString = function() { 160 | return "[object Blob]"; 161 | }; 162 | FB_proto.close = function() { 163 | this.size = 0; 164 | delete this.data; 165 | }; 166 | return FakeBlobBuilder; 167 | }(view)); 168 | 169 | view.Blob = function(blobParts, options) { 170 | var type = options ? (options.type || "") : ""; 171 | var builder = new BlobBuilder(); 172 | if (blobParts) { 173 | for (var i = 0, len = blobParts.length; i < len; i++) { 174 | builder.append(blobParts[i]); 175 | } 176 | } 177 | return builder.getBlob(type); 178 | }; 179 | }(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this)); 180 | -------------------------------------------------------------------------------- /test/vendor/FileSaver.js: -------------------------------------------------------------------------------- 1 | /* FileSaver.js 2 | * A saveAs() FileSaver implementation. 3 | * 2014-05-27 4 | * 5 | * By Eli Grey, http://eligrey.com 6 | * License: X11/MIT 7 | * See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md 8 | */ 9 | 10 | /*global self */ 11 | /*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */ 12 | 13 | /*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ 14 | 15 | var saveAs = saveAs 16 | // IE 10+ (native saveAs) 17 | || (typeof navigator !== "undefined" && 18 | navigator.msSaveOrOpenBlob && navigator.msSaveOrOpenBlob.bind(navigator)) 19 | // Everyone else 20 | || (function(view) { 21 | "use strict"; 22 | // IE <10 is explicitly unsupported 23 | if (typeof navigator !== "undefined" && 24 | /MSIE [1-9]\./.test(navigator.userAgent)) { 25 | return; 26 | } 27 | var 28 | doc = view.document 29 | // only get URL when necessary in case Blob.js hasn't overridden it yet 30 | , get_URL = function() { 31 | return view.URL || view.webkitURL || view; 32 | } 33 | , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a") 34 | , can_use_save_link = !view.externalHost && "download" in save_link 35 | , click = function(node) { 36 | var event = doc.createEvent("MouseEvents"); 37 | event.initMouseEvent( 38 | "click", true, false, view, 0, 0, 0, 0, 0 39 | , false, false, false, false, 0, null 40 | ); 41 | node.dispatchEvent(event); 42 | } 43 | , webkit_req_fs = view.webkitRequestFileSystem 44 | , req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem 45 | , throw_outside = function(ex) { 46 | (view.setImmediate || view.setTimeout)(function() { 47 | throw ex; 48 | }, 0); 49 | } 50 | , force_saveable_type = "application/octet-stream" 51 | , fs_min_size = 0 52 | , deletion_queue = [] 53 | , process_deletion_queue = function() { 54 | var i = deletion_queue.length; 55 | while (i--) { 56 | var file = deletion_queue[i]; 57 | if (typeof file === "string") { // file is an object URL 58 | get_URL().revokeObjectURL(file); 59 | } else { // file is a File 60 | file.remove(); 61 | } 62 | } 63 | deletion_queue.length = 0; // clear queue 64 | } 65 | , dispatch = function(filesaver, event_types, event) { 66 | event_types = [].concat(event_types); 67 | var i = event_types.length; 68 | while (i--) { 69 | var listener = filesaver["on" + event_types[i]]; 70 | if (typeof listener === "function") { 71 | try { 72 | listener.call(filesaver, event || filesaver); 73 | } catch (ex) { 74 | throw_outside(ex); 75 | } 76 | } 77 | } 78 | } 79 | , FileSaver = function(blob, name) { 80 | // First try a.download, then web filesystem, then object URLs 81 | var 82 | filesaver = this 83 | , type = blob.type 84 | , blob_changed = false 85 | , object_url 86 | , target_view 87 | , get_object_url = function() { 88 | var object_url = get_URL().createObjectURL(blob); 89 | deletion_queue.push(object_url); 90 | return object_url; 91 | } 92 | , dispatch_all = function() { 93 | dispatch(filesaver, "writestart progress write writeend".split(" ")); 94 | } 95 | // on any filesys errors revert to saving with object URLs 96 | , fs_error = function() { 97 | // don't create more object URLs than needed 98 | if (blob_changed || !object_url) { 99 | object_url = get_object_url(blob); 100 | } 101 | if (target_view) { 102 | target_view.location.href = object_url; 103 | } else { 104 | window.open(object_url, "_blank"); 105 | } 106 | filesaver.readyState = filesaver.DONE; 107 | dispatch_all(); 108 | } 109 | , abortable = function(func) { 110 | return function() { 111 | if (filesaver.readyState !== filesaver.DONE) { 112 | return func.apply(this, arguments); 113 | } 114 | }; 115 | } 116 | , create_if_not_found = {create: true, exclusive: false} 117 | , slice 118 | ; 119 | filesaver.readyState = filesaver.INIT; 120 | if (!name) { 121 | name = "download"; 122 | } 123 | if (can_use_save_link) { 124 | object_url = get_object_url(blob); 125 | save_link.href = object_url; 126 | save_link.download = name; 127 | click(save_link); 128 | filesaver.readyState = filesaver.DONE; 129 | dispatch_all(); 130 | return; 131 | } 132 | // Object and web filesystem URLs have a problem saving in Google Chrome when 133 | // viewed in a tab, so I force save with application/octet-stream 134 | // http://code.google.com/p/chromium/issues/detail?id=91158 135 | if (view.chrome && type && type !== force_saveable_type) { 136 | slice = blob.slice || blob.webkitSlice; 137 | blob = slice.call(blob, 0, blob.size, force_saveable_type); 138 | blob_changed = true; 139 | } 140 | // Since I can't be sure that the guessed media type will trigger a download 141 | // in WebKit, I append .download to the filename. 142 | // https://bugs.webkit.org/show_bug.cgi?id=65440 143 | if (webkit_req_fs && name !== "download") { 144 | name += ".download"; 145 | } 146 | if (type === force_saveable_type || webkit_req_fs) { 147 | target_view = view; 148 | } 149 | if (!req_fs) { 150 | fs_error(); 151 | return; 152 | } 153 | fs_min_size += blob.size; 154 | req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) { 155 | fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) { 156 | var save = function() { 157 | dir.getFile(name, create_if_not_found, abortable(function(file) { 158 | file.createWriter(abortable(function(writer) { 159 | writer.onwriteend = function(event) { 160 | target_view.location.href = file.toURL(); 161 | deletion_queue.push(file); 162 | filesaver.readyState = filesaver.DONE; 163 | dispatch(filesaver, "writeend", event); 164 | }; 165 | writer.onerror = function() { 166 | var error = writer.error; 167 | if (error.code !== error.ABORT_ERR) { 168 | fs_error(); 169 | } 170 | }; 171 | "writestart progress write abort".split(" ").forEach(function(event) { 172 | writer["on" + event] = filesaver["on" + event]; 173 | }); 174 | writer.write(blob); 175 | filesaver.abort = function() { 176 | writer.abort(); 177 | filesaver.readyState = filesaver.DONE; 178 | }; 179 | filesaver.readyState = filesaver.WRITING; 180 | }), fs_error); 181 | }), fs_error); 182 | }; 183 | dir.getFile(name, {create: false}, abortable(function(file) { 184 | // delete file if it already exists 185 | file.remove(); 186 | save(); 187 | }), abortable(function(ex) { 188 | if (ex.code === ex.NOT_FOUND_ERR) { 189 | save(); 190 | } else { 191 | fs_error(); 192 | } 193 | })); 194 | }), fs_error); 195 | }), fs_error); 196 | } 197 | , FS_proto = FileSaver.prototype 198 | , saveAs = function(blob, name) { 199 | return new FileSaver(blob, name); 200 | } 201 | ; 202 | FS_proto.abort = function() { 203 | var filesaver = this; 204 | filesaver.readyState = filesaver.DONE; 205 | dispatch(filesaver, "abort"); 206 | }; 207 | FS_proto.readyState = FS_proto.INIT = 0; 208 | FS_proto.WRITING = 1; 209 | FS_proto.DONE = 2; 210 | 211 | FS_proto.error = 212 | FS_proto.onwritestart = 213 | FS_proto.onprogress = 214 | FS_proto.onwrite = 215 | FS_proto.onabort = 216 | FS_proto.onerror = 217 | FS_proto.onwriteend = 218 | null; 219 | 220 | view.addEventListener("unload", process_deletion_queue, false); 221 | saveAs.unload = function() { 222 | process_deletion_queue(); 223 | view.removeEventListener("unload", process_deletion_queue, false); 224 | }; 225 | return saveAs; 226 | }( 227 | typeof self !== "undefined" && self 228 | || typeof window !== "undefined" && window 229 | || this.content 230 | )); 231 | // `self` is undefined in Firefox for Android content script context 232 | // while `this` is nsIContentFrameMessageManager 233 | // with an attribute `content` that corresponds to the window 234 | 235 | if (typeof module !== "undefined" && module !== null) { 236 | module.exports = saveAs; 237 | } else if ((typeof define !== "undefined" && define !== null) && (define.amd != null)) { 238 | define([], function() { 239 | return saveAs; 240 | }); 241 | } 242 | --------------------------------------------------------------------------------