diff --git a/bits/80_parseods.js b/bits/80_parseods.js index 4327c53..ada6741 100644 --- a/bits/80_parseods.js +++ b/bits/80_parseods.js @@ -27,14 +27,236 @@ var number_formats_ods = { quarter: ["\\Qm", "m\\\"th quarter\""] }; +/* Note: ODS can stick styles in content.xml or styles.xml, FODS blurs lines */ +function parse_ods_styles(d/*:string*/, _opts, _nfm) { + var number_format_map = _nfm || {}; + var str = xlml_normalize(d); + xlmlregex.lastIndex = 0; + str = str.replace(//mg,"").replace(//gm,""); + var Rn, NFtag, NF = "", tNF = "", y, etpos = 0, tidx = -1, infmt = false, payload = ""; + while((Rn = xlmlregex.exec(str))) { + switch((Rn[3]=Rn[3].replace(/_.*$/,""))) { + /* Number Format Definitions */ + case 'number-style': // 16.29.2 + case 'currency-style': // 16.29.8 + case 'percentage-style': // 16.29.10 + case 'date-style': // 16.29.11 + case 'time-style': // 16.29.19 + case 'text-style': // 16.29.26 + if(Rn[1]==='/') { + infmt = false; + if(NFtag['truncate-on-overflow'] == "false") { + if(NF.match(/h/)) NF = NF.replace(/h+/, "[$&]"); + else if(NF.match(/m/)) NF = NF.replace(/m+/, "[$&]"); + else if(NF.match(/s/)) NF = NF.replace(/s+/, "[$&]"); + } + number_format_map[NFtag.name] = NF; + NF = ""; + } else if(Rn[0].charAt(Rn[0].length-2) !== '/') { + infmt = true; + NF = ""; + NFtag = parsexmltag(Rn[0], false); + } break; -function parse_content_xml(d/*:string*/, _opts)/*:Workbook*/ { + // LibreOffice bug https://bugs.documentfoundation.org/show_bug.cgi?id=149484 + case 'boolean-style': // 16.29.24 + if(Rn[1]==='/') { + infmt = false; + number_format_map[NFtag.name] = "General"; + NF = ""; + } else if(Rn[0].charAt(Rn[0].length-2) !== '/') { + infmt = true; + NF = ""; + NFtag = parsexmltag(Rn[0], false); + } break; + + /* Number Format Elements */ + case 'boolean': // 16.29.25 + NF += "General"; // ODF spec is unfortunately underspecified here + break; + + case 'text': // 16.29.27 + if(Rn[1]==='/') { + payload = str.slice(tidx, xlmlregex.lastIndex - Rn[0].length); + // NOTE: Excel has a different interpretation of "%%" and friends + if(payload == "%" && NFtag[0] == ' 16.29.12 + y = parsexmltag(Rn[0], false); + switch(y["style"]) { + case "short": NF += "d"; break; + case "long": NF += "dd"; break; + default: NF += "dd"; break; // TODO: error condition + } + } break; + + case 'day-of-week': { // 16.29.16 + y = parsexmltag(Rn[0], false); + switch(y["style"]) { + case "short": NF += "ddd"; break; + case "long": NF += "dddd"; break; + default: NF += "ddd"; break; + } + } break; + + case 'era': { // 16.29.15 TODO: proper mapping + y = parsexmltag(Rn[0], false); + switch(y["style"]) { + case "short": NF += "ee"; break; + case "long": NF += "eeee"; break; + default: NF += "eeee"; break; // TODO: error condition + } + } break; + + case 'hours': { // 16.29.20 + y = parsexmltag(Rn[0], false); + switch(y["style"]) { + case "short": NF += "h"; break; + case "long": NF += "hh"; break; + default: NF += "hh"; break; // TODO: error condition + } + } break; + + case 'minutes': { // 16.29.21 + y = parsexmltag(Rn[0], false); + switch(y["style"]) { + case "short": NF += "m"; break; + case "long": NF += "mm"; break; + default: NF += "mm"; break; // TODO: error condition + } + } break; + + case 'month': { // 16.29.13 + y = parsexmltag(Rn[0], false); + if(y["textual"]) NF += "mm"; + switch(y["style"]) { + case "short": NF += "m"; break; + case "long": NF += "mm"; break; + default: NF += "m"; break; + } + } break; + + case 'seconds': { // 16.29.22 + y = parsexmltag(Rn[0], false); + switch(y["style"]) { + case "short": NF += "s"; break; + case "long": NF += "ss"; break; + default: NF += "ss"; break; // TODO: error condition + } + if(y["decimal-places"]) NF += "." + fill("0", +y["decimal-places"]); + } break; + + case 'year': { // 16.29.14 + y = parsexmltag(Rn[0], false); + switch(y["style"]) { + case "short": NF += "yy"; break; + case "long": NF += "yyyy"; break; + default: NF += "yy"; break; // TODO: error condition + } + } break; + + case 'am-pm': // 16.29.23 + NF += "AM/PM"; // LO autocorrects A/P -> AM/PM + break; + + case 'week-of-year': // 16.29.17 + case 'quarter': // 16.29.18 + console.error("Excel does not support ODS format token " + Rn[3]); + break; + + case 'fill-character': // 16.29.5 + if(Rn[1]==='/') { + payload = str.slice(tidx, xlmlregex.lastIndex - Rn[0].length); + // NOTE: Excel has a different interpretation of "%%" and friends + NF += '"' + payload.replace(/"/g, '""') + '"*'; + } else if(Rn[0].charAt(Rn[0].length-2) !== '/') { + tidx = xlmlregex.lastIndex; + } break; + + case 'scientific-number': // 16.29.6 + // TODO: find a mapping for all parameters + y = parsexmltag(Rn[0], false); + NF += "0." + fill("0", +y["min-decimal-places"] || +y["decimal-places"] || 2) + fill("?", +y["decimal-places"] - +y["min-decimal-places"] || 0) + "E" + (parsexmlbool(y["forced-exponent-sign"]) ? "+" : "") + fill("0", +y["min-exponent-digits"] || 2); + break; + + case 'fraction': // 16.29.7 + // TODO: find a mapping for all parameters + y = parsexmltag(Rn[0], false); + if(!+y["min-integer-digits"]) NF += "#"; + else NF += fill("0", +y["min-integer-digits"]); + NF += " "; + NF += fill("?", +y["min-numerator-digits"] || 1); + NF += "/"; + if(+y["denominator-value"]) NF += y["denominator-value"]; + else NF += fill("?", +y["min-denominator-digits"] || 1); + break; + + case 'currency-symbol': // 16.29.9 + // TODO: localization with [$-...] + if(Rn[1]==='/') { + NF += '"' + str.slice(tidx, xlmlregex.lastIndex - Rn[0].length).replace(/"/g, '""') + '"'; + } else if(Rn[0].charAt(Rn[0].length-2) !== '/') { + tidx = xlmlregex.lastIndex; + } else NF += "$"; + break; + + case 'text-properties': // 16.29.29 + y = parsexmltag(Rn[0], false); + switch((y["color"]||"").toLowerCase().replace("#", "")) { + case "ff0000": case "red": NF = "[Red]" + NF; break; + } + break; + + case 'text-content': // 16.29.28 + NF += "@"; + break; + + case 'map': // 16.3 + // TODO: handle more complex maps + y = parsexmltag(Rn[0], false); + if(unescapexml(y["condition"]) == "value()>=0") NF = number_format_map[y["apply-style-name"]] + ";" + NF; + else console.error("ODS number format may be incorrect: " + y["condition"]); + break; + + case 'number': // 16.29.3 + // TODO: handle all the attributes + if(Rn[1]==='/') break; + y = parsexmltag(Rn[0], false); + tNF = ""; + tNF += fill("0", +y["min-integer-digits"] || 1); + if(parsexmlbool(y["grouping"])) tNF = commaify(fill("#", Math.max(0, 4 - tNF.length)) + tNF); + if(+y["min-decimal-places"] || +y["decimal-places"]) tNF += "."; + if(+y["min-decimal-places"]) tNF += fill("0", +y["min-decimal-places"] || 1); + if(+y["decimal-places"] - (+y["min-decimal-places"]||0)) tNF += fill("#", +y["decimal-places"] - (+y["min-decimal-places"]||0)); + NF += tNF; + break; + + case 'embedded-text': // 16.29.4 + // TODO: verify interplay with grouping et al + if(Rn[1]==='/') { + if(etpos == 0) NF += '"' + str.slice(tidx, xlmlregex.lastIndex - Rn[0].length).replace(/"/g, '""') + '"'; + else NF = NF.slice(0, etpos) + '"' + str.slice(tidx, xlmlregex.lastIndex - Rn[0].length).replace(/"/g, '""') + '"' + NF.slice(etpos); + } else if(Rn[0].charAt(Rn[0].length-2) !== '/') { + tidx = xlmlregex.lastIndex; + etpos = -+parsexmltag(Rn[0], false)["position"] || 0; + } break; + + }} + return number_format_map; +} + +function parse_content_xml(d/*:string*/, _opts, _nfm)/*:Workbook*/ { var opts = _opts || {}; if(DENSE != null && opts.dense == null) opts.dense = DENSE; var str = xlml_normalize(d); var state/*:Array*/ = [], tmp; var tag/*:: = {}*/; - var NFtag = {name:""}, NF = "", pidx = 0; + var nfidx, NF = "", pidx = 0; var sheetag/*:: = {name:"", '名称':""}*/; var rowtag/*:: = {'行号':""}*/; var Sheets = {}, SheetNames/*:Array*/ = []; @@ -45,7 +267,7 @@ function parse_content_xml(d/*:string*/, _opts)/*:Workbook*/ { var textR = []; var R = -1, C = -1, range = {s: {r:1000000,c:10000000}, e: {r:0, c:0}}; var row_ol = 0; - var number_format_map = {}; + var number_format_map = _nfm || {}, styles = {}; var merges/*:Array*/ = [], mrange = {}, mR = 0, mC = 0; var rowinfo/*:Array*/ = [], rowpeat = 1, colpeat = 1; var arrayf/*:Array<[Range, string]>*/ = []; @@ -56,7 +278,7 @@ function parse_content_xml(d/*:string*/, _opts)/*:Workbook*/ { var creator = "", creatoridx = 0; var isstub = false, intable = false; var i = 0; - var baddate = 1; + var baddate = 0; xlmlregex.lastIndex = 0; str = str.replace(//mg,"").replace(//gm,""); while((Rn = xlmlregex.exec(str))) switch((Rn[3]=Rn[3].replace(/_.*$/,""))) { @@ -114,6 +336,7 @@ function parse_content_xml(d/*:string*/, _opts)/*:Workbook*/ { colpeat = parseInt(ctag['number-columns-repeated']||"1", 10); q = ({t:'z', v:null/*:: , z:null, w:"",c:[]*/}/*:any*/); if(ctag.formula && opts.cellFormula != false) q.f = ods_to_csf_formula(unescapexml(ctag.formula)); + if(ctag["style-name"] && styles[ctag["style-name"]]) q.z = styles[ctag["style-name"]]; if((ctag['数据类型'] || ctag['value-type']) == "string") { q.t = "s"; q.v = unescapexml(ctag['string-value'] || ""); if(opts.dense) { @@ -136,6 +359,7 @@ function parse_content_xml(d/*:string*/, _opts)/*:Workbook*/ { ctag = parsexmltag(Rn[0], false); comments = []; comment = ({}/*:any*/); q = ({t:ctag['数据类型'] || ctag['value-type'], v:null/*:: , z:null, w:"",c:[]*/}/*:any*/); + if(ctag["style-name"] && styles[ctag["style-name"]]) q.z = styles[ctag["style-name"]]; if(opts.cellFormula) { if(ctag.formula) ctag.formula = unescapexml(ctag.formula); if(ctag['number-matrix-columns-spanned'] && ctag['number-matrix-rows-spanned']) { @@ -163,16 +387,16 @@ function parse_content_xml(d/*:string*/, _opts)/*:Workbook*/ { /* 19.385 office:value-type */ switch(q.t) { - case 'boolean': q.t = 'b'; q.v = parsexmlbool(ctag['boolean-value']); break; + case 'boolean': q.t = 'b'; q.v = parsexmlbool(ctag['boolean-value']) || (+ctag['boolean-value'] >= 1); break; case 'float': q.t = 'n'; q.v = parseFloat(ctag.value); break; case 'percentage': q.t = 'n'; q.v = parseFloat(ctag.value); break; case 'currency': q.t = 'n'; q.v = parseFloat(ctag.value); break; case 'date': q.t = 'd'; q.v = parseDate(ctag['date-value']); if(!opts.cellDates) { q.t = 'n'; q.v = datenum(q.v, WB.WBProps.date1904) - baddate; } - q.z = 'm/d/yy'; break; + if(!q.z) q.z = 'm/d/yy'; break; case 'time': q.t = 'n'; q.v = parse_isodur(ctag['time-value'])/86400; if(opts.cellDates) { q.t = 'd'; q.v = numdate(q.v); } - q.z = 'HH:MM:SS'; break; + if(!q.z) q.z = 'HH:MM:SS'; break; case 'number': q.t = 'n'; q.v = parseFloat(ctag['数据数值']); break; default: if(q.t === 'string' || q.t === 'text' || !q.t) { @@ -267,23 +491,24 @@ function parse_content_xml(d/*:string*/, _opts)/*:Workbook*/ { textp = ""; textpidx = 0; textR = []; break; - case 'scientific-number': // TODO: - break; - case 'currency-symbol': // TODO: - break; - case 'currency-style': // TODO: + case 'scientific-number': // + case 'currency-symbol': // + case 'fill-character': // 16.29.5 break; + + case 'text-style': // 16.27.25 + case 'boolean-style': // 16.27.23 case 'number-style': // 16.27.2 + case 'currency-style': // 16.29.8 case 'percentage-style': // 16.27.9 case 'date-style': // 16.27.10 case 'time-style': // 16.27.18 if(Rn[1]==='/'){ - number_format_map[NFtag.name] = NF; - if((tmp=state.pop())[0]!==Rn[3]) throw "Bad state: "+tmp; + var xlmlidx = xlmlregex.lastIndex; + parse_ods_styles(str.slice(nfidx, xlmlregex.lastIndex), _opts, number_format_map); + xlmlregex.lastIndex = xlmlidx; } else if(Rn[0].charAt(Rn[0].length-2) !== '/') { - NF = ""; - NFtag = parsexmltag(Rn[0], false); - state.push([Rn[3], true]); + nfidx = xlmlregex.lastIndex - Rn[0].length; } break; case 'script': break; // 3.13 @@ -292,8 +517,10 @@ function parse_content_xml(d/*:string*/, _opts)/*:Workbook*/ { case 'default-style': // TODO: case 'page-layout': break; // TODO: - case 'style': // 16.2 - break; + case 'style': { // 16.2 + var styletag = parsexmltag(Rn[0], false); + if(styletag["family"] == "table-cell" && number_format_map[styletag["data-style-name"]]) styles[styletag["name"]] = number_format_map[styletag["data-style-name"]]; + } break; case 'map': break; // 16.3 case 'font-face': break; // 16.21 @@ -331,9 +558,7 @@ function parse_content_xml(d/*:string*/, _opts)/*:Workbook*/ { NF += number_formats_ods[Rn[3]][tag.style==='long'?1:0]; break; } break; - case 'boolean-style': break; // 16.27.23 case 'boolean': break; // 16.27.24 - case 'text-style': break; // 16.27.25 case 'text': // 16.27.26 if(Rn[0].slice(-2) === "/>") break; else if(Rn[1]==="/") switch(state[state.length-1][0]) { @@ -564,9 +789,11 @@ function parse_content_xml(d/*:string*/, _opts)/*:Workbook*/ { function parse_ods(zip/*:ZIPFile*/, opts/*:?ParseOpts*/)/*:Workbook*/ { opts = opts || ({}/*:any*/); if(safegetzipfile(zip, 'META-INF/manifest.xml')) parse_manifest(getzipdata(zip, 'META-INF/manifest.xml'), opts); + var styles = getzipstr(zip, 'styles.xml'); + var Styles = styles && parse_ods_styles(utf8read(styles), opts); var content = getzipstr(zip, 'content.xml'); if(!content) throw new Error("Missing content.xml in ODS / UOF file"); - var wb = parse_content_xml(utf8read(content), opts); + var wb = parse_content_xml(utf8read(content), opts, Styles); if(safegetzipfile(zip, 'meta.xml')) wb.Props = parse_core_props(getzipdata(zip, 'meta.xml')); return wb; } diff --git a/bits/81_writeods.js b/bits/81_writeods.js index 88b6369..5d0cd92 100644 --- a/bits/81_writeods.js +++ b/bits/81_writeods.js @@ -30,6 +30,157 @@ var write_styles_ods/*:{(wb:any, opts:any):string}*/ = /* @__PURE__ */(function( return XML_HEADER + payload; }; })(); + +// TODO: find out if anyone actually read the spec. LO has some wild errors +function write_number_format_ods(nf/*:string*/, nfidx/*:string*/)/*:string*/ { + var type = "number", payload = "", nopts = { "style:name": nfidx }, c = "", i = 0; + nf = nf.replace(/"[$]"/g, "$"); + /* TODO: replace with an actual parser based on a real grammar */ + j: { + // TODO: support style maps + if(nf.indexOf(";") > -1) { + console.error("Unsupported ODS Style Map exported. Using first branch of " + nf); + nf = nf.slice(0, nf.indexOf(";")); + } + + if(nf == "@") { type = "text"; payload = ""; break j; } + + /* currency flag */ + if(nf.indexOf(/\$/) > -1) { type = "currency"; } + + /* opening string literal */ + if(nf[i] == '"') { + c = ""; + while(nf[++i] != '"' || nf[++i] == '"') c += nf[i]; --i; + if(nf[i+1] == "*") { + i++; + payload += '' + escapexml(c.replace(/""/g, '"')) + ''; + } else { + payload += '' + escapexml(c.replace(/""/g, '"')) + ''; + } + nf = nf.slice(i+1); i = 0; + } + + /* fractions */ + var t = nf.match(/# (\?+)\/(\?+)/); + if(t) { payload += writextag("number:fraction", null, {"number:min-integer-digits":0, "number:min-numerator-digits": t[1].length, "number:max-denominator-value": Math.max(+(t[1].replace(/./g, "9")), +(t[2].replace(/./g, "9"))) }); break j; } + if((t=nf.match(/# (\?+)\/(\d+)/))) { payload += writextag("number:fraction", null, {"number:min-integer-digits":0, "number:min-numerator-digits": t[1].length, "number:denominator-value": +t[2]}); break j; } + + /* percentages */ + if((t=nf.match(/(\d+)(|\.\d+)%/))) { type = "percentage"; payload += writextag("number:number", null, {"number:decimal-places": t[2] && t.length - 1 || 0, "number:min-decimal-places": t[2] && t.length - 1 || 0, "number:min-integer-digits": t[1].length }) + "%"; break j; } + + /* datetime */ + var has_time = false; + if(["y","m","d"].indexOf(nf[0]) > -1) { + type = "date"; + k: for(; i < nf.length; ++i) switch((c = nf[i].toLowerCase())) { + case "h": case "s": has_time = true; --i; break k; + case "m": + l: for(var h = i+1; h < nf.length; ++h) switch(nf[h]) { + case "y": case "d": break l; + case "h": case "s": has_time = true; --i; break k; + } + /* falls through */ + case "y": case "d": + while((nf[++i]||"").toLowerCase() == c[0]) c += c[0]; --i; + switch(c) { + case "y": case "yy": payload += ""; break; + case "yyy": case "yyyy": payload += ''; break; + case "mmmmm": console.error("ODS has no equivalent of format |mmmmm|"); + /* falls through */ + case "m": case "mm": case "mmm": case "mmmm": + payload += ''; + break; + case "d": case "dd": payload += ''; break; + case "ddd": case "dddd": payload += ''; break; + } + break; + case '"': + while(nf[++i] != '"' || nf[++i] == '"') c += nf[i]; --i; + payload += '' + escapexml(c.slice(1).replace(/""/g, '"')) + ''; + break; + case '/': payload += '' + escapexml(c) + ''; break; + default: console.error("unrecognized character " + c + " in ODF format " + nf); + } + if(!has_time) break j; + nf = nf.slice(i+1); i = 0; + } + if(nf.match(/^\[?[hms]/)) { + if(type == "number") type = "time"; + if(nf.match(/\[/)) { + nf = nf.replace(/[\[\]]/g, ""); + nopts['number:truncate-on-overflow'] = "false"; + } + for(; i < nf.length; ++i) switch((c = nf[i].toLowerCase())) { + case "h": case "m": case "s": + while((nf[++i]||"").toLowerCase() == c[0]) c += c[0]; --i; + switch(c) { + case "h": case "hh": payload += ''; break; + case "m": case "mm": payload += ''; break; + case "s": case "ss": + if(nf[i+1] == ".") do { c += nf[i+1]; ++i; } while(nf[i+1] == "0"); + payload += ''; break; + } + break; + case '"': + while(nf[++i] != '"' || nf[++i] == '"') c += nf[i]; --i; + payload += '' + escapexml(c.slice(1).replace(/""/g, '"')) + ''; + break; + case '/': payload += '' + escapexml(c) + ''; break; + case "a": + if(nf.slice(i, i+3).toLowerCase() == "a/p") { payload += ''; i += 2; break; } // Note: ODF does not support A/P + if(nf.slice(i, i+5).toLowerCase() == "am/pm") { payload += ''; i += 4; break; } + /* falls through */ + default: console.error("unrecognized character " + c + " in ODF format " + nf); + } + break j; + } + + /* currency flag */ + if(nf.indexOf(/\$/) > -1) { type = "currency"; } + + /* should be in a char loop */ + if(nf[0] == "$") { payload += '$'; nf = nf.slice(1); i = 0; } + i = 0; if(nf[i] == '"') { + while(nf[++i] != '"' || nf[++i] == '"') c += nf[i]; --i; + if(nf[i+1] == "*") { + i++; + payload += '' + escapexml(c.replace(/""/g, '"')) + ''; + } else { + payload += '' + escapexml(c.replace(/""/g, '"')) + ''; + } + nf = nf.slice(i+1); i = 0; + } + + /* number TODO: interstitial text e.g. 000)000-0000 */ + var np = nf.match(/([#0][0#,]*)(\.[0#]*|)(E[+]?0*|)/i); + if(!np || !np[0]) console.error("Could not find numeric part of " + nf); + else { + var base = np[1].replace(/,/g, ""); + payload += ' -1 ? ' number:grouping="true"' : "") + + (np[2] && ' number:decimal-places="' + (np[2].length - 1) + '"' || ' number:decimal-places="0"') + + (np[3] && np[3].indexOf("+") > -1 ? ' number:forced-exponent-sign="true"' : "" ) + + (np[3] ? ' number:min-exponent-digits="' + np[3].match(/0+/)[0].length + '"' : "" ) + + '>' + + /* TODO: interstitial text placeholders */ + ''; + i = np.index + np[0].length; + } + + /* residual text */ + if(nf[i] == '"') { + c = ""; + while(nf[++i] != '"' || nf[++i] == '"') c += nf[i]; --i; + payload += '' + escapexml(c.replace(/""/g, '"')) + ''; + } + } + + if(!payload) { console.error("Could not generate ODS number format for |" + nf + "|"); return ""; } + return writextag("number:" + type + "-style", payload, nopts); +} + function write_names_ods(Names, SheetNames, idx) { var scoped = Names.filter(function(name) { return name.Sheet == (idx == -1 ? null : idx); }); if(!scoped.length) return ""; @@ -54,7 +205,7 @@ var write_content_ods/*:{(wb:any, opts:any):string}*/ = /* @__PURE__ */(function var null_cell_xml = ' \n'; var covered_cell_xml = ' \n'; - var write_ws = function(ws, wb/*:Workbook*/, i/*:number*//*::, opts*/)/*:string*/ { + var write_ws = function(ws, wb/*:Workbook*/, i/*:number*/, opts, nfs)/*:string*/ { /* Section 9 Tables */ var o/*:Array*/ = []; o.push(' \n'); @@ -130,6 +281,7 @@ var write_content_ods/*:{(wb:any, opts:any):string}*/ = /* @__PURE__ */(function if(_tgt.charAt(0) != "#" && !_tgt.match(/^\w+:/)) _tgt = '../' + _tgt; text_p = writextag('text:a', text_p, {'xlink:href': _tgt.replace(/&/g, "&")}); } + if(nfs[cell.z]) ct["table:style-name"] = "ce" + nfs[cell.z].slice(1); o.push(' ' + writextag('table:table-cell', writextag('text:p', text_p, {}), ct) + '\n'); } o.push(' \n'); @@ -142,14 +294,6 @@ var write_content_ods/*:{(wb:any, opts:any):string}*/ = /* @__PURE__ */(function var write_automatic_styles_ods = function(o/*:Array*/, wb) { o.push(' \n'); - o.push(' \n'); - o.push(' \n'); - o.push(' /\n'); - o.push(' \n'); - o.push(' /\n'); - o.push(' \n'); - o.push(' \n'); - /* column styles */ var cidx = 0; wb.SheetNames.map(function(n) { return wb.Sheets[n]; }).forEach(function(ws) { @@ -190,12 +334,38 @@ var write_content_ods/*:{(wb:any, opts:any):string}*/ = /* @__PURE__ */(function o.push(' \n'); o.push(' \n'); - /* table cells, text */ + o.push(' \n'); + o.push(' \n'); + o.push(' /\n'); + o.push(' \n'); + o.push(' /\n'); + o.push(' \n'); + o.push(' \n'); + + /* number formats, table cells, text */ + var nfs = {}; + var nfi = 69; + wb.SheetNames.map(function(n) { return wb.Sheets[n]; }).forEach(function(ws) { + if(!ws) return; + var range = decode_range(ws["!ref"]); + for(var R = 0; R <= range.e.r; ++R) for(var C = 0; C <= range.e.c; ++C) { + var c = Array.isArray(ws) ? (ws[R]||[])[C] : ws[encode_cell({r:R,c:C})]; + if(!c || !c.z || c.z.toLowerCase() == "general") continue; + if(!nfs[c.z]) { + var out = write_number_format_ods(c.z, "N" + nfi); + if(out) { nfs[c.z] = "N" + nfi; ++nfi; o.push(out + "\n"); } + } + } + }); o.push(' \n'); + keys(nfs).forEach(function(nf) { + o.push('\n'); + }); /* page-layout */ o.push(' \n'); + return nfs; }; return function wcx(wb, opts) { @@ -248,15 +418,15 @@ var write_content_ods/*:{(wb:any, opts:any):string}*/ = /* @__PURE__ */(function if(opts.bookType == "fods") { o.push('\n'); - o.push(write_meta_ods().replace(/office:document-meta/g, "office:meta")); + o.push(write_meta_ods().replace(//, "").replace(/<\/office:document-meta>/, "") + "\n"); // TODO: settings (equiv of settings.xml for ODS) } else o.push('\n'); // o.push(' \n'); - write_automatic_styles_ods(o, wb); + var nfs = write_automatic_styles_ods(o, wb); o.push(' \n'); o.push(' \n'); if(((wb.Workbook||{}).WBProps||{}).date1904) o.push(' \n \n \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, nfs)); if((wb.Workbook||{}).Names) o.push(write_names_ods(wb.Workbook.Names, wb.SheetNames, -1)); o.push(' \n'); o.push(' \n');