Skip to content

Commit

Permalink
Initial version of the library
Browse files Browse the repository at this point in the history
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
  • Loading branch information
anowak committed Jul 31, 2014
1 parent f8d0b57 commit 8795a4c
Show file tree
Hide file tree
Showing 17 changed files with 818 additions and 0 deletions.
31 changes: 31 additions & 0 deletions bower.json
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
]
}
69 changes: 69 additions & 0 deletions coffeelint.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
69 changes: 69 additions & 0 deletions gulpfile.coffee
Original file line number Diff line number Diff line change
@@ -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']
41 changes: 41 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>",
"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"
}
}
9 changes: 9 additions & 0 deletions src/api.coffee
Original file line number Diff line number Diff line change
@@ -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)
8 changes: 8 additions & 0 deletions src/assets/content_types.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="xml" ContentType=
"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml" />
<Default Extension="rels" ContentType=
"application/vnd.openxmlformats-package.relationships+xml" />
<Default Extension="htm" ContentType="text/html" />
</Types>
7 changes: 7 additions & 0 deletions src/assets/document.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:altChunk r:id="htmlChunk" xmlns:r=
"http://schemas.openxmlformats.org/officeDocument/2006/relationships" />
</w:body>
</w:document>
5 changes: 5 additions & 0 deletions src/assets/document.xml.rels
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/aFChunk"
Target="/word/afchunk.htm" Id="htmlChunk" />
</Relationships>
6 changes: 6 additions & 0 deletions src/assets/rels.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
Target="/word/document.xml" Id="R09c83fafc067488e" />
</Relationships>
22 changes: 22 additions & 0 deletions src/internal.coffee
Original file line number Diff line number Diff line change
@@ -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'
61 changes: 61 additions & 0 deletions test/index.coffee
Original file line number Diff line number Diff line change
@@ -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
46 changes: 46 additions & 0 deletions test/sample.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>HTML-DOCX test</title>
<script src="http://tinymce.cachefly.net/4.1/tinymce.min.js"></script>
<script src="vendor/FileSaver.js"></script>
<script src="../build/html-docx.js"></script>
</head>
<body>
<p>Enter/paste your document here:</p>
<textarea id="content" cols="60" rows="10"></textarea>
<button id="convert">Convert</button>
<div id="download-area"></div>

<script>
tinymce.init({
selector: '#content',
plugins: [
"advlist autolink lists link image charmap print preview anchor",
"searchreplace visualblocks code fullscreen",
"insertdatetime media table contextmenu paste"
],
toolbar: "insertfile undo redo | styleselect | bold italic | " +
"alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | " +
"link image"
});
document.getElementById('convert').addEventListener('click', function(e) {
e.preventDefault();
var content = tinymce.get('content').getContent();
var converted = htmlDocx.asBlob(content);

saveAs(converted, 'test.docx');

var link = document.createElement('a');
link.href = URL.createObjectURL(converted);
link.download = 'document.docx';
link.appendChild(
document.createTextNode('Click here if your download has not started automatically'));
var downloadArea = document.getElementById('download-area');
downloadArea.innerHTML = '';
downloadArea.appendChild(link);
});
</script>
</body>
</html>
Loading

0 comments on commit 8795a4c

Please sign in to comment.