docx2html
Version:
a javascript docx converter to html based on docx4js
321 lines (287 loc) • 8.63 kB
JavaScript
import Converter from './converter'
import JSZip from 'jszip'
var createDocument, CSSStyleDeclaration
export default class Document extends Converter{
get tag(){return 'html'}
convert(){
this.doc=this.constructor.create(this.options)
this.content=this.doc
let contentStyle=this.content.style
contentStyle.backgroundColor='transparent'
contentStyle.minHeight='1000px'
contentStyle.width='100%'
contentStyle.paddingTop='20px'
contentStyle.overflow='auto'
var style=this.doc.createStyle('*')
style.margin='0'
style.border='0'
style.padding='0'
style.boxSizing='border-box'
style=this.doc.createStyle('table')
style.width='100%'
style.borderCollapse='collapse'
style.wordBreak='break-word'
style=this.doc.createStyle('section')
style.margin='auto'
style.backgroundColor='white'
style.color='black'
style.position='relative'
style.zIndex=0
style=this.doc.createStyle('p:empty:before')
style.content='""'
style.display='inline-block'
style=this.doc.createStyle('ul')
style.listStyle="none"
style=this.doc.createStyle('ul>li>p')
style.position='relative'
style=this.doc.createStyle('ul .marker')
style.position='absolute'
style=this.doc.createStyle('a')
style.textDecoration='none'
style=this.doc.createStyle('.unsupported')
style.outline="2px red solid"
style=this.doc.createStyle('.warning')
style.outline="1px yellow solid"
this.convertStyle()
}
convertStyle(){
var bgStyle=this.wordModel.getBackgroundStyle()
if(!bgStyle)
return
var style=this.doc.createStyle('section')
switch(typeof bgStyle){
case 'object':// fill
console.warn('not support fill color on document background yet')
break
default:
style.backgroundColor=bgStyle
break
}
}
/**
* opt: {
* template: function(style, html, props){ return (html)},
extendScript: "http://a.com/a.js"
}
*/
toString(opt){
return this.doc.toString(opt,this.props)
}
release(){
this.doc.release()
}
asZip(opt){
return this.doc.asZip(opt,this.props)
}
download(opt){
return this.doc.download(opt, this.props)
}
/**
* opt=extend(toString.opt,{
saveImage: function(arrayBuffer, doc.props): promise(url) {},
saveHtml: function(){}
})
*/
save (opt){
return this.doc.save(opt, this.props)
}
static create(opt){
var selfConverter=this
return (function(document){
var doc=(function browserDoc(){
var uid=0;
var root=Object.assign(document.createElement('div'),{
id : "A",
section: null,
createElement: document.createElement.bind(document),
createTextNode: document.createTextNode.bind(document),
createStyleSheet(){
if(this.stylesheet)
return this.stylesheet;
var elStyle=this.createElement('style')
this.body.appendChild(elStyle,null);
return this.stylesheet=elStyle.sheet
},
getStyleText(){
var styles=[]
for(var i=0, rules=this.stylesheet.cssRules, len=rules.length;i<len;i++)
styles.push(rules[i].cssText)
return styles.join('\r\n')
},
uid(){
return this.id+(uid++)
},
toString(opt, props=selfConverter.props){
if(opt && typeof opt.template!="undefined" && $.isFunction(opt.template))
return opt.template(this.getStyleText(), this._html(), props)
var html=['<!doctype html>\r\n<html><head><meta charset=utf-8><meta key="generator" value="docx2html"><title>'+(props.name||'')+'</title><style>']
html.push(this.getStyleText())
html.push('</style></head><body>')
html.push(this._html())
opt && opt.extendScript && html.push('<script src="'+opt.extendScript+'"></script>')
html.push('</body><html>')
return html.join('\r\n')
},
_html(){
var divs=this.querySelectorAll('p>div, span>div')
if(divs.length==0)
return this.outerHTML
/**
* illegal <p> <div/> </p>
* DOM operation directly in onload
*/
var divcontainer=doc.createElement('div'), uid=0
divcontainer.id='divcontainer'
divcontainer.style.display="none"
this.appendChild(divcontainer)
for(var i=divs.length-1;i>-1;i--){
var div=divs[i],
parent=div.parentNode;
if(!div.id)
div.id='_z'+(++uid)
if(!parent.id)
parent.id='_y'+uid
div.setAttribute('data-parent',parent.id)
div.setAttribute('data-index',indexOf(div,parent.childNodes))
divcontainer.appendChild(divs[i])
}
var html=this.outerHTML+'\n\r<script>('+this._transformer.toString()+')();</script>'
this._transformer();
return html
},
_transformer(){
var a=document.querySelector('#divcontainer')
for(var divs=a.childNodes, i=divs.length-1;i>-1;i--){
var div=divs[i],
parentId=div.getAttribute('data-parent'),
index=parseInt(div.getAttribute('data-index')),
parent=document.querySelector('#'+parentId);
parent.insertBefore(div,parent.childNodes[index])
}
a.parentNode.removeChild(a)
}
});
function indexOf(el, els){
for(var i=els.length-1;i>0;i--)
if(el==els[i])
return i
return 0
}
(opt && opt.container || document.body).appendChild(root);
root.body=root
return root
})();
return (function mixin(doc){
var stylesheet=doc.createStyleSheet()
var relStyles={}, styles={}
return Object.assign(selfConverter[$.isNode ? 'nodefy' : 'browserify'](doc,stylesheet, opt),{
createStyle(selector){
if(styles[selector])
return styles[selector]
var rules=stylesheet.cssRules,len=rules.length
stylesheet.insertRule(selector.split(',').map(function(a){
return a.trim()[0]=='#' ? a : '#'+this.id+' '+a
}.bind(this)).join(',')+'{}',len)
return styles[selector]=stylesheet.cssRules[len].style
},
stylePath(a, parent){
if(parent)
return relStyles[a]=parent
var paths=[a],parent=a
while(parent=relStyles[parent])
paths.unshift(parent)
return paths.join(' ')
},
release(){
delete this.section
this._release()
}
})
})(doc)
})($.isNode ? createDocument() : document)
}
static nodefy(doc, stylesheet, opt){
return Object.assign(doc,{
_release(){
},
asImageURL(buffer){
if(opt && typeof(opt.asImageURL)!='undefined')
return opt.asImageURL(buffer)
return "image://notsupport"
},
asZip(){
throw new Error('not support')
},
download(){
throw new Error('not support')
},
save(){
throw new Error('not support')
}
})
}
static browserify(doc, stylesheet, opt){
var Proto_Blob=(function(a){
a=URL.createObjectURL(new Blob()).split('/');
a.pop();
return a.join('/')
})(),
Reg_Proto_Blob=new RegExp(Proto_Blob+"/([\\w\\d-]+)","gi");
return Object.assign(doc,{
asZip(opt, props){
var zip=new JSZip(),hasImage=false;
var f=zip.folder('images')
Object.keys(this.images).forEach(function(a){
hasImage=true
f.file(a.split('/').pop(),this[a])
},this.images)
zip.file('props.json',JSON.stringify(props));
zip.file('main.html',hasImage ? this.toString(opt).replace(Proto_Blob,'images') : this.toString())
return zip
},
download(opt, props){
var a=document.createElement("a")
document.body.appendChild(a)
a.href=URL.createObjectURL(this.asZip(opt,props).generate({type:'blob'}))
a.download=(props.name||"document")+'.zip'
a.click()
URL.revokeObjectURL(a.href)
document.body.removeChild(a)
},
save(opt, props){
var hasImage=false, images={}, me=this;
return $.Deferred.when((this.images && Object.keys(this.images)||[]).map(function(a){
hasImage=true
return opt.saveImage(this[a],props)
.then(function(url){return images[a]=url})
},this.images))
.then(function(){
var html=me.toString(opt, props);
if(hasImage)
html=html.replace(Reg_Proto_Blob,function(a,id){return images[a]});
return opt.saveHtml(html, props)
})
},
images:{},
asImageURL(arrayBuffer){
var url=URL.createObjectURL(new Blob([arrayBuffer],
{type:"image/"+(typeof(arrayBuffer)=='string' ? 'svg+xml' : '*')}));
this.images[url]=arrayBuffer
return url
},
_release(){
Object.keys(this.images).forEach(function(b){
URL.revokeObjectURL(b)
})
delete this.images
}
})
}
}
(function(isNode, m){
if(!isNode) return;
createDocument=require(m).jsdom
let window=createDocument().defaultView
global.btoa=window.btoa
CSSStyleDeclaration=window.CSSStyleDeclaration
})($.isNode, "jsdom")