Skip to content

Commit 1e4871f

Browse files
SheetJSDevsaarCiklum
authored andcommitted
XLSB/XLSM sheet rels and hyperlinks
- XLSB/XLSM workbook/worksheet code names for VBA - XLSX/XLSB write hyperlinks - updated CFB to 0.11.1 Fixes: - fixes SheetJS#615 h/t @johnothetree (XLSM CodeName) - fixes protobi#93 h/t @SheetJSDev (Write Hyperlinks) - fixes protobi#156 h/t @MayaGi (XLSX Write Hyperlinks) - fixes SheetJS#344 h/t @slonoed (XLSX Write Hyperlinks)
1 parent 27dc7ff commit 1e4871f

File tree

16 files changed

+333
-245
lines changed

16 files changed

+333
-245
lines changed

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ FLOWAUX=$(patsubst %.js,%.flow.js,$(AUXTARGETS))
1515
AUXSCPTS=xlsxworker1.js xlsxworker2.js xlsxworker.js
1616
FLOWTGTS=$(TARGET) $(AUXTARGETS) $(AUXSCPTS)
1717
UGLIFYOPTS=--support-ie8
18+
CLOSURE=/usr/local/lib/node_modules/google-closure-compiler/compiler.jar
1819

1920
## Main Targets
2021

@@ -144,6 +145,7 @@ lint: $(TARGET) $(AUXTARGETS) ## Run jshint and jscs checks
144145
@jshint --show-non-errors package.json bower.json
145146
@jshint --show-non-errors --extract=always $(HTMLLINT)
146147
@jscs $(TARGET) $(AUXTARGETS)
148+
if [ -e $(CLOSURE) ]; then java -jar $(CLOSURE) $(REQS) $(FLOWTARGET) --jscomp_warning=reportUnknownTypes >/dev/null; fi
147149

148150
.PHONY: flow
149151
flow: lint ## Run flow checker

bits/18_cfb.js

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ type SectorList = {
3030
/* [MS-CFB] v20130118 */
3131
var CFB = (function _CFB(){
3232
var exports = {};
33-
exports.version = '0.11.0';
33+
exports.version = '0.11.1';
3434
function parse(file) {
3535
var mver = 3; // major version
3636
var ssz = 512; // sector size
@@ -51,7 +51,7 @@ var mv = check_get_mver(blob);
5151
mver = mv[0];
5252
switch(mver) {
5353
case 3: ssz = 512; break; case 4: ssz = 4096; break;
54-
default: throw "Major Version: Expected 3 or 4 saw " + mver;
54+
default: throw new Error("Major Version: Expected 3 or 4 saw " + mver);
5555
}
5656

5757
/* reprocess header */
@@ -63,7 +63,7 @@ check_shifts(blob, mver);
6363

6464
// Number of Directory Sectors
6565
var nds = blob.read_shift(4, 'i');
66-
if(mver === 3 && nds !== 0) throw '# Directory Sectors: Expected 0 saw ' + nds;
66+
if(mver === 3 && nds !== 0) throw new Error('# Directory Sectors: Expected 0 saw ' + nds);
6767

6868
// Number of FAT Sectors
6969
//var nfs = blob.read_shift(4, 'i');
@@ -149,13 +149,14 @@ function check_shifts(blob, mver) {
149149
var shift = 0x09;
150150

151151
// Byte Order
152-
blob.chk('feff', 'Byte Order: ');
152+
//blob.chk('feff', 'Byte Order: '); // note: some writers put 0xffff
153+
blob.l += 2;
153154

154155
// Sector Shift
155156
switch((shift = blob.read_shift(2))) {
156-
case 0x09: if(mver !== 3) throw 'MajorVersion/SectorShift Mismatch'; break;
157-
case 0x0c: if(mver !== 4) throw 'MajorVersion/SectorShift Mismatch'; break;
158-
default: throw 'Sector Shift: Expected 9 or 12 saw ' + shift;
157+
case 0x09: if(mver != 3) throw new Error('Sector Shift: Expected 9 saw ' + shift); break;
158+
case 0x0c: if(mver != 4) throw new Error('Sector Shift: Expected 12 saw ' + shift); break;
159+
default: throw new Error('Sector Shift: Expected 9 or 12 saw ' + shift);
159160
}
160161

161162
// Mini Sector Shift
@@ -237,7 +238,7 @@ function make_find_path(FullPaths, Paths, FileIndex, files, root_name) {
237238
function sleuth_fat(idx, cnt, sectors, ssz, fat_addrs) {
238239
var q;
239240
if(idx === ENDOFCHAIN) {
240-
if(cnt !== 0) throw "DIFAT chain shorter than expected";
241+
if(cnt !== 0) throw new Error("DIFAT chain shorter than expected");
241242
} else if(idx !== -1 /*FREESECT*/) {
242243
var sector = sectors[idx], m = (ssz>>>2)-1;
243244
if(!sector) return;
@@ -263,7 +264,7 @@ function get_sector_list(sectors, start, fat_addrs, ssz, chkd) {
263264
buf_chain.push(sectors[j]);
264265
var addr = fat_addrs[Math.floor(j*4/ssz)];
265266
jj = ((j*4) & modulus);
266-
if(ssz < 4 + jj) throw "FAT boundary crossed: " + j + " 4 "+ssz;
267+
if(ssz < 4 + jj) throw new Error("FAT boundary crossed: " + j + " 4 "+ssz);
267268
if(!sectors[addr]) break;
268269
j = __readInt32LE(sectors[addr], jj);
269270
}
@@ -286,7 +287,7 @@ function make_sector_list(sectors, dir_start, fat_addrs, ssz/*:number*/)/*:any*/
286287
buf_chain.push(sectors[j]);
287288
var addr = fat_addrs[Math.floor(j*4/ssz)];
288289
jj = ((j*4) & modulus);
289-
if(ssz < 4 + jj) throw "FAT boundary crossed: " + j + " 4 "+ssz;
290+
if(ssz < 4 + jj) throw new Error("FAT boundary crossed: " + j + " 4 "+ssz);
290291
if(!sectors[addr]) break;
291292
j = __readInt32LE(sectors[addr], jj);
292293
}

bits/28_binstructs.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ function parse_RichStr(data, length/*:number*/)/*:XLString*/ {
1818
z.r = rgsStrRun;
1919
}
2020
else z.r = "<t>" + escapexml(str) + "</t>";
21-
if((flags & 2) !== 0) { /* fExtStr */
22-
/* TODO: phonetic string */
23-
}
21+
//if((flags & 2) !== 0) { /* fExtStr */
22+
// /* TODO: phonetic string */
23+
//}
2424
data.l = start + length;
2525
return z;
2626
}
@@ -50,7 +50,8 @@ function write_XLSBCell(cell/*:any*/, o/*:?Block*/) {
5050

5151

5252
/* [MS-XLSB] 2.5.21 */
53-
function parse_XLSBCodeName (data, length) { return parse_XLWideString(data, length); }
53+
var parse_XLSBCodeName = parse_XLWideString;
54+
var write_XLSBCodeName = write_XLWideString;
5455

5556
/* [MS-XLSB] 2.5.166 */
5657
function parse_XLNullableWideString(data)/*:string*/ {

bits/31_rels.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
var RELS = ({
33
WB: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
44
SHEET: "http://sheetjs.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
5+
HLINK: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
56
VBA: "http://schemas.microsoft.com/office/2006/relationships/vbaProject"
67
}/*:any*/);
78

89
/* 9.3.3 Representing Relationships */
910
function get_rels_path(file/*:string*/)/*:string*/ {
1011
var n = file.lastIndexOf("/");
11-
return file.substr(0,n) + '/_rels' + file.substr(n) + ".rels";
12+
return file.substr(0,n+1) + '_rels/' + file.substr(n+1) + ".rels";
1213
}
1314

1415
function parse_rels(data/*:?string*/, currentFilePath/*:string*/) {
@@ -51,3 +52,17 @@ function write_rels(rels)/*:string*/ {
5152
if(o.length>2){ o[o.length] = ('</Relationships>'); o[1]=o[1].replace("/>",">"); }
5253
return o.join("");
5354
}
55+
56+
function add_rels(rels, rId, f, type, relobj)/*:number*/ {
57+
if(!relobj) relobj = {};
58+
if(!rels['!id']) rels['!id'] = {};
59+
if(rId < 0) for(rId = 1; rels['!id']['rId' + rId]; ++rId){}
60+
relobj.Id = 'rId' + rId;
61+
relobj.Type = type;
62+
relobj.Target = f;
63+
if(relobj.Type == RELS.HLINK) relobj.TargetMode = "External";
64+
if(rels['!id'][relobj.Id]) throw new Error("Cannot rewrite rId " + rId);
65+
rels['!id'][relobj.Id] = relobj;
66+
rels[('/' + relobj.Target).replace("//","/")] = relobj;
67+
return rId;
68+
}

bits/40_harb.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ function dbf_to_aoa(buf, opts)/*:AOA*/ {
6363
case 0x83: memo = true; break;
6464
case 0x8B: memo = true; break;
6565
case 0xF5: memo = true; break;
66-
default: process.exit(); throw new Error("DBF Unsupported Version: " + ft.toString(16));
66+
default: throw new Error("DBF Unsupported Version: " + ft.toString(16));
6767
}
6868
var filedate = new Date(d.read_shift(1) + 1900, d.read_shift(1) - 1, d.read_shift(1));
6969
var nrow = d.read_shift(4);

bits/67_wsxml.js

Lines changed: 23 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ function write_ws_xml_cell(cell, ref, ws, opts, idx, wb) {
155155
var ff = cell.F && cell.F.substr(0, ref.length) == ref ? {t:"array", ref:cell.F} : null;
156156
v = writextag('f', escapexml(cell.f), ff) + (cell.v != null ? v : "");
157157
}
158+
if(cell.l) ws['!links'].push([ref, cell.l]);
158159
return writextag('c', v, o);
159160
}
160161

@@ -290,7 +291,7 @@ return function parse_ws_xml_data(sdata, s, opts, guess, themes, styles) {
290291
}
291292
}; })();
292293

293-
function write_ws_xml_data(ws/*:Worksheet*/, opts, idx/*:number*/, wb/*:Workbook*/)/*:string*/ {
294+
function write_ws_xml_data(ws/*:Worksheet*/, opts, idx/*:number*/, wb/*:Workbook*/, rels)/*:string*/ {
294295
var o = [], r = [], range = safe_decode_range(ws['!ref']), cell, ref, rr = "", cols = [], R=0, C=0;
295296
for(C = range.s.c; C <= range.e.c; ++C) cols[C] = encode_col(C);
296297
for(R = range.s.r; R <= range.e.r; ++R) {
@@ -311,111 +312,42 @@ var WS_XML_ROOT = writextag('worksheet', null, {
311312
'xmlns:r': XMLNS.r
312313
});
313314

314-
function write_ws_xml(idx, opts, wb) {
315-
var o = [XML_HEADER, WS_XML_ROOT];
316-
var s = wb.SheetNames[idx], sidx = 0, rdata = "";
317-
var ws = wb.Sheets[s];
318-
if (ws === undefined) ws = {};
319-
var ref = ws['!ref'];
320-
if (ref === undefined) ref = 'A1';
321-
o[o.length] = (writextag('dimension', null, {'ref': ref}));
322-
323-
var kids = [];
324-
if (ws['!freeze']) {
325-
var pane = '';
326-
pane = writextag('pane', null, ws['!freeze'])
327-
kids.push(pane)
328-
329-
var selection = writextag('selection', null, {
330-
pane: "topLeft"
331-
})
332-
kids.push(selection)
333-
334-
var selection = writextag('selection', null, {
335-
pane: "bottomLeft"
336-
})
337-
kids.push(selection)
338-
339-
var selection = writextag('selection', null, {
340-
pane: "bottomRight",
341-
activeCell: ws['!freeze'],
342-
sqref: ws['!freeze']
343-
})
344-
kids.push(selection)
345-
}
346-
347-
348-
//<selection pane="bottomRight" activeCell="A4" sqref="A4"/>
349-
350-
var sheetView = writextag('sheetView', kids.join('') || undefined, {
351-
showGridLines: opts.showGridLines == false ? '0' : '1',
352-
tabSelected: opts.tabSelected === undefined ? '0' : opts.tabSelected, // see issue #26, need to set WorkbookViews if this is set
353-
workbookViewId: opts.workbookViewId === undefined ? '0' : opts.workbookViewId
354-
});
355-
o[o.length] = writextag('sheetViews', sheetView);
356-
357-
if (ws['!cols'] !== undefined && ws['!cols'].length > 0) o[o.length] = (write_ws_xml_cols(ws, ws['!cols']));
358-
o[sidx = o.length] = '<sheetData/>';
359-
if (ws['!ref'] !== undefined) {
360-
rdata = write_ws_xml_data(ws, opts, idx, wb);
361-
if (rdata.length > 0) o[o.length] = (rdata);
362-
}
363-
if (o.length > sidx + 1) {
364-
o[o.length] = ('</sheetData>');
365-
o[sidx] = o[sidx].replace("/>", ">");
366-
}
367-
368-
if (ws['!merges'] !== undefined && ws['!merges'].length > 0) o[o.length] = (write_ws_xml_merges(ws['!merges']));
369-
370-
if (ws['!pageSetup'] !== undefined) o[o.length] = write_ws_xml_pagesetup(ws['!pageSetup']);
371-
if (ws['!rowBreaks'] !== undefined) o[o.length] = write_ws_xml_row_breaks(ws['!rowBreaks']);
372-
if (ws['!colBreaks'] !== undefined) o[o.length] = write_ws_xml_col_breaks(ws['!colBreaks']);
373-
374-
if (o.length > 2) {
375-
o[o.length] = ('</worksheet>');
376-
o[1] = o[1].replace("/>", ">");
377-
}
378-
return o.join("");
379-
}
380-
381-
function write_ws_xml_row_breaks(breaks) {
382-
var brk = [];
383-
for (var i = 0; i < breaks.length; i++) {
384-
var thisBreak = '' + (breaks[i]);
385-
var nextBreak = '' + (breaks[i + 1] || '16383');
386-
brk.push(writextag('brk', null, {id: thisBreak, max: nextBreak, man: '1'}))
387-
}
388-
return writextag('rowBreaks', brk.join(' '), {count: brk.length, manualBreakCount: brk.length})
389-
}
390-
function write_ws_xml_col_breaks(breaks) {
391-
var brk = [];
392-
for (var i = 0; i < breaks.length; i++) {
393-
var thisBreak = '' + (breaks[i]);
394-
var nextBreak = '' + (breaks[i + 1] || '1048575');
395-
brk.push(writextag('brk', null, {id: thisBreak, max: nextBreak, man: '1'}))
396-
}
397-
return writextag('colBreaks', brk.join(' '), {count: brk.length, manualBreakCount: brk.length})
398-
399-
function write_ws_xml(idx/*:number*/, opts, wb/*:Workbook*/)/*:string*/ {
315+
function write_ws_xml(idx/*:number*/, opts, wb/*:Workbook*/, rels)/*:string*/ {
400316
var o = [XML_HEADER, WS_XML_ROOT];
401317
var s = wb.SheetNames[idx], sidx = 0, rdata = "";
402318
var ws = wb.Sheets[s];
403319
if(ws === undefined) ws = {};
404320
var ref = ws['!ref']; if(ref === undefined) ref = 'A1';
321+
if(!rels) rels = {};
322+
323+
o[o.length] = (writextag('sheetPr', null, {'codeName': escapexml(wb.SheetNames[idx])}));
405324
o[o.length] = (writextag('dimension', null, {'ref': ref}));
406325

407326
if(ws['!cols'] !== undefined && ws['!cols'].length > 0) o[o.length] = (write_ws_xml_cols(ws, ws['!cols']));
408327
o[sidx = o.length] = '<sheetData/>';
328+
ws['!links'] = [];
409329
if(ws['!ref'] != null) {
410-
rdata = write_ws_xml_data(ws, opts, idx, wb);
330+
rdata = write_ws_xml_data(ws, opts, idx, wb, rels);
411331
if(rdata.length > 0) o[o.length] = (rdata);
412332
}
413333
if(o.length>sidx+1) { o[o.length] = ('</sheetData>'); o[sidx]=o[sidx].replace("/>",">"); }
414334

415335
if(ws['!merges'] != null && ws['!merges'].length > 0) o[o.length] = (write_ws_xml_merges(ws['!merges']));
416336

417-
if(o.length>2) { o[o.length] = ('</worksheet>'); o[1]=o[1].replace("/>",">"); }
418-
337+
var relc = -1, rel;
338+
if(ws['!links'].length > 0) {
339+
o[o.length] = "<hyperlinks>";
340+
ws['!links'].forEach(function(l) {
341+
if(!l[1].Target) return;
342+
var rId = add_rels(rels, -1, escapexml(l[1].Target).replace(/#.*$/, ""), RELS.HLINK);
343+
rel = ({"ref":l[0], "r:id":"rId"+rId}/*:any*/);
344+
if((relc = l[1].Target.indexOf("#")) > -1) rel.location = escapexml(l[1].Target.substr(relc+1));
345+
if(l[1].Tooltip) rel.tooltip = escapexml(l[1].Tooltip);
346+
o[o.length] = writextag("hyperlink",null,rel);
347+
});
348+
o[o.length] = "</hyperlinks>";
349+
}
419350
delete ws['!links'];
351+
if(o.length>2) { o[o.length] = ('</worksheet>'); o[1]=o[1].replace("/>",">"); }
420352
return o.join("");
421-
}}
353+
}

bits/68_wsbin.js

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ function parse_BrtWsProp(data, length) {
5858
z.name = parse_XLSBCodeName(data, length - 19);
5959
return z;
6060
}
61+
function write_BrtWsProp(str, o) {
62+
if(o == null) o = new_buf(80+4*str.length);
63+
for(var i = 0; i < 11; ++i) o.write_shift(1,0);
64+
o.write_shift(-4,-1);
65+
o.write_shift(-4,-1);
66+
write_XLSBCodeName(str, o);
67+
return o.slice(0, o.l);
68+
}
6169

6270
/* [MS-XLSB] 2.4.303 BrtCellBlank */
6371
function parse_BrtCellBlank(data, length) {
@@ -228,6 +236,17 @@ function parse_BrtHLink(data, length, opts) {
228236
data.l = end;
229237
return {rfx:rfx, relId:relId, loc:loc, Tooltip:tooltip, display:display};
230238
}
239+
function write_BrtHLink(l, rId, o) {
240+
if(o == null) o = new_buf(50+4*l[1].Target.length);
241+
write_UncheckedRfX({s:decode_cell(l[0]), e:decode_cell(l[0])}, o);
242+
write_RelID("rId" + rId, o);
243+
var locidx = l[1].Target.indexOf("#");
244+
var location = locidx == -1 ? "" : l[1].Target.substr(locidx+1);
245+
write_XLWideString(location || "", o);
246+
write_XLWideString(l[1].Tooltip || "", o);
247+
write_XLWideString("", o);
248+
return o.slice(0, o.l);
249+
}
231250

232251
/* [MS-XLSB] 2.4.6 BrtArrFmla */
233252
function parse_BrtArrFmla(data, length, opts) {
@@ -533,7 +552,7 @@ function parse_ws_bin(data, opts, rels, wb, themes, styles)/*:Worksheet*/ {
533552
}
534553

535554
/* TODO: something useful -- this is a stub */
536-
function write_ws_bin_cell(ba/*:BufArray*/, cell/*:Cell*/, R/*:number*/, C/*:number*/, opts) {
555+
function write_ws_bin_cell(ba/*:BufArray*/, cell/*:Cell*/, R/*:number*/, C/*:number*/, opts, ws/*:Worksheet*/) {
537556
if(cell.v === undefined) return "";
538557
var vv = ""; var olddate = null;
539558
switch(cell.t) {
@@ -550,6 +569,7 @@ function write_ws_bin_cell(ba/*:BufArray*/, cell/*:Cell*/, R/*:number*/, C/*:num
550569
var o/*:any*/ = ({r:R, c:C}/*:any*/);
551570
/* TODO: cell style */
552571
//o.s = get_cell_style(opts.cellXfs, cell, opts);
572+
if(cell.l) ws['!links'].push([encode_cell(o), cell.l]);
553573
switch(cell.t) {
554574
case 's': case 'str':
555575
if(opts.bookSST) {
@@ -590,7 +610,7 @@ function write_CELLTABLE(ba, ws/*:Worksheet*/, idx/*:number*/, opts, wb/*:Workbo
590610
ref = cols[C] + rr;
591611
if(!ws[ref]) continue;
592612
/* write cell */
593-
write_ws_bin_cell(ba, ws[ref], R, C, opts);
613+
write_ws_bin_cell(ba, ws[ref], R, C, opts, ws);
594614
}
595615
}
596616
write_record(ba, 'BrtEndSheetData');
@@ -610,12 +630,23 @@ function write_COLINFOS(ba, ws/*:Worksheet*/, idx/*:number*/, opts, wb/*:Workboo
610630
write_record(ba, 'BrtEndColInfos');
611631
}
612632

613-
function write_ws_bin(idx/*:number*/, opts, wb/*:Workbook*/) {
633+
function write_HLINKS(ba, ws/*:Worksheet*/, rels) {
634+
/* *BrtHLink */
635+
ws['!links'].forEach(function(l) {
636+
if(!l[1].Target) return;
637+
var rId = add_rels(rels, -1, l[1].Target.replace(/#.*$/, ""), RELS.HLINK);
638+
write_record(ba, "BrtHLink", write_BrtHLink(l, rId));
639+
});
640+
delete ws['!links'];
641+
}
642+
643+
function write_ws_bin(idx/*:number*/, opts, wb/*:Workbook*/, rels) {
614644
var ba = buf_array();
615645
var s = wb.SheetNames[idx], ws = wb.Sheets[s] || {};
616646
var r = safe_decode_range(ws['!ref'] || "A1");
647+
ws['!links'] = [];
617648
write_record(ba, "BrtBeginSheet");
618-
/* [BrtWsProp] */
649+
write_record(ba, "BrtWsProp", write_BrtWsProp(s));
619650
write_record(ba, "BrtWsDim", write_BrtWsDim(r));
620651
/* [WSVIEWS2] */
621652
/* [WSFMTINFO] */
@@ -633,7 +664,7 @@ function write_ws_bin(idx/*:number*/, opts, wb/*:Workbook*/) {
633664
/* [BrtPhoneticInfo] */
634665
/* *CONDITIONALFORMATTING */
635666
/* [DVALS] */
636-
/* *BrtHLink */
667+
write_HLINKS(ba, ws, rels);
637668
/* [BrtPrintOptions] */
638669
/* [BrtMargins] */
639670
/* [BrtPageSetup] */

0 commit comments

Comments
 (0)