1
forked from sheetjs/sheetjs

Hyperlink cleanup [ci skip]

- XLS normalize URLs and use moniker (fixes )
- ODS row heights and column widths (fixes )
- DSV parse of files with leading <, deviating from Excel (fixes )
- read `ArrayBuffer` with no type (fixes )
- removed invalid test files that throw errors due to string length
- AOA/JSON null optional map to #NULL! (fixes )
This commit is contained in:
SheetJS 2021-09-30 03:28:03 -04:00
parent 8658054872
commit e59de73f99
14 changed files with 181 additions and 58 deletions

@ -135,7 +135,12 @@ function sheet_add_aoa(_ws/*:?Worksheet*/, data/*:AOA*/, opts/*:?any*/)/*:Worksh
if(data[R][C] && typeof data[R][C] === 'object' && !Array.isArray(data[R][C]) && !(data[R][C] instanceof Date)) cell = data[R][C]; if(data[R][C] && typeof data[R][C] === 'object' && !Array.isArray(data[R][C]) && !(data[R][C] instanceof Date)) cell = data[R][C];
else { else {
if(Array.isArray(cell.v)) { cell.f = data[R][C][1]; cell.v = cell.v[0]; } if(Array.isArray(cell.v)) { cell.f = data[R][C][1]; cell.v = cell.v[0]; }
if(cell.v === null) { if(cell.f) cell.t = 'n'; else if(!o.sheetStubs) continue; else cell.t = 'z'; } if(cell.v === null) {
if(cell.f) cell.t = 'n';
else if(o.nullError) { cell.t = 'e'; cell.v = 0; }
else if(!o.sheetStubs) continue;
else cell.t = 'z';
}
else if(typeof cell.v === 'number') cell.t = 'n'; else if(typeof cell.v === 'number') cell.t = 'n';
else if(typeof cell.v === 'boolean') cell.t = 'b'; else if(typeof cell.v === 'boolean') cell.t = 'b';
else if(cell.v instanceof Date) { else if(cell.v instanceof Date) {

@ -57,7 +57,7 @@ function write_rdf(rdf) {
} }
/* TODO: pull properties */ /* TODO: pull properties */
var write_meta_ods/*:{(wb:Workbook, opts:any):string}*/ = (function() { var write_meta_ods/*:{(wb:Workbook, opts:any):string}*/ = (function() {
var payload = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><office:document-meta xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xlink="http://www.w3.org/1999/xlink" office:version="1.2"><office:meta><meta:generator>Sheet' + 'JS ' + XLSX.version + '</meta:generator></office:meta></office:document-meta>'; var payload = '<office:document-meta xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xlink="http://www.w3.org/1999/xlink" office:version="1.2"><office:meta><meta:generator>Sheet' + 'JS ' + XLSX.version + '</meta:generator></office:meta></office:document-meta>';
return function wmo(/*:: wb: Workbook, opts: any*/)/*:string*/ { return function wmo(/*:: wb: Workbook, opts: any*/)/*:string*/ {
return payload; return payload;
}; };

@ -523,16 +523,17 @@ function parse_URLMoniker(blob/*::, length, opts*/) {
/* [MS-OSHARED] 2.3.7.8 FileMoniker TODO: all fields */ /* [MS-OSHARED] 2.3.7.8 FileMoniker TODO: all fields */
function parse_FileMoniker(blob/*::, length*/) { function parse_FileMoniker(blob/*::, length*/) {
blob.l += 2; //var cAnti = blob.read_shift(2); var cAnti = blob.read_shift(2);
var preamble = ""; while(cAnti-- > 0) preamble += "../";
var ansiPath = blob.read_shift(0, 'lpstr-ansi'); var ansiPath = blob.read_shift(0, 'lpstr-ansi');
blob.l += 2; //var endServer = blob.read_shift(2); blob.l += 2; //var endServer = blob.read_shift(2);
if(blob.read_shift(2) != 0xDEAD) throw new Error("Bad FileMoniker"); if(blob.read_shift(2) != 0xDEAD) throw new Error("Bad FileMoniker");
var sz = blob.read_shift(4); var sz = blob.read_shift(4);
if(sz === 0) return ansiPath.replace(/\\/g,"/"); if(sz === 0) return preamble + ansiPath.replace(/\\/g,"/");
var bytes = blob.read_shift(4); var bytes = blob.read_shift(4);
if(blob.read_shift(2) != 3) throw new Error("Bad FileMoniker"); if(blob.read_shift(2) != 3) throw new Error("Bad FileMoniker");
var unicodePath = blob.read_shift(bytes>>1, 'utf16le').replace(chr0,""); var unicodePath = blob.read_shift(bytes>>1, 'utf16le').replace(chr0,"");
return unicodePath; return preamble + unicodePath;
} }
/* [MS-OSHARED] 2.3.7.2 HyperlinkMoniker TODO: all the monikers */ /* [MS-OSHARED] 2.3.7.2 HyperlinkMoniker TODO: all the monikers */
@ -551,6 +552,13 @@ function parse_HyperlinkString(blob/*::, length*/) {
var o = len > 0 ? blob.read_shift(len, 'utf16le').replace(chr0, "") : ""; var o = len > 0 ? blob.read_shift(len, 'utf16le').replace(chr0, "") : "";
return o; return o;
} }
function write_HyperlinkString(str/*:string*/, o) {
if(!o) o = new_buf(6 + str.length * 2);
o.write_shift(4, 1 + str.length);
for(var i = 0; i < str.length; ++i) o.write_shift(2, str.charCodeAt(i));
o.write_shift(2, 0);
return o;
}
/* [MS-OSHARED] 2.3.7.1 Hyperlink Object */ /* [MS-OSHARED] 2.3.7.1 Hyperlink Object */
function parse_Hyperlink(blob, length)/*:Hyperlink*/ { function parse_Hyperlink(blob, length)/*:Hyperlink*/ {
@ -571,6 +579,7 @@ function parse_Hyperlink(blob, length)/*:Hyperlink*/ {
var target = targetFrameName||moniker||oleMoniker||""; var target = targetFrameName||moniker||oleMoniker||"";
if(target && Loc) target+="#"+Loc; if(target && Loc) target+="#"+Loc;
if(!target) target = "#" + Loc; if(!target) target = "#" + Loc;
if((flags & 0x0002) && target.charAt(0) == "/" && target.charAt(1) != "/") target = "file://" + target;
var out = ({Target:target}/*:any*/); var out = ({Target:target}/*:any*/);
if(guid) out.guid = guid; if(guid) out.guid = guid;
if(fileTime) out.time = fileTime; if(fileTime) out.time = fileTime;
@ -580,29 +589,31 @@ function parse_Hyperlink(blob, length)/*:Hyperlink*/ {
function write_Hyperlink(hl) { function write_Hyperlink(hl) {
var out = new_buf(512), i = 0; var out = new_buf(512), i = 0;
var Target = hl.Target; var Target = hl.Target;
var F = Target.indexOf("#") > -1 ? 0x1f : 0x17; if(Target.slice(0,7) == "file://") Target = Target.slice(7);
var hashidx = Target.indexOf("#");
var F = hashidx > -1 ? 0x1f : 0x17;
switch(Target.charAt(0)) { case "#": F=0x1c; break; case ".": F&=~2; break; } switch(Target.charAt(0)) { case "#": F=0x1c; break; case ".": F&=~2; break; }
out.write_shift(4,2); out.write_shift(4, F); out.write_shift(4,2); out.write_shift(4, F);
var data = [8,6815827,6619237,4849780,83]; for(i = 0; i < data.length; ++i) out.write_shift(4, data[i]); var data = [8,6815827,6619237,4849780,83]; for(i = 0; i < data.length; ++i) out.write_shift(4, data[i]);
if(F == 0x1C) { if(F == 0x1C) {
Target = Target.slice(1); Target = Target.slice(1);
out.write_shift(4, Target.length + 1); write_HyperlinkString(Target, out);
for(i = 0; i < Target.length; ++i) out.write_shift(2, Target.charCodeAt(i));
out.write_shift(2, 0);
} else if(F & 0x02) { } else if(F & 0x02) {
data = "e0 c9 ea 79 f9 ba ce 11 8c 82 00 aa 00 4b a9 0b".split(" "); data = "e0 c9 ea 79 f9 ba ce 11 8c 82 00 aa 00 4b a9 0b".split(" ");
for(i = 0; i < data.length; ++i) out.write_shift(1, parseInt(data[i], 16)); for(i = 0; i < data.length; ++i) out.write_shift(1, parseInt(data[i], 16));
out.write_shift(4, 2*(Target.length + 1)); var Pretarget = hashidx > -1 ? Target.slice(0, hashidx) : Target;
for(i = 0; i < Target.length; ++i) out.write_shift(2, Target.charCodeAt(i)); out.write_shift(4, 2*(Pretarget.length + 1));
for(i = 0; i < Pretarget.length; ++i) out.write_shift(2, Pretarget.charCodeAt(i));
out.write_shift(2, 0); out.write_shift(2, 0);
if(F & 0x08) write_HyperlinkString(hashidx > -1 ? Target.slice(hashidx+1): "", out);
} else { } else {
data = "03 03 00 00 00 00 00 00 c0 00 00 00 00 00 00 46".split(" "); data = "03 03 00 00 00 00 00 00 c0 00 00 00 00 00 00 46".split(" ");
for(i = 0; i < data.length; ++i) out.write_shift(1, parseInt(data[i], 16)); for(i = 0; i < data.length; ++i) out.write_shift(1, parseInt(data[i], 16));
var P = 0; var P = 0;
while(Target.slice(P*3,P*3+3)=="../"||Target.slice(P*3,P*3+3)=="..\\") ++P; while(Target.slice(P*3,P*3+3)=="../"||Target.slice(P*3,P*3+3)=="..\\") ++P;
out.write_shift(2, P); out.write_shift(2, P);
out.write_shift(4, Target.length + 1); out.write_shift(4, Target.length - 3 * P + 1);
for(i = 0; i < Target.length; ++i) out.write_shift(1, Target.charCodeAt(i) & 0xFF); for(i = 0; i < Target.length - 3 * P; ++i) out.write_shift(1, Target.charCodeAt(i + 3 * P) & 0xFF);
out.write_shift(1, 0); out.write_shift(1, 0);
out.write_shift(2, 0xFFFF); out.write_shift(2, 0xFFFF);
out.write_shift(2, 0xDEAD); out.write_shift(2, 0xDEAD);

@ -352,7 +352,7 @@ var SYLK = (function() {
Au:'ù', Bu:'ú', Cu:'û', Hu:'ü', Au:'ù', Bu:'ú', Cu:'û', Hu:'ü',
KC:'Ç', Kc:'ç', q:'æ', z:'œ', a:'Æ', j:'Œ', KC:'Ç', Kc:'ç', q:'æ', z:'œ', a:'Æ', j:'Œ',
DN:209, Dn:241, Hy:255, DN:209, Dn:241, Hy:255,
S:169, c:170, R:174, B:180, S:169, c:170, R:174, "B ":180,
/*::[*/0/*::]*/:176, /*::[*/1/*::]*/:177, /*::[*/2/*::]*/:178, /*::[*/0/*::]*/:176, /*::[*/1/*::]*/:177, /*::[*/2/*::]*/:178,
/*::[*/3/*::]*/:179, /*::[*/5/*::]*/:181, /*::[*/6/*::]*/:182, /*::[*/3/*::]*/:179, /*::[*/5/*::]*/:181, /*::[*/6/*::]*/:182,
/*::[*/7/*::]*/:183, Q:185, k:186, b:208, i:216, l:222, s:240, y:248, /*::[*/7/*::]*/:183, Q:185, k:186, b:208, i:216, l:222, s:240, y:248,

@ -156,9 +156,9 @@ function parse_ws_xml_hlinks(s, data/*:Array<string>*/, rels) {
var rel = ((rels || {})['!id']||[])[val.id]; var rel = ((rels || {})['!id']||[])[val.id];
if(rel) { if(rel) {
val.Target = rel.Target; val.Target = rel.Target;
if(val.location) val.Target += "#"+val.location; if(val.location) val.Target += "#"+unescapexml(val.location);
} else { } else {
val.Target = "#" + val.location; val.Target = "#" + unescapexml(val.location);
rel = {Target: val.Target, TargetMode: 'Internal'}; rel = {Target: val.Target, TargetMode: 'Internal'};
} }
val.Rel = rel; val.Rel = rel;

@ -183,6 +183,7 @@ function parse_xlml_xml(d, _opts)/*:Workbook*/ {
else str = utf8read(str); else str = utf8read(str);
} }
var opening = str.slice(0, 1024).toLowerCase(), ishtml = false; var opening = str.slice(0, 1024).toLowerCase(), ishtml = false;
if((opening.indexOf(">") & 1023) > Math.min((opening.indexOf(",") & 1023), (opening.indexOf(";")&1023))) { var _o = dup(opts); _o.type = "string"; return PRN.to_workbook(str, _o); }
if(opening.indexOf("<?xml") == -1) ["html", "table", "head", "meta", "script", "style", "div"].forEach(function(tag) { if(opening.indexOf("<" + tag) >= 0) ishtml = true; }); if(opening.indexOf("<?xml") == -1) ["html", "table", "head", "meta", "script", "style", "div"].forEach(function(tag) { if(opening.indexOf("<" + tag) >= 0) ishtml = true; });
if(ishtml) return HTML_.to_workbook(str, opts); if(ishtml) return HTML_.to_workbook(str, opts);
var Rn; var Rn;
@ -226,7 +227,7 @@ function parse_xlml_xml(d, _opts)/*:Workbook*/ {
} else cursheet[encode_col(c) + encode_row(r)] = cell; } else cursheet[encode_col(c) + encode_row(r)] = cell;
} }
if(cell.HRef) { if(cell.HRef) {
cell.l = ({Target:cell.HRef}/*:any*/); cell.l = ({Target:unescapexml(cell.HRef)}/*:any*/);
if(cell.HRefScreenTip) cell.l.Tooltip = cell.HRefScreenTip; if(cell.HRefScreenTip) cell.l.Tooltip = cell.HRefScreenTip;
delete cell.HRef; delete cell.HRefScreenTip; delete cell.HRef; delete cell.HRefScreenTip;
} }

@ -221,7 +221,7 @@ var parse_content_xml = (function() {
case 'scripts': // 3.12 <office:scripts> case 'scripts': // 3.12 <office:scripts>
case 'styles': // TODO <office:styles> case 'styles': // TODO <office:styles>
case 'font-face-decls': // 3.14 <office:font-face-decls> case 'font-face-decls': // 3.14 <office:font-face-decls>
case 'master-styles': //3.15.4 <office:master-styles> -- relevant for FODS case 'master-styles': // 3.15.4 <office:master-styles> -- relevant for FODS
if(Rn[1]==='/'){if((tmp=state.pop())[0]!==Rn[3]) throw "Bad state: "+tmp;} 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], true]); else if(Rn[0].charAt(Rn[0].length-2) !== '/') state.push([Rn[3], true]);
break; break;
@ -515,11 +515,11 @@ var parse_content_xml = (function() {
if(Rn[1]!== '/') { if(Rn[1]!== '/') {
atag = parsexmltag(Rn[0], false); atag = parsexmltag(Rn[0], false);
if(!atag.href) break; if(!atag.href) break;
atag.Target = atag.href; delete atag.href; atag.Target = unescapexml(atag.href); delete atag.href;
if(atag.Target.charAt(0) == "#" && atag.Target.indexOf(".") > -1) { if(atag.Target.charAt(0) == "#" && atag.Target.indexOf(".") > -1) {
_Ref = ods_to_csf_3D(atag.Target.slice(1)); _Ref = ods_to_csf_3D(atag.Target.slice(1));
atag.Target = "#" + _Ref[0] + "!" + _Ref[1]; atag.Target = "#" + _Ref[0] + "!" + _Ref[1];
} } else if(atag.Target.match(/^\.\.[\\\/]/)) atag.Target = atag.Target.slice(3);
} }
break; break;

@ -37,9 +37,17 @@ var write_content_ods/*:{(wb:any, opts:any):string}*/ = (function() {
var R=0,C=0, range = decode_range(ws['!ref']||"A1"); var R=0,C=0, range = decode_range(ws['!ref']||"A1");
var marr/*:Array<Range>*/ = ws['!merges'] || [], mi = 0; var marr/*:Array<Range>*/ = ws['!merges'] || [], mi = 0;
var dense = Array.isArray(ws); var dense = Array.isArray(ws);
for(R = 0; R < range.s.r; ++R) o.push(' <table:table-row></table:table-row>\n'); if(ws["!cols"]) {
for(C = 0; C <= range.e.c; ++C) o.push(' <table:table-column' + (ws["!cols"][C] ? ' table:style-name="co' + ws["!cols"][C].ods + '"' : '') + '></table:table-column>\n');
}
var H = "", ROWS = ws["!rows"]||[];
for(R = 0; R < range.s.r; ++R) {
H = ROWS[R] ? ' table:style-name="ro' + ROWS[R].ods + '"' : "";
o.push(' <table:table-row' + H + '></table:table-row>\n');
}
for(; R <= range.e.r; ++R) { for(; R <= range.e.r; ++R) {
o.push(' <table:table-row>\n'); H = ROWS[R] ? ' table:style-name="ro' + ROWS[R].ods + '"' : "";
o.push(' <table:table-row' + H + '>\n');
for(C=0; C < range.s.c; ++C) o.push(null_cell_xml); for(C=0; C < range.s.c; ++C) o.push(null_cell_xml);
for(; C <= range.e.c; ++C) { for(; C <= range.e.c; ++C) {
var skip = false, ct = {}, textp = ""; var skip = false, ct = {}, textp = "";
@ -92,8 +100,11 @@ var write_content_ods/*:{(wb:any, opts:any):string}*/ = (function() {
} }
var text_p = write_text_p(textp); var text_p = write_text_p(textp);
if(cell.l && cell.l.Target) { if(cell.l && cell.l.Target) {
var _tgt = cell.l.Target; _tgt = _tgt.charAt(0) == "#" ? "#" + csf_to_ods_3D(_tgt.slice(1)) : _tgt; var _tgt = cell.l.Target;
text_p = writextag('text:a', text_p, {'xlink:href': _tgt}); _tgt = _tgt.charAt(0) == "#" ? "#" + csf_to_ods_3D(_tgt.slice(1)) : _tgt;
// TODO: choose correct parent path format based on link delimiters
if(_tgt.charAt(0) != "#" && !_tgt.match(/^\w+:/)) _tgt = '../' + _tgt;
text_p = writextag('text:a', text_p, {'xlink:href': _tgt.replace(/&/g, "&amp;")});
} }
o.push(' ' + writextag('table:table-cell', writextag('text:p', text_p, {}), ct) + '\n'); o.push(' ' + writextag('table:table-cell', writextag('text:p', text_p, {}), ct) + '\n');
} }
@ -103,7 +114,7 @@ var write_content_ods/*:{(wb:any, opts:any):string}*/ = (function() {
return o.join(""); return o.join("");
}; };
var write_automatic_styles_ods = function(o/*:Array<string>*/) { var write_automatic_styles_ods = function(o/*:Array<string>*/, wb) {
o.push(' <office:automatic-styles>\n'); o.push(' <office:automatic-styles>\n');
o.push(' <number:date-style style:name="N37" number:automatic-order="true">\n'); o.push(' <number:date-style style:name="N37" number:automatic-order="true">\n');
@ -114,6 +125,41 @@ var write_content_ods/*:{(wb:any, opts:any):string}*/ = (function() {
o.push(' <number:year/>\n'); o.push(' <number:year/>\n');
o.push(' </number:date-style>\n'); o.push(' </number:date-style>\n');
/* column styles */
var cidx = 0;
wb.SheetNames.map(function(n) { return wb.Sheets[n]; }).forEach(function(ws) {
if(!ws) return;
if(ws["!cols"]) {
for(var C = 0; C < ws["!cols"].length; ++C) if(ws["!cols"][C]) {
var colobj = ws["!cols"][C];
if(colobj.width == null && colobj.wpx == null && colobj.wch == null) continue;
process_col(colobj);
colobj.ods = cidx;
var w = ws["!cols"][C].wpx + "px";
o.push(' <style:style style:name="co' + cidx + '" style:family="table-column">\n');
o.push(' <style:table-column-properties fo:break-before="auto" style:column-width="' + w + '"/>\n');
o.push(' </style:style>\n');
++cidx;
}
}
});
/* row styles */
var ridx = 0;
wb.SheetNames.map(function(n) { return wb.Sheets[n]; }).forEach(function(ws) {
if(!ws) return;
if(ws["!rows"]) {
for(var R = 0; R < ws["!rows"].length; ++R) if(ws["!rows"][R]) {
ws["!rows"][R].ods = ridx;
var h = ws["!rows"][R].hpx + "px";
o.push(' <style:style style:name="ro' + ridx + '" style:family="table-row">\n');
o.push(' <style:table-row-properties fo:break-before="auto" style:row-height="' + h + '"/>\n');
o.push(' </style:style>\n');
++ridx;
}
}
});
/* table */ /* table */
o.push(' <style:style style:name="ta1" style:family="table">\n'); // style:master-page-name="mp1">\n'); o.push(' <style:style style:name="ta1" style:family="table">\n'); // style:master-page-name="mp1">\n');
o.push(' <style:table-properties table:display="true" style:writing-mode="lr-tb"/>\n'); o.push(' <style:table-properties table:display="true" style:writing-mode="lr-tb"/>\n');
@ -175,9 +221,13 @@ var write_content_ods/*:{(wb:any, opts:any):string}*/ = (function() {
'office:mimetype': "application/vnd.oasis.opendocument.spreadsheet" 'office:mimetype': "application/vnd.oasis.opendocument.spreadsheet"
}); });
if(opts.bookType == "fods") o.push('<office:document' + attr + fods + '>\n'); if(opts.bookType == "fods") {
else o.push('<office:document-content' + attr + '>\n'); o.push('<office:document' + attr + fods + '>\n');
write_automatic_styles_ods(o); o.push(write_meta_ods().replace(/office:document-meta/g, "office:meta"));
// TODO: settings (equiv of settings.xml for ODS)
} else o.push('<office:document-content' + attr + '>\n');
// o.push(' <office:scripts/>\n');
write_automatic_styles_ods(o, wb);
o.push(' <office:body>\n'); o.push(' <office:body>\n');
o.push(' <office:spreadsheet>\n'); o.push(' <office:spreadsheet>\n');
for(var i = 0; i != wb.SheetNames.length; ++i) o.push(write_ws(wb.Sheets[wb.SheetNames[i]], wb, i, opts)); for(var i = 0; i != wb.SheetNames.length; ++i) o.push(write_ws(wb.Sheets[wb.SheetNames[i]], wb, i, opts));
@ -217,7 +267,7 @@ function write_ods(wb/*:any*/, opts/*:any*/) {
/* TODO: this is hard-coded to satiate excel */ /* TODO: this is hard-coded to satiate excel */
f = "meta.xml"; f = "meta.xml";
zip_add_file(zip, f, write_meta_ods(/*::wb, opts*/)); zip_add_file(zip, f, XML_HEADER + write_meta_ods(/*::wb, opts*/));
manifest.push([f, "text/xml"]); manifest.push([f, "text/xml"]);
rdf.push([f, "MetadataFile"]); rdf.push([f, "MetadataFile"]);

@ -66,9 +66,9 @@ function read_prn(data, d, o, str) {
function readSync(data/*:RawData*/, opts/*:?ParseOpts*/)/*:Workbook*/ { function readSync(data/*:RawData*/, opts/*:?ParseOpts*/)/*:Workbook*/ {
reset_cp(); reset_cp();
if(typeof ArrayBuffer !== 'undefined' && data instanceof ArrayBuffer) return readSync(new Uint8Array(data), opts);
var d = data, n = [0,0,0,0], str = false;
var o = opts||{}; var o = opts||{};
if(typeof ArrayBuffer !== 'undefined' && data instanceof ArrayBuffer) return readSync(new Uint8Array(data), (o = dup(o), o.type = "array", o));
var d = data, n = [0,0,0,0], str = false;
if(o.cellStyles) { o.cellNF = true; o.sheetStubs = true; } if(o.cellStyles) { o.cellNF = true; o.sheetStubs = true; }
_ssfopts = {}; _ssfopts = {};
if(o.dateNF) _ssfopts.dateNF = o.dateNF; if(o.dateNF) _ssfopts.dateNF = o.dateNF;

@ -23,13 +23,14 @@ function make_json_row(sheet/*:Worksheet*/, r/*:Range*/, R/*:number*/, cols/*:Ar
var v = val.v; var v = val.v;
switch(val.t){ switch(val.t){
case 'z': if(v == null) break; continue; case 'z': if(v == null) break; continue;
case 'e': v = void 0; break; case 'e': v = (v == 0 ? null : void 0); break;
case 's': case 'd': case 'b': case 'n': break; case 's': case 'd': case 'b': case 'n': break;
default: throw new Error('unrecognized type ' + val.t); default: throw new Error('unrecognized type ' + val.t);
} }
if(hdr[C] != null) { if(hdr[C] != null) {
if(v == null) { if(v == null) {
if(defval !== undefined) row[hdr[C]] = defval; if(val.t == "e" && v === null) row[hdr[C]] = null;
else if(defval !== undefined) row[hdr[C]] = defval;
else if(raw && v === null) row[hdr[C]] = null; else if(raw && v === null) row[hdr[C]] = null;
else continue; else continue;
} else { } else {
@ -221,6 +222,7 @@ function sheet_add_json(_ws/*:?Worksheet*/, js/*:Array<any>*/, opts)/*:Worksheet
if(!o.cellDates) { t = 'n'; v = datenum(v); } if(!o.cellDates) { t = 'n'; v = datenum(v); }
z = (o.dateNF || SSF._table[14]); z = (o.dateNF || SSF._table[14]);
} }
else if(v === null && o.nullError) { t = 'e'; v = 0; }
if(!cell) ws[ref] = cell = ({t:t, v:v}/*:any*/); if(!cell) ws[ref] = cell = ({t:t, v:v}/*:any*/);
else { else {
cell.t = t; cell.v = v; cell.t = t; cell.v = v;

@ -1,5 +1,14 @@
#### Hyperlinks #### Hyperlinks
<details>
<summary><b>Format Support</b> (click to show)</summary>
**Cell Hyperlinks**: XLSX/M, XLSB, BIFF8 XLS, XLML, ODS
**Tooltips**: XLSX/M, XLSB, BIFF8 XLS, XLML
</details>
Hyperlinks are stored in the `l` key of cell objects. The `Target` field of the Hyperlinks are stored in the `l` key of cell objects. The `Target` field of the
hyperlink object is the target of the link, including the URI fragment. Tooltips hyperlink object is the target of the link, including the URI fragment. Tooltips
are stored in the `Tooltip` field and are displayed when you move your mouse are stored in the `Tooltip` field and are displayed when you move your mouse
@ -9,16 +18,55 @@ For example, the following snippet creates a link from cell `A3` to
<https://sheetjs.com> with the tip `"Find us @ SheetJS.com!"`: <https://sheetjs.com> with the tip `"Find us @ SheetJS.com!"`:
```js ```js
ws['A3'].l = { Target:"https://sheetjs.com", Tooltip:"Find us @ SheetJS.com!" }; ws['A1'].l = { Target:"https://sheetjs.com", Tooltip:"Find us @ SheetJS.com!" };
``` ```
Note that Excel does not automatically style hyperlinks -- they will generally Note that Excel does not automatically style hyperlinks -- they will generally
be displayed as normal text. be displayed as normal text.
_Remote Links_
HTTP / HTTPS links can be used directly:
```js
ws['A2'].l = { Target:"https://docs.sheetjs.com/#hyperlinks" };
ws['A3'].l = { Target:"http://localhost:7262/yes_localhost_works" };
```
Excel also supports `mailto` email links with subject line:
```js
ws['A4'].l = { Target:"mailto:ignored@dev.null" };
ws['A5'].l = { Target:"mailto:ignored@dev.null?subject=Test Subject" };
```
_Local Links_
Links to absolute paths should use the `file://` URI scheme:
```js
ws['B1'].l = { Target:"file:///SheetJS/t.xlsx" }; /* Link to /SheetJS/t.xlsx */
ws['B2'].l = { Target:"file:///c:/SheetJS.xlsx" }; /* Link to c:\SheetJS.xlsx */
```
Links to relative paths can be specified without a scheme:
```js
ws['B3'].l = { Target:"SheetJS.xlsb" }; /* Link to SheetJS.xlsb */
ws['B4'].l = { Target:"../SheetJS.xlsm" }; /* Link to ../SheetJS.xlsm */
```
Relative Paths have undefined behavior in the SpreadsheetML 2003 format. Excel
2019 will treat a `..\` parent mark as two levels up.
_Internal Links_
Links where the target is a cell or range or defined name in the same workbook Links where the target is a cell or range or defined name in the same workbook
("Internal Links") are marked with a leading hash character: ("Internal Links") are marked with a leading hash character:
```js ```js
ws['A2'].l = { Target:"#E2" }; /* link to cell E2 */ ws['C1'].l = { Target:"#E2" }; /* Link to cell E2 */
ws['C2'].l = { Target:"#Sheet2!E2" }; /* Link to cell E2 in sheet Sheet2 */
ws['C3'].l = { Target:"#SomeDefinedName" }; /* Link to Defined Name */
``` ```

@ -22,11 +22,12 @@ as the corresponding styles. Dates are stored as date or numbers. Array holes
and explicit `undefined` values are skipped. `null` values may be stubbed. All and explicit `undefined` values are skipped. `null` values may be stubbed. All
other values are stored as strings. The function takes an options argument: other values are stored as strings. The function takes an options argument:
| Option Name | Default | Description | | Option Name | Default | Description |
| :---------- | :------: | :-------------------------------------------------- | | :---------- | :-----: | :--------------------------------------------------- |
|`dateNF` | FMT 14 | Use specified date format in string output | |`dateNF` | FMT 14 | Use specified date format in string output |
|`cellDates` | false | Store dates as type `d` (default is `n`) | |`cellDates` | false | Store dates as type `d` (default is `n`) |
|`sheetStubs` | false | Create cell objects of type `z` for `null` values | |`sheetStubs` | false | Create cell objects of type `z` for `null` values |
|`nullError` | false | If true, emit `#NULL!` error cells for `null` values |
<details> <details>
<summary><b>Examples</b> (click to show)</summary> <summary><b>Examples</b> (click to show)</summary>
@ -46,12 +47,13 @@ var ws = XLSX.utils.aoa_to_sheet([
existing worksheet object. It follows the same process as `aoa_to_sheet` and existing worksheet object. It follows the same process as `aoa_to_sheet` and
accepts an options argument: accepts an options argument:
| Option Name | Default | Description | | Option Name | Default | Description |
| :---------- | :------: | :-------------------------------------------------- | | :---------- | :-----: | :--------------------------------------------------- |
|`dateNF` | FMT 14 | Use specified date format in string output | |`dateNF` | FMT 14 | Use specified date format in string output |
|`cellDates` | false | Store dates as type `d` (default is `n`) | |`cellDates` | false | Store dates as type `d` (default is `n`) |
|`sheetStubs` | false | Create cell objects of type `z` for `null` values | |`sheetStubs` | false | Create cell objects of type `z` for `null` values |
|`origin` | | Use specified cell as starting point (see below) | |`nullError` | false | If true, emit `#NULL!` error cells for `null` values |
|`origin` | | Use specified cell as starting point (see below) |
`origin` is expected to be one of: `origin` is expected to be one of:
@ -110,11 +112,14 @@ default column order is determined by the first appearance of the field using
|`dateNF` | FMT 14 | Use specified date format in string output | |`dateNF` | FMT 14 | Use specified date format in string output |
|`cellDates` | false | Store dates as type `d` (default is `n`) | |`cellDates` | false | Store dates as type `d` (default is `n`) |
|`skipHeader` | false | If true, do not include header row in output | |`skipHeader` | false | If true, do not include header row in output |
|`nullError` | false | If true, emit `#NULL!` error cells for `null` values |
- All fields from each row will be written. If `header` is an array and it does - All fields from each row will be written. If `header` is an array and it does
not contain a particular field, the key will be appended to the array. not contain a particular field, the key will be appended to the array.
- Cell types are deduced from the type of each value. For example, a `Date` - Cell types are deduced from the type of each value. For example, a `Date`
object will generate a Date cell, while a string will generate a Text cell. object will generate a Date cell, while a string will generate a Text cell.
- Null values will be skipped by default. If `nullError` is true, an error cell
corresponding to `#NULL!` will be written to the worksheet.
<details> <details>
<summary><b>Examples</b> (click to show)</summary> <summary><b>Examples</b> (click to show)</summary>
@ -145,13 +150,14 @@ var ws = XLSX.utils.json_to_sheet([
worksheet object. It follows the same process as `json_to_sheet` and accepts worksheet object. It follows the same process as `json_to_sheet` and accepts
an options argument: an options argument:
| Option Name | Default | Description | | Option Name | Default | Description |
| :---------- | :------: | :-------------------------------------------------- | | :---------- | :-----: | :--------------------------------------------------- |
|`header` | | Use specified column order (default `Object.keys`) | |`header` | | Use specified column order (default `Object.keys`) |
|`dateNF` | FMT 14 | Use specified date format in string output | |`dateNF` | FMT 14 | Use specified date format in string output |
|`cellDates` | false | Store dates as type `d` (default is `n`) | |`cellDates` | false | Store dates as type `d` (default is `n`) |
|`skipHeader` | false | If true, do not include header row in output | |`skipHeader` | false | If true, do not include header row in output |
|`origin` | | Use specified cell as starting point (see below) | |`nullError` | false | If true, emit `#NULL!` error cells for `null` values |
|`origin` | | Use specified cell as starting point (see below) |
`origin` is expected to be one of: `origin` is expected to be one of:

@ -41,7 +41,7 @@
"fflate": "^0.3.8", "fflate": "^0.3.8",
"ssf": "~0.11.2", "ssf": "~0.11.2",
"wmf": "~1.0.1", "wmf": "~1.0.1",
"word": "~0.4.0" "word": "~0.3.0"
}, },
"devDependencies": { "devDependencies": {
"@sheetjs/uglify-js": "~2.7.3", "@sheetjs/uglify-js": "~2.7.3",

@ -277,9 +277,9 @@ apachepoi_evaluate_formula_with_structured_table_references.xlsx
apachepoi_headerFooterTest.xlsx apachepoi_headerFooterTest.xlsx
apachepoi_noSharedStringTable.xlsx apachepoi_noSharedStringTable.xlsx
apachepoi_picture.xlsx apachepoi_picture.xlsx
apachepoi_poc-shared-strings.xlsx # apachepoi_poc-shared-strings.xlsx # string length exceeds 32767 chars
apachepoi_poc-xmlbomb-empty.xlsx # apachepoi_poc-xmlbomb-empty.xlsx # string length exceeds 32767 chars
apachepoi_poc-xmlbomb.xlsx # apachepoi_poc-xmlbomb.xlsx # string length exceeds 32767 chars
# apachepoi_protected_passtika.xlsx # password # apachepoi_protected_passtika.xlsx # password
apachepoi_ref-56737.xlsx apachepoi_ref-56737.xlsx
apachepoi_ref2-56737.xlsx apachepoi_ref2-56737.xlsx
@ -777,7 +777,7 @@ apachepoi_46136-NoWarnings.xls
apachepoi_46136-WithWarnings.xls apachepoi_46136-WithWarnings.xls
apachepoi_46137.xls apachepoi_46137.xls
apachepoi_46250.xls apachepoi_46250.xls
apachepoi_46368.xls # apachepoi_46368.xls # string length exceeds 32767 chars
apachepoi_46445.xls apachepoi_46445.xls
apachepoi_46515.xls apachepoi_46515.xls
apachepoi_46670_http.xls apachepoi_46670_http.xls