mirror of
https://github.com/optilude/xlsx-template.git
synced 2026-07-02 08:27:39 +08:00
Implement and test adjustment of named tables, ranges and merged cells
This commit is contained in:
@@ -115,12 +115,12 @@ attach it to an email or do whatever you want with it.
|
||||
* Column (array) and table (array-of-objects) insertions cause rows and cells to
|
||||
be inserted or removed. When this happens, only a limited number of
|
||||
adjustments are made:
|
||||
* Merged cells to the right of cells where insertions or deletions are made
|
||||
are moved right or left, appropriately. This may not work well if cells
|
||||
are merged across rows, unless all rows have the same number of
|
||||
insertions.
|
||||
* Merged cells or named tables below rows where further rows are inserted
|
||||
are moved down.
|
||||
* Merged cells and named cells/ranges to the right of cells where insertions
|
||||
or deletions are made are moved right or left, appropriately. This may
|
||||
not work well if cells are merged across rows, unless all rows have the
|
||||
same number of insertions.
|
||||
* Merged cells, named tables or named cells/ranges below rows where further
|
||||
rows are inserted are moved down.
|
||||
Formulae are not adjusted.
|
||||
* As a corollary to this, it is not always easy to build formulae that refer
|
||||
to cells in a table (e.g. summing all rows) where the exact number of rows
|
||||
|
||||
+136
-32
@@ -44,9 +44,11 @@ module.exports = (function() {
|
||||
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(path.join(self.prefix, '_rels', path.basename(workbookPath) + '.rels')).asText()).getroot();
|
||||
self.sheets = self.loadSheets(self.prefix, self.workbook, self.workbookRels);
|
||||
|
||||
self.sharedStringsPath = path.join(self.prefix, self.workbookRels.find("Relationship[@Type='" + SHARED_STRINGS_RELATIONSHIP + "']").attrib.Target);
|
||||
self.sharedStrings = [];
|
||||
@@ -60,16 +62,15 @@ module.exports = (function() {
|
||||
* 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(sheet, substitutions) {
|
||||
Workbook.prototype.substitute = function(sheetName, substitutions) {
|
||||
var self = this;
|
||||
|
||||
var sheetFilename = self.getSheetFilename(sheet),
|
||||
root = etree.parse(self.archive.file(sheetFilename).asText()).getroot();
|
||||
var sheet = self.loadSheet(sheetName);
|
||||
|
||||
var sheetData = root.find("sheetData"),
|
||||
var sheetData = sheet.root.find("sheetData"),
|
||||
currentRow = null,
|
||||
totalRowsInserted = 0,
|
||||
namedTables = self.loadTables(root, sheetFilename),
|
||||
namedTables = self.loadTables(sheet.root, sheet.filename),
|
||||
rows = [];
|
||||
|
||||
sheetData.findall("row").forEach(function(row) {
|
||||
@@ -118,7 +119,7 @@ module.exports = (function() {
|
||||
if(newCellsInserted !== 0) {
|
||||
appendCell = false; // don't double-insert cells
|
||||
cellsInserted += newCellsInserted;
|
||||
self.pushRight(root, cell.attrib.r, 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
|
||||
@@ -128,7 +129,7 @@ module.exports = (function() {
|
||||
|
||||
if(newCellsInserted !== 0) {
|
||||
cellsInserted += newCellsInserted;
|
||||
self.pushRight(root, cell.attrib.r, newCellsInserted);
|
||||
self.pushRight(self.workbook, sheet.root, cell.attrib.r, newCellsInserted);
|
||||
}
|
||||
} else {
|
||||
string = self.substituteScalar(cell, string, placeholder, substitution);
|
||||
@@ -157,7 +158,7 @@ module.exports = (function() {
|
||||
rows.push(row);
|
||||
++totalRowsInserted;
|
||||
});
|
||||
self.pushDown(root, namedTables, currentRow, newTableRows.length);
|
||||
self.pushDown(self.workbook, sheet.root, namedTables, currentRow, newTableRows.length);
|
||||
}
|
||||
|
||||
}); // rows loop
|
||||
@@ -169,7 +170,8 @@ module.exports = (function() {
|
||||
self.substituteTableColumnHeaders(namedTables, substitutions);
|
||||
|
||||
// Write back the modified XML trees
|
||||
self.archive.file(sheetFilename, etree.tostring(root));
|
||||
self.archive.file(sheet.filename, etree.tostring(sheet.root));
|
||||
self.archive.file(self.workbookPath, etree.tostring(self.workbook));
|
||||
self.writeSharedStrings();
|
||||
self.writeTables(namedTables);
|
||||
};
|
||||
@@ -249,22 +251,51 @@ module.exports = (function() {
|
||||
return idx;
|
||||
};
|
||||
|
||||
// Get sheet filename
|
||||
Workbook.prototype.getSheetFilename = function(sheet) {
|
||||
// Get a list of sheet ids, names and filenames
|
||||
Workbook.prototype.loadSheets = function(prefix, workbook, workbookRels) {
|
||||
var self = this;
|
||||
|
||||
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 = path.join(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 sheetReference = (typeof(sheet) === "number") ?
|
||||
self.workbook.find("sheets/sheet[@sheetId='" + sheet + "']") :
|
||||
self.workbook.find("sheets/sheet[@name='" + sheet + "']");
|
||||
var info = null;
|
||||
|
||||
if(sheetReference === null) {
|
||||
throw new Error("Sheet " + sheet + "not found");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
var sheetId = sheetReference.attrib['r:id'],
|
||||
relationship = self.workbookRels.find("Relationship[@Id='" + sheetId + "']");
|
||||
if(info === null) {
|
||||
throw new Error("Sheet " + sheet + " not found");
|
||||
}
|
||||
|
||||
return path.join(self.prefix, relationship.attrib.Target);
|
||||
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
|
||||
@@ -392,18 +423,26 @@ module.exports = (function() {
|
||||
return matches;
|
||||
};
|
||||
|
||||
// Split a reference into an object with keys `row` and `col`.
|
||||
// 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]+)/);
|
||||
var match = ref.match(/(?:(.+)!)?(\$)?([A-Z]+)(\$)?([0-9]+)/);
|
||||
return {
|
||||
col: match[1],
|
||||
row: parseInt(match[2], 10)
|
||||
table: match[1] || null,
|
||||
colAbsolute: Boolean(match[2]),
|
||||
col: match[3],
|
||||
rowAbsolute: Boolean(match[4]),
|
||||
row: parseInt(match[5], 10)
|
||||
};
|
||||
};
|
||||
|
||||
// Join an object with keys `row` and `col` into a single reference string
|
||||
Workbook.prototype.joinRef = function(ref) {
|
||||
return ref.col.toUpperCase() + Number(ref.row).toString();
|
||||
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".
|
||||
@@ -456,6 +495,11 @@ module.exports = (function() {
|
||||
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;
|
||||
@@ -711,9 +755,9 @@ module.exports = (function() {
|
||||
return range.start + ":" + range.end;
|
||||
};
|
||||
|
||||
// Look for any merged cell definitions to the right of `currentCell` and
|
||||
// push right by `numCols`.
|
||||
Workbook.prototype.pushRight = function(sheet, currentCell, numCols) {
|
||||
// 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),
|
||||
@@ -739,12 +783,43 @@ module.exports = (function() {
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: Named cells/ranges
|
||||
// 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 or named table definitions below `currentRow`
|
||||
// and push down by `numRows` (used when rows are inserted).
|
||||
Workbook.prototype.pushDown = function(sheet, tables, currentRow, numRows) {
|
||||
// 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
|
||||
@@ -790,7 +865,36 @@ module.exports = (function() {
|
||||
|
||||
});
|
||||
|
||||
// TODO: Named cells/ranges
|
||||
// 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.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),
|
||||
namedCol = self.charToNum(namedRef.col);
|
||||
|
||||
if(namedRef.row > currentRow) {
|
||||
namedRef.row += numRows;
|
||||
name.text = self.joinRef(namedRef);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
return Workbook;
|
||||
|
||||
+32
-4
@@ -366,7 +366,7 @@ describe("CRUD operations", function() {
|
||||
|
||||
});
|
||||
|
||||
it("moves named tables and merged cells", function(done) {
|
||||
it("moves named tables, named cells and merged cells", function(done) {
|
||||
|
||||
fs.readFile(path.join(__dirname, 'templates', 'test-named-tables.xlsx'), function(err, data) {
|
||||
expect(err).toBeNull();
|
||||
@@ -390,12 +390,40 @@ describe("CRUD operations", function() {
|
||||
archive = new zip(newData, {base64: false, checkCRC32: true});
|
||||
|
||||
var sharedStrings = etree.parse(t.archive.file("xl/sharedStrings.xml").asText()).getroot(),
|
||||
sheet1 = etree.parse(t.archive.file("xl/worksheets/sheet1.xml").asText()).getroot();
|
||||
sheet1 = etree.parse(t.archive.file("xl/worksheets/sheet1.xml").asText()).getroot(),
|
||||
workbook = etree.parse(t.archive.file("xl/workbook.xml").asText()).getroot(),
|
||||
table1 = etree.parse(t.archive.file("xl/tables/table1.xml").asText()).getroot(),
|
||||
table2 = etree.parse(t.archive.file("xl/tables/table2.xml").asText()).getroot(),
|
||||
table3 = etree.parse(t.archive.file("xl/tables/table3.xml").asText()).getroot();
|
||||
|
||||
// TODO: Tests
|
||||
// Named ranges have moved
|
||||
expect(workbook.find("./definedNames/definedName[@name='BelowTable']").text).toEqual("Tables!$B$18");
|
||||
expect(workbook.find("./definedNames/definedName[@name='Moving']").text).toEqual("Tables!$G$8");
|
||||
expect(workbook.find("./definedNames/definedName[@name='RangeBelowTable']").text).toEqual("Tables!$B$19:$C$19");
|
||||
expect(workbook.find("./definedNames/definedName[@name='RangeRightOfTable']").text).toEqual("Tables!$E$14:$F$14");
|
||||
expect(workbook.find("./definedNames/definedName[@name='RightOfTable']").text).toEqual("Tables!$F$8");
|
||||
|
||||
// Merged cells have moved
|
||||
expect(sheet1.find("./mergeCells/mergeCell[@ref='B2:C2']")).not.toBeNull(); // title - unchanged
|
||||
|
||||
expect(sheet1.find("./mergeCells/mergeCell[@ref='B10:C10']")).toBeNull(); // pushed down
|
||||
expect(sheet1.find("./mergeCells/mergeCell[@ref='B12:C12']")).not.toBeNull(); // pushed down
|
||||
|
||||
expect(sheet1.find("./mergeCells/mergeCell[@ref='E7:F7']")).toBeNull(); // pushed down and accross
|
||||
expect(sheet1.find("./mergeCells/mergeCell[@ref='G8:H8']")).not.toBeNull(); // pushed down and accross
|
||||
|
||||
// Table ranges and autofilter definitions have moved
|
||||
expect(table1.attrib.ref).toEqual("B4:C6"); // Grown
|
||||
expect(table1.find("./autoFilter").attrib.ref).toEqual("B4:C6"); // Grown
|
||||
|
||||
expect(table2.attrib.ref).toEqual("B8:E10"); // Grown and pushed down
|
||||
expect(table2.find("./autoFilter").attrib.ref).toEqual("B8:E10"); // Grown and pushed down
|
||||
|
||||
expect(table3.attrib.ref).toEqual("C14:D16"); // Grown and pushed down
|
||||
expect(table3.find("./autoFilter").attrib.ref).toEqual("C14:D16"); // Grown and pushed down
|
||||
|
||||
// XXX: For debugging only
|
||||
fs.writeFileSync('test.xlsx', newData, 'binary');
|
||||
// fs.writeFileSync('test.xlsx', newData, 'binary');
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
+50
-2
@@ -137,16 +137,50 @@ describe("Helpers", function() {
|
||||
|
||||
});
|
||||
|
||||
describe('isRange', function() {
|
||||
|
||||
it("Returns true if there is a colon", function() {
|
||||
var t = new XlsxTemplate();
|
||||
expect(t.isRange("A1:A2")).toEqual(true);
|
||||
expect(t.isRange("$A$1:$A$2")).toEqual(true);
|
||||
expect(t.isRange("Table!$A$1:$A$2")).toEqual(true);
|
||||
});
|
||||
|
||||
it("Returns false if there is not a colon", function() {
|
||||
var t = new XlsxTemplate();
|
||||
expect(t.isRange("A1")).toEqual(false);
|
||||
expect(t.isRange("$A$1")).toEqual(false);
|
||||
expect(t.isRange("Table!$A$1")).toEqual(false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('splitRef', function() {
|
||||
|
||||
it("splits single digit and letter values", function() {
|
||||
var t = new XlsxTemplate();
|
||||
expect(t.splitRef("A1")).toEqual({col: "A", row: 1});
|
||||
expect(t.splitRef("A1")).toEqual({table: null, col: "A", colAbsolute: false, row: 1, rowAbsolute: false});
|
||||
});
|
||||
|
||||
it("splits multiple digit and letter values", function() {
|
||||
var t = new XlsxTemplate();
|
||||
expect(t.splitRef("AB12")).toEqual({col: "AB", row: 12});
|
||||
expect(t.splitRef("AB12")).toEqual({table: null, col: "AB", colAbsolute: false, row: 12, rowAbsolute: false});
|
||||
});
|
||||
|
||||
it("splits absolute references", function() {
|
||||
var t = new XlsxTemplate();
|
||||
expect(t.splitRef("$AB12")).toEqual({table: null, col: "AB", colAbsolute: true, row: 12, rowAbsolute: false});
|
||||
expect(t.splitRef("AB$12")).toEqual({table: null, col: "AB", colAbsolute: false, row: 12, rowAbsolute: true});
|
||||
expect(t.splitRef("$AB$12")).toEqual({table: null, col: "AB", colAbsolute: true, row: 12, rowAbsolute: true});
|
||||
});
|
||||
|
||||
it("splits references with tables", function() {
|
||||
var t = new XlsxTemplate();
|
||||
expect(t.splitRef("Table one!AB12")).toEqual({table: "Table one", col: "AB", colAbsolute: false, row: 12, rowAbsolute: false});
|
||||
expect(t.splitRef("Table one!$AB12")).toEqual({table: "Table one", col: "AB", colAbsolute: true, row: 12, rowAbsolute: false});
|
||||
expect(t.splitRef("Table one!$AB12")).toEqual({table: "Table one", col: "AB", colAbsolute: true, row: 12, rowAbsolute: false});
|
||||
expect(t.splitRef("Table one!AB$12")).toEqual({table: "Table one", col: "AB", colAbsolute: false, row: 12, rowAbsolute: true});
|
||||
expect(t.splitRef("Table one!$AB$12")).toEqual({table: "Table one", col: "AB", colAbsolute: true, row: 12, rowAbsolute: true});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -191,6 +225,20 @@ describe("Helpers", function() {
|
||||
expect(t.joinRef({col: "AB", row: 12})).toEqual("AB12");
|
||||
});
|
||||
|
||||
it("joins multiple digit and letter values and absolute references", function() {
|
||||
var t = new XlsxTemplate();
|
||||
expect(t.joinRef({col: "AB", colAbsolute: true, row: 12, rowAbsolute: false})).toEqual("$AB12");
|
||||
expect(t.joinRef({col: "AB", colAbsolute: true, row: 12, rowAbsolute: true})).toEqual("$AB$12");
|
||||
expect(t.joinRef({col: "AB", colAbsolute: false, row: 12, rowAbsolute: false})).toEqual("AB12");
|
||||
});
|
||||
|
||||
it("joins multiple digit and letter values and tables", function() {
|
||||
var t = new XlsxTemplate();
|
||||
expect(t.joinRef({table: "Table one", col: "AB", colAbsolute: true, row: 12, rowAbsolute: false})).toEqual("Table one!$AB12");
|
||||
expect(t.joinRef({table: "Table one", col: "AB", colAbsolute: true, row: 12, rowAbsolute: true})).toEqual("Table one!$AB$12");
|
||||
expect(t.joinRef({table: "Table one", col: "AB", colAbsolute: false, row: 12, rowAbsolute: false})).toEqual("Table one!AB12");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('nexCol', function() {
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user