From 3a310bd3a7e17fb8501519b97ee36e44deb9aaa4 Mon Sep 17 00:00:00 2001 From: SheetJS Date: Sun, 2 Apr 2017 02:47:25 -0400 Subject: [PATCH] Read/Write Comments - XLSB/XLSX/XLML write comments - BIFF2 write date cells (see #586 h/t @roccomuso) - ODS read cell comments (fixed #315 h/t @yisk) - XLSX / XLSB emit empty comments when necessary - changed node detection logic (fixes #614 h/t @mhenris) - fixes #605 h/t @ylbweb - fixes #233 h/t @hanxi @osecki - fixes #192 h/t @abarik1981 @stla - fixes #183 h/t @aravindkoneru @ryangallen --- CHANGELOG.md | 2 + README.md | 22 ++- bits/05_buf.js | 2 +- bits/22_xmlutils.js | 5 + bits/24_hoppers.js | 2 + bits/28_binstructs.js | 14 +- bits/30_ctype.js | 11 +- bits/31_rels.js | 1 + bits/40_harb.js | 53 ++++++ bits/45_styutils.js | 2 +- bits/55_vml.js | 43 +++++ bits/56_cmntcommon.js | 1 + bits/57_cmntxml.js | 39 ++++- bits/58_cmntbin.js | 44 ++++- bits/66_wscommon.js | 1 + bits/67_wsxml.js | 14 +- bits/68_wsbin.js | 13 +- bits/74_xmlbin.js | 6 +- bits/75_xlml.js | 18 ++- bits/78_writebiff.js | 9 +- bits/80_parseods.js | 25 ++- bits/86_writezip.js | 29 +++- bits/88_write.js | 7 +- docbits/66_comments.md | 17 ++ docbits/73_sheetprops.md | 2 +- docbits/80_parseopts.md | 2 +- misc/docs/SUMMARY.md | 1 + test.js | 50 +++++- tests/write.js | 10 +- xlsx.flow.js | 339 +++++++++++++++++++++++++++++++++------ xlsx.js | 339 +++++++++++++++++++++++++++++++++------ 31 files changed, 971 insertions(+), 152 deletions(-) create mode 100644 bits/55_vml.js create mode 100644 docbits/66_comments.md diff --git a/CHANGELOG.md b/CHANGELOG.md index e91f3dd..1508cc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ changes may not be included if they are not expected to break existing code. ## Unreleased +* default output format changed to XLSB +* comment text line endings are now normalized ## 0.9.7 (2017-03-28) diff --git a/README.md b/README.md index dd8e2a1..8a2bf8c 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ with a unified JS representation, and ES3/ES5 browser compatibility back to IE6. + [Formulae](#formulae) + [Column Properties](#column-properties) + [Hyperlinks](#hyperlinks) + + [Cell Comments](#cell-comments) + [Sheet Visibility](#sheet-visibility) - [Parsing Options](#parsing-options) * [Input Type](#input-type) @@ -756,6 +757,23 @@ ws['A3'].l = { Target:"http://sheetjs.com", Tooltip:"Find us @ SheetJS.com!" }; Note that Excel does not automatically style hyperlinks -- they will generally be displayed as normal text. +#### Cell Comments + +Cell comments are objects stored in the `c` array of cell objects. The actual +contents of the comment are split into blocks based on the comment author. The +`a` field of each comment object is the author of the comment and the `t` field +is the plaintext representation. + +For example, the following snippet appends a cell comment into cell `A1`: + +```js +if(!ws.A1.c) ws.A1.c = []; +ws.A1.c.push({a:"SheetJS", t:"I'm a little comment, short and stout!"}); +``` + +Note: XLSB enforces a 54 character limit on the Author name. Names longer than +54 characters may cause issues with other formats. + #### Sheet Visibility Excel enables hiding sheets in the lower tab bar. The sheet data is stored in @@ -780,7 +798,7 @@ With : ``` Non-Excel formats do not support the Very Hidden state. The best way to test -if a sheet is visible is to check if the `Hidden` property is logical negation: +if a sheet is visible is to check if the `Hidden` property is logical truth: ```js > wb.Workbook.Sheets.map(function(x) { return [x.name, !x.Hidden] }) @@ -856,7 +874,7 @@ file but Excel will know how to handle it. This library applies similar logic: | `0x50` | ZIP Archive | XLSB or XLSX/M or ODS or UOS2 or plaintext | | `0x49` | Plain Text | SYLK or plaintext | | `0x54` | Plain Text | DIF or plaintext | -| `0xFE` | UTF8 Text | SpreadsheetML or Flat ODS or UOS1 or plaintext | +| `0xFE` | UTF16 Encoded | SpreadsheetML or Flat ODS or UOS1 or plaintext | DBF files are detected based on the first byte as well as the third and fourth bytes (corresponding to month and day of the file date) diff --git a/bits/05_buf.js b/bits/05_buf.js index 79dda9b..f6334a9 100644 --- a/bits/05_buf.js +++ b/bits/05_buf.js @@ -1,4 +1,4 @@ -var has_buf = (typeof Buffer !== 'undefined'); +var has_buf = (typeof Buffer !== 'undefined' && typeof process !== 'undefined' && typeof process.versions !== 'undefined' && process.versions.node); function new_raw_buf(len/*:number*/) { /* jshint -W056 */ diff --git a/bits/22_xmlutils.js b/bits/22_xmlutils.js index 970788b..944faa8 100644 --- a/bits/22_xmlutils.js +++ b/bits/22_xmlutils.js @@ -60,6 +60,9 @@ var xlml_fixstr/*:StringConv*/ = (function() { function entrepl($$/*:string*/,$1/*:string*/)/*:string*/ { return String.fromCharCode(parseInt($1,10)); } return function xlml_fixstr(str/*:string*/)/*:string*/ { return str.replace(entregex,entrepl); }; })(); +var xlml_unfixstr/*:StringConv*/ = (function() { + return function xlml_unfixstr(str/*:string*/)/*:string*/ { return str.replace(/(\r\n|[\r\n])/g,"\ "); }; +})(); function parsexmlbool(value/*:any*/, tag/*:?string*/)/*:boolean*/ { switch(value) { @@ -184,5 +187,7 @@ var XLMLNS = ({ 'x': 'urn:schemas-microsoft-com:office:excel', 'ss': 'urn:schemas-microsoft-com:office:spreadsheet', 'dt': 'uuid:C2F41010-65B3-11d1-A29F-00AA00C14882', + 'mv': 'http://macVmlSchemaUri', + 'v': 'urn:schemas-microsoft-com:vml', 'html': 'http://www.w3.org/TR/REC-html40' }/*:any*/); diff --git a/bits/24_hoppers.js b/bits/24_hoppers.js index 5771f9a..1d24cde 100644 --- a/bits/24_hoppers.js +++ b/bits/24_hoppers.js @@ -10,7 +10,9 @@ function recordhopper(data, cb/*:RecordHopperCB*/, opts/*:?any*/) { tmpbyte = data.read_shift(1); length = tmpbyte & 0x7F; for(cntbyte = 1; cntbyte <4 && (tmpbyte & 0x80); ++cntbyte) length += ((tmpbyte = data.read_shift(1)) & 0x7F)<<(7*cntbyte); + var tgt = data.l + length; var d = R.f(data, length, opts); + data.l = tgt; if(cb(d, R, RT)) return; } } diff --git a/bits/28_binstructs.js b/bits/28_binstructs.js index 3adca12..684c6bc 100644 --- a/bits/28_binstructs.js +++ b/bits/28_binstructs.js @@ -17,7 +17,7 @@ function parse_RichStr(data, length/*:number*/)/*:XLString*/ { for(var i = 0; i != dwSizeStrRun; ++i) rgsStrRun.push(parse_StrRun(data)); z.r = rgsStrRun; } - else z.r = "" + escapexml(str) + ""; + else z.r = [{ich:0, ifnt:0}]; //if((flags & 2) !== 0) { /* fExtStr */ // /* TODO: phonetic string */ //} @@ -26,10 +26,10 @@ function parse_RichStr(data, length/*:number*/)/*:XLString*/ { } function write_RichStr(str/*:XLString*/, o/*:?Block*/)/*:Block*/ { /* TODO: formatted string */ - if(o == null) o = new_buf(5+2*str.t.length); + var _null = false; if(o == null) { _null = true; o = new_buf(15+4*str.t.length); } o.write_shift(1,0); write_XLWideString(str.t, o); - return o; + return _null ? o.slice(0, o.l) : o; } /* [MS-XLSB] 2.5.9 */ @@ -59,10 +59,10 @@ function parse_XLNullableWideString(data)/*:string*/ { return cchCharacters === 0 || cchCharacters === 0xFFFFFFFF ? "" : data.read_shift(cchCharacters, 'dbcs'); } function write_XLNullableWideString(data/*:string*/, o) { - if(!o) o = new_buf(127); + var _null = false; if(o == null) { _null = true; o = new_buf(127); } o.write_shift(4, data.length > 0 ? data.length : 0xFFFFFFFF); if(data.length > 0) o.write_shift(0, data, 'dbcs'); - return o; + return _null ? o.slice(0, o.l) : o; } /* [MS-XLSB] 2.5.168 */ @@ -71,10 +71,10 @@ function parse_XLWideString(data)/*:string*/ { return cchCharacters === 0 ? "" : data.read_shift(cchCharacters, 'dbcs'); } function write_XLWideString(data/*:string*/, o) { - if(o == null) o = new_buf(4+2*data.length); + var _null = false; if(o == null) { _null = true; o = new_buf(4+2*data.length); } o.write_shift(4, data.length); if(data.length > 0) o.write_shift(0, data, 'dbcs'); - return o; + return _null ? o.slice(0, o.l) : o; } /* [MS-XLSB] 2.5.165 */ diff --git a/bits/30_ctype.js b/bits/30_ctype.js index fdc0baa..e6b72f7 100644 --- a/bits/30_ctype.js +++ b/bits/30_ctype.js @@ -23,10 +23,6 @@ var ct2type/*{[string]:string}*/ = ({ "application/vnd.openxmlformats-officedocument.customXmlProperties+xml": "TODO", "application/vnd.openxmlformats-officedocument.spreadsheetml.customProperty": "TODO", - /* Comments */ - "application/vnd.ms-excel.comments": "comments", - "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml": "comments", - /* PivotTable */ "application/vnd.ms-excel.pivotTable": "TODO", "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml": "TODO", @@ -158,6 +154,10 @@ var CT_LIST = (function(){ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml", xlsb: "application/vnd.ms-excel.sharedStrings" }, + comments: { /* Comments */ + xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml", + xlsb: "application/vnd.ms-excel.comments" + }, sheets: { /* Worksheet */ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml", xlsb: "application/vnd.ms-excel.worksheet" @@ -226,6 +226,7 @@ var CTYPE_XML_ROOT = writextag('Types', null, { var CTYPE_DEFAULTS = [ ['xml', 'application/xml'], ['bin', 'application/vnd.ms-excel.sheet.binary.macroEnabled.main'], + ['vml', 'application/vnd.openxmlformats-officedocument.vmlDrawing'], /* from test files */ ['bmp', 'image/bmp'], ['png', 'image/png'], @@ -276,7 +277,7 @@ function write_ct(ct, opts)/*:string*/ { ['strs', 'styles'].forEach(f1); ['coreprops', 'extprops', 'custprops'].forEach(f3); f3('vba'); - f3('comments'); + f2('comments'); if(o.length>2){ o[o.length] = (''); o[1]=o[1].replace("/>",">"); } return o.join(""); } diff --git a/bits/31_rels.js b/bits/31_rels.js index 4e4d4e2..aadfe9d 100644 --- a/bits/31_rels.js +++ b/bits/31_rels.js @@ -3,6 +3,7 @@ var RELS = ({ WB: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument", SHEET: "http://sheetjs.openxmlformats.org/officeDocument/2006/relationships/officeDocument", HLINK: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", + VML: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing", VBA: "http://schemas.microsoft.com/office/2006/relationships/vbaProject" }/*:any*/); diff --git a/bits/40_harb.js b/bits/40_harb.js index bb1dd9a..0f8bb5b 100644 --- a/bits/40_harb.js +++ b/bits/40_harb.js @@ -359,3 +359,56 @@ var DIF = (function() { from_sheet: sheet_to_dif }; })(); + +var PRN = (function() { + function prn_to_aoa(d/*:RawData*/, opts)/*:AOA*/ { + switch(opts.type) { + case 'base64': return prn_to_aoa_str(Base64.decode(d), opts); + case 'binary': return prn_to_aoa_str(d, opts); + case 'buffer': return prn_to_aoa_str(d.toString('binary'), opts); + case 'array': return prn_to_aoa_str(cc2str(d), opts); + } + throw new Error("Unrecognized type " + opts.type); + } + + function set_text_arr(data/*:string*/, arr/*:AOA*/, R/*:number*/, C/*:number*/) { + if(data === 'TRUE') arr[R][C] = true; + else if(data === 'FALSE') arr[R][C] = false; + else if(+data == +data) arr[R][C] = +data; + else if(data !== "") arr[R][C] = data; + } + + function prn_to_aoa_str(f/*:string*/, opts)/*:AOA*/ { + var arr/*:AOA*/ = ([]/*:any*/); + if(!f || f.length === 0) return arr; + var lines = f.split(/[\r\n]/); + var L = lines.length - 1; + while(L >= 0 && lines[L].length === 0) --L; + var start = 10, idx = 0; + var R = 0; + for(; R <= L; ++R) { + idx = lines[R].indexOf(" "); + if(idx == -1) idx = lines[R].length; else idx++; + start = Math.max(start, idx); + } + for(R = 0; R <= L; ++R) { + arr[R] = []; + /* TODO: confirm that widths are always 10 */ + var C = 0; + set_text_arr(lines[R].slice(0, start).trim(), arr, R, C); + for(C = 1; C <= (lines[R].length - start)/10 + 1; ++C) + set_text_arr(lines[R].slice(start+(C-1)*10,start+C*10).trim(),arr,R,C); + } + return arr; + } + + function prn_to_sheet(str/*:string*/, opts)/*:Worksheet*/ { return aoa_to_sheet(prn_to_aoa(str, opts), opts); } + + function prn_to_workbook(str/*:string*/, opts)/*:Workbook*/ { return sheet_to_workbook(prn_to_sheet(str, opts), opts); } + + return { + to_workbook: prn_to_workbook, + to_sheet: prn_to_sheet + }; +})(); + diff --git a/bits/45_styutils.js b/bits/45_styutils.js index a854375..674ff57 100644 --- a/bits/45_styutils.js +++ b/bits/45_styutils.js @@ -61,7 +61,7 @@ function cycle_width(collw) { return char2width(px2char(width2px(collw))); } /* XLSX/XLSB/XLS specify width in units of MDW */ function find_mdw_colw(collw) { var delta = Infinity, _MDW = MIN_MDW; - for(MDW=MIN_MDW; MDW/,">"), + writextag("o:shapelayout", writextag("o:idmap", null, {'v:ext':"edit", 'data':rId}), {'v:ext':"edit"}), + writextag("v:shapetype", [ + writextag("v:stroke", null, {joinstyle:"miter"}), + writextag("v:path", null, {gradientshapeok:"t", 'o:connecttype':"rect"}) + ].join(""), {id:"_x0000_t202", 'o:spt':202, coordsize:csize.join(","),path:bbox}) + ]; + while(_shapeid < rId * 1000) _shapeid += 1000; + + comments.map(function(x) { return decode_cell(x[0]); }).forEach(function(c,i) { o = o.concat([ + '', + writextag('v:fill', writextag("o:fill", null, {type:"gradientUnscaled", 'v:ext':"view"}), {'color2':"#BEFF82", 'angle':"-180", 'type':"gradient"}), + writextag("v:shadow", null, {on:"t", 'obscured':"t"}), + writextag("v:path", null, {'o:connecttype':"none"}), + '
', + '', + '', + '', + /* Part 4 19.4.2.3 Anchor (Anchor) */ + writetag('x:Anchor', [c.c, 0, c.r, 0, c.c+3, 100, c.r+5, 100].join(",")), + writetag('x:AutoFill', "False"), + writetag('x:Row', String(c.r)), + writetag('x:Column', String(c.c)), + '', + '' + ]); }); + o.push(''); + return o.join(""); +} + diff --git a/bits/56_cmntcommon.js b/bits/56_cmntcommon.js index 888cd20..c0b00a0 100644 --- a/bits/56_cmntcommon.js +++ b/bits/56_cmntcommon.js @@ -1,3 +1,4 @@ +RELS.CMNT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"; function parse_comments(zip, dirComments, sheets, sheetRels, opts) { for(var i = 0; i != dirComments.length; ++i) { diff --git a/bits/57_cmntxml.js b/bits/57_cmntxml.js index 57429ce..fc999d7 100644 --- a/bits/57_cmntxml.js +++ b/bits/57_cmntxml.js @@ -1,5 +1,6 @@ -/* 18.7.3 CT_Comment */ +/* 18.7 Comments */ function parse_comments_xml(data/*:string*/, opts)/*:Array*/ { + /* 18.7.6 CT_Comments */ if(data.match(/<(?:\w+:)?comments *\/>/)) return []; var authors = []; var commentList = []; @@ -15,19 +16,43 @@ function parse_comments_xml(data/*:string*/, opts)/*:Array*/ { var cm = x.match(/<(?:\w+:)?comment[^>]*>/); if(!cm) return; var y = parsexmltag(cm[0]); - var comment/*:Comment*/ = ({ author: y.authorId && authors[y.authorId] ? authors[y.authorId] : undefined, ref: y.ref, guid: y.guid }/*:any*/); + var comment/*:Comment*/ = ({ author: y.authorId && authors[y.authorId] ? authors[y.authorId] : "sheetjsghost", ref: y.ref, guid: y.guid }/*:any*/); var cell = decode_cell(y.ref); if(opts.sheetRows && opts.sheetRows <= cell.r) return; var textMatch = x.match(/<(?:\w+:)?text>([^\u2603]*)<\/(?:\w+:)?text>/); - if (!textMatch || !textMatch[1]) return; // a comment may contain an empty text tag. - var rt = parse_si(textMatch[1]); - if(!rt) return; + var rt = (!textMatch || !textMatch[1]) ? {r:"",t:"",h:""} : parse_si(textMatch[1]); comment.r = rt.r; - comment.t = rt.t; + comment.t = rt.t.replace(/\r\n/g,"\n").replace(/\r/g,"\n"); if(opts.cellHTML) comment.h = rt.h; commentList.push(comment); }); return commentList; } -function write_comments_xml(data, opts) { } +var CMNT_XML_ROOT = writextag('comments', null, { 'xmlns': XMLNS.main[0] }); +function write_comments_xml(data, opts) { + var o = [XML_HEADER, CMNT_XML_ROOT]; + + var iauthor = []; + o.push(""); + data.map(function(x) { return x[1]; }).forEach(function(comment) { + comment.map(function(x) { return escapexml(x.a); }).forEach(function(a) { + if(iauthor.indexOf(a) > -1) return; + iauthor.push(a); + o.push("" + a + ""); + }); + }); + o.push(""); + o.push(""); + data.forEach(function(d) { + d[1].forEach(function(c) { + /* 18.7.3 CT_Comment */ + o.push(''); + o.push(writetag("t", c.t)); + o.push(''); + }); + }); + o.push(""); + if(o.length>2) { o[o.length] = (''); o[1]=o[1].replace("/>",">"); } + return o.join(""); +} diff --git a/bits/58_cmntbin.js b/bits/58_cmntbin.js index 51579f2..b8be3ba 100644 --- a/bits/58_cmntbin.js +++ b/bits/58_cmntbin.js @@ -8,6 +8,16 @@ function parse_BrtBeginComment(data, length) { data.l += 16; /*var guid = parse_GUID(data); */ return out; } +function write_BrtBeginComment(data, o) { + if(o == null) o = new_buf(36); + o.write_shift(4, data[1].iauthor); + write_UncheckedRfX(data[0], o); + o.write_shift(4, 0); + o.write_shift(4, 0); + o.write_shift(4, 0); + o.write_shift(4, 0); + return o; +} /* [MS-XLSB] 2.4.324 BrtCommentAuthor */ var parse_BrtCommentAuthor = parse_XLWideString; @@ -30,6 +40,7 @@ function parse_comments_bin(data, opts) { c.author = authors[c.iauthor]; delete c.iauthor; if(opts.sheetRows && opts.sheetRows <= c.rfx.r) break; + if(!c.t) c.t = ""; delete c.rfx; out.push(c); break; case 'BrtBeginComments': break; case 'BrtEndComments': break; @@ -43,4 +54,35 @@ function parse_comments_bin(data, opts) { return out; } -function write_comments_bin(data, opts) { } +function write_comments_bin(data, opts) { + var ba = buf_array(); + var iauthor = []; + write_record(ba, "BrtBeginComments"); + { /* COMMENTAUTHORS */ + write_record(ba, "BrtBeginCommentAuthors"); + data.forEach(function(comment) { + comment[1].forEach(function(c) { + if(iauthor.indexOf(c.a) > -1) return; + iauthor.push(c.a.substr(0,54)); + write_record(ba, "BrtCommentAuthor", write_XLWideString(c.a.substr(0, 54))); + }); + }); + write_record(ba, "BrtEndCommentAuthors"); + } + { /* COMMENTLIST */ + write_record(ba, "BrtBeginCommentList"); + data.forEach(function(comment) { + comment[1].forEach(function(c) { + c.iauthor = iauthor.indexOf(c.a); + var range = {s:decode_cell(comment[0])}; range.e = range.s; + write_record(ba, "BrtBeginComment", write_BrtBeginComment([range, c])); + if(c.t && c.t.length > 0) write_record(ba, "BrtCommentText", write_RichStr(c)); + write_record(ba, "BrtEndComment"); + delete c.iauthor; + }); + }); + write_record(ba, "BrtEndCommentList"); + } + write_record(ba, "BrtEndComments"); + return ba.end(); +} diff --git a/bits/66_wscommon.js b/bits/66_wscommon.js index 73182aa..7a26a03 100644 --- a/bits/66_wscommon.js +++ b/bits/66_wscommon.js @@ -20,6 +20,7 @@ function col_obj_w(C/*:number*/, col) { else if(col.wpx != null) width = px2char(col.wpx); else if(col.wch != null) width = col.wch; if(width > -1) { p.width = char2width(width); p.customWidth = 1; } + else p.width = col.width; return p; } diff --git a/bits/67_wsxml.js b/bits/67_wsxml.js index 711c4f3..6013fe4 100644 --- a/bits/67_wsxml.js +++ b/bits/67_wsxml.js @@ -156,6 +156,7 @@ function write_ws_xml_cell(cell, ref, ws, opts, idx, wb) { v = writextag('f', escapexml(cell.f), ff) + (cell.v != null ? v : ""); } if(cell.l) ws['!links'].push([ref, cell.l]); + if(cell.c) ws['!comments'].push([ref, cell.c]); return writextag('c', v, o); } @@ -330,6 +331,7 @@ function write_ws_xml(idx/*:number*/, opts, wb/*:Workbook*/, rels)/*:string*/ { if(ws === undefined) ws = {}; var ref = ws['!ref']; if(ref === undefined) ref = 'A1'; if(!rels) rels = {}; + ws['!comments'] = []; o[o.length] = (writextag('sheetPr', null, {'codeName': escapexml(wb.SheetNames[idx])})); o[o.length] = (writextag('dimension', null, {'ref': ref})); @@ -348,12 +350,12 @@ function write_ws_xml(idx/*:number*/, opts, wb/*:Workbook*/, rels)/*:string*/ { if(ws['!merges'] != null && ws['!merges'].length > 0) o[o.length] = (write_ws_xml_merges(ws['!merges'])); - var relc = -1, rel; + var relc = -1, rel, rId = -1; if(ws['!links'].length > 0) { o[o.length] = ""; ws['!links'].forEach(function(l) { if(!l[1].Target) return; - var rId = add_rels(rels, -1, escapexml(l[1].Target).replace(/#.*$/, ""), RELS.HLINK); + rId = add_rels(rels, -1, escapexml(l[1].Target).replace(/#.*$/, ""), RELS.HLINK); rel = ({"ref":l[0], "r:id":"rId"+rId}/*:any*/); if((relc = l[1].Target.indexOf("#")) > -1) rel.location = escapexml(l[1].Target.substr(relc+1)); if(l[1].Tooltip) rel.tooltip = escapexml(l[1].Tooltip); @@ -362,6 +364,14 @@ function write_ws_xml(idx/*:number*/, opts, wb/*:Workbook*/, rels)/*:string*/ { o[o.length] = ""; } delete ws['!links']; + + if(ws['!comments'].length > 0) { + rId = add_rels(rels, -1, "../drawings/vmlDrawing" + (idx+1) + ".vml", RELS.VML); + o[o.length] = writextag("legacyDrawing", null, {"r:id":"rId" + rId}); + ws['!legacy'] = rId; + } +// + if(o.length>2) { o[o.length] = (''); o[1]=o[1].replace("/>",">"); } return o.join(""); } diff --git a/bits/68_wsbin.js b/bits/68_wsbin.js index 2a2b271..08381fe 100644 --- a/bits/68_wsbin.js +++ b/bits/68_wsbin.js @@ -570,6 +570,7 @@ function write_ws_bin_cell(ba/*:BufArray*/, cell/*:Cell*/, R/*:number*/, C/*:num /* TODO: cell style */ //o.s = get_cell_style(opts.cellXfs, cell, opts); if(cell.l) ws['!links'].push([encode_cell(o), cell.l]); + if(cell.c) ws['!comments'].push([encode_cell(o), cell.c]); switch(cell.t) { case 's': case 'str': if(opts.bookSST) { @@ -639,12 +640,22 @@ function write_HLINKS(ba, ws/*:Worksheet*/, rels) { }); delete ws['!links']; } +function write_LEGACYDRAWING(ba, ws/*:Worksheet*/, idx/*:number*/, rels) { + /* [BrtLegacyDrawing] */ + if(ws['!comments'].length > 0) { + var rId = add_rels(rels, -1, "../drawings/vmlDrawing" + (idx+1) + ".vml", RELS.VML); + write_record(ba, "BrtLegacyDrawing", write_RelID("rId" + rId)); + ws['!legacy'] = rId; + } +} function write_ws_bin(idx/*:number*/, opts, wb/*:Workbook*/, rels) { var ba = buf_array(); var s = wb.SheetNames[idx], ws = wb.Sheets[s] || {}; var r = safe_decode_range(ws['!ref'] || "A1"); ws['!links'] = []; + /* passed back to write_zip and removed there */ + ws['!comments'] = []; write_record(ba, "BrtBeginSheet"); write_record(ba, "BrtWsProp", write_BrtWsProp(s)); write_record(ba, "BrtWsDim", write_BrtWsDim(r)); @@ -676,7 +687,7 @@ function write_ws_bin(idx/*:number*/, opts, wb/*:Workbook*/, rels) { /* [IGNOREECS] */ /* [SMARTTAGS] */ /* [BrtDrawing] */ - /* [BrtLegacyDrawing] */ + write_LEGACYDRAWING(ba, ws, idx, rels); /* [BrtLegacyDrawingHF] */ /* [BrtBkHim] */ /* [OLEOBJECTS] */ diff --git a/bits/74_xmlbin.js b/bits/74_xmlbin.js index 5cee155..c5d549b 100644 --- a/bits/74_xmlbin.js +++ b/bits/74_xmlbin.js @@ -62,11 +62,11 @@ function write_sty(data, name/*:string*/, opts) { function write_sst(data/*:SST*/, name/*:string*/, opts) { return (name.slice(-4)===".bin" ? write_sst_bin : write_sst_xml)(data, opts); } -/* -function write_cmnt(data, name:string, opts) { + +function write_cmnt(data/*:Array*/, name/*:string*/, opts) { return (name.slice(-4)===".bin" ? write_comments_bin : write_comments_xml)(data, opts); } - +/* function write_cc(data, name:string, opts) { return (name.slice(-4)===".bin" ? write_cc_bin : write_cc_xml)(data, opts); } diff --git a/bits/75_xlml.js b/bits/75_xlml.js index fbc73b1..de76639 100644 --- a/bits/75_xlml.js +++ b/bits/75_xlml.js @@ -120,7 +120,7 @@ function parse_xlml_data(xml, ss, data, cell/*:any*/, base, styles, csty, row, a if(!cell.t) cell.t = 'n'; break; case 'Error': cell.t = 'e'; cell.v = RBErr[xml]; cell.w = xml; break; - default: cell.t = 's'; cell.v = xlml_fixstr(ss); break; + default: cell.t = 's'; cell.v = xlml_fixstr(ss||xml); break; } safe_format_xlml(cell, nf, o); if(o.cellFormula != null) { @@ -129,7 +129,7 @@ function parse_xlml_data(xml, ss, data, cell/*:any*/, base, styles, csty, row, a /* strictly speaking, the leading = is required but some writers omit */ if(fstr.charCodeAt(0) == 61 /* = */) fstr = fstr.substr(1); cell.f = rc_to_a1(fstr, base); - cell.Formula = undefined; + delete cell.Formula; if(cell.ArrayRange == "RC") cell.F = rc_to_a1("RC:RC", base); else if(cell.ArrayRange) { cell.F = rc_to_a1(cell.ArrayRange, base); @@ -152,7 +152,8 @@ function parse_xlml_data(xml, ss, data, cell/*:any*/, base, styles, csty, row, a } function xlml_clean_comment(comment/*:any*/) { - comment.t = comment.v; + comment.t = comment.v || ""; + comment.t = comment.t.replace(/\r\n/g,"\n").replace(/\r/g,"\n"); comment.v = comment.w = comment.ixfe = undefined; } @@ -803,7 +804,14 @@ function write_ws_xlml_wsopts(ws/*:Worksheet*/, opts, idx/*:number*/, wb/*:Workb if(o.length == 0) return ""; return writextag("WorksheetOptions", o.join(""), {xmlns:XLMLNS.x}); } -/* TODO */ +function write_ws_xlml_comment(comments) { + return comments.map(function(c) { + // TODO: formatted text + var t = xlml_unfixstr(c.t||""); + var d =writextag("ss:Data", t, {"xmlns":"http://www.w3.org/TR/REC-html40"}); + return writextag("Comment", d, {"ss:Author":c.a}); + }).join(""); +} function write_ws_xlml_cell(cell, ref, ws, opts, idx, wb, addr)/*:string*/{ if(!cell || cell.v == undefined && cell.f == undefined) return ""; @@ -846,6 +854,8 @@ function write_ws_xlml_cell(cell, ref, ws, opts, idx, wb, addr)/*:string*/{ } var m = '' + _v + ''; + if((cell.c||[]).length > 0) m += write_ws_xlml_comment(cell.c); + return writextag("Cell", m, attr); } /* TODO */ diff --git a/bits/78_writebiff.js b/bits/78_writebiff.js index 6e4b439..259c41c 100644 --- a/bits/78_writebiff.js +++ b/bits/78_writebiff.js @@ -58,11 +58,12 @@ function write_BIFF2LABEL(r, c, val) { function write_ws_biff_cell(ba/*:BufArray*/, cell/*:Cell*/, R/*:number*/, C/*:number*/, opts) { if(cell.v != null) switch(cell.t) { - case 'n': - if((cell.v == (cell.v|0)) && (cell.v >= 0) && (cell.v < 65536)) - write_biff_rec(ba, 0x0002, write_BIFF2INT(R, C, cell.v)); + case 'd': case 'n': + var v = cell.t == 'd' ? datenum(cell.v) : cell.v; + if((v == (v|0)) && (v >= 0) && (v < 65536)) + write_biff_rec(ba, 0x0002, write_BIFF2INT(R, C, v)); else - write_biff_rec(ba, 0x0003, write_BIFF2NUMBER(R,C, cell.v)); + write_biff_rec(ba, 0x0003, write_BIFF2NUMBER(R,C, v)); return; case 'b': case 'e': write_biff_rec(ba, 0x0005, write_BIFF2BERR(R, C, cell.v, cell.t)); return; /* TODO: codepage, sst */ diff --git a/bits/80_parseods.js b/bits/80_parseods.js index 7d9fa71..696b8d3 100644 --- a/bits/80_parseods.js +++ b/bits/80_parseods.js @@ -32,6 +32,8 @@ var parse_content_xml = (function() { var number_format_map = {}; var merges = [], mrange = {}, mR = 0, mC = 0; var arrayf = []; + var comments = [], comment = {}; + var creator = "", creatoridx = 0; var rept = 1, isstub = false; var i = 0; xlmlregex.lastIndex = 0; @@ -77,6 +79,7 @@ var parse_content_xml = (function() { if(C < range.s.c) range.s.c = C; if(R < range.s.r) range.s.r = R; ctag = parsexmltag(Rn[0], false); + comments = []; comment = {}; q = ({t:ctag['数据类型'] || ctag['value-type'], v:null/*:: , z:null, w:""*/}/*:any*/); if(opts.cellFormula) { if(ctag.formula) ctag.formula = unescapexml(ctag.formula); @@ -126,6 +129,7 @@ var parse_content_xml = (function() { q.v = textp || ''; isstub = textpidx == 0; } + if(comments.length > 0) { q.c = comments; comments = []; } if(textp) q.w = textp; if(!isstub || opts.sheetStubs) { if(!(opts.sheetRows && opts.sheetRows < R)) { @@ -150,6 +154,23 @@ var parse_content_xml = (function() { else if(Rn[0].charAt(Rn[0].length-2) !== '/') state.push([Rn[3], true]); break; + case 'annotation': // 14.1 + if(Rn[1]==='/'){ + if((tmp=state.pop())[0]!==Rn[3]) throw "Bad state: "+tmp; + comment.t = textp; + comment.a = creator; + comments.push(comment); + } + else if(Rn[0].charAt(Rn[0].length-2) !== '/') {state.push([Rn[3], false]);} + creator = ""; creatorpidx = 0; + textp = ""; textpidx = 0; + break; + + case 'creator': // 4.3.2.7 + if(Rn[1]==='/') { creator = str.slice(creatoridx,Rn.index); } + else creatoridx = Rn.index + Rn[0].length; + break; + /* ignore state */ case 'meta': case '元数据': // TODO: FODS/UOF case 'settings': // TODO: @@ -165,7 +186,6 @@ var parse_content_xml = (function() { case 'list-style': // 16.30 case 'form': // 13.13 case 'dde-links': // 9.8 - case 'annotation': // 14.1 case 'event-listeners': // TODO if(Rn[1]==='/'){if((tmp=state.pop())[0]!==Rn[3]) throw "Bad state: "+tmp;} else if(Rn[0].charAt(Rn[0].length-2) !== '/') state.push([Rn[3], false]); @@ -272,7 +292,7 @@ var parse_content_xml = (function() { case 'span': break; // case 'line-break': break; // 6.1.5 case 'p': case '文本串': - if(Rn[1]==='/') textp = parse_text_p(str.slice(textpidx,Rn.index), textptag); + if(Rn[1]==='/') textp = (textp.length > 0 ? textp + "\n" : "") + parse_text_p(str.slice(textpidx,Rn.index), textptag); else { textptag = parsexmltag(Rn[0], false); textpidx = Rn.index + Rn[0].length; } break; // case 's': break; // @@ -326,7 +346,6 @@ var parse_content_xml = (function() { case 'event-listener': // TODO /* TODO: FODS Properties */ case 'initial-creator': - case 'creator': case 'creation-date': case 'generator': case 'document-statistic': diff --git a/bits/86_writezip.js b/bits/86_writezip.js index 9a6e664..0978da4 100644 --- a/bits/86_writezip.js +++ b/bits/86_writezip.js @@ -1,4 +1,5 @@ function write_zip(wb/*:Workbook*/, opts/*:WriteOpts*/)/*:ZIP*/ { + _shapeid = 1024; if(opts.bookType == "ods") return write_ods(wb, opts); if(wb && !wb.SSF) { wb.SSF = SSF.get_table(); @@ -57,8 +58,26 @@ function write_zip(wb/*:Workbook*/, opts/*:WriteOpts*/)/*:ZIP*/ { f = "xl/worksheets/sheet" + rId + "." + wbext; var wsrels = {'!id':{}}; zip.file(f, write_ws(rId-1, f, opts, wb, wsrels)); + ct.sheets.push(f); - add_rels(opts.wbrels, rId, "worksheets/sheet" + rId + "." + wbext, RELS.WS[0]); + add_rels(opts.wbrels, -1, "worksheets/sheet" + rId + "." + wbext, RELS.WS[0]); + + var ws = wb.Sheets[wb.SheetNames[rId-1]]; + if(ws) { + var comments = ws['!comments']; + if(comments && comments.length > 0) { + var cf = "xl/comments" + rId + "." + wbext; + zip.file(cf, write_cmnt(comments, cf, opts)); + ct.comments.push(cf); + add_rels(wsrels, -1, "../comments" + rId + "." + wbext, RELS.CMNT); + } + if(ws['!legacy']) { + zip.file("xl/drawings/vmlDrawing" + (rId) + ".vml", write_comments_vml(rId, ws['!comments'])); + } + delete ws['!comments']; + delete ws['!legacy']; + } + if(wsrels['!id'].rId1) zip.file(get_rels_path(f), write_rels(wsrels)); // get_rels_path('') } @@ -66,7 +85,7 @@ function write_zip(wb/*:Workbook*/, opts/*:WriteOpts*/)/*:ZIP*/ { f = "xl/sharedStrings." + wbext; zip.file(f, write_sst(opts.Strings, f, opts)); ct.strs.push(f); - add_rels(opts.wbrels, ++rId, "sharedStrings." + wbext, RELS.SST); + add_rels(opts.wbrels, -1, "sharedStrings." + wbext, RELS.SST); } /* TODO: something more intelligent with themes */ @@ -74,20 +93,20 @@ function write_zip(wb/*:Workbook*/, opts/*:WriteOpts*/)/*:ZIP*/ { f = "xl/theme/theme1.xml"; zip.file(f, write_theme(wb.Themes, opts)); ct.themes.push(f); - add_rels(opts.wbrels, ++rId, "theme/theme1.xml", RELS.THEME); + add_rels(opts.wbrels, -1, "theme/theme1.xml", RELS.THEME); /* TODO: something more intelligent with styles */ f = "xl/styles." + wbext; zip.file(f, write_sty(wb, f, opts)); ct.styles.push(f); - add_rels(opts.wbrels, ++rId, "styles." + wbext, RELS.STY); + add_rels(opts.wbrels, -1, "styles." + wbext, RELS.STY); if(wb.vbaraw && vbafmt) { f = "xl/vbaProject.bin"; zip.file(f, wb.vbaraw); ct.vba.push(f); - add_rels(opts.wbrels, ++rId, "vbaProject.bin", RELS.VBA); + add_rels(opts.wbrels, -1, "vbaProject.bin", RELS.VBA); } zip.file("[Content_Types].xml", write_ct(ct, opts)); diff --git a/bits/88_write.js b/bits/88_write.js index 7092727..5f82b55 100644 --- a/bits/88_write.js +++ b/bits/88_write.js @@ -45,7 +45,7 @@ function write_binary_type(out, opts/*:WriteOpts*/) { function writeSync(wb/*:Workbook*/, opts/*:?WriteOpts*/) { check_wb(wb); var o = opts||{}; - switch(o.bookType || 'xlsx') { + switch(o.bookType || 'xlsb') { case 'xml': case 'xlml': return write_string_type(write_xlml(wb, o), o); case 'slk': @@ -63,21 +63,20 @@ function writeSync(wb/*:Workbook*/, opts/*:?WriteOpts*/) { } function resolve_book_type(o/*?WriteFileOpts*/) { - if(!o.bookType) switch(o.file.slice(-5).toLowerCase()) { + if(!o.bookType) switch(o.file.slice(o.file.lastIndexOf(".")).toLowerCase()) { case '.xlsx': o.bookType = 'xlsx'; break; case '.xlsm': o.bookType = 'xlsm'; break; case '.xlsb': o.bookType = 'xlsb'; break; case '.fods': o.bookType = 'fods'; break; case '.xlml': o.bookType = 'xlml'; break; case '.sylk': o.bookType = 'sylk'; break; - default: switch(o.file.slice(-4).toLowerCase()) { case '.xls': o.bookType = 'biff2'; break; case '.xml': o.bookType = 'xml'; break; case '.ods': o.bookType = 'ods'; break; case '.csv': o.bookType = 'csv'; break; case '.dif': o.bookType = 'dif'; break; case '.slk': o.bookType = 'sylk'; break; - }} + } } function writeFileSync(wb/*:Workbook*/, filename/*:string*/, opts/*:?WriteFileOpts*/) { diff --git a/docbits/66_comments.md b/docbits/66_comments.md new file mode 100644 index 0000000..a8f28d6 --- /dev/null +++ b/docbits/66_comments.md @@ -0,0 +1,17 @@ +#### Cell Comments + +Cell comments are objects stored in the `c` array of cell objects. The actual +contents of the comment are split into blocks based on the comment author. The +`a` field of each comment object is the author of the comment and the `t` field +is the plaintext representation. + +For example, the following snippet appends a cell comment into cell `A1`: + +```js +if(!ws.A1.c) ws.A1.c = []; +ws.A1.c.push({a:"SheetJS", t:"I'm a little comment, short and stout!"}); +``` + +Note: XLSB enforces a 54 character limit on the Author name. Names longer than +54 characters may cause issues with other formats. + diff --git a/docbits/73_sheetprops.md b/docbits/73_sheetprops.md index 05e97c9..601d91a 100644 --- a/docbits/73_sheetprops.md +++ b/docbits/73_sheetprops.md @@ -22,7 +22,7 @@ With : ``` Non-Excel formats do not support the Very Hidden state. The best way to test -if a sheet is visible is to check if the `Hidden` property is logical negation: +if a sheet is visible is to check if the `Hidden` property is logical truth: ```js > wb.Workbook.Sheets.map(function(x) { return [x.name, !x.Hidden] }) diff --git a/docbits/80_parseopts.md b/docbits/80_parseopts.md index 3136e27..988b1fd 100644 --- a/docbits/80_parseopts.md +++ b/docbits/80_parseopts.md @@ -67,7 +67,7 @@ file but Excel will know how to handle it. This library applies similar logic: | `0x50` | ZIP Archive | XLSB or XLSX/M or ODS or UOS2 or plaintext | | `0x49` | Plain Text | SYLK or plaintext | | `0x54` | Plain Text | DIF or plaintext | -| `0xFE` | UTF8 Text | SpreadsheetML or Flat ODS or UOS1 or plaintext | +| `0xFE` | UTF16 Encoded | SpreadsheetML or Flat ODS or UOS1 or plaintext | DBF files are detected based on the first byte as well as the third and fourth bytes (corresponding to month and day of the file date) diff --git a/misc/docs/SUMMARY.md b/misc/docs/SUMMARY.md index f319ff4..bb62cbe 100644 --- a/misc/docs/SUMMARY.md +++ b/misc/docs/SUMMARY.md @@ -26,6 +26,7 @@ + [Formulae](README.md#formulae) + [Column Properties](README.md#column-properties) + [Hyperlinks](README.md#hyperlinks) + + [Cell Comments](README.md#cell-comments) + [Sheet Visibility](README.md#sheet-visibility) - [Parsing Options](README.md#parsing-options) * [Input Type](README.md#input-type) diff --git a/test.js b/test.js index fcc043c..35e181c 100644 --- a/test.js +++ b/test.js @@ -44,6 +44,7 @@ var paths = { cstxml: dir + 'comments_stress_test.xls.xml', cstxlsx: dir + 'comments_stress_test.xlsx', cstxlsb: dir + 'comments_stress_test.xlsb', + cstods: dir + 'comments_stress_test.ods', fstxls: dir + 'formula_stress_test.xls', fstxml: dir + 'formula_stress_test.xls.xml', fstxlsx: dir + 'formula_stress_test.xlsx', @@ -221,6 +222,21 @@ describe('should parse test files', function() { }); }); +/* comments_stress_test family */ +function check_comments(wb) { + var ws0 = wb.Sheets.Sheet2; + assert.equal(ws0.A1.c[0].a, 'Author'); + assert.equal(ws0.A1.c[0].t, 'Author:\nGod thinks this is good'); + assert.equal(ws0.C1.c[0].a, 'Author'); + assert.equal(ws0.C1.c[0].t, 'I really hope that xlsx decides not to use magic like rPr'); + + var ws3 = wb.Sheets.Sheet4; + assert.equal(ws3.B1.c[0].a, 'Author'); + assert.equal(ws3.B1.c[0].t, 'The next comment is empty'); + assert.equal(ws3.B2.c[0].a, 'Author'); + assert.equal(ws3.B2.c[0].t, ''); +} + describe('parse options', function() { var html_cell_types = ['s']; var bef = (function() { @@ -664,12 +680,27 @@ describe('parse features', function() { [wb1,wb2,wb3,wb4].map(function(wb) { return wb.Sheets[sheet]; }).forEach(function(ws, i) { assert.equal(ws.B1.c.length, 1,"must have 1 comment"); assert.equal(ws.B1.c[0].a, "Yegor Kozlov","must have the same author"); - assert.equal(ws.B1.c[0].t.replace(/\r\n/g,"\n").replace(/\r/g,"\n"), "Yegor Kozlov:\nfirst cell", "must have the concatenated texts"); + assert.equal(ws.B1.c[0].t, "Yegor Kozlov:\nfirst cell", "must have the concatenated texts"); if(i > 0) return; assert.equal(ws.B1.c[0].r, 'Yegor Kozlov:\r\nfirst cell', "must have the rich text representation"); assert.equal(ws.B1.c[0].h, 'Yegor Kozlov:
first cell
', "must have the html representation"); }); }); + [ + ['xlsx', paths.cstxlsx], + ['xlsb', paths.cstxlsb], + ['xls', paths.cstxls], + ['xlml', paths.cstxml], + ['ods', paths.cstods] + ].forEach(function(m) { it(m[0] + ' stress test', function() { + var wb = X.readFile(m[1]); + check_comments(wb); + var ws0 = wb.Sheets.Sheet2; + assert.equal(ws0.A1.c[0].a, 'Author'); + assert.equal(ws0.A1.c[0].t, 'Author:\nGod thinks this is good'); + assert.equal(ws0.C1.c[0].a, 'Author'); + assert.equal(ws0.C1.c[0].t, 'I really hope that xlsx decides not to use magic like rPr'); + }); }); }); describe('should parse core properties and custom properties', function() { @@ -1043,6 +1074,7 @@ describe('roundtrip features', function() { }); }); }); + describe('should preserve sheet visibility', function() { [ ['xlml', paths.svxml], ['xlsx', paths.svxlsx], @@ -1061,6 +1093,22 @@ describe('roundtrip features', function() { }); }); }); + + describe('should preserve cell comments', function() { [ + ['xlsx', paths.cstxlsx], + ['xlsb', paths.cstxlsb], + //['xls', paths.cstxlsx], + ['xlml', paths.cstxml] + //['ods', paths.cstods] + ].forEach(function(w) { + it(w[0], function() { + var wb1 = X.readFile(w[1]); + var wb2 = X.read(X.write(wb1, {bookType:w[0], type:"buffer"}), {type:"buffer"}); + check_comments(wb1); + check_comments(wb2); + }); + }); + }); }); function password_file(x){return x.match(/^password.*\.xls$/); } diff --git a/tests/write.js b/tests/write.js index 2ebf6fd..e30c49a 100644 --- a/tests/write.js +++ b/tests/write.js @@ -77,9 +77,6 @@ ws['A3'].l = { Target: "http://sheetjs.com", Tooltip: "Visit us " //ws['B2'].z = "0.0"; wb.SSF[60] = "0.0"; // Custom console.log("JSON Data: "); console.log(XLSX.utils.sheet_to_json(ws, {header:1})); -console.log("Worksheet Model:") -console.log(ws); - /* TEST: hidden sheets */ wb.SheetNames.push("Hidden"); wb.Sheets["Hidden"] = XLSX.utils.aoa_to_sheet(["Hidden".split(""), [1,2,3]]); @@ -100,6 +97,13 @@ wb.Props = { CreatedDate: new Date(2017,1,19) } +/* TEST: comments */ +ws['A4'].c = []; +ws['A4'].c.push({a:"SheetJS",t:"I'm a little comment, short and stout!"}); + +console.log("Worksheet Model:") +console.log(ws); + /* write file */ XLSX.writeFile(wb, 'sheetjs.xlsx', {bookSST:true}); XLSX.writeFile(wb, 'sheetjs.xlsm'); diff --git a/xlsx.flow.js b/xlsx.flow.js index 09622b5..53e5cd2 100644 --- a/xlsx.flow.js +++ b/xlsx.flow.js @@ -77,7 +77,7 @@ var Base64 = (function make_b64(){ } }; })(); -var has_buf = (typeof Buffer !== 'undefined'); +var has_buf = (typeof Buffer !== 'undefined' && typeof process !== 'undefined' && typeof process.versions !== 'undefined' && process.versions.node); function new_raw_buf(len/*:number*/) { /* jshint -W056 */ @@ -1608,6 +1608,9 @@ var xlml_fixstr/*:StringConv*/ = (function() { function entrepl($$/*:string*/,$1/*:string*/)/*:string*/ { return String.fromCharCode(parseInt($1,10)); } return function xlml_fixstr(str/*:string*/)/*:string*/ { return str.replace(entregex,entrepl); }; })(); +var xlml_unfixstr/*:StringConv*/ = (function() { + return function xlml_unfixstr(str/*:string*/)/*:string*/ { return str.replace(/(\r\n|[\r\n])/g,"\ "); }; +})(); function parsexmlbool(value/*:any*/, tag/*:?string*/)/*:boolean*/ { switch(value) { @@ -1732,6 +1735,8 @@ var XLMLNS = ({ 'x': 'urn:schemas-microsoft-com:office:excel', 'ss': 'urn:schemas-microsoft-com:office:spreadsheet', 'dt': 'uuid:C2F41010-65B3-11d1-A29F-00AA00C14882', + 'mv': 'http://macVmlSchemaUri', + 'v': 'urn:schemas-microsoft-com:vml', 'html': 'http://www.w3.org/TR/REC-html40' }/*:any*/); function read_double_le(b, idx/*:number*/)/*:number*/ { @@ -1937,7 +1942,9 @@ function recordhopper(data, cb/*:RecordHopperCB*/, opts/*:?any*/) { tmpbyte = data.read_shift(1); length = tmpbyte & 0x7F; for(cntbyte = 1; cntbyte <4 && (tmpbyte & 0x80); ++cntbyte) length += ((tmpbyte = data.read_shift(1)) & 0x7F)<<(7*cntbyte); + var tgt = data.l + length; var d = R.f(data, length, opts); + data.l = tgt; if(cb(d, R, RT)) return; } } @@ -2130,7 +2137,7 @@ function parse_RichStr(data, length/*:number*/)/*:XLString*/ { for(var i = 0; i != dwSizeStrRun; ++i) rgsStrRun.push(parse_StrRun(data)); z.r = rgsStrRun; } - else z.r = "" + escapexml(str) + ""; + else z.r = [{ich:0, ifnt:0}]; //if((flags & 2) !== 0) { /* fExtStr */ // /* TODO: phonetic string */ //} @@ -2139,10 +2146,10 @@ function parse_RichStr(data, length/*:number*/)/*:XLString*/ { } function write_RichStr(str/*:XLString*/, o/*:?Block*/)/*:Block*/ { /* TODO: formatted string */ - if(o == null) o = new_buf(5+2*str.t.length); + var _null = false; if(o == null) { _null = true; o = new_buf(15+4*str.t.length); } o.write_shift(1,0); write_XLWideString(str.t, o); - return o; + return _null ? o.slice(0, o.l) : o; } /* [MS-XLSB] 2.5.9 */ @@ -2172,10 +2179,10 @@ function parse_XLNullableWideString(data)/*:string*/ { return cchCharacters === 0 || cchCharacters === 0xFFFFFFFF ? "" : data.read_shift(cchCharacters, 'dbcs'); } function write_XLNullableWideString(data/*:string*/, o) { - if(!o) o = new_buf(127); + var _null = false; if(o == null) { _null = true; o = new_buf(127); } o.write_shift(4, data.length > 0 ? data.length : 0xFFFFFFFF); if(data.length > 0) o.write_shift(0, data, 'dbcs'); - return o; + return _null ? o.slice(0, o.l) : o; } /* [MS-XLSB] 2.5.168 */ @@ -2184,10 +2191,10 @@ function parse_XLWideString(data)/*:string*/ { return cchCharacters === 0 ? "" : data.read_shift(cchCharacters, 'dbcs'); } function write_XLWideString(data/*:string*/, o) { - if(o == null) o = new_buf(4+2*data.length); + var _null = false; if(o == null) { _null = true; o = new_buf(4+2*data.length); } o.write_shift(4, data.length); if(data.length > 0) o.write_shift(0, data, 'dbcs'); - return o; + return _null ? o.slice(0, o.l) : o; } /* [MS-XLSB] 2.5.165 */ @@ -2580,10 +2587,6 @@ var ct2type/*{[string]:string}*/ = ({ "application/vnd.openxmlformats-officedocument.customXmlProperties+xml": "TODO", "application/vnd.openxmlformats-officedocument.spreadsheetml.customProperty": "TODO", - /* Comments */ - "application/vnd.ms-excel.comments": "comments", - "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml": "comments", - /* PivotTable */ "application/vnd.ms-excel.pivotTable": "TODO", "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml": "TODO", @@ -2715,6 +2718,10 @@ var CT_LIST = (function(){ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml", xlsb: "application/vnd.ms-excel.sharedStrings" }, + comments: { /* Comments */ + xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml", + xlsb: "application/vnd.ms-excel.comments" + }, sheets: { /* Worksheet */ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml", xlsb: "application/vnd.ms-excel.worksheet" @@ -2783,6 +2790,7 @@ var CTYPE_XML_ROOT = writextag('Types', null, { var CTYPE_DEFAULTS = [ ['xml', 'application/xml'], ['bin', 'application/vnd.ms-excel.sheet.binary.macroEnabled.main'], + ['vml', 'application/vnd.openxmlformats-officedocument.vmlDrawing'], /* from test files */ ['bmp', 'image/bmp'], ['png', 'image/png'], @@ -2833,7 +2841,7 @@ function write_ct(ct, opts)/*:string*/ { ['strs', 'styles'].forEach(f1); ['coreprops', 'extprops', 'custprops'].forEach(f3); f3('vba'); - f3('comments'); + f2('comments'); if(o.length>2){ o[o.length] = (''); o[1]=o[1].replace("/>",">"); } return o.join(""); } @@ -2842,6 +2850,7 @@ var RELS = ({ WB: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument", SHEET: "http://sheetjs.openxmlformats.org/officeDocument/2006/relationships/officeDocument", HLINK: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", + VML: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing", VBA: "http://schemas.microsoft.com/office/2006/relationships/vbaProject" }/*:any*/); @@ -5109,6 +5118,59 @@ var DIF = (function() { from_sheet: sheet_to_dif }; })(); + +var PRN = (function() { + function prn_to_aoa(d/*:RawData*/, opts)/*:AOA*/ { + switch(opts.type) { + case 'base64': return prn_to_aoa_str(Base64.decode(d), opts); + case 'binary': return prn_to_aoa_str(d, opts); + case 'buffer': return prn_to_aoa_str(d.toString('binary'), opts); + case 'array': return prn_to_aoa_str(cc2str(d), opts); + } + throw new Error("Unrecognized type " + opts.type); + } + + function set_text_arr(data/*:string*/, arr/*:AOA*/, R/*:number*/, C/*:number*/) { + if(data === 'TRUE') arr[R][C] = true; + else if(data === 'FALSE') arr[R][C] = false; + else if(+data == +data) arr[R][C] = +data; + else if(data !== "") arr[R][C] = data; + } + + function prn_to_aoa_str(f/*:string*/, opts)/*:AOA*/ { + var arr/*:AOA*/ = ([]/*:any*/); + if(!f || f.length === 0) return arr; + var lines = f.split(/[\r\n]/); + var L = lines.length - 1; + while(L >= 0 && lines[L].length === 0) --L; + var start = 10, idx = 0; + var R = 0; + for(; R <= L; ++R) { + idx = lines[R].indexOf(" "); + if(idx == -1) idx = lines[R].length; else idx++; + start = Math.max(start, idx); + } + for(R = 0; R <= L; ++R) { + arr[R] = []; + /* TODO: confirm that widths are always 10 */ + var C = 0; + set_text_arr(lines[R].slice(0, start).trim(), arr, R, C); + for(C = 1; C <= (lines[R].length - start)/10 + 1; ++C) + set_text_arr(lines[R].slice(start+(C-1)*10,start+C*10).trim(),arr,R,C); + } + return arr; + } + + function prn_to_sheet(str/*:string*/, opts)/*:Worksheet*/ { return aoa_to_sheet(prn_to_aoa(str, opts), opts); } + + function prn_to_workbook(str/*:string*/, opts)/*:Workbook*/ { return sheet_to_workbook(prn_to_sheet(str, opts), opts); } + + return { + to_workbook: prn_to_workbook, + to_sheet: prn_to_sheet + }; +})(); + /* 18.4.1 charset to codepage mapping */ var CS2CP = ({ /*::[*/0/*::]*/: 1252, /* ANSI */ @@ -5609,7 +5671,7 @@ function cycle_width(collw) { return char2width(px2char(width2px(collw))); } /* XLSX/XLSB/XLS specify width in units of MDW */ function find_mdw_colw(collw) { var delta = Infinity, _MDW = MIN_MDW; - for(MDW=MIN_MDW; MDW/,">"), + writextag("o:shapelayout", writextag("o:idmap", null, {'v:ext':"edit", 'data':rId}), {'v:ext':"edit"}), + writextag("v:shapetype", [ + writextag("v:stroke", null, {joinstyle:"miter"}), + writextag("v:path", null, {gradientshapeok:"t", 'o:connecttype':"rect"}) + ].join(""), {id:"_x0000_t202", 'o:spt':202, coordsize:csize.join(","),path:bbox}) + ]; + while(_shapeid < rId * 1000) _shapeid += 1000; + + comments.map(function(x) { return decode_cell(x[0]); }).forEach(function(c,i) { o = o.concat([ + '', + writextag('v:fill', writextag("o:fill", null, {type:"gradientUnscaled", 'v:ext':"view"}), {'color2':"#BEFF82", 'angle':"-180", 'type':"gradient"}), + writextag("v:shadow", null, {on:"t", 'obscured':"t"}), + writextag("v:path", null, {'o:connecttype':"none"}), + '
', + '', + '', + '', + /* Part 4 19.4.2.3 Anchor (Anchor) */ + writetag('x:Anchor', [c.c, 0, c.r, 0, c.c+3, 100, c.r+5, 100].join(",")), + writetag('x:AutoFill', "False"), + writetag('x:Row', String(c.r)), + writetag('x:Column', String(c.c)), + '', + '' + ]); }); + o.push(''); + return o.join(""); +} + +RELS.CMNT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"; function parse_comments(zip, dirComments, sheets, sheetRels, opts) { for(var i = 0; i != dirComments.length; ++i) { @@ -6613,8 +6719,9 @@ function insertCommentsIntoSheet(sheetName, sheet, comments) { }); } -/* 18.7.3 CT_Comment */ +/* 18.7 Comments */ function parse_comments_xml(data/*:string*/, opts)/*:Array*/ { + /* 18.7.6 CT_Comments */ if(data.match(/<(?:\w+:)?comments *\/>/)) return []; var authors = []; var commentList = []; @@ -6630,22 +6737,46 @@ function parse_comments_xml(data/*:string*/, opts)/*:Array*/ { var cm = x.match(/<(?:\w+:)?comment[^>]*>/); if(!cm) return; var y = parsexmltag(cm[0]); - var comment/*:Comment*/ = ({ author: y.authorId && authors[y.authorId] ? authors[y.authorId] : undefined, ref: y.ref, guid: y.guid }/*:any*/); + var comment/*:Comment*/ = ({ author: y.authorId && authors[y.authorId] ? authors[y.authorId] : "sheetjsghost", ref: y.ref, guid: y.guid }/*:any*/); var cell = decode_cell(y.ref); if(opts.sheetRows && opts.sheetRows <= cell.r) return; var textMatch = x.match(/<(?:\w+:)?text>([^\u2603]*)<\/(?:\w+:)?text>/); - if (!textMatch || !textMatch[1]) return; // a comment may contain an empty text tag. - var rt = parse_si(textMatch[1]); - if(!rt) return; + var rt = (!textMatch || !textMatch[1]) ? {r:"",t:"",h:""} : parse_si(textMatch[1]); comment.r = rt.r; - comment.t = rt.t; + comment.t = rt.t.replace(/\r\n/g,"\n").replace(/\r/g,"\n"); if(opts.cellHTML) comment.h = rt.h; commentList.push(comment); }); return commentList; } -function write_comments_xml(data, opts) { } +var CMNT_XML_ROOT = writextag('comments', null, { 'xmlns': XMLNS.main[0] }); +function write_comments_xml(data, opts) { + var o = [XML_HEADER, CMNT_XML_ROOT]; + + var iauthor = []; + o.push(""); + data.map(function(x) { return x[1]; }).forEach(function(comment) { + comment.map(function(x) { return escapexml(x.a); }).forEach(function(a) { + if(iauthor.indexOf(a) > -1) return; + iauthor.push(a); + o.push("" + a + ""); + }); + }); + o.push(""); + o.push(""); + data.forEach(function(d) { + d[1].forEach(function(c) { + /* 18.7.3 CT_Comment */ + o.push(''); + o.push(writetag("t", c.t)); + o.push(''); + }); + }); + o.push(""); + if(o.length>2) { o[o.length] = (''); o[1]=o[1].replace("/>",">"); } + return o.join(""); +} /* [MS-XLSB] 2.4.28 BrtBeginComment */ function parse_BrtBeginComment(data, length) { var out = {}; @@ -6656,6 +6787,16 @@ function parse_BrtBeginComment(data, length) { data.l += 16; /*var guid = parse_GUID(data); */ return out; } +function write_BrtBeginComment(data, o) { + if(o == null) o = new_buf(36); + o.write_shift(4, data[1].iauthor); + write_UncheckedRfX(data[0], o); + o.write_shift(4, 0); + o.write_shift(4, 0); + o.write_shift(4, 0); + o.write_shift(4, 0); + return o; +} /* [MS-XLSB] 2.4.324 BrtCommentAuthor */ var parse_BrtCommentAuthor = parse_XLWideString; @@ -6678,6 +6819,7 @@ function parse_comments_bin(data, opts) { c.author = authors[c.iauthor]; delete c.iauthor; if(opts.sheetRows && opts.sheetRows <= c.rfx.r) break; + if(!c.t) c.t = ""; delete c.rfx; out.push(c); break; case 'BrtBeginComments': break; case 'BrtEndComments': break; @@ -6691,7 +6833,38 @@ function parse_comments_bin(data, opts) { return out; } -function write_comments_bin(data, opts) { } +function write_comments_bin(data, opts) { + var ba = buf_array(); + var iauthor = []; + write_record(ba, "BrtBeginComments"); + { /* COMMENTAUTHORS */ + write_record(ba, "BrtBeginCommentAuthors"); + data.forEach(function(comment) { + comment[1].forEach(function(c) { + if(iauthor.indexOf(c.a) > -1) return; + iauthor.push(c.a.substr(0,54)); + write_record(ba, "BrtCommentAuthor", write_XLWideString(c.a.substr(0, 54))); + }); + }); + write_record(ba, "BrtEndCommentAuthors"); + } + { /* COMMENTLIST */ + write_record(ba, "BrtBeginCommentList"); + data.forEach(function(comment) { + comment[1].forEach(function(c) { + c.iauthor = iauthor.indexOf(c.a); + var range = {s:decode_cell(comment[0])}; range.e = range.s; + write_record(ba, "BrtBeginComment", write_BrtBeginComment([range, c])); + if(c.t && c.t.length > 0) write_record(ba, "BrtCommentText", write_RichStr(c)); + write_record(ba, "BrtEndComment"); + delete c.iauthor; + }); + }); + write_record(ba, "BrtEndCommentList"); + } + write_record(ba, "BrtEndComments"); + return ba.end(); +} RELS.DS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/dialogsheet"; RELS.MS = "http://schemas.microsoft.com/office/2006/relationships/xlMacrosheet"; @@ -9031,6 +9204,7 @@ function col_obj_w(C/*:number*/, col) { else if(col.wpx != null) width = px2char(col.wpx); else if(col.wch != null) width = col.wch; if(width > -1) { p.width = char2width(width); p.customWidth = 1; } + else p.width = col.width; return p; } @@ -9240,6 +9414,7 @@ function write_ws_xml_cell(cell, ref, ws, opts, idx, wb) { v = writextag('f', escapexml(cell.f), ff) + (cell.v != null ? v : ""); } if(cell.l) ws['!links'].push([ref, cell.l]); + if(cell.c) ws['!comments'].push([ref, cell.c]); return writextag('c', v, o); } @@ -9414,6 +9589,7 @@ function write_ws_xml(idx/*:number*/, opts, wb/*:Workbook*/, rels)/*:string*/ { if(ws === undefined) ws = {}; var ref = ws['!ref']; if(ref === undefined) ref = 'A1'; if(!rels) rels = {}; + ws['!comments'] = []; o[o.length] = (writextag('sheetPr', null, {'codeName': escapexml(wb.SheetNames[idx])})); o[o.length] = (writextag('dimension', null, {'ref': ref})); @@ -9432,12 +9608,12 @@ function write_ws_xml(idx/*:number*/, opts, wb/*:Workbook*/, rels)/*:string*/ { if(ws['!merges'] != null && ws['!merges'].length > 0) o[o.length] = (write_ws_xml_merges(ws['!merges'])); - var relc = -1, rel; + var relc = -1, rel, rId = -1; if(ws['!links'].length > 0) { o[o.length] = ""; ws['!links'].forEach(function(l) { if(!l[1].Target) return; - var rId = add_rels(rels, -1, escapexml(l[1].Target).replace(/#.*$/, ""), RELS.HLINK); + rId = add_rels(rels, -1, escapexml(l[1].Target).replace(/#.*$/, ""), RELS.HLINK); rel = ({"ref":l[0], "r:id":"rId"+rId}/*:any*/); if((relc = l[1].Target.indexOf("#")) > -1) rel.location = escapexml(l[1].Target.substr(relc+1)); if(l[1].Tooltip) rel.tooltip = escapexml(l[1].Tooltip); @@ -9446,6 +9622,14 @@ function write_ws_xml(idx/*:number*/, opts, wb/*:Workbook*/, rels)/*:string*/ { o[o.length] = ""; } delete ws['!links']; + + if(ws['!comments'].length > 0) { + rId = add_rels(rels, -1, "../drawings/vmlDrawing" + (idx+1) + ".vml", RELS.VML); + o[o.length] = writextag("legacyDrawing", null, {"r:id":"rId" + rId}); + ws['!legacy'] = rId; + } +// + if(o.length>2) { o[o.length] = (''); o[1]=o[1].replace("/>",">"); } return o.join(""); } @@ -10021,6 +10205,7 @@ function write_ws_bin_cell(ba/*:BufArray*/, cell/*:Cell*/, R/*:number*/, C/*:num /* TODO: cell style */ //o.s = get_cell_style(opts.cellXfs, cell, opts); if(cell.l) ws['!links'].push([encode_cell(o), cell.l]); + if(cell.c) ws['!comments'].push([encode_cell(o), cell.c]); switch(cell.t) { case 's': case 'str': if(opts.bookSST) { @@ -10090,12 +10275,22 @@ function write_HLINKS(ba, ws/*:Worksheet*/, rels) { }); delete ws['!links']; } +function write_LEGACYDRAWING(ba, ws/*:Worksheet*/, idx/*:number*/, rels) { + /* [BrtLegacyDrawing] */ + if(ws['!comments'].length > 0) { + var rId = add_rels(rels, -1, "../drawings/vmlDrawing" + (idx+1) + ".vml", RELS.VML); + write_record(ba, "BrtLegacyDrawing", write_RelID("rId" + rId)); + ws['!legacy'] = rId; + } +} function write_ws_bin(idx/*:number*/, opts, wb/*:Workbook*/, rels) { var ba = buf_array(); var s = wb.SheetNames[idx], ws = wb.Sheets[s] || {}; var r = safe_decode_range(ws['!ref'] || "A1"); ws['!links'] = []; + /* passed back to write_zip and removed there */ + ws['!comments'] = []; write_record(ba, "BrtBeginSheet"); write_record(ba, "BrtWsProp", write_BrtWsProp(s)); write_record(ba, "BrtWsDim", write_BrtWsDim(r)); @@ -10127,7 +10322,7 @@ function write_ws_bin(idx/*:number*/, opts, wb/*:Workbook*/, rels) { /* [IGNOREECS] */ /* [SMARTTAGS] */ /* [BrtDrawing] */ - /* [BrtLegacyDrawing] */ + write_LEGACYDRAWING(ba, ws, idx, rels); /* [BrtLegacyDrawingHF] */ /* [BrtBkHim] */ /* [OLEOBJECTS] */ @@ -10815,11 +11010,11 @@ function write_sty(data, name/*:string*/, opts) { function write_sst(data/*:SST*/, name/*:string*/, opts) { return (name.slice(-4)===".bin" ? write_sst_bin : write_sst_xml)(data, opts); } -/* -function write_cmnt(data, name:string, opts) { + +function write_cmnt(data/*:Array*/, name/*:string*/, opts) { return (name.slice(-4)===".bin" ? write_comments_bin : write_comments_xml)(data, opts); } - +/* function write_cc(data, name:string, opts) { return (name.slice(-4)===".bin" ? write_cc_bin : write_cc_xml)(data, opts); } @@ -10946,7 +11141,7 @@ function parse_xlml_data(xml, ss, data, cell/*:any*/, base, styles, csty, row, a if(!cell.t) cell.t = 'n'; break; case 'Error': cell.t = 'e'; cell.v = RBErr[xml]; cell.w = xml; break; - default: cell.t = 's'; cell.v = xlml_fixstr(ss); break; + default: cell.t = 's'; cell.v = xlml_fixstr(ss||xml); break; } safe_format_xlml(cell, nf, o); if(o.cellFormula != null) { @@ -10955,7 +11150,7 @@ function parse_xlml_data(xml, ss, data, cell/*:any*/, base, styles, csty, row, a /* strictly speaking, the leading = is required but some writers omit */ if(fstr.charCodeAt(0) == 61 /* = */) fstr = fstr.substr(1); cell.f = rc_to_a1(fstr, base); - cell.Formula = undefined; + delete cell.Formula; if(cell.ArrayRange == "RC") cell.F = rc_to_a1("RC:RC", base); else if(cell.ArrayRange) { cell.F = rc_to_a1(cell.ArrayRange, base); @@ -10978,7 +11173,8 @@ function parse_xlml_data(xml, ss, data, cell/*:any*/, base, styles, csty, row, a } function xlml_clean_comment(comment/*:any*/) { - comment.t = comment.v; + comment.t = comment.v || ""; + comment.t = comment.t.replace(/\r\n/g,"\n").replace(/\r/g,"\n"); comment.v = comment.w = comment.ixfe = undefined; } @@ -11629,7 +11825,14 @@ function write_ws_xlml_wsopts(ws/*:Worksheet*/, opts, idx/*:number*/, wb/*:Workb if(o.length == 0) return ""; return writextag("WorksheetOptions", o.join(""), {xmlns:XLMLNS.x}); } -/* TODO */ +function write_ws_xlml_comment(comments) { + return comments.map(function(c) { + // TODO: formatted text + var t = xlml_unfixstr(c.t||""); + var d =writextag("ss:Data", t, {"xmlns":"http://www.w3.org/TR/REC-html40"}); + return writextag("Comment", d, {"ss:Author":c.a}); + }).join(""); +} function write_ws_xlml_cell(cell, ref, ws, opts, idx, wb, addr)/*:string*/{ if(!cell || cell.v == undefined && cell.f == undefined) return ""; @@ -11672,6 +11875,8 @@ function write_ws_xlml_cell(cell, ref, ws, opts, idx, wb, addr)/*:string*/{ } var m = '' + _v + ''; + if((cell.c||[]).length > 0) m += write_ws_xlml_comment(cell.c); + return writextag("Cell", m, attr); } /* TODO */ @@ -13808,11 +14013,12 @@ function write_BIFF2LABEL(r, c, val) { function write_ws_biff_cell(ba/*:BufArray*/, cell/*:Cell*/, R/*:number*/, C/*:number*/, opts) { if(cell.v != null) switch(cell.t) { - case 'n': - if((cell.v == (cell.v|0)) && (cell.v >= 0) && (cell.v < 65536)) - write_biff_rec(ba, 0x0002, write_BIFF2INT(R, C, cell.v)); + case 'd': case 'n': + var v = cell.t == 'd' ? datenum(cell.v) : cell.v; + if((v == (v|0)) && (v >= 0) && (v < 65536)) + write_biff_rec(ba, 0x0002, write_BIFF2INT(R, C, v)); else - write_biff_rec(ba, 0x0003, write_BIFF2NUMBER(R,C, cell.v)); + write_biff_rec(ba, 0x0003, write_BIFF2NUMBER(R,C, v)); return; case 'b': case 'e': write_biff_rec(ba, 0x0005, write_BIFF2BERR(R, C, cell.v, cell.t)); return; /* TODO: codepage, sst */ @@ -13953,6 +14159,8 @@ var parse_content_xml = (function() { var number_format_map = {}; var merges = [], mrange = {}, mR = 0, mC = 0; var arrayf = []; + var comments = [], comment = {}; + var creator = "", creatoridx = 0; var rept = 1, isstub = false; var i = 0; xlmlregex.lastIndex = 0; @@ -13998,6 +14206,7 @@ var parse_content_xml = (function() { if(C < range.s.c) range.s.c = C; if(R < range.s.r) range.s.r = R; ctag = parsexmltag(Rn[0], false); + comments = []; comment = {}; q = ({t:ctag['数据类型'] || ctag['value-type'], v:null/*:: , z:null, w:""*/}/*:any*/); if(opts.cellFormula) { if(ctag.formula) ctag.formula = unescapexml(ctag.formula); @@ -14047,6 +14256,7 @@ var parse_content_xml = (function() { q.v = textp || ''; isstub = textpidx == 0; } + if(comments.length > 0) { q.c = comments; comments = []; } if(textp) q.w = textp; if(!isstub || opts.sheetStubs) { if(!(opts.sheetRows && opts.sheetRows < R)) { @@ -14071,6 +14281,23 @@ var parse_content_xml = (function() { else if(Rn[0].charAt(Rn[0].length-2) !== '/') state.push([Rn[3], true]); break; + case 'annotation': // 14.1 + if(Rn[1]==='/'){ + if((tmp=state.pop())[0]!==Rn[3]) throw "Bad state: "+tmp; + comment.t = textp; + comment.a = creator; + comments.push(comment); + } + else if(Rn[0].charAt(Rn[0].length-2) !== '/') {state.push([Rn[3], false]);} + creator = ""; creatorpidx = 0; + textp = ""; textpidx = 0; + break; + + case 'creator': // 4.3.2.7 + if(Rn[1]==='/') { creator = str.slice(creatoridx,Rn.index); } + else creatoridx = Rn.index + Rn[0].length; + break; + /* ignore state */ case 'meta': case '元数据': // TODO: FODS/UOF case 'settings': // TODO: @@ -14086,7 +14313,6 @@ var parse_content_xml = (function() { case 'list-style': // 16.30 case 'form': // 13.13 case 'dde-links': // 9.8 - case 'annotation': // 14.1 case 'event-listeners': // TODO if(Rn[1]==='/'){if((tmp=state.pop())[0]!==Rn[3]) throw "Bad state: "+tmp;} else if(Rn[0].charAt(Rn[0].length-2) !== '/') state.push([Rn[3], false]); @@ -14193,7 +14419,7 @@ var parse_content_xml = (function() { case 'span': break; // case 'line-break': break; // 6.1.5 case 'p': case '文本串': - if(Rn[1]==='/') textp = parse_text_p(str.slice(textpidx,Rn.index), textptag); + if(Rn[1]==='/') textp = (textp.length > 0 ? textp + "\n" : "") + parse_text_p(str.slice(textpidx,Rn.index), textptag); else { textptag = parsexmltag(Rn[0], false); textpidx = Rn.index + Rn[0].length; } break; // case 's': break; // @@ -14247,7 +14473,6 @@ var parse_content_xml = (function() { case 'event-listener': // TODO /* TODO: FODS Properties */ case 'initial-creator': - case 'creator': case 'creation-date': case 'generator': case 'document-statistic': @@ -14665,6 +14890,7 @@ function parse_zip(zip/*:ZIP*/, opts/*:?ParseOpts*/)/*:Workbook*/ { return out; } function write_zip(wb/*:Workbook*/, opts/*:WriteOpts*/)/*:ZIP*/ { + _shapeid = 1024; if(opts.bookType == "ods") return write_ods(wb, opts); if(wb && !wb.SSF) { wb.SSF = SSF.get_table(); @@ -14723,8 +14949,26 @@ function write_zip(wb/*:Workbook*/, opts/*:WriteOpts*/)/*:ZIP*/ { f = "xl/worksheets/sheet" + rId + "." + wbext; var wsrels = {'!id':{}}; zip.file(f, write_ws(rId-1, f, opts, wb, wsrels)); + ct.sheets.push(f); - add_rels(opts.wbrels, rId, "worksheets/sheet" + rId + "." + wbext, RELS.WS[0]); + add_rels(opts.wbrels, -1, "worksheets/sheet" + rId + "." + wbext, RELS.WS[0]); + + var ws = wb.Sheets[wb.SheetNames[rId-1]]; + if(ws) { + var comments = ws['!comments']; + if(comments && comments.length > 0) { + var cf = "xl/comments" + rId + "." + wbext; + zip.file(cf, write_cmnt(comments, cf, opts)); + ct.comments.push(cf); + add_rels(wsrels, -1, "../comments" + rId + "." + wbext, RELS.CMNT); + } + if(ws['!legacy']) { + zip.file("xl/drawings/vmlDrawing" + (rId) + ".vml", write_comments_vml(rId, ws['!comments'])); + } + delete ws['!comments']; + delete ws['!legacy']; + } + if(wsrels['!id'].rId1) zip.file(get_rels_path(f), write_rels(wsrels)); // get_rels_path('') } @@ -14732,7 +14976,7 @@ function write_zip(wb/*:Workbook*/, opts/*:WriteOpts*/)/*:ZIP*/ { f = "xl/sharedStrings." + wbext; zip.file(f, write_sst(opts.Strings, f, opts)); ct.strs.push(f); - add_rels(opts.wbrels, ++rId, "sharedStrings." + wbext, RELS.SST); + add_rels(opts.wbrels, -1, "sharedStrings." + wbext, RELS.SST); } /* TODO: something more intelligent with themes */ @@ -14740,20 +14984,20 @@ function write_zip(wb/*:Workbook*/, opts/*:WriteOpts*/)/*:ZIP*/ { f = "xl/theme/theme1.xml"; zip.file(f, write_theme(wb.Themes, opts)); ct.themes.push(f); - add_rels(opts.wbrels, ++rId, "theme/theme1.xml", RELS.THEME); + add_rels(opts.wbrels, -1, "theme/theme1.xml", RELS.THEME); /* TODO: something more intelligent with styles */ f = "xl/styles." + wbext; zip.file(f, write_sty(wb, f, opts)); ct.styles.push(f); - add_rels(opts.wbrels, ++rId, "styles." + wbext, RELS.STY); + add_rels(opts.wbrels, -1, "styles." + wbext, RELS.STY); if(wb.vbaraw && vbafmt) { f = "xl/vbaProject.bin"; zip.file(f, wb.vbaraw); ct.vba.push(f); - add_rels(opts.wbrels, ++rId, "vbaProject.bin", RELS.VBA); + add_rels(opts.wbrels, -1, "vbaProject.bin", RELS.VBA); } zip.file("[Content_Types].xml", write_ct(ct, opts)); @@ -14858,7 +15102,7 @@ function write_binary_type(out, opts/*:WriteOpts*/) { function writeSync(wb/*:Workbook*/, opts/*:?WriteOpts*/) { check_wb(wb); var o = opts||{}; - switch(o.bookType || 'xlsx') { + switch(o.bookType || 'xlsb') { case 'xml': case 'xlml': return write_string_type(write_xlml(wb, o), o); case 'slk': @@ -14876,21 +15120,20 @@ function writeSync(wb/*:Workbook*/, opts/*:?WriteOpts*/) { } function resolve_book_type(o/*?WriteFileOpts*/) { - if(!o.bookType) switch(o.file.slice(-5).toLowerCase()) { + if(!o.bookType) switch(o.file.slice(o.file.lastIndexOf(".")).toLowerCase()) { case '.xlsx': o.bookType = 'xlsx'; break; case '.xlsm': o.bookType = 'xlsm'; break; case '.xlsb': o.bookType = 'xlsb'; break; case '.fods': o.bookType = 'fods'; break; case '.xlml': o.bookType = 'xlml'; break; case '.sylk': o.bookType = 'sylk'; break; - default: switch(o.file.slice(-4).toLowerCase()) { case '.xls': o.bookType = 'biff2'; break; case '.xml': o.bookType = 'xml'; break; case '.ods': o.bookType = 'ods'; break; case '.csv': o.bookType = 'csv'; break; case '.dif': o.bookType = 'dif'; break; case '.slk': o.bookType = 'sylk'; break; - }} + } } function writeFileSync(wb/*:Workbook*/, filename/*:string*/, opts/*:?WriteFileOpts*/) { diff --git a/xlsx.js b/xlsx.js index 70f0274..1cc1e4f 100644 --- a/xlsx.js +++ b/xlsx.js @@ -76,7 +76,7 @@ var Base64 = (function make_b64(){ } }; })(); -var has_buf = (typeof Buffer !== 'undefined'); +var has_buf = (typeof Buffer !== 'undefined' && typeof process !== 'undefined' && typeof process.versions !== 'undefined' && process.versions.node); function new_raw_buf(len) { /* jshint -W056 */ @@ -1559,6 +1559,9 @@ var xlml_fixstr = (function() { function entrepl($$,$1) { return String.fromCharCode(parseInt($1,10)); } return function xlml_fixstr(str) { return str.replace(entregex,entrepl); }; })(); +var xlml_unfixstr = (function() { + return function xlml_unfixstr(str) { return str.replace(/(\r\n|[\r\n])/g,"\ "); }; +})(); function parsexmlbool(value, tag) { switch(value) { @@ -1683,6 +1686,8 @@ var XLMLNS = ({ 'x': 'urn:schemas-microsoft-com:office:excel', 'ss': 'urn:schemas-microsoft-com:office:spreadsheet', 'dt': 'uuid:C2F41010-65B3-11d1-A29F-00AA00C14882', + 'mv': 'http://macVmlSchemaUri', + 'v': 'urn:schemas-microsoft-com:vml', 'html': 'http://www.w3.org/TR/REC-html40' }); function read_double_le(b, idx) { @@ -1886,7 +1891,9 @@ function recordhopper(data, cb, opts) { tmpbyte = data.read_shift(1); length = tmpbyte & 0x7F; for(cntbyte = 1; cntbyte <4 && (tmpbyte & 0x80); ++cntbyte) length += ((tmpbyte = data.read_shift(1)) & 0x7F)<<(7*cntbyte); + var tgt = data.l + length; var d = R.f(data, length, opts); + data.l = tgt; if(cb(d, R, RT)) return; } } @@ -2078,7 +2085,7 @@ function parse_RichStr(data, length) { for(var i = 0; i != dwSizeStrRun; ++i) rgsStrRun.push(parse_StrRun(data)); z.r = rgsStrRun; } - else z.r = "" + escapexml(str) + ""; + else z.r = [{ich:0, ifnt:0}]; //if((flags & 2) !== 0) { /* fExtStr */ // /* TODO: phonetic string */ //} @@ -2087,10 +2094,10 @@ function parse_RichStr(data, length) { } function write_RichStr(str, o) { /* TODO: formatted string */ - if(o == null) o = new_buf(5+2*str.t.length); + var _null = false; if(o == null) { _null = true; o = new_buf(15+4*str.t.length); } o.write_shift(1,0); write_XLWideString(str.t, o); - return o; + return _null ? o.slice(0, o.l) : o; } /* [MS-XLSB] 2.5.9 */ @@ -2120,10 +2127,10 @@ function parse_XLNullableWideString(data) { return cchCharacters === 0 || cchCharacters === 0xFFFFFFFF ? "" : data.read_shift(cchCharacters, 'dbcs'); } function write_XLNullableWideString(data, o) { - if(!o) o = new_buf(127); + var _null = false; if(o == null) { _null = true; o = new_buf(127); } o.write_shift(4, data.length > 0 ? data.length : 0xFFFFFFFF); if(data.length > 0) o.write_shift(0, data, 'dbcs'); - return o; + return _null ? o.slice(0, o.l) : o; } /* [MS-XLSB] 2.5.168 */ @@ -2132,10 +2139,10 @@ function parse_XLWideString(data) { return cchCharacters === 0 ? "" : data.read_shift(cchCharacters, 'dbcs'); } function write_XLWideString(data, o) { - if(o == null) o = new_buf(4+2*data.length); + var _null = false; if(o == null) { _null = true; o = new_buf(4+2*data.length); } o.write_shift(4, data.length); if(data.length > 0) o.write_shift(0, data, 'dbcs'); - return o; + return _null ? o.slice(0, o.l) : o; } /* [MS-XLSB] 2.5.165 */ @@ -2528,10 +2535,6 @@ var ct2type/*{[string]:string}*/ = ({ "application/vnd.openxmlformats-officedocument.customXmlProperties+xml": "TODO", "application/vnd.openxmlformats-officedocument.spreadsheetml.customProperty": "TODO", - /* Comments */ - "application/vnd.ms-excel.comments": "comments", - "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml": "comments", - /* PivotTable */ "application/vnd.ms-excel.pivotTable": "TODO", "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml": "TODO", @@ -2663,6 +2666,10 @@ var CT_LIST = (function(){ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml", xlsb: "application/vnd.ms-excel.sharedStrings" }, + comments: { /* Comments */ + xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml", + xlsb: "application/vnd.ms-excel.comments" + }, sheets: { /* Worksheet */ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml", xlsb: "application/vnd.ms-excel.worksheet" @@ -2731,6 +2738,7 @@ var CTYPE_XML_ROOT = writextag('Types', null, { var CTYPE_DEFAULTS = [ ['xml', 'application/xml'], ['bin', 'application/vnd.ms-excel.sheet.binary.macroEnabled.main'], + ['vml', 'application/vnd.openxmlformats-officedocument.vmlDrawing'], /* from test files */ ['bmp', 'image/bmp'], ['png', 'image/png'], @@ -2781,7 +2789,7 @@ function write_ct(ct, opts) { ['strs', 'styles'].forEach(f1); ['coreprops', 'extprops', 'custprops'].forEach(f3); f3('vba'); - f3('comments'); + f2('comments'); if(o.length>2){ o[o.length] = (''); o[1]=o[1].replace("/>",">"); } return o.join(""); } @@ -2790,6 +2798,7 @@ var RELS = ({ WB: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument", SHEET: "http://sheetjs.openxmlformats.org/officeDocument/2006/relationships/officeDocument", HLINK: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", + VML: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing", VBA: "http://schemas.microsoft.com/office/2006/relationships/vbaProject" }); @@ -5055,6 +5064,59 @@ var DIF = (function() { from_sheet: sheet_to_dif }; })(); + +var PRN = (function() { + function prn_to_aoa(d, opts) { + switch(opts.type) { + case 'base64': return prn_to_aoa_str(Base64.decode(d), opts); + case 'binary': return prn_to_aoa_str(d, opts); + case 'buffer': return prn_to_aoa_str(d.toString('binary'), opts); + case 'array': return prn_to_aoa_str(cc2str(d), opts); + } + throw new Error("Unrecognized type " + opts.type); + } + + function set_text_arr(data, arr, R, C) { + if(data === 'TRUE') arr[R][C] = true; + else if(data === 'FALSE') arr[R][C] = false; + else if(+data == +data) arr[R][C] = +data; + else if(data !== "") arr[R][C] = data; + } + + function prn_to_aoa_str(f, opts) { + var arr = ([]); + if(!f || f.length === 0) return arr; + var lines = f.split(/[\r\n]/); + var L = lines.length - 1; + while(L >= 0 && lines[L].length === 0) --L; + var start = 10, idx = 0; + var R = 0; + for(; R <= L; ++R) { + idx = lines[R].indexOf(" "); + if(idx == -1) idx = lines[R].length; else idx++; + start = Math.max(start, idx); + } + for(R = 0; R <= L; ++R) { + arr[R] = []; + /* TODO: confirm that widths are always 10 */ + var C = 0; + set_text_arr(lines[R].slice(0, start).trim(), arr, R, C); + for(C = 1; C <= (lines[R].length - start)/10 + 1; ++C) + set_text_arr(lines[R].slice(start+(C-1)*10,start+C*10).trim(),arr,R,C); + } + return arr; + } + + function prn_to_sheet(str, opts) { return aoa_to_sheet(prn_to_aoa(str, opts), opts); } + + function prn_to_workbook(str, opts) { return sheet_to_workbook(prn_to_sheet(str, opts), opts); } + + return { + to_workbook: prn_to_workbook, + to_sheet: prn_to_sheet + }; +})(); + /* 18.4.1 charset to codepage mapping */ var CS2CP = ({ 0: 1252, /* ANSI */ @@ -5555,7 +5617,7 @@ function cycle_width(collw) { return char2width(px2char(width2px(collw))); } /* XLSX/XLSB/XLS specify width in units of MDW */ function find_mdw_colw(collw) { var delta = Infinity, _MDW = MIN_MDW; - for(MDW=MIN_MDW; MDW/,">"), + writextag("o:shapelayout", writextag("o:idmap", null, {'v:ext':"edit", 'data':rId}), {'v:ext':"edit"}), + writextag("v:shapetype", [ + writextag("v:stroke", null, {joinstyle:"miter"}), + writextag("v:path", null, {gradientshapeok:"t", 'o:connecttype':"rect"}) + ].join(""), {id:"_x0000_t202", 'o:spt':202, coordsize:csize.join(","),path:bbox}) + ]; + while(_shapeid < rId * 1000) _shapeid += 1000; + + comments.map(function(x) { return decode_cell(x[0]); }).forEach(function(c,i) { o = o.concat([ + '', + writextag('v:fill', writextag("o:fill", null, {type:"gradientUnscaled", 'v:ext':"view"}), {'color2':"#BEFF82", 'angle':"-180", 'type':"gradient"}), + writextag("v:shadow", null, {on:"t", 'obscured':"t"}), + writextag("v:path", null, {'o:connecttype':"none"}), + '
', + '', + '', + '', + /* Part 4 19.4.2.3 Anchor (Anchor) */ + writetag('x:Anchor', [c.c, 0, c.r, 0, c.c+3, 100, c.r+5, 100].join(",")), + writetag('x:AutoFill', "False"), + writetag('x:Row', String(c.r)), + writetag('x:Column', String(c.c)), + '', + '' + ]); }); + o.push(''); + return o.join(""); +} + +RELS.CMNT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"; function parse_comments(zip, dirComments, sheets, sheetRels, opts) { for(var i = 0; i != dirComments.length; ++i) { @@ -6559,8 +6665,9 @@ function insertCommentsIntoSheet(sheetName, sheet, comments) { }); } -/* 18.7.3 CT_Comment */ +/* 18.7 Comments */ function parse_comments_xml(data, opts) { + /* 18.7.6 CT_Comments */ if(data.match(/<(?:\w+:)?comments *\/>/)) return []; var authors = []; var commentList = []; @@ -6576,22 +6683,46 @@ function parse_comments_xml(data, opts) { var cm = x.match(/<(?:\w+:)?comment[^>]*>/); if(!cm) return; var y = parsexmltag(cm[0]); - var comment = ({ author: y.authorId && authors[y.authorId] ? authors[y.authorId] : undefined, ref: y.ref, guid: y.guid }); + var comment = ({ author: y.authorId && authors[y.authorId] ? authors[y.authorId] : "sheetjsghost", ref: y.ref, guid: y.guid }); var cell = decode_cell(y.ref); if(opts.sheetRows && opts.sheetRows <= cell.r) return; var textMatch = x.match(/<(?:\w+:)?text>([^\u2603]*)<\/(?:\w+:)?text>/); - if (!textMatch || !textMatch[1]) return; // a comment may contain an empty text tag. - var rt = parse_si(textMatch[1]); - if(!rt) return; + var rt = (!textMatch || !textMatch[1]) ? {r:"",t:"",h:""} : parse_si(textMatch[1]); comment.r = rt.r; - comment.t = rt.t; + comment.t = rt.t.replace(/\r\n/g,"\n").replace(/\r/g,"\n"); if(opts.cellHTML) comment.h = rt.h; commentList.push(comment); }); return commentList; } -function write_comments_xml(data, opts) { } +var CMNT_XML_ROOT = writextag('comments', null, { 'xmlns': XMLNS.main[0] }); +function write_comments_xml(data, opts) { + var o = [XML_HEADER, CMNT_XML_ROOT]; + + var iauthor = []; + o.push(""); + data.map(function(x) { return x[1]; }).forEach(function(comment) { + comment.map(function(x) { return escapexml(x.a); }).forEach(function(a) { + if(iauthor.indexOf(a) > -1) return; + iauthor.push(a); + o.push("" + a + ""); + }); + }); + o.push(""); + o.push(""); + data.forEach(function(d) { + d[1].forEach(function(c) { + /* 18.7.3 CT_Comment */ + o.push(''); + o.push(writetag("t", c.t)); + o.push(''); + }); + }); + o.push(""); + if(o.length>2) { o[o.length] = (''); o[1]=o[1].replace("/>",">"); } + return o.join(""); +} /* [MS-XLSB] 2.4.28 BrtBeginComment */ function parse_BrtBeginComment(data, length) { var out = {}; @@ -6602,6 +6733,16 @@ function parse_BrtBeginComment(data, length) { data.l += 16; /*var guid = parse_GUID(data); */ return out; } +function write_BrtBeginComment(data, o) { + if(o == null) o = new_buf(36); + o.write_shift(4, data[1].iauthor); + write_UncheckedRfX(data[0], o); + o.write_shift(4, 0); + o.write_shift(4, 0); + o.write_shift(4, 0); + o.write_shift(4, 0); + return o; +} /* [MS-XLSB] 2.4.324 BrtCommentAuthor */ var parse_BrtCommentAuthor = parse_XLWideString; @@ -6624,6 +6765,7 @@ function parse_comments_bin(data, opts) { c.author = authors[c.iauthor]; delete c.iauthor; if(opts.sheetRows && opts.sheetRows <= c.rfx.r) break; + if(!c.t) c.t = ""; delete c.rfx; out.push(c); break; case 'BrtBeginComments': break; case 'BrtEndComments': break; @@ -6637,7 +6779,38 @@ function parse_comments_bin(data, opts) { return out; } -function write_comments_bin(data, opts) { } +function write_comments_bin(data, opts) { + var ba = buf_array(); + var iauthor = []; + write_record(ba, "BrtBeginComments"); + { /* COMMENTAUTHORS */ + write_record(ba, "BrtBeginCommentAuthors"); + data.forEach(function(comment) { + comment[1].forEach(function(c) { + if(iauthor.indexOf(c.a) > -1) return; + iauthor.push(c.a.substr(0,54)); + write_record(ba, "BrtCommentAuthor", write_XLWideString(c.a.substr(0, 54))); + }); + }); + write_record(ba, "BrtEndCommentAuthors"); + } + { /* COMMENTLIST */ + write_record(ba, "BrtBeginCommentList"); + data.forEach(function(comment) { + comment[1].forEach(function(c) { + c.iauthor = iauthor.indexOf(c.a); + var range = {s:decode_cell(comment[0])}; range.e = range.s; + write_record(ba, "BrtBeginComment", write_BrtBeginComment([range, c])); + if(c.t && c.t.length > 0) write_record(ba, "BrtCommentText", write_RichStr(c)); + write_record(ba, "BrtEndComment"); + delete c.iauthor; + }); + }); + write_record(ba, "BrtEndCommentList"); + } + write_record(ba, "BrtEndComments"); + return ba.end(); +} RELS.DS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/dialogsheet"; RELS.MS = "http://schemas.microsoft.com/office/2006/relationships/xlMacrosheet"; @@ -8976,6 +9149,7 @@ function col_obj_w(C, col) { else if(col.wpx != null) width = px2char(col.wpx); else if(col.wch != null) width = col.wch; if(width > -1) { p.width = char2width(width); p.customWidth = 1; } + else p.width = col.width; return p; } @@ -9185,6 +9359,7 @@ function write_ws_xml_cell(cell, ref, ws, opts, idx, wb) { v = writextag('f', escapexml(cell.f), ff) + (cell.v != null ? v : ""); } if(cell.l) ws['!links'].push([ref, cell.l]); + if(cell.c) ws['!comments'].push([ref, cell.c]); return writextag('c', v, o); } @@ -9359,6 +9534,7 @@ function write_ws_xml(idx, opts, wb, rels) { if(ws === undefined) ws = {}; var ref = ws['!ref']; if(ref === undefined) ref = 'A1'; if(!rels) rels = {}; + ws['!comments'] = []; o[o.length] = (writextag('sheetPr', null, {'codeName': escapexml(wb.SheetNames[idx])})); o[o.length] = (writextag('dimension', null, {'ref': ref})); @@ -9377,12 +9553,12 @@ function write_ws_xml(idx, opts, wb, rels) { if(ws['!merges'] != null && ws['!merges'].length > 0) o[o.length] = (write_ws_xml_merges(ws['!merges'])); - var relc = -1, rel; + var relc = -1, rel, rId = -1; if(ws['!links'].length > 0) { o[o.length] = ""; ws['!links'].forEach(function(l) { if(!l[1].Target) return; - var rId = add_rels(rels, -1, escapexml(l[1].Target).replace(/#.*$/, ""), RELS.HLINK); + rId = add_rels(rels, -1, escapexml(l[1].Target).replace(/#.*$/, ""), RELS.HLINK); rel = ({"ref":l[0], "r:id":"rId"+rId}); if((relc = l[1].Target.indexOf("#")) > -1) rel.location = escapexml(l[1].Target.substr(relc+1)); if(l[1].Tooltip) rel.tooltip = escapexml(l[1].Tooltip); @@ -9391,6 +9567,14 @@ function write_ws_xml(idx, opts, wb, rels) { o[o.length] = ""; } delete ws['!links']; + + if(ws['!comments'].length > 0) { + rId = add_rels(rels, -1, "../drawings/vmlDrawing" + (idx+1) + ".vml", RELS.VML); + o[o.length] = writextag("legacyDrawing", null, {"r:id":"rId" + rId}); + ws['!legacy'] = rId; + } +// + if(o.length>2) { o[o.length] = (''); o[1]=o[1].replace("/>",">"); } return o.join(""); } @@ -9966,6 +10150,7 @@ function write_ws_bin_cell(ba, cell, R, C, opts, ws) { /* TODO: cell style */ //o.s = get_cell_style(opts.cellXfs, cell, opts); if(cell.l) ws['!links'].push([encode_cell(o), cell.l]); + if(cell.c) ws['!comments'].push([encode_cell(o), cell.c]); switch(cell.t) { case 's': case 'str': if(opts.bookSST) { @@ -10035,12 +10220,22 @@ function write_HLINKS(ba, ws, rels) { }); delete ws['!links']; } +function write_LEGACYDRAWING(ba, ws, idx, rels) { + /* [BrtLegacyDrawing] */ + if(ws['!comments'].length > 0) { + var rId = add_rels(rels, -1, "../drawings/vmlDrawing" + (idx+1) + ".vml", RELS.VML); + write_record(ba, "BrtLegacyDrawing", write_RelID("rId" + rId)); + ws['!legacy'] = rId; + } +} function write_ws_bin(idx, opts, wb, rels) { var ba = buf_array(); var s = wb.SheetNames[idx], ws = wb.Sheets[s] || {}; var r = safe_decode_range(ws['!ref'] || "A1"); ws['!links'] = []; + /* passed back to write_zip and removed there */ + ws['!comments'] = []; write_record(ba, "BrtBeginSheet"); write_record(ba, "BrtWsProp", write_BrtWsProp(s)); write_record(ba, "BrtWsDim", write_BrtWsDim(r)); @@ -10072,7 +10267,7 @@ function write_ws_bin(idx, opts, wb, rels) { /* [IGNOREECS] */ /* [SMARTTAGS] */ /* [BrtDrawing] */ - /* [BrtLegacyDrawing] */ + write_LEGACYDRAWING(ba, ws, idx, rels); /* [BrtLegacyDrawingHF] */ /* [BrtBkHim] */ /* [OLEOBJECTS] */ @@ -10760,11 +10955,11 @@ function write_sty(data, name, opts) { function write_sst(data, name, opts) { return (name.slice(-4)===".bin" ? write_sst_bin : write_sst_xml)(data, opts); } -/* -function write_cmnt(data, name:string, opts) { + +function write_cmnt(data, name, opts) { return (name.slice(-4)===".bin" ? write_comments_bin : write_comments_xml)(data, opts); } - +/* function write_cc(data, name:string, opts) { return (name.slice(-4)===".bin" ? write_cc_bin : write_cc_xml)(data, opts); } @@ -10889,7 +11084,7 @@ function parse_xlml_data(xml, ss, data, cell, base, styles, csty, row, arrayf, o if(!cell.t) cell.t = 'n'; break; case 'Error': cell.t = 'e'; cell.v = RBErr[xml]; cell.w = xml; break; - default: cell.t = 's'; cell.v = xlml_fixstr(ss); break; + default: cell.t = 's'; cell.v = xlml_fixstr(ss||xml); break; } safe_format_xlml(cell, nf, o); if(o.cellFormula != null) { @@ -10898,7 +11093,7 @@ function parse_xlml_data(xml, ss, data, cell, base, styles, csty, row, arrayf, o /* strictly speaking, the leading = is required but some writers omit */ if(fstr.charCodeAt(0) == 61 /* = */) fstr = fstr.substr(1); cell.f = rc_to_a1(fstr, base); - cell.Formula = undefined; + delete cell.Formula; if(cell.ArrayRange == "RC") cell.F = rc_to_a1("RC:RC", base); else if(cell.ArrayRange) { cell.F = rc_to_a1(cell.ArrayRange, base); @@ -10921,7 +11116,8 @@ function parse_xlml_data(xml, ss, data, cell, base, styles, csty, row, arrayf, o } function xlml_clean_comment(comment) { - comment.t = comment.v; + comment.t = comment.v || ""; + comment.t = comment.t.replace(/\r\n/g,"\n").replace(/\r/g,"\n"); comment.v = comment.w = comment.ixfe = undefined; } @@ -11570,7 +11766,14 @@ function write_ws_xlml_wsopts(ws, opts, idx, wb) { if(o.length == 0) return ""; return writextag("WorksheetOptions", o.join(""), {xmlns:XLMLNS.x}); } -/* TODO */ +function write_ws_xlml_comment(comments) { + return comments.map(function(c) { + // TODO: formatted text + var t = xlml_unfixstr(c.t||""); + var d =writextag("ss:Data", t, {"xmlns":"http://www.w3.org/TR/REC-html40"}); + return writextag("Comment", d, {"ss:Author":c.a}); + }).join(""); +} function write_ws_xlml_cell(cell, ref, ws, opts, idx, wb, addr){ if(!cell || cell.v == undefined && cell.f == undefined) return ""; @@ -11613,6 +11816,8 @@ function write_ws_xlml_cell(cell, ref, ws, opts, idx, wb, addr){ } var m = '' + _v + ''; + if((cell.c||[]).length > 0) m += write_ws_xlml_comment(cell.c); + return writextag("Cell", m, attr); } /* TODO */ @@ -13749,11 +13954,12 @@ function write_BIFF2LABEL(r, c, val) { function write_ws_biff_cell(ba, cell, R, C, opts) { if(cell.v != null) switch(cell.t) { - case 'n': - if((cell.v == (cell.v|0)) && (cell.v >= 0) && (cell.v < 65536)) - write_biff_rec(ba, 0x0002, write_BIFF2INT(R, C, cell.v)); + case 'd': case 'n': + var v = cell.t == 'd' ? datenum(cell.v) : cell.v; + if((v == (v|0)) && (v >= 0) && (v < 65536)) + write_biff_rec(ba, 0x0002, write_BIFF2INT(R, C, v)); else - write_biff_rec(ba, 0x0003, write_BIFF2NUMBER(R,C, cell.v)); + write_biff_rec(ba, 0x0003, write_BIFF2NUMBER(R,C, v)); return; case 'b': case 'e': write_biff_rec(ba, 0x0005, write_BIFF2BERR(R, C, cell.v, cell.t)); return; /* TODO: codepage, sst */ @@ -13894,6 +14100,8 @@ var parse_content_xml = (function() { var number_format_map = {}; var merges = [], mrange = {}, mR = 0, mC = 0; var arrayf = []; + var comments = [], comment = {}; + var creator = "", creatoridx = 0; var rept = 1, isstub = false; var i = 0; xlmlregex.lastIndex = 0; @@ -13939,6 +14147,7 @@ var parse_content_xml = (function() { if(C < range.s.c) range.s.c = C; if(R < range.s.r) range.s.r = R; ctag = parsexmltag(Rn[0], false); + comments = []; comment = {}; q = ({t:ctag['数据类型'] || ctag['value-type'], v:null}); if(opts.cellFormula) { if(ctag.formula) ctag.formula = unescapexml(ctag.formula); @@ -13988,6 +14197,7 @@ var parse_content_xml = (function() { q.v = textp || ''; isstub = textpidx == 0; } + if(comments.length > 0) { q.c = comments; comments = []; } if(textp) q.w = textp; if(!isstub || opts.sheetStubs) { if(!(opts.sheetRows && opts.sheetRows < R)) { @@ -14012,6 +14222,23 @@ var parse_content_xml = (function() { else if(Rn[0].charAt(Rn[0].length-2) !== '/') state.push([Rn[3], true]); break; + case 'annotation': // 14.1 + if(Rn[1]==='/'){ + if((tmp=state.pop())[0]!==Rn[3]) throw "Bad state: "+tmp; + comment.t = textp; + comment.a = creator; + comments.push(comment); + } + else if(Rn[0].charAt(Rn[0].length-2) !== '/') {state.push([Rn[3], false]);} + creator = ""; creatorpidx = 0; + textp = ""; textpidx = 0; + break; + + case 'creator': // 4.3.2.7 + if(Rn[1]==='/') { creator = str.slice(creatoridx,Rn.index); } + else creatoridx = Rn.index + Rn[0].length; + break; + /* ignore state */ case 'meta': case '元数据': // TODO: FODS/UOF case 'settings': // TODO: @@ -14027,7 +14254,6 @@ var parse_content_xml = (function() { case 'list-style': // 16.30 case 'form': // 13.13 case 'dde-links': // 9.8 - case 'annotation': // 14.1 case 'event-listeners': // TODO if(Rn[1]==='/'){if((tmp=state.pop())[0]!==Rn[3]) throw "Bad state: "+tmp;} else if(Rn[0].charAt(Rn[0].length-2) !== '/') state.push([Rn[3], false]); @@ -14134,7 +14360,7 @@ var parse_content_xml = (function() { case 'span': break; // case 'line-break': break; // 6.1.5 case 'p': case '文本串': - if(Rn[1]==='/') textp = parse_text_p(str.slice(textpidx,Rn.index), textptag); + if(Rn[1]==='/') textp = (textp.length > 0 ? textp + "\n" : "") + parse_text_p(str.slice(textpidx,Rn.index), textptag); else { textptag = parsexmltag(Rn[0], false); textpidx = Rn.index + Rn[0].length; } break; // case 's': break; // @@ -14188,7 +14414,6 @@ var parse_content_xml = (function() { case 'event-listener': // TODO /* TODO: FODS Properties */ case 'initial-creator': - case 'creator': case 'creation-date': case 'generator': case 'document-statistic': @@ -14605,6 +14830,7 @@ function parse_zip(zip, opts) { return out; } function write_zip(wb, opts) { + _shapeid = 1024; if(opts.bookType == "ods") return write_ods(wb, opts); if(wb && !wb.SSF) { wb.SSF = SSF.get_table(); @@ -14661,8 +14887,26 @@ f = "docProps/app.xml"; f = "xl/worksheets/sheet" + rId + "." + wbext; var wsrels = {'!id':{}}; zip.file(f, write_ws(rId-1, f, opts, wb, wsrels)); + ct.sheets.push(f); - add_rels(opts.wbrels, rId, "worksheets/sheet" + rId + "." + wbext, RELS.WS[0]); + add_rels(opts.wbrels, -1, "worksheets/sheet" + rId + "." + wbext, RELS.WS[0]); + + var ws = wb.Sheets[wb.SheetNames[rId-1]]; + if(ws) { + var comments = ws['!comments']; + if(comments && comments.length > 0) { + var cf = "xl/comments" + rId + "." + wbext; + zip.file(cf, write_cmnt(comments, cf, opts)); + ct.comments.push(cf); + add_rels(wsrels, -1, "../comments" + rId + "." + wbext, RELS.CMNT); + } + if(ws['!legacy']) { + zip.file("xl/drawings/vmlDrawing" + (rId) + ".vml", write_comments_vml(rId, ws['!comments'])); + } + delete ws['!comments']; + delete ws['!legacy']; + } + if(wsrels['!id'].rId1) zip.file(get_rels_path(f), write_rels(wsrels)); // get_rels_path('') } @@ -14670,7 +14914,7 @@ f = "docProps/app.xml"; f = "xl/sharedStrings." + wbext; zip.file(f, write_sst(opts.Strings, f, opts)); ct.strs.push(f); - add_rels(opts.wbrels, ++rId, "sharedStrings." + wbext, RELS.SST); + add_rels(opts.wbrels, -1, "sharedStrings." + wbext, RELS.SST); } /* TODO: something more intelligent with themes */ @@ -14678,20 +14922,20 @@ f = "docProps/app.xml"; f = "xl/theme/theme1.xml"; zip.file(f, write_theme(wb.Themes, opts)); ct.themes.push(f); - add_rels(opts.wbrels, ++rId, "theme/theme1.xml", RELS.THEME); + add_rels(opts.wbrels, -1, "theme/theme1.xml", RELS.THEME); /* TODO: something more intelligent with styles */ f = "xl/styles." + wbext; zip.file(f, write_sty(wb, f, opts)); ct.styles.push(f); - add_rels(opts.wbrels, ++rId, "styles." + wbext, RELS.STY); + add_rels(opts.wbrels, -1, "styles." + wbext, RELS.STY); if(wb.vbaraw && vbafmt) { f = "xl/vbaProject.bin"; zip.file(f, wb.vbaraw); ct.vba.push(f); - add_rels(opts.wbrels, ++rId, "vbaProject.bin", RELS.VBA); + add_rels(opts.wbrels, -1, "vbaProject.bin", RELS.VBA); } zip.file("[Content_Types].xml", write_ct(ct, opts)); @@ -14795,7 +15039,7 @@ function write_binary_type(out, opts) { function writeSync(wb, opts) { check_wb(wb); var o = opts||{}; - switch(o.bookType || 'xlsx') { + switch(o.bookType || 'xlsb') { case 'xml': case 'xlml': return write_string_type(write_xlml(wb, o), o); case 'slk': @@ -14813,21 +15057,20 @@ function writeSync(wb, opts) { } function resolve_book_type(o/*?WriteFileOpts*/) { - if(!o.bookType) switch(o.file.slice(-5).toLowerCase()) { + if(!o.bookType) switch(o.file.slice(o.file.lastIndexOf(".")).toLowerCase()) { case '.xlsx': o.bookType = 'xlsx'; break; case '.xlsm': o.bookType = 'xlsm'; break; case '.xlsb': o.bookType = 'xlsb'; break; case '.fods': o.bookType = 'fods'; break; case '.xlml': o.bookType = 'xlml'; break; case '.sylk': o.bookType = 'sylk'; break; - default: switch(o.file.slice(-4).toLowerCase()) { case '.xls': o.bookType = 'biff2'; break; case '.xml': o.bookType = 'xml'; break; case '.ods': o.bookType = 'ods'; break; case '.csv': o.bookType = 'csv'; break; case '.dif': o.bookType = 'dif'; break; case '.slk': o.bookType = 'sylk'; break; - }} + } } function writeFileSync(wb, filename, opts) {