From 5f7a0f2ad27437423516d41fd8bc45bc57dc8b54 Mon Sep 17 00:00:00 2001 From: Pieter Sheth-Voss Date: Sun, 1 Mar 2015 23:18:10 -0500 Subject: [PATCH] Removed dependence on cheerio, jquery; added xmlbuilder; --- bits/90_utils.js | 349 ------------------------------------- bits/91_xmlbuilder.js | 74 ++++++++ bits/92_stylebuilder.js | 370 ++++++++++++++++++++++++++++++++++++++++ lab/wb/xl/styles-1.xml | 0 xlsx.js | 341 +++++++++++++++++++++++------------- 5 files changed, 663 insertions(+), 471 deletions(-) create mode 100644 bits/91_xmlbuilder.js create mode 100644 bits/92_stylebuilder.js create mode 100644 lab/wb/xl/styles-1.xml diff --git a/bits/90_utils.js b/bits/90_utils.js index 7b05f4a..b4cd099 100644 --- a/bits/90_utils.js +++ b/bits/90_utils.js @@ -197,355 +197,6 @@ var utils = { sheet_to_row_object_array: sheet_to_row_object_array }; -if ((typeof 'module' != 'undefined' && typeof require != 'undefined') || (typeof $ != 'undefined')) { - var StyleBuilder = function (options) { - - if(typeof module !== "undefined" && typeof require !== 'undefined' ) { - var cheerio = require('cheerio'); - createElement = function(str) { return cheerio(cheerio(str, null, null, {xmlMode: true})); }; - } - else if (typeof jQuery !== 'undefined' || typeof $ !== 'undefined') { - createElement = function(str) { - return $($.parseXML(str).documentElement); - } //http://stackoverflow.com/a/11719466 - } - else { - createElement = function() { } // this class should never have been instantiated - } - var customNumFmtId = 164; - - var table_fmt = { - 0: 'General', - 1: '0', - 2: '0.00', - 3: '#,##0', - 4: '#,##0.00', - 9: '0%', - 10: '0.00%', - 11: '0.00E+00', - 12: '# ?/?', - 13: '# ??/??', - 14: 'm/d/yy', - 15: 'd-mmm-yy', - 16: 'd-mmm', - 17: 'mmm-yy', - 18: 'h:mm AM/PM', - 19: 'h:mm:ss AM/PM', - 20: 'h:mm', - 21: 'h:mm:ss', - 22: 'm/d/yy h:mm', - 37: '#,##0 ;(#,##0)', - 38: '#,##0 ;[Red](#,##0)', - 39: '#,##0.00;(#,##0.00)', - 40: '#,##0.00;[Red](#,##0.00)', - 45: 'mm:ss', - 46: '[h]:mm:ss', - 47: 'mmss.0', - 48: '##0.0E+0', - 49: '@', - 56: '"上午/下午 "hh"時"mm"分"ss"秒 "', - 65535: 'General' - }; - var fmt_table = {}; - - for (var idx in table_fmt) { - fmt_table[table_fmt[idx]] = idx; - - } - - - var baseXmlprefix = ''; - var baseXml = - '\ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - '; - - _hashIndex = {}; - _listIndex = []; - - return { - - initialize: function (options) { - if (typeof cheerio !== 'undefined') { - this.$styles = cheerio.load(baseXml, {xmlMode: true}); - this.$styles.find = function(q) { return this(q)} - } - else { - this.$styles = $($.parseXML(baseXml).documentElement); - } - - - // need to specify styles at index 0 and 1. - // the second style MUST be gray125 for some reason - - var defaultStyle = options.defaultCellStyle; - if (!defaultStyle) defaultStyle = { - font: {name: 'Calibri', sz: '11'}, - fill: { fgColor: { patternType: "none"}}, - border: {}, - numFmt: null - }; - if (!defaultStyle.border) { defaultStyle.border = {}} - - var gray125Style = JSON.parse(JSON.stringify(defaultStyle)); - gray125Style.fill = { fgColor: { patternType: "gray125"}} - - this.addStyles([defaultStyle, gray125Style]); - return this; - }, - - // create a style entry and returns an integer index that can be used in the cell .s property - // these format of this object follows the emerging Common Spreadsheet Format - addStyle: function (attributes) { - var attributes = this._duckTypeStyle(attributes); - var hashKey = JSON.stringify(attributes); - var index = _hashIndex[hashKey]; - if (index == undefined) { - index = this._addXf(attributes || {}); - _hashIndex[hashKey] = index; - - } - else { - index = _hashIndex[hashKey]; - } - return index; - }, - - // create style entries and returns array of integer indexes that can be used in cell .s property - addStyles: function (styles) { - var self = this; - return styles.map(function (style) { - return self.addStyle(style); - }) - }, - - _duckTypeStyle: function(attributes) { - - if (typeof attributes == 'object' && (attributes.patternFill || attributes.fgColor)) { - return {fill: attributes }; // this must be read via XLSX.parseFile(...) - } - else if (attributes.font || attributes.numFmt || attributes.border || attributes.fill) { - return attributes; - } - else { - return this._getStyleCSS(attributes) - } - }, - - _getStyleCSS: function(css) { - return css; //TODO - }, - - // Create an record for the style as well as corresponding , , , - // Right now this is simple and creates a , , , for every - // We could perhaps get fancier and avoid duplicating auxiliary entries as Excel presumably intended, but bother. - _addXf: function (attributes) { - - - var fontId = this._addFont(attributes.font); - var fillId = this._addFill(attributes.fill); - var borderId = this._addBorder(attributes.border); - var numFmtId = this._addNumFmt(attributes.numFmt); - - var $xf = createElement('') - .attr("numFmtId", numFmtId) - .attr("fontId", fontId) - .attr("fillId", fillId) - .attr("borderId", 0) - .attr("xfId", "0"); - - if (fontId > 0) { - $xf.attr('applyFont', "1"); - } - if (fillId > 0) { - $xf.attr('applyFill', "1"); - } - if (borderId > 0) { - $xf.attr('applyBorder', "1"); - } - if (numFmtId > 0) { - $xf.attr('applyNumberFormat', "1"); - } - - if (attributes.alignment) { - var $alignment = createElement(''); - if (attributes.alignment.horizontal) { $alignment.attr('horizontal', attributes.alignment.horizontal);} - if (attributes.alignment.vertical) { $alignment.attr('vertical', attributes.alignment.vertical);} - if (attributes.alignment.indent) { $alignment.attr('indent', attributes.alignment.indent);} - if (attributes.alignment.wrapText) { $alignment.attr('wrapText', attributes.alignment.wrapText);} - $xf.append($alignment).attr('applyAlignment',1) - - } - - var $cellXfs = this.$styles.find('cellXfs'); - - $cellXfs.append($xf); - var count = +$cellXfs.attr('count') + 1; - - $cellXfs.attr('count', count); - return count - 1; - }, - - _addFont: function (attributes) { - if (!attributes) { - return 0; - } - - var $font = createElement('', null, null, {xmlMode: true}); - - $font.append(createElement('').attr('val', attributes.sz)) - .append(createElement('').attr('theme', '1')) - .append(createElement('').attr('val', attributes.name)) -// .append(createElement('').attr('val', '2')) -// .append(createElement('').attr('val', 'minor')); - - if (attributes.bold) $font.append(''); - if (attributes.underline) $font.append(''); - if (attributes.italic) $font.append(''); - - if (attributes.color) { - if (attributes.color.theme) { - $font.append(createElement('').attr('theme', attributes.color.theme)); - } else if (attributes.color.rgb) { - $font.append(createElement('').attr('rgb', attributes.color.rgb)); - } - } - - - var $fonts = this.$styles.find('fonts'); - $fonts.append($font); - - var count = $fonts.children().length; - $fonts.attr('count', count); - return count - 1; - }, - - _addNumFmt: function (numFmt) { - if (!numFmt) { - return 0; - } - - if (typeof numFmt == 'string') { - var numFmtIdx = fmt_table[numFmt]; - if (numFmtIdx >= 0) { - return numFmtIdx; // we found a match against built in formats - } - } - - if (numFmt == +numFmt) { - return numFmt; // we're matching an integer against some known code - } - - var $numFmt = createElement('', null, null, {xmlMode: true}) - .attr("numFmtId", ++customNumFmtId ) - .attr("formatCode", numFmt); - - var $numFmts = this.$styles.find('numFmts'); - $numFmts.append($numFmt); - - var count = $numFmts.children().length; - $numFmts.attr('count', count); - return customNumFmtId; - }, - - _addFill: function (attributes) { - - if (!attributes) { - return 0; - } - var $patternFill = createElement('', null, null, {xmlMode: true}) - .attr('patternType', attributes.patternType || 'solid'); - - if (attributes.fgColor) { - //Excel doesn't like it when we set both rgb and theme+tint, but xlsx.parseFile() sets both - //var $fgColor = createElement('', null, null, {xmlMode: true}).attr(attributes.fgColor) - if (attributes.fgColor.rgb) { - - if (attributes.fgColor.rgb.length == 6) { - attributes.fgColor.rgb = "FF" + attributes.fgColor.rgb /// add alpha to an RGB as Excel expects aRGB - } - var $fgColor = createElement('', null, null, {xmlMode: true}). - attr('rgb', attributes.fgColor.rgb); - $patternFill.append($fgColor); - } - else if (attributes.fgColor.theme) { - var $fgColor = createElement('', null, null, {xmlMode: true}); - $fgColor.attr('theme', attributes.fgColor.theme); - if (attributes.fgColor.tint) { - $fgColor.attr('tint', attributes.fgColor.tint); - } - $patternFill.append($fgColor); - } - - if (!attributes.bgColor) { - attributes.bgColor = { "indexed": "64"} - } - } - - if (attributes.bgColor) { - var $bgColor = createElement('', null, null, {xmlMode: true}).attr(attributes.bgColor); - $patternFill.append($bgColor); - } - - var $fill = createElement('') - .append($patternFill); - - this.$styles.find('fills').append($fill); - var $fills = this.$styles.find('fills') - $fills.append($fill); - - var count = $fills.children().length; - $fills.attr('count', count); - return count - 1; - }, - - _addBorder: function (attributes) { - if (!attributes) { - return 0; - } - var $border = createElement('') - .append('') - .append('') - .append('') - .append('') - .append(''); - - var $borders = this.$styles.find('borders'); - $borders.append($border); - - var count = $borders.children().length; - $borders.attr('count', count); - return count; - }, - - toXml: function () { - if (this.$styles.find('numFmts').children().length == 0) { - this.$styles.find('numFmts').remove(); - } - if (this.$styles.xml) { return this.$styles.xml(); } - else { - var s = new XMLSerializer(); //http://stackoverflow.com/a/5744268 - return baseXmlprefix + s.serializeToString(this.$styles[0]);; - } - } - }.initialize(options||{}); - } -} diff --git a/bits/91_xmlbuilder.js b/bits/91_xmlbuilder.js new file mode 100644 index 0000000..b2e4e26 --- /dev/null +++ b/bits/91_xmlbuilder.js @@ -0,0 +1,74 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////// +var XmlNode = (function () { + function XmlNode(tagName, attributes, children) { + + if (!(this instanceof XmlNode)) { + return new XmlNode(tagName, attributes, children); + } + this.tagName = tagName; + this._attributes = attributes || {}; + this._children = children || []; + this._prefix = ''; + return this; + } + + XmlNode.prototype.createElement = function () { + return new XmlNode(arguments) + } + + XmlNode.prototype.children = function() { + return this._children; + } + + XmlNode.prototype.append = function (node) { + this._children.push(node); + return this; + } + + XmlNode.prototype.prefix = function (prefix) { + if (arguments.length==0) { return this._prefix;} + this._prefix = prefix; + return this; + } + + XmlNode.prototype.attr = function (attr, value) { + if (arguments.length == 0) { + return this._attributes; + } + else if (typeof attr == 'string' && arguments.length == 1) { + return this._attributes.attr[attr]; + } + if (typeof attr == 'object' && arguments.length == 1) { + for (var key in attr) { + this._attributes[key] = attr[key]; + } + } + else if (arguments.length == 2 && typeof attr == 'string') { + this._attributes[attr] = value; + } + return this; + } + + XmlNode.prototype.toXml = function (node) { + if (!node) node = this; + var xml = node._prefix; + xml += '<' + node.tagName; + if (node._attributes) { + for (var key in node._attributes) { + xml += ' ' + key + '="' + node._attributes[key] + '"' + } + } + if (node._children && node._children.length > 0) { + xml += ">"; + for (var i = 0; i < node._children.length; i++) { + xml += this.toXml(node._children[i]); + } + xml += ''; + } + else { + xml += '/>'; + } + return xml; + } + return XmlNode; +})(); diff --git a/bits/92_stylebuilder.js b/bits/92_stylebuilder.js new file mode 100644 index 0000000..7a96132 --- /dev/null +++ b/bits/92_stylebuilder.js @@ -0,0 +1,370 @@ +///////////////////////////////////////////////////////////////////////////////////////////////////// +if ((typeof 'module' != 'undefined' && typeof require != 'undefined') || (typeof $ != 'undefined')) { + var StyleBuilder = function (options) { + + var customNumFmtId = 164; + + + + var table_fmt = { + 0: 'General', + 1: '0', + 2: '0.00', + 3: '#,##0', + 4: '#,##0.00', + 9: '0%', + 10: '0.00%', + 11: '0.00E+00', + 12: '# ?/?', + 13: '# ??/??', + 14: 'm/d/yy', + 15: 'd-mmm-yy', + 16: 'd-mmm', + 17: 'mmm-yy', + 18: 'h:mm AM/PM', + 19: 'h:mm:ss AM/PM', + 20: 'h:mm', + 21: 'h:mm:ss', + 22: 'm/d/yy h:mm', + 37: '#,##0 ;(#,##0)', + 38: '#,##0 ;[Red](#,##0)', + 39: '#,##0.00;(#,##0.00)', + 40: '#,##0.00;[Red](#,##0.00)', + 45: 'mm:ss', + 46: '[h]:mm:ss', + 47: 'mmss.0', + 48: '##0.0E+0', + 49: '@', + 56: '"上午/下午 "hh"時"mm"分"ss"秒 "', + 65535: 'General' + }; + var fmt_table = {}; + + for (var idx in table_fmt) { + fmt_table[table_fmt[idx]] = idx; + } + + + + + + // cache style specs to avoid excessive duplication + _hashIndex = {}; + _listIndex = []; + + +// console.log(this.$styles.toXml()); +// process.exit() +// var baseXmlprefix = ''; +// var baseXml = +// '\ +// \ +// \ +// \ +// \ +// \ +// \ +// \ +// \ +// \ +// \ +// \ +// \ +// \ +// \ +// \ +// '; + + return { + + initialize: function (options) { + + this.$fonts = XmlNode('fonts').attr('count',0).attr("x14ac:knownFonts","1"); + this.$fills = XmlNode('fills').attr('count',0); + this.$borders = XmlNode('borders').attr('count',0); + this.$numFmts = XmlNode('numFmts').attr('count',0); + this.$cellStyleXfs = XmlNode('cellStyleXfs'); + this.$xf = XmlNode('xf') + .attr('numFmtId', 0) + .attr('fontId', 0) + .attr('fillId', 0) + .attr('borderId', 0); + + this.$cellXfs = XmlNode('cellXfs').attr('count',0); + this.$cellStyles = XmlNode('cellStyles') + .append(XmlNode('cellStyle') + .attr('name', 'Normal') + .attr('xfId',0) + .attr('builtinId',0) + ); + this.$dxfs = XmlNode('dxfs').attr('count', "0"); + this.$tableStyles = XmlNode('tableStyles') + .attr('count','0') + .attr('defaultTableStyle','TableStyleMedium9') + .attr('defaultPivotStyle','PivotStyleMedium4') + + + this.$styles = XmlNode('styleSheet') + .attr('xmlns:mc','http://schemas.openxmlformats.org/markup-compatibility/2006') + .attr('xmlns:x14ac','http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac') + .attr('xmlns','http://schemas.openxmlformats.org/spreadsheetml/2006/main') + .attr('mc:Ignorable','x14ac') + .prefix('') + .append(this.$numFmts) + .append(this.$fonts) + .append(this.$fills) + .append(this.$borders) + .append(this.$cellStyleXfs.append(this.$xf)) + .append(this.$cellXfs) + .append(this.$cellStyles) + .append(this.$dxfs) + .append(this.$tableStyles); + + + // need to specify styles at index 0 and 1. + // the second style MUST be gray125 for some reason + + var defaultStyle = options.defaultCellStyle || {}; + if (!defaultStyle.font) defaultStyle.font = {name: 'Calibri', sz: '11'}; + if (!defaultStyle.font.name) defaultStyle.font.name = 'Calibri'; + if (!defaultStyle.font.sz) defaultStyle.font.sz = 11; + if (!defaultStyle.fill) defaultStyle.fill = { fgColor: { patternType: "none"}}; + if (!defaultStyle.border) defaultStyle.border = {}; + if (!defaultStyle.numFmt) defaultStyle.numFmt = 0; + + this.defaultStyle = defaultStyle; + + var gray125Style = JSON.parse(JSON.stringify(defaultStyle)); + gray125Style.fill = { fgColor: { patternType: "gray125"}} + + this.addStyles([defaultStyle, gray125Style]); + return this; + }, + + // create a style entry and returns an integer index that can be used in the cell .s property + // these format of this object follows the emerging Common Spreadsheet Format + addStyle: function (attributes) { + + var hashKey = JSON.stringify(attributes); + var index = _hashIndex[hashKey]; + if (index == undefined) { + + index = this._addXf(attributes); //_listIndex.push(attributes) -1; + _hashIndex[hashKey] = index; + } + else { + index = _hashIndex[hashKey]; + } + console.log(index, hashKey.substr(0,20)) + return index; + }, + + // create style entries and returns array of integer indexes that can be used in cell .s property + addStyles: function (styles) { + var self = this; + return styles.map(function (style) { + return self.addStyle(style); + }) + }, + + _duckTypeStyle: function(attributes) { + + if (typeof attributes == 'object' && (attributes.patternFill || attributes.fgColor)) { + return {fill: attributes }; // this must be read via XLSX.parseFile(...) + } + else if (attributes.font || attributes.numFmt || attributes.border || attributes.fill) { + return attributes; + } + else { + return this._getStyleCSS(attributes) + } + }, + + _getStyleCSS: function(css) { + return css; //TODO + }, + + // Create an record for the style as well as corresponding , , , + // Right now this is simple and creates a , , , for every + // We could perhaps get fancier and avoid duplicating auxiliary entries as Excel presumably intended, but bother. + _addXf: function (attributes) { + + + var fontId = this._addFont(attributes.font); + var fillId = this._addFill(attributes.fill); + var borderId = this._addBorder(attributes.border); + var numFmtId = this._addNumFmt(attributes.numFmt); + + var $xf = XmlNode('xf') + .attr("numFmtId", numFmtId) + .attr("fontId", fontId) + .attr("fillId", fillId) + .attr("borderId", 0) + .attr("xfId", "0"); + + if (fontId > 0) { + $xf.attr('applyFont', "1"); + } + if (fillId > 0) { + $xf.attr('applyFill', "1"); + } + if (borderId > 0) { + $xf.attr('applyBorder', "1"); + } + if (numFmtId > 0) { + $xf.attr('applyNumberFormat', "1"); + } + + if (attributes.alignment) { + var $alignment = XmlNode('alignment'); + if (attributes.alignment.horizontal) { $alignment.attr('horizontal', attributes.alignment.horizontal);} + if (attributes.alignment.vertical) { $alignment.attr('vertical', attributes.alignment.vertical);} + if (attributes.alignment.indent) { $alignment.attr('indent', attributes.alignment.indent);} + if (attributes.alignment.wrapText) { $alignment.attr('wrapText', attributes.alignment.wrapText);} + $xf.append($alignment).attr('applyAlignment',1) + + } + + this.$cellXfs.append($xf); + var count = +this.$cellXfs.children().length; + + this.$cellXfs.attr('count', count); + return count - 1; + }, + + _addFont: function (attributes) { + + if (!attributes) { return 0; } + + var $font = XmlNode('font') + .append(XmlNode('sz').attr('val', attributes.sz || this.defaultStyle.font.sz)) + .append(XmlNode('name').attr('val', attributes.name || this.defaultStyle.font.name)) + + if (attributes.bold) $font.append(XmlNode('b')); + if (attributes.underline) $font.append(XmlNode('u')); + if (attributes.italic) $font.append(XmlNode('i')); + + + if (attributes.color) { + if (attributes.color.theme) { + $font.append(XmlNode('color').attr('theme', attributes.color.theme)) + + if (attributes.color.tint) { //tint only if theme + $font.append(XmlNode('tint').attr('theme', attributes.color.tint)) + } + + } else if (attributes.color.rgb) { // not both rgb and theme + $font.append(XmlNode('rgb').attr('theme', attributes.color.rgb)) + } + } + + this.$fonts.append($font); + + var count = this.$fonts.children().length; + this.$fonts.attr('count', count); + return count - 1; + }, + + _addNumFmt: function (numFmt) { + if (!numFmt) { return 0; } + + if (typeof numFmt == 'string') { + var numFmtIdx = fmt_table[numFmt]; + if (numFmtIdx >= 0) { + return numFmtIdx; // we found a match against built in formats + } + } + + if (numFmt == +numFmt) { + return numFmt; // we're matching an integer against some known code + } + + var $numFmt = XmlNode(numFmt) + .attr('numFmtId', (++customNumFmtId)) + .attr('formatCode', numFmt); + + + this.$numFmts.append($numFmt); + + var count = this.$numFmts.children().length; + this.$numFmts.attr('count', count); + return customNumFmtId; + }, + + _addFill: function (attributes) { + + if (!attributes) { return 0; } + + var $patternFill = XmlNode('patternFill') + .attr('patternType', attributes.patternType || 'solid'); + + if (attributes.fgColor) { + var $fgColor = XmlNode('fgColor'); + + //Excel doesn't like it when we set both rgb and theme+tint, but xlsx.parseFile() sets both + //var $fgColor = createElement('', null, null, {xmlMode: true}).attr(attributes.fgColor) + if (attributes.fgColor.rgb) { + + if (attributes.fgColor.rgb.length == 6) { + attributes.fgColor.rgb = "FF" + attributes.fgColor.rgb /// add alpha to an RGB as Excel expects aRGB + } + + $fgColor.attr('rgb', attributes.fgColor.rgb); + $patternFill.append($fgColor); + } + else if (attributes.fgColor.theme) { + $fgColor.attr('theme', attributes.fgColor.theme); + if (attributes.fgColor.tint) { + $fgColor.attr('tint', attributes.fgColor.tint); + } + $patternFill.append($fgColor); + } + + if (!attributes.bgColor) { + attributes.bgColor = { "indexed": "64"} + } + } + + if (attributes.bgColor) { + var $bgColor = XmlNode('bgColor').attr(attributes.bgColor); + $patternFill.append($bgColor); + } + + var $fill = XmlNode('fill') + .append($patternFill); + + this.$fills.append($fill); + + var count = this.$fills.children().length; + this.$fills.attr('count', count); + return count - 1; + }, + + _addBorder: function (attributes) { + if (!attributes) { + return 0; + } + var $border = XmlNode('border') + .append(new XmlNode('left')) + .append(new XmlNode('right')) + .append(new XmlNode('top')) + .append(new XmlNode('bottom')) + .append(new XmlNode('diagonal')); + + this.$borders.append($border); + + var count = this.$borders.children().length; + this.$borders.attr('count', count); + return count; + }, + + toXml: function () { + var xml =this.$styles.toXml(); + console.log(xml); + return xml; + } + }.initialize(options||{}); + } +} + diff --git a/lab/wb/xl/styles-1.xml b/lab/wb/xl/styles-1.xml new file mode 100644 index 0000000..e69de29 diff --git a/xlsx.js b/xlsx.js index b11d145..8462dce 100644 --- a/xlsx.js +++ b/xlsx.js @@ -5412,26 +5412,93 @@ var utils = { sheet_to_row_object_array: sheet_to_row_object_array }; + + + +///////////////////////////////////////////////////////////////////////////////////////////////////// +var XmlNode = (function () { + function XmlNode(tagName, attributes, children) { + + if (!(this instanceof XmlNode)) { + return new XmlNode(tagName, attributes, children); + } + this.tagName = tagName; + this._attributes = attributes || {}; + this._children = children || []; + this._prefix = ''; + return this; + } + + XmlNode.prototype.createElement = function () { + return new XmlNode(arguments) + } + + XmlNode.prototype.children = function() { + return this._children; + } + + XmlNode.prototype.append = function (node) { + this._children.push(node); + return this; + } + + XmlNode.prototype.prefix = function (prefix) { + if (arguments.length==0) { return this._prefix;} + this._prefix = prefix; + return this; + } + + XmlNode.prototype.attr = function (attr, value) { + if (arguments.length == 0) { + return this._attributes; + } + else if (typeof attr == 'string' && arguments.length == 1) { + return this._attributes.attr[attr]; + } + if (typeof attr == 'object' && arguments.length == 1) { + for (var key in attr) { + this._attributes[key] = attr[key]; + } + } + else if (arguments.length == 2 && typeof attr == 'string') { + this._attributes[attr] = value; + } + return this; + } + + + + XmlNode.prototype.toXml = function (node) { + if (!node) node = this; + var xml = node._prefix; + xml += '<' + node.tagName; + if (node._attributes) { + for (var key in node._attributes) { + xml += ' ' + key + '="' + node._attributes[key] + '"' + } + } + if (node._children && node._children.length > 0) { + xml += ">"; + for (var i = 0; i < node._children.length; i++) { + xml += this.toXml(node._children[i]); + } + xml += ''; + } + else { + xml += '/>'; + } + return xml; + } + return XmlNode; +})(); +///////////////////////////////////////////////////////////////////////////////////////////////////// if ((typeof 'module' != 'undefined' && typeof require != 'undefined') || (typeof $ != 'undefined')) { var StyleBuilder = function (options) { - if(typeof module !== "undefined" && typeof require !== 'undefined' ) { - var cheerio = require('cheerio'); - createElement = function(str) { return cheerio(cheerio(str, null, null, {xmlMode: true})); }; - } - else if (typeof jQuery !== 'undefined' || typeof $ !== 'undefined') { - createElement = function(str) { - return $($.parseXML(str).documentElement); - } //http://stackoverflow.com/a/11719466 - } - else { - createElement = function() { } // this class should never have been instantiated - } - - var customNumFmtId = 164; + var table_fmt = { 0: 'General', 1: '0', @@ -5468,57 +5535,98 @@ if ((typeof 'module' != 'undefined' && typeof require != 'undefined') || (typeo for (var idx in table_fmt) { fmt_table[table_fmt[idx]] = idx; - } - var baseXmlprefix = ''; - var baseXml = - '\ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - '; + + + // cache style specs to avoid excessive duplication _hashIndex = {}; _listIndex = []; + +// console.log(this.$styles.toXml()); +// process.exit() +// var baseXmlprefix = ''; +// var baseXml = +// '\ +// \ +// \ +// \ +// \ +// \ +// \ +// \ +// \ +// \ +// \ +// \ +// \ +// \ +// \ +// \ +// '; + return { initialize: function (options) { - if (typeof cheerio !== 'undefined') { - this.$styles = cheerio.load(baseXml, {xmlMode: true}); - this.$styles.find = function(q) { return this(q)} - } - else { - this.$styles = $($.parseXML(baseXml).documentElement); - } + + this.$fonts = XmlNode('fonts').attr('count',0).attr("x14ac:knownFonts","1"); + this.$fills = XmlNode('fills').attr('count',0); + this.$borders = XmlNode('borders').attr('count',0); + this.$numFmts = XmlNode('numFmts').attr('count',0); + this.$cellStyleXfs = XmlNode('cellStyleXfs'); + this.$xf = XmlNode('xf') + .attr('numFmtId', 0) + .attr('fontId', 0) + .attr('fillId', 0) + .attr('borderId', 0); + + this.$cellXfs = XmlNode('cellXfs').attr('count',0); + this.$cellStyles = XmlNode('cellStyles') + .append(XmlNode('cellStyle') + .attr('name', 'Normal') + .attr('xfId',0) + .attr('builtinId',0) + ); + this.$dxfs = XmlNode('dxfs').attr('count', "0"); + this.$tableStyles = XmlNode('tableStyles') + .attr('count','0') + .attr('defaultTableStyle','TableStyleMedium9') + .attr('defaultPivotStyle','PivotStyleMedium4') + + + this.$styles = XmlNode('styleSheet') + .attr('xmlns:mc','http://schemas.openxmlformats.org/markup-compatibility/2006') + .attr('xmlns:x14ac','http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac') + .attr('xmlns','http://schemas.openxmlformats.org/spreadsheetml/2006/main') + .attr('mc:Ignorable','x14ac') + .prefix('') + .append(this.$numFmts) + .append(this.$fonts) + .append(this.$fills) + .append(this.$borders) + .append(this.$cellStyleXfs.append(this.$xf)) + .append(this.$cellXfs) + .append(this.$cellStyles) + .append(this.$dxfs) + .append(this.$tableStyles); // need to specify styles at index 0 and 1. // the second style MUST be gray125 for some reason - var defaultStyle = options.defaultCellStyle; - if (!defaultStyle) defaultStyle = { - font: {name: 'Calibri', sz: '11'}, - fill: { fgColor: { patternType: "none"}}, - border: {}, - numFmt: null - }; - if (!defaultStyle.border) { defaultStyle.border = {}} + var defaultStyle = options.defaultCellStyle || {}; + if (!defaultStyle.font) defaultStyle.font = {name: 'Calibri', sz: '11'}; + if (!defaultStyle.font.name) defaultStyle.font.name = 'Calibri'; + if (!defaultStyle.font.sz) defaultStyle.font.sz = 11; + if (!defaultStyle.fill) defaultStyle.fill = { fgColor: { patternType: "none"}}; + if (!defaultStyle.border) defaultStyle.border = {}; + if (!defaultStyle.numFmt) defaultStyle.numFmt = 0; + + this.defaultStyle = defaultStyle; var gray125Style = JSON.parse(JSON.stringify(defaultStyle)); gray125Style.fill = { fgColor: { patternType: "gray125"}} @@ -5530,17 +5638,18 @@ if ((typeof 'module' != 'undefined' && typeof require != 'undefined') || (typeo // create a style entry and returns an integer index that can be used in the cell .s property // these format of this object follows the emerging Common Spreadsheet Format addStyle: function (attributes) { - var attributes = this._duckTypeStyle(attributes); + var hashKey = JSON.stringify(attributes); var index = _hashIndex[hashKey]; if (index == undefined) { - index = this._addXf(attributes || {}); - _hashIndex[hashKey] = index; + index = this._addXf(attributes); //_listIndex.push(attributes) -1; + _hashIndex[hashKey] = index; } else { index = _hashIndex[hashKey]; } + console.log(index, hashKey.substr(0,20)) return index; }, @@ -5580,7 +5689,7 @@ if ((typeof 'module' != 'undefined' && typeof require != 'undefined') || (typeo var borderId = this._addBorder(attributes.border); var numFmtId = this._addNumFmt(attributes.numFmt); - var $xf = createElement('') + var $xf = XmlNode('xf') .attr("numFmtId", numFmtId) .attr("fontId", fontId) .attr("fillId", fillId) @@ -5601,7 +5710,7 @@ if ((typeof 'module' != 'undefined' && typeof require != 'undefined') || (typeo } if (attributes.alignment) { - var $alignment = createElement(''); + var $alignment = XmlNode('alignment'); if (attributes.alignment.horizontal) { $alignment.attr('horizontal', attributes.alignment.horizontal);} if (attributes.alignment.vertical) { $alignment.attr('vertical', attributes.alignment.vertical);} if (attributes.alignment.indent) { $alignment.attr('indent', attributes.alignment.indent);} @@ -5610,53 +5719,48 @@ if ((typeof 'module' != 'undefined' && typeof require != 'undefined') || (typeo } - var $cellXfs = this.$styles.find('cellXfs'); + this.$cellXfs.append($xf); + var count = +this.$cellXfs.children().length; - $cellXfs.append($xf); - var count = +$cellXfs.attr('count') + 1; - - $cellXfs.attr('count', count); + this.$cellXfs.attr('count', count); return count - 1; }, _addFont: function (attributes) { - if (!attributes) { - return 0; - } - var $font = createElement('', null, null, {xmlMode: true}); + if (!attributes) { return 0; } - $font.append(createElement('').attr('val', attributes.sz)) - .append(createElement('').attr('theme', '1')) - .append(createElement('').attr('val', attributes.name)) -// .append(createElement('').attr('val', '2')) -// .append(createElement('').attr('val', 'minor')); + var $font = XmlNode('font') + .append(XmlNode('sz').attr('val', attributes.sz || this.defaultStyle.font.sz)) + .append(XmlNode('name').attr('val', attributes.name || this.defaultStyle.font.name)) + + if (attributes.bold) $font.append(XmlNode('b')); + if (attributes.underline) $font.append(XmlNode('u')); + if (attributes.italic) $font.append(XmlNode('i')); - if (attributes.bold) $font.append(''); - if (attributes.underline) $font.append(''); - if (attributes.italic) $font.append(''); if (attributes.color) { if (attributes.color.theme) { - $font.append(createElement('').attr('theme', attributes.color.theme)); - } else if (attributes.color.rgb) { - $font.append(createElement('').attr('rgb', attributes.color.rgb)); + $font.append(XmlNode('color').attr('theme', attributes.color.theme)) + + if (attributes.color.tint) { //tint only if theme + $font.append(XmlNode('tint').attr('theme', attributes.color.tint)) + } + + } else if (attributes.color.rgb) { // not both rgb and theme + $font.append(XmlNode('rgb').attr('theme', attributes.color.rgb)) } } + this.$fonts.append($font); - var $fonts = this.$styles.find('fonts'); - $fonts.append($font); - - var count = $fonts.children().length; - $fonts.attr('count', count); + var count = this.$fonts.children().length; + this.$fonts.attr('count', count); return count - 1; }, _addNumFmt: function (numFmt) { - if (!numFmt) { - return 0; - } + if (!numFmt) { return 0; } if (typeof numFmt == 'string') { var numFmtIdx = fmt_table[numFmt]; @@ -5669,27 +5773,28 @@ if ((typeof 'module' != 'undefined' && typeof require != 'undefined') || (typeo return numFmt; // we're matching an integer against some known code } - var $numFmt = createElement('', null, null, {xmlMode: true}) - .attr("numFmtId", ++customNumFmtId ) - .attr("formatCode", numFmt); + var $numFmt = XmlNode(numFmt) + .attr('numFmtId', (++customNumFmtId)) + .attr('formatCode', numFmt); - var $numFmts = this.$styles.find('numFmts'); - $numFmts.append($numFmt); - var count = $numFmts.children().length; - $numFmts.attr('count', count); + this.$numFmts.append($numFmt); + + var count = this.$numFmts.children().length; + this.$numFmts.attr('count', count); return customNumFmtId; }, _addFill: function (attributes) { - if (!attributes) { - return 0; - } - var $patternFill = createElement('', null, null, {xmlMode: true}) + if (!attributes) { return 0; } + + var $patternFill = XmlNode('patternFill') .attr('patternType', attributes.patternType || 'solid'); if (attributes.fgColor) { + var $fgColor = XmlNode('fgColor'); + //Excel doesn't like it when we set both rgb and theme+tint, but xlsx.parseFile() sets both //var $fgColor = createElement('', null, null, {xmlMode: true}).attr(attributes.fgColor) if (attributes.fgColor.rgb) { @@ -5697,12 +5802,11 @@ if ((typeof 'module' != 'undefined' && typeof require != 'undefined') || (typeo if (attributes.fgColor.rgb.length == 6) { attributes.fgColor.rgb = "FF" + attributes.fgColor.rgb /// add alpha to an RGB as Excel expects aRGB } - var $fgColor = createElement('', null, null, {xmlMode: true}). - attr('rgb', attributes.fgColor.rgb); + + $fgColor.attr('rgb', attributes.fgColor.rgb); $patternFill.append($fgColor); } else if (attributes.fgColor.theme) { - var $fgColor = createElement('', null, null, {xmlMode: true}); $fgColor.attr('theme', attributes.fgColor.theme); if (attributes.fgColor.tint) { $fgColor.attr('tint', attributes.fgColor.tint); @@ -5716,19 +5820,17 @@ if ((typeof 'module' != 'undefined' && typeof require != 'undefined') || (typeo } if (attributes.bgColor) { - var $bgColor = createElement('', null, null, {xmlMode: true}).attr(attributes.bgColor); + var $bgColor = XmlNode('bgColor').attr(attributes.bgColor); $patternFill.append($bgColor); } - var $fill = createElement('') + var $fill = XmlNode('fill') .append($patternFill); - this.$styles.find('fills').append($fill); - var $fills = this.$styles.find('fills') - $fills.append($fill); + this.$fills.append($fill); - var count = $fills.children().length; - $fills.attr('count', count); + var count = this.$fills.children().length; + this.$fills.attr('count', count); return count - 1; }, @@ -5736,34 +5838,29 @@ if ((typeof 'module' != 'undefined' && typeof require != 'undefined') || (typeo if (!attributes) { return 0; } - var $border = createElement('') - .append('') - .append('') - .append('') - .append('') - .append(''); + var $border = XmlNode('border') + .append(new XmlNode('left')) + .append(new XmlNode('right')) + .append(new XmlNode('top')) + .append(new XmlNode('bottom')) + .append(new XmlNode('diagonal')); - var $borders = this.$styles.find('borders'); - $borders.append($border); + this.$borders.append($border); - var count = $borders.children().length; - $borders.attr('count', count); + var count = this.$borders.children().length; + this.$borders.attr('count', count); return count; }, toXml: function () { - if (this.$styles.find('numFmts').children().length == 0) { - this.$styles.find('numFmts').remove(); - } - if (this.$styles.xml) { return this.$styles.xml(); } - else { - var s = new XMLSerializer(); //http://stackoverflow.com/a/5744268 - return baseXmlprefix + s.serializeToString(this.$styles[0]);; - } + var xml =this.$styles.toXml(); + console.log(xml); + return xml; } }.initialize(options||{}); } } + XLSX.parseZip = parse_zip; XLSX.read = readSync; XLSX.readFile = readFileSync;