mirror of
https://github.com/optilude/xlsx-template.git
synced 2026-07-02 08:27:39 +08:00
f8a6f94c79
Fix #47
1035 lines
38 KiB
JavaScript
1035 lines
38 KiB
JavaScript
/*jshint globalstrict:true, devel:true */
|
|
/*eslint no-var:0 */
|
|
/*global require, module, Buffer */
|
|
"use strict";
|
|
|
|
var path = require('path'),
|
|
zip = require('jszip'),
|
|
etree = require('elementtree');
|
|
|
|
module.exports = (function() {
|
|
|
|
var DOCUMENT_RELATIONSHIP = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
|
|
CALC_CHAIN_RELATIONSHIP = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/calcChain",
|
|
SHARED_STRINGS_RELATIONSHIP = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings";
|
|
|
|
/**
|
|
* Create a new workbook. Either pass the raw data of a .xlsx file,
|
|
* or call `loadTemplate()` later.
|
|
*/
|
|
var Workbook = function(data) {
|
|
var self = this;
|
|
|
|
self.archive = null;
|
|
self.sharedStrings = [];
|
|
self.sharedStringsLookup = {};
|
|
|
|
if(data) {
|
|
self.loadTemplate(data);
|
|
}
|
|
};
|
|
|
|
var _get_simple = function (obj, desc) {
|
|
if (desc.indexOf("[") >=0 ) {
|
|
var specification = desc.split(/[[[\]]/);
|
|
var property = specification[0];
|
|
var index = specification[1];
|
|
return obj[property][index];
|
|
}
|
|
|
|
return obj[desc];
|
|
}
|
|
|
|
/**
|
|
* Based on http://stackoverflow.com/questions/8051975
|
|
* Mimic https://lodash.com/docs#get
|
|
*/
|
|
var _get = function(obj, desc, defaultValue) {
|
|
var arr = desc.split('.');
|
|
try {
|
|
while (arr.length) {
|
|
obj = _get_simple(obj, arr.shift());
|
|
}
|
|
} catch(ex) {
|
|
/* invalid chain */
|
|
obj = undefined;
|
|
}
|
|
return obj === undefined ? defaultValue : obj;
|
|
}
|
|
|
|
/**
|
|
* Delete unused sheets if needed
|
|
*/
|
|
Workbook.prototype.deleteSheet = function(sheetName){
|
|
var self = this;
|
|
var sheet = self.loadSheet(sheetName);
|
|
|
|
Object.keys(self.archive.files).forEach(function(key){
|
|
if (key.indexOf(sheet.id) != -1)
|
|
delete self.archive.files[key];
|
|
})
|
|
|
|
var id = self.sheets.findIndex(function(el){return el.id == sheet.id});
|
|
self.sheets.splice(id, 1);
|
|
|
|
self.workbook.find("sheets").delItem(id);
|
|
|
|
var el = self.workbook.find("definedNames");
|
|
if (el){
|
|
el.delItem(id);
|
|
el.findall("definedName").forEach(function(item, index){
|
|
item.attrib.localSheetId = index.toString()
|
|
})
|
|
}
|
|
self.archive.file(self.workbookPath, etree.tostring(self.workbook));
|
|
};
|
|
|
|
/**
|
|
* Load a .xlsx file from a byte array.
|
|
*/
|
|
Workbook.prototype.loadTemplate = function(data) {
|
|
var self = this;
|
|
|
|
if(Buffer.isBuffer(data)) {
|
|
data = data.toString('binary');
|
|
}
|
|
|
|
self.archive = new zip(data, {base64: false, checkCRC32: true});
|
|
|
|
// Load relationships
|
|
var rels = etree.parse(self.archive.file("_rels/.rels").asText()).getroot(),
|
|
workbookPath = rels.find("Relationship[@Type='" + DOCUMENT_RELATIONSHIP + "']").attrib.Target;
|
|
|
|
self.workbookPath = workbookPath;
|
|
self.prefix = path.dirname(workbookPath);
|
|
self.workbook = etree.parse(self.archive.file(workbookPath).asText()).getroot();
|
|
self.workbookRels = etree.parse(self.archive.file(self.prefix + "/" + '_rels' + "/" + path.basename(workbookPath) + '.rels').asText()).getroot();
|
|
self.sheets = self.loadSheets(self.prefix, self.workbook, self.workbookRels);
|
|
self.calChainRel = self.workbookRels.find("Relationship[@Type='" + CALC_CHAIN_RELATIONSHIP + "']")
|
|
|
|
if (self.calChainRel) {
|
|
self.calcChainPath = self.prefix + "/" + self.calChainRel.attrib.Target;
|
|
}
|
|
|
|
self.sharedStringsPath = self.prefix + "/" + self.workbookRels.find("Relationship[@Type='" + SHARED_STRINGS_RELATIONSHIP + "']").attrib.Target;
|
|
self.sharedStrings = [];
|
|
etree.parse(self.archive.file(self.sharedStringsPath).asText()).getroot().findall('si').forEach(function(si) {
|
|
var t = {text:''};
|
|
si.findall('t').forEach(function(tmp){
|
|
t.text += tmp.text;
|
|
});
|
|
si.findall('r/t').forEach(function(tmp){
|
|
t.text += tmp.text;
|
|
});
|
|
self.sharedStrings.push(t.text);
|
|
self.sharedStringsLookup[t.text] = self.sharedStrings.length - 1;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Interpolate values for the sheet with the given number (1-based) or
|
|
* name (if a string) using the given substitutions (an object).
|
|
*/
|
|
Workbook.prototype.substitute = function(sheetName, substitutions) {
|
|
var self = this;
|
|
|
|
var sheet = self.loadSheet(sheetName);
|
|
|
|
var dimension = sheet.root.find("dimension"),
|
|
sheetData = sheet.root.find("sheetData"),
|
|
currentRow = null,
|
|
totalRowsInserted = 0,
|
|
totalColumnsInserted = 0,
|
|
namedTables = self.loadTables(sheet.root, sheet.filename),
|
|
rows = [];
|
|
|
|
sheetData.findall("row").forEach(function(row) {
|
|
row.attrib.r = currentRow = self.getCurrentRow(row, totalRowsInserted);
|
|
rows.push(row);
|
|
|
|
var cells = [],
|
|
cellsInserted = 0,
|
|
newTableRows = [];
|
|
|
|
row.findall("c").forEach(function(cell) {
|
|
var appendCell = true;
|
|
cell.attrib.r = self.getCurrentCell(cell, currentRow, cellsInserted);
|
|
|
|
//Here we are forcing the values in formulas to be recalculated
|
|
var formulas = cell.findall('f');
|
|
if (formulas && formulas.length > 0) {
|
|
cell.findall('v').forEach(function(v){
|
|
cell.remove(v);
|
|
});
|
|
}
|
|
|
|
// If c[@t="s"] (string column), look up /c/v@text as integer in
|
|
// `this.sharedStrings`
|
|
if(cell.attrib.t === "s") {
|
|
|
|
// Look for a shared string that may contain placeholders
|
|
var cellValue = cell.find("v"),
|
|
stringIndex = parseInt(cellValue.text, 10),
|
|
string = self.sharedStrings[stringIndex];
|
|
|
|
if(string === undefined) {
|
|
return;
|
|
}
|
|
|
|
// Loop over placeholders
|
|
self.extractPlaceholders(string).forEach(function(placeholder) {
|
|
|
|
// Only substitute things for which we have a substitution
|
|
var substitution = _get(substitutions, placeholder.name, ''),
|
|
newCellsInserted = 0;
|
|
|
|
if(placeholder.full && placeholder.type === "table" && substitution instanceof Array) {
|
|
newCellsInserted = self.substituteTable(
|
|
row, newTableRows,
|
|
cells, cell,
|
|
namedTables, substitution, placeholder.key
|
|
);
|
|
|
|
// don't double-insert cells
|
|
if (newCellsInserted !== 0
|
|
|| substitution.length <= 1) {
|
|
appendCell = false;
|
|
}
|
|
|
|
// Did we insert new columns (array values)?
|
|
if(newCellsInserted !== 0) {
|
|
cellsInserted += newCellsInserted;
|
|
self.pushRight(self.workbook, sheet.root, cell.attrib.r, newCellsInserted);
|
|
}
|
|
} else if(placeholder.full && placeholder.type === "normal" && substitution instanceof Array) {
|
|
appendCell = false; // don't double-insert cells
|
|
newCellsInserted = self.substituteArray(
|
|
cells, cell, substitution
|
|
);
|
|
|
|
if(newCellsInserted !== 0) {
|
|
cellsInserted += newCellsInserted;
|
|
self.pushRight(self.workbook, sheet.root, cell.attrib.r, newCellsInserted);
|
|
}
|
|
} else {
|
|
if (placeholder.key) {
|
|
substitution = _get(substitutions, placeholder.name + '.' + placeholder.key);
|
|
}
|
|
string = self.substituteScalar(cell, string, placeholder, substitution);
|
|
}
|
|
});
|
|
}
|
|
|
|
// if we are inserting columns, we may not want to keep the original cell anymore
|
|
if(appendCell) {
|
|
cells.push(cell);
|
|
}
|
|
|
|
}); // cells loop
|
|
|
|
// We may have inserted columns, so re-build the children of the row
|
|
self.replaceChildren(row, cells);
|
|
|
|
// Update row spans attribute
|
|
if(cellsInserted !== 0) {
|
|
self.updateRowSpan(row, cellsInserted);
|
|
|
|
if(cellsInserted > totalColumnsInserted) {
|
|
totalColumnsInserted = cellsInserted;
|
|
}
|
|
|
|
}
|
|
|
|
// Add newly inserted rows
|
|
if(newTableRows.length > 0) {
|
|
newTableRows.forEach(function(row) {
|
|
rows.push(row);
|
|
++totalRowsInserted;
|
|
});
|
|
self.pushDown(self.workbook, sheet.root, namedTables, currentRow, newTableRows.length);
|
|
}
|
|
|
|
}); // rows loop
|
|
|
|
// We may have inserted rows, so re-build the children of the sheetData
|
|
self.replaceChildren(sheetData, rows);
|
|
|
|
// Update placeholders in table column headers
|
|
self.substituteTableColumnHeaders(namedTables, substitutions);
|
|
|
|
// Update <dimension /> if we added rows or columns
|
|
if(dimension) {
|
|
if(totalRowsInserted > 0 || totalColumnsInserted > 0) {
|
|
var dimensionRange = self.splitRange(dimension.attrib.ref),
|
|
dimensionEndRef = self.splitRef(dimensionRange.end);
|
|
|
|
dimensionEndRef.row += totalRowsInserted;
|
|
dimensionEndRef.col = self.numToChar(self.charToNum(dimensionEndRef.col) + totalColumnsInserted);
|
|
dimensionRange.end = self.joinRef(dimensionEndRef);
|
|
|
|
dimension.attrib.ref = self.joinRange(dimensionRange);
|
|
}
|
|
}
|
|
|
|
// Write back the modified XML trees
|
|
self.archive.file(sheet.filename, etree.tostring(sheet.root));
|
|
self.archive.file(self.workbookPath, etree.tostring(self.workbook));
|
|
|
|
// Remove calc chain - Excel will re-build, and we may have moved some formulae
|
|
if(self.calcChainPath && self.archive.file(self.calcChainPath)) {
|
|
self.archive.remove(self.calcChainPath);
|
|
}
|
|
|
|
self.writeSharedStrings();
|
|
self.writeTables(namedTables);
|
|
};
|
|
|
|
/**
|
|
* Generate a new binary .xlsx file
|
|
*/
|
|
Workbook.prototype.generate = function(options) {
|
|
var self = this;
|
|
|
|
if(!options) {
|
|
options = {
|
|
base64: false
|
|
}
|
|
}
|
|
|
|
return self.archive.generate(options);
|
|
};
|
|
|
|
// Helpers
|
|
|
|
// Write back the new shared strings list
|
|
Workbook.prototype.writeSharedStrings = function() {
|
|
var self = this;
|
|
|
|
var root = etree.parse(self.archive.file(self.sharedStringsPath).asText()).getroot(),
|
|
children = root.getchildren();
|
|
|
|
root.delSlice(0, children.length);
|
|
|
|
self.sharedStrings.forEach(function(string) {
|
|
var si = new etree.Element("si"),
|
|
t = new etree.Element("t");
|
|
|
|
t.text = string;
|
|
si.append(t);
|
|
root.append(si);
|
|
});
|
|
|
|
root.attrib.count = self.sharedStrings.length;
|
|
root.attrib.uniqueCount = self.sharedStrings.length;
|
|
|
|
self.archive.file(self.sharedStringsPath, etree.tostring(root));
|
|
};
|
|
|
|
// Add a new shared string
|
|
Workbook.prototype.addSharedString = function(s) {
|
|
var self = this;
|
|
|
|
var idx = self.sharedStrings.length;
|
|
self.sharedStrings.push(s);
|
|
self.sharedStringsLookup[s] = idx;
|
|
|
|
return idx;
|
|
};
|
|
|
|
// Get the number of a shared string, adding a new one if necessary.
|
|
Workbook.prototype.stringIndex = function(s) {
|
|
var self = this;
|
|
|
|
var idx = self.sharedStringsLookup[s];
|
|
if(idx === undefined) {
|
|
idx = self.addSharedString(s);
|
|
}
|
|
return idx;
|
|
};
|
|
|
|
// Replace a shared string with a new one at the same index. Return the
|
|
// index.
|
|
Workbook.prototype.replaceString = function(oldString, newString) {
|
|
var self = this;
|
|
|
|
var idx = self.sharedStringsLookup[oldString];
|
|
if(idx === undefined) {
|
|
idx = self.addSharedString(newString);
|
|
} else {
|
|
self.sharedStrings[idx] = newString;
|
|
delete self.sharedStringsLookup[oldString];
|
|
self.sharedStringsLookup[newString] = idx;
|
|
}
|
|
|
|
return idx;
|
|
};
|
|
|
|
// Get a list of sheet ids, names and filenames
|
|
Workbook.prototype.loadSheets = function(prefix, workbook, workbookRels) {
|
|
var sheets = [];
|
|
|
|
workbook.findall("sheets/sheet").forEach(function(sheet) {
|
|
var sheetId = sheet.attrib.sheetId,
|
|
relId = sheet.attrib['r:id'],
|
|
relationship = workbookRels.find("Relationship[@Id='" + relId + "']"),
|
|
filename = prefix + "/" + relationship.attrib.Target;
|
|
|
|
sheets.push({
|
|
id: parseInt(sheetId, 10),
|
|
name: sheet.attrib.name,
|
|
filename: filename
|
|
});
|
|
});
|
|
|
|
return sheets;
|
|
};
|
|
|
|
// Get sheet a sheet, including filename and name
|
|
Workbook.prototype.loadSheet = function(sheet) {
|
|
var self = this;
|
|
|
|
var info = null;
|
|
|
|
for(var i = 0; i < self.sheets.length; ++i) {
|
|
if((typeof(sheet) === "number" && self.sheets[i].id === sheet) || (self.sheets[i].name === sheet)) {
|
|
info = self.sheets[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(info === null) {
|
|
throw new Error("Sheet " + sheet + " not found");
|
|
}
|
|
|
|
return {
|
|
filename: info.filename,
|
|
name: info.name,
|
|
id: info.id,
|
|
root: etree.parse(self.archive.file(info.filename).asText()).getroot()
|
|
};
|
|
};
|
|
|
|
// Load tables for a given sheet
|
|
Workbook.prototype.loadTables = function(sheet, sheetFilename) {
|
|
var self = this;
|
|
|
|
var sheetDirectory = path.dirname(sheetFilename),
|
|
sheetName = path.basename(sheetFilename),
|
|
relsFilename = sheetDirectory + "/" + '_rels' + "/" + sheetName + '.rels',
|
|
relsFile = self.archive.file(relsFilename),
|
|
tables = []; // [{filename: ..., root: ....}]
|
|
|
|
if(relsFile === null) {
|
|
return tables;
|
|
}
|
|
|
|
var rels = etree.parse(relsFile.asText()).getroot();
|
|
|
|
sheet.findall("tableParts/tablePart").forEach(function(tablePart) {
|
|
var relationshipId = tablePart.attrib['r:id'],
|
|
target = rels.find("Relationship[@Id='" + relationshipId + "']").attrib.Target,
|
|
tableFilename = target.replace('..', self.prefix),
|
|
tableTree = etree.parse(self.archive.file(tableFilename).asText());
|
|
|
|
tables.push({
|
|
filename: tableFilename,
|
|
root: tableTree.getroot()
|
|
});
|
|
});
|
|
|
|
return tables;
|
|
};
|
|
|
|
// Write back possibly-modified tables
|
|
Workbook.prototype.writeTables = function(tables) {
|
|
var self = this;
|
|
|
|
tables.forEach(function(namedTable) {
|
|
self.archive.file(namedTable.filename, etree.tostring(namedTable.root));
|
|
});
|
|
};
|
|
|
|
// Perform substitution in table headers
|
|
Workbook.prototype.substituteTableColumnHeaders = function(tables, substitutions) {
|
|
var self = this;
|
|
|
|
tables.forEach(function(table) {
|
|
var root = table.root,
|
|
columns = root.find("tableColumns"),
|
|
autoFilter = root.find("autoFilter"),
|
|
tableRange = self.splitRange(root.attrib.ref),
|
|
idx = 0,
|
|
inserted = 0,
|
|
newColumns = [];
|
|
|
|
columns.findall("tableColumn").forEach(function(col) {
|
|
++idx;
|
|
col.attrib.id = Number(idx).toString();
|
|
newColumns.push(col);
|
|
|
|
var name = col.attrib.name;
|
|
|
|
self.extractPlaceholders(name).forEach(function(placeholder) {
|
|
var substitution = substitutions[placeholder.name];
|
|
if(substitution === undefined) {
|
|
return;
|
|
}
|
|
|
|
// Array -> new columns
|
|
if(placeholder.full && placeholder.type === "normal" && substitution instanceof Array) {
|
|
substitution.forEach(function(element, i) {
|
|
var newCol = col;
|
|
if(i > 0) {
|
|
newCol = self.cloneElement(newCol);
|
|
newCol.attrib.id = Number(++idx).toString();
|
|
newColumns.push(newCol);
|
|
++inserted;
|
|
tableRange.end = self.nextCol(tableRange.end);
|
|
}
|
|
newCol.attrib.name = self.stringify(element);
|
|
});
|
|
// Normal placeholder
|
|
} else {
|
|
name = name.replace(placeholder.placeholder, self.stringify(substitution));
|
|
col.attrib.name = name;
|
|
}
|
|
});
|
|
});
|
|
|
|
self.replaceChildren(columns, newColumns);
|
|
|
|
// Update range if we inserted columns
|
|
if(inserted > 0) {
|
|
columns.attrib.count = Number(idx).toString();
|
|
root.attrib.ref = self.joinRange(tableRange);
|
|
if(autoFilter !== null) {
|
|
// XXX: This is a simplification that may stomp on some configurations
|
|
autoFilter.attrib.ref = self.joinRange(tableRange);
|
|
}
|
|
}
|
|
|
|
//update ranges for totalsRowCount
|
|
var tableRoot = table.root,
|
|
tableRange = self.splitRange(tableRoot.attrib.ref),
|
|
tableStart = self.splitRef(tableRange.start),
|
|
tableEnd = self.splitRef(tableRange.end);
|
|
|
|
if (tableRoot.attrib.totalsRowCount) {
|
|
var autoFilter = tableRoot.find("autoFilter");
|
|
if(autoFilter !== null) {
|
|
autoFilter.attrib.ref = self.joinRange({
|
|
start: self.joinRef(tableStart),
|
|
end: self.joinRef(tableEnd),
|
|
});
|
|
}
|
|
|
|
++tableEnd.row;
|
|
tableRoot.attrib.ref = self.joinRange({
|
|
start: self.joinRef(tableStart),
|
|
end: self.joinRef(tableEnd),
|
|
});
|
|
|
|
}
|
|
});
|
|
};
|
|
|
|
// Return a list of tokens that may exist in the string.
|
|
// Keys are: `placeholder` (the full placeholder, including the `${}`
|
|
// delineators), `name` (the name part of the token), `key` (the object key
|
|
// for `table` tokens), `full` (boolean indicating whether this placeholder
|
|
// is the entirety of the string) and `type` (one of `table` or `cell`)
|
|
Workbook.prototype.extractPlaceholders = function(string) {
|
|
// Yes, that's right. It's a bunch of brackets and question marks and stuff.
|
|
var re = /\${(?:(.+?):)?(.+?)(?:\.(.+?))?}/g;
|
|
|
|
var match = null, matches = [];
|
|
while((match = re.exec(string)) !== null) {
|
|
matches.push({
|
|
placeholder: match[0],
|
|
type: match[1] || 'normal',
|
|
name: match[2],
|
|
key: match[3],
|
|
full: match[0].length === string.length
|
|
});
|
|
}
|
|
|
|
return matches;
|
|
};
|
|
|
|
// Split a reference into an object with keys `row` and `col` and,
|
|
// optionally, `table`, `rowAbsolute` and `colAbsolute`.
|
|
Workbook.prototype.splitRef = function(ref) {
|
|
var match = ref.match(/(?:(.+)!)?(\$)?([A-Z]+)(\$)?([0-9]+)/);
|
|
return {
|
|
table: match && match[1] || null,
|
|
colAbsolute: Boolean(match && match[2]),
|
|
col: match && match[3],
|
|
rowAbsolute: Boolean(match && match[4]),
|
|
row: parseInt(match && match[5], 10)
|
|
};
|
|
};
|
|
|
|
// Join an object with keys `row` and `col` into a single reference string
|
|
Workbook.prototype.joinRef = function(ref) {
|
|
return (ref.table? ref.table + "!" : "") +
|
|
(ref.colAbsolute? "$" : "") +
|
|
ref.col.toUpperCase() +
|
|
(ref.rowAbsolute? "$" : "" )+
|
|
Number(ref.row).toString();
|
|
};
|
|
|
|
// Get the next column's cell reference given a reference like "B2".
|
|
Workbook.prototype.nextCol = function(ref) {
|
|
var self = this;
|
|
ref = ref.toUpperCase();
|
|
return ref.replace(/[A-Z]+/, function(match) {
|
|
return self.numToChar(self.charToNum(match) + 1);
|
|
});
|
|
};
|
|
|
|
// Get the next row's cell reference given a reference like "B2".
|
|
Workbook.prototype.nextRow = function(ref) {
|
|
ref = ref.toUpperCase();
|
|
return ref.replace(/[0-9]+/, function(match) {
|
|
return (parseInt(match, 10) + 1).toString();
|
|
});
|
|
};
|
|
|
|
// Turn a reference like "AA" into a number like 27
|
|
Workbook.prototype.charToNum = function(str) {
|
|
var num = 0;
|
|
for(var idx = str.length - 1, iteration = 0; idx >= 0; --idx, ++iteration) {
|
|
var thisChar = str.charCodeAt(idx) - 64, // A -> 1; B -> 2; ... Z->26
|
|
multiplier = Math.pow(26, iteration);
|
|
num += multiplier * thisChar;
|
|
}
|
|
return num;
|
|
};
|
|
|
|
// Turn a number like 27 into a reference like "AA"
|
|
Workbook.prototype.numToChar = function(num) {
|
|
var str = "";
|
|
|
|
|
|
for(var i = 0; num > 0; ++i) {
|
|
var remainder = num % 26,
|
|
charCode = remainder + 64;
|
|
num = (num - remainder) / 26;
|
|
|
|
// Compensate for the fact that we don't represent zero, e.g. A = 1, Z = 26, but AA = 27
|
|
if(remainder === 0) { // 26 -> Z
|
|
charCode = 90;
|
|
--num;
|
|
}
|
|
|
|
str = String.fromCharCode(charCode) + str;
|
|
}
|
|
|
|
return str;
|
|
};
|
|
|
|
// Is ref a range?
|
|
Workbook.prototype.isRange = function(ref) {
|
|
return ref.indexOf(':') !== -1;
|
|
};
|
|
|
|
// Is ref inside the table defined by startRef and endRef?
|
|
Workbook.prototype.isWithin = function(ref, startRef, endRef) {
|
|
var self = this;
|
|
|
|
var start = self.splitRef(startRef),
|
|
end = self.splitRef(endRef),
|
|
target = self.splitRef(ref);
|
|
|
|
start.col = self.charToNum(start.col);
|
|
end.col = self.charToNum(end.col);
|
|
target.col = self.charToNum(target.col);
|
|
|
|
return (
|
|
start.row <= target.row && target.row <= end.row &&
|
|
start.col <= target.col && target.col <= end.col
|
|
);
|
|
|
|
};
|
|
|
|
// Turn a value of any type into a string
|
|
Workbook.prototype.stringify = function (value) {
|
|
if(value instanceof Date) {
|
|
//In Excel date is a number of days since 01/01/1900
|
|
// timestamp in ms to days + number of days from 1900 to 1970
|
|
return Number( (value.getTime()/(1000*60*60*24)) + 25569);
|
|
} else if(typeof(value) === "number" || typeof(value) === "boolean") {
|
|
return Number(value).toString();
|
|
} else if(typeof(value) === "string") {
|
|
return String(value).toString();
|
|
}
|
|
|
|
return "";
|
|
};
|
|
|
|
// Insert a substitution value into a cell (c tag)
|
|
Workbook.prototype.insertCellValue = function(cell, substitution) {
|
|
var self = this;
|
|
|
|
var cellValue = cell.find("v"),
|
|
stringified = self.stringify(substitution);
|
|
|
|
if(typeof(substitution) === "number" || substitution instanceof Date) {
|
|
delete cell.attrib.t;
|
|
cellValue.text = stringified;
|
|
} else if(typeof(substitution) === "boolean" ) {
|
|
cell.attrib.t = "b";
|
|
cellValue.text = stringified;
|
|
} else {
|
|
cell.attrib.t = "s";
|
|
cellValue.text = Number(self.stringIndex(stringified)).toString();
|
|
}
|
|
|
|
return stringified;
|
|
};
|
|
|
|
// Perform substitution of a single value
|
|
Workbook.prototype.substituteScalar = function(cell, string, placeholder, substitution) {
|
|
var self = this;
|
|
|
|
if(placeholder.full && typeof(substitution) === "string") {
|
|
self.replaceString(string, substitution);
|
|
}
|
|
|
|
if(placeholder.full) {
|
|
return self.insertCellValue(cell, substitution);
|
|
} else {
|
|
var newString = string.replace(placeholder.placeholder, self.stringify(substitution));
|
|
cell.attrib.t = "s";
|
|
self.replaceString(string, newString);
|
|
return newString;
|
|
}
|
|
|
|
};
|
|
|
|
// Perform a columns substitution from an array
|
|
Workbook.prototype.substituteArray = function(cells, cell, substitution) {
|
|
var self = this;
|
|
|
|
var newCellsInserted = -1, // we technically delete one before we start adding back
|
|
currentCell = cell.attrib.r;
|
|
|
|
// add a cell for each element in the list
|
|
substitution.forEach(function(element) {
|
|
++newCellsInserted;
|
|
|
|
if(newCellsInserted > 0) {
|
|
currentCell = self.nextCol(currentCell);
|
|
}
|
|
|
|
var newCell = self.cloneElement(cell);
|
|
self.insertCellValue(newCell, element);
|
|
|
|
newCell.attrib.r = currentCell;
|
|
cells.push(newCell);
|
|
});
|
|
|
|
return newCellsInserted;
|
|
};
|
|
|
|
// Perform a table substitution. May update `newTableRows` and `cells` and change `cell`.
|
|
// Returns total number of new cells inserted on the original row.
|
|
Workbook.prototype.substituteTable = function(row, newTableRows, cells, cell, namedTables, substitution, key) {
|
|
var self = this,
|
|
newCellsInserted = 0; // on the original row
|
|
|
|
// if no elements, blank the cell, but don't delete it
|
|
if(substitution.length === 0) {
|
|
delete cell.attrib.t;
|
|
self.replaceChildren(cell, []);
|
|
} else {
|
|
|
|
var parentTables = namedTables.filter(function(namedTable) {
|
|
var range = self.splitRange(namedTable.root.attrib.ref);
|
|
return self.isWithin(cell.attrib.r, range.start, range.end);
|
|
});
|
|
|
|
substitution.forEach(function(element, idx) {
|
|
var newRow, newCell,
|
|
newCellsInsertedOnNewRow = 0,
|
|
newCells = [],
|
|
value = _get(element, key, '');
|
|
|
|
if(idx === 0) { // insert in the row where the placeholders are
|
|
|
|
if(value instanceof Array) {
|
|
newCellsInserted = self.substituteArray(cells, cell, value);
|
|
} else {
|
|
self.insertCellValue(cell, value);
|
|
}
|
|
|
|
} else { // insert new rows (or reuse rows just inserted)
|
|
|
|
// Do we have an existing row to use? If not, create one.
|
|
if((idx - 1) < newTableRows.length) {
|
|
newRow = newTableRows[idx - 1];
|
|
} else {
|
|
newRow = self.cloneElement(row, false);
|
|
newRow.attrib.r = self.getCurrentRow(row, newTableRows.length + 1);
|
|
newTableRows.push(newRow);
|
|
}
|
|
|
|
// Create a new cell
|
|
newCell = self.cloneElement(cell);
|
|
newCell.attrib.r = self.joinRef({
|
|
row: newRow.attrib.r,
|
|
col: self.splitRef(newCell.attrib.r).col
|
|
});
|
|
|
|
if(value instanceof Array) {
|
|
newCellsInsertedOnNewRow = self.substituteArray(newCells, newCell, value);
|
|
|
|
// Add each of the new cells created by substituteArray()
|
|
newCells.forEach(function(newCell) {
|
|
newRow.append(newCell);
|
|
});
|
|
|
|
self.updateRowSpan(newRow, newCellsInsertedOnNewRow);
|
|
} else {
|
|
self.insertCellValue(newCell, value);
|
|
|
|
// Add the cell that previously held the placeholder
|
|
newRow.append(newCell);
|
|
}
|
|
|
|
// expand named table range if necessary
|
|
parentTables.forEach(function(namedTable) {
|
|
var tableRoot = namedTable.root,
|
|
autoFilter = tableRoot.find("autoFilter"),
|
|
range = self.splitRange(tableRoot.attrib.ref);
|
|
|
|
if(!self.isWithin(newCell.attrib.r, range.start, range.end)) {
|
|
range.end = self.nextRow(range.end);
|
|
tableRoot.attrib.ref = self.joinRange(range);
|
|
if(autoFilter !== null) {
|
|
// XXX: This is a simplification that may stomp on some configurations
|
|
autoFilter.attrib.ref = tableRoot.attrib.ref;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
return newCellsInserted;
|
|
};
|
|
|
|
// Clone an element. If `deep` is true, recursively clone children
|
|
Workbook.prototype.cloneElement = function(element, deep) {
|
|
var self = this;
|
|
|
|
var newElement = etree.Element(element.tag, element.attrib);
|
|
newElement.text = element.text;
|
|
newElement.tail = element.tail;
|
|
|
|
if(deep !== false) {
|
|
element.getchildren().forEach(function(child) {
|
|
newElement.append(self.cloneElement(child, deep));
|
|
});
|
|
}
|
|
|
|
return newElement;
|
|
};
|
|
|
|
// Replace all children of `parent` with the nodes in the list `children`
|
|
Workbook.prototype.replaceChildren = function(parent, children) {
|
|
parent.delSlice(0, parent.len());
|
|
children.forEach(function(child) {
|
|
parent.append(child);
|
|
});
|
|
};
|
|
|
|
// Calculate the current row based on a source row and a number of new rows
|
|
// that have been inserted above
|
|
Workbook.prototype.getCurrentRow = function(row, rowsInserted) {
|
|
return parseInt(row.attrib.r, 10) + rowsInserted;
|
|
};
|
|
|
|
// Calculate the current cell based on asource cell, the current row index,
|
|
// and a number of new cells that have been inserted so far
|
|
Workbook.prototype.getCurrentCell = function(cell, currentRow, cellsInserted) {
|
|
var self = this;
|
|
|
|
var colRef = self.splitRef(cell.attrib.r).col,
|
|
colNum = self.charToNum(colRef);
|
|
|
|
return self.joinRef({
|
|
row: currentRow,
|
|
col: self.numToChar(colNum + cellsInserted)
|
|
});
|
|
};
|
|
|
|
// Adjust the row `spans` attribute by `cellsInserted`
|
|
Workbook.prototype.updateRowSpan = function(row, cellsInserted) {
|
|
if(cellsInserted !== 0 && row.attrib.spans) {
|
|
var rowSpan = row.attrib.spans.split(':').map(function(f) { return parseInt(f, 10); });
|
|
rowSpan[1] += cellsInserted;
|
|
row.attrib.spans = rowSpan.join(":");
|
|
}
|
|
};
|
|
|
|
// Split a range like "A1:B1" into {start: "A1", end: "B1"}
|
|
Workbook.prototype.splitRange = function(range) {
|
|
var split = range.split(":");
|
|
return {
|
|
start: split[0],
|
|
end: split[1]
|
|
};
|
|
};
|
|
|
|
// Join into a a range like "A1:B1" an object like {start: "A1", end: "B1"}
|
|
Workbook.prototype.joinRange = function(range) {
|
|
return range.start + ":" + range.end;
|
|
};
|
|
|
|
// Look for any merged cell or named range definitions to the right of
|
|
// `currentCell` and push right by `numCols`.
|
|
Workbook.prototype.pushRight = function(workbook, sheet, currentCell, numCols) {
|
|
var self = this;
|
|
|
|
var cellRef = self.splitRef(currentCell),
|
|
currentRow = cellRef.row,
|
|
currentCol = self.charToNum(cellRef.col);
|
|
|
|
// Update merged cells on the same row, at a higher column
|
|
sheet.findall("mergeCells/mergeCell").forEach(function(mergeCell) {
|
|
var mergeRange = self.splitRange(mergeCell.attrib.ref),
|
|
mergeStart = self.splitRef(mergeRange.start),
|
|
mergeStartCol = self.charToNum(mergeStart.col),
|
|
mergeEnd = self.splitRef(mergeRange.end),
|
|
mergeEndCol = self.charToNum(mergeEnd.col);
|
|
|
|
if(mergeStart.row === currentRow && currentCol < mergeStartCol) {
|
|
mergeStart.col = self.numToChar(mergeStartCol + numCols);
|
|
mergeEnd.col = self.numToChar(mergeEndCol + numCols);
|
|
|
|
mergeCell.attrib.ref = self.joinRange({
|
|
start: self.joinRef(mergeStart),
|
|
end: self.joinRef(mergeEnd),
|
|
});
|
|
}
|
|
});
|
|
|
|
// Named cells/ranges
|
|
workbook.findall("definedNames/definedName").forEach(function(name) {
|
|
var ref = name.text;
|
|
|
|
if(self.isRange(ref)) {
|
|
var namedRange = self.splitRange(ref),
|
|
namedStart = self.splitRef(namedRange.start),
|
|
namedStartCol = self.charToNum(namedStart.col),
|
|
namedEnd = self.splitRef(namedRange.end),
|
|
namedEndCol = self.charToNum(namedEnd.col);
|
|
|
|
if(namedStart.row === currentRow && currentCol < namedStartCol) {
|
|
namedStart.col = self.numToChar(namedStartCol + numCols);
|
|
namedEnd.col = self.numToChar(namedEndCol + numCols);
|
|
|
|
name.text = self.joinRange({
|
|
start: self.joinRef(namedStart),
|
|
end: self.joinRef(namedEnd),
|
|
});
|
|
}
|
|
} else {
|
|
var namedRef = self.splitRef(ref),
|
|
namedCol = self.charToNum(namedRef.col);
|
|
|
|
if(namedRef.row === currentRow && currentCol < namedCol) {
|
|
namedRef.col = self.numToChar(namedCol + numCols);
|
|
|
|
name.text = self.joinRef(namedRef);
|
|
}
|
|
}
|
|
|
|
});
|
|
};
|
|
|
|
// Look for any merged cell, named table or named range definitions below
|
|
// `currentRow` and push down by `numRows` (used when rows are inserted).
|
|
Workbook.prototype.pushDown = function(workbook, sheet, tables, currentRow, numRows) {
|
|
var self = this;
|
|
|
|
// Update merged cells below this row
|
|
sheet.findall("mergeCells/mergeCell").forEach(function(mergeCell) {
|
|
var mergeRange = self.splitRange(mergeCell.attrib.ref),
|
|
mergeStart = self.splitRef(mergeRange.start),
|
|
mergeEnd = self.splitRef(mergeRange.end);
|
|
|
|
if(mergeStart.row > currentRow) {
|
|
mergeStart.row += numRows;
|
|
mergeEnd.row += numRows;
|
|
|
|
mergeCell.attrib.ref = self.joinRange({
|
|
start: self.joinRef(mergeStart),
|
|
end: self.joinRef(mergeEnd),
|
|
});
|
|
|
|
}
|
|
});
|
|
|
|
// Update named tables below this row
|
|
tables.forEach(function(table) {
|
|
var tableRoot = table.root,
|
|
tableRange = self.splitRange(tableRoot.attrib.ref),
|
|
tableStart = self.splitRef(tableRange.start),
|
|
tableEnd = self.splitRef(tableRange.end);
|
|
|
|
|
|
if(tableStart.row > currentRow) {
|
|
tableStart.row += numRows;
|
|
tableEnd.row += numRows;
|
|
|
|
tableRoot.attrib.ref = self.joinRange({
|
|
start: self.joinRef(tableStart),
|
|
end: self.joinRef(tableEnd),
|
|
});
|
|
|
|
var autoFilter = tableRoot.find("autoFilter");
|
|
if(autoFilter !== null) {
|
|
// XXX: This is a simplification that may stomp on some configurations
|
|
autoFilter.attrib.ref = tableRoot.attrib.ref;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Named cells/ranges
|
|
workbook.findall("definedNames/definedName").forEach(function(name) {
|
|
var ref = name.text;
|
|
|
|
if(self.isRange(ref)) {
|
|
var namedRange = self.splitRange(ref),
|
|
namedStart = self.splitRef(namedRange.start),
|
|
namedEnd = self.splitRef(namedRange.end);
|
|
|
|
if(namedStart){
|
|
if(namedStart.row > currentRow) {
|
|
namedStart.row += numRows;
|
|
namedEnd.row += numRows;
|
|
|
|
name.text = self.joinRange({
|
|
start: self.joinRef(namedStart),
|
|
end: self.joinRef(namedEnd),
|
|
});
|
|
|
|
}
|
|
}
|
|
} else {
|
|
var namedRef = self.splitRef(ref);
|
|
|
|
if(namedRef.row > currentRow) {
|
|
namedRef.row += numRows;
|
|
name.text = self.joinRef(namedRef);
|
|
}
|
|
}
|
|
|
|
});
|
|
};
|
|
|
|
return Workbook;
|
|
})();
|