fix: 修复中介导入成功条数计算错误

问题:
- 导入成功条数显示为负数
- 原因:成功数量计算使用 validRecords.size() - failures.size()
- 但没有使用实际的数据库操作返回值

修复:
- saveBatchWithUpsert 和 saveBatch 方法现在返回 int
- 累加实际的数据库影响行数
- 使用 actualSuccessCount 变量跟踪真实成功数量

影响范围:
- CcdiIntermediaryPersonImportServiceImpl
- CcdiIntermediaryEntityImportServiceImpl
This commit is contained in:
wkc
2026-02-08 17:18:18 +08:00
parent bb0d68c41d
commit 5ec5913759
2058 changed files with 234134 additions and 269 deletions

View File

@@ -0,0 +1,191 @@
const fs = require('fs');
const fastCsv = require('fast-csv');
const customParseFormat = require('dayjs/plugin/customParseFormat');
const utc = require('dayjs/plugin/utc');
const dayjs = require('dayjs').extend(customParseFormat).extend(utc);
const StreamBuf = require('../utils/stream-buf');
const {
fs: {exists},
} = require('../utils/utils');
/* eslint-disable quote-props */
const SpecialValues = {
true: true,
false: false,
'#N/A': {error: '#N/A'},
'#REF!': {error: '#REF!'},
'#NAME?': {error: '#NAME?'},
'#DIV/0!': {error: '#DIV/0!'},
'#NULL!': {error: '#NULL!'},
'#VALUE!': {error: '#VALUE!'},
'#NUM!': {error: '#NUM!'},
};
/* eslint-ensable quote-props */
class CSV {
constructor(workbook) {
this.workbook = workbook;
this.worksheet = null;
}
async readFile(filename, options) {
options = options || {};
if (!(await exists(filename))) {
throw new Error(`File not found: ${filename}`);
}
const stream = fs.createReadStream(filename);
const worksheet = await this.read(stream, options);
stream.close();
return worksheet;
}
read(stream, options) {
options = options || {};
return new Promise((resolve, reject) => {
const worksheet = this.workbook.addWorksheet(options.sheetName);
const dateFormats = options.dateFormats || [
'YYYY-MM-DD[T]HH:mm:ssZ',
'YYYY-MM-DD[T]HH:mm:ss',
'MM-DD-YYYY',
'YYYY-MM-DD',
];
const map =
options.map ||
function(datum) {
if (datum === '') {
return null;
}
const datumNumber = Number(datum);
if (!Number.isNaN(datumNumber) && datumNumber !== Infinity) {
return datumNumber;
}
const dt = dateFormats.reduce((matchingDate, currentDateFormat) => {
if (matchingDate) {
return matchingDate;
}
const dayjsObj = dayjs(datum, currentDateFormat, true);
if (dayjsObj.isValid()) {
return dayjsObj;
}
return null;
}, null);
if (dt) {
return new Date(dt.valueOf());
}
const special = SpecialValues[datum];
if (special !== undefined) {
return special;
}
return datum;
};
const csvStream = fastCsv
.parse(options.parserOptions)
.on('data', data => {
worksheet.addRow(data.map(map));
})
.on('end', () => {
csvStream.emit('worksheet', worksheet);
});
csvStream.on('worksheet', resolve).on('error', reject);
stream.pipe(csvStream);
});
}
/**
* @deprecated since version 4.0. You should use `CSV#read` instead. Please follow upgrade instruction: https://github.com/exceljs/exceljs/blob/master/UPGRADE-4.0.md
*/
createInputStream() {
throw new Error(
'`CSV#createInputStream` is deprecated. You should use `CSV#read` instead. This method will be removed in version 5.0. Please follow upgrade instruction: https://github.com/exceljs/exceljs/blob/master/UPGRADE-4.0.md'
);
}
write(stream, options) {
return new Promise((resolve, reject) => {
options = options || {};
// const encoding = options.encoding || 'utf8';
// const separator = options.separator || ',';
// const quoteChar = options.quoteChar || '\'';
const worksheet = this.workbook.getWorksheet(options.sheetName || options.sheetId);
const csvStream = fastCsv.format(options.formatterOptions);
stream.on('finish', () => {
resolve();
});
csvStream.on('error', reject);
csvStream.pipe(stream);
const {dateFormat, dateUTC} = options;
const map =
options.map ||
(value => {
if (value) {
if (value.text || value.hyperlink) {
return value.hyperlink || value.text || '';
}
if (value.formula || value.result) {
return value.result || '';
}
if (value instanceof Date) {
if (dateFormat) {
return dateUTC
? dayjs.utc(value).format(dateFormat)
: dayjs(value).format(dateFormat);
}
return dateUTC ? dayjs.utc(value).format() : dayjs(value).format();
}
if (value.error) {
return value.error;
}
if (typeof value === 'object') {
return JSON.stringify(value);
}
}
return value;
});
const includeEmptyRows = options.includeEmptyRows === undefined || options.includeEmptyRows;
let lastRow = 1;
if (worksheet) {
worksheet.eachRow((row, rowNumber) => {
if (includeEmptyRows) {
while (lastRow++ < rowNumber - 1) {
csvStream.write([]);
}
}
const {values} = row;
values.shift();
csvStream.write(values.map(map));
lastRow = rowNumber;
});
}
csvStream.end();
});
}
writeFile(filename, options) {
options = options || {};
const streamOptions = {
encoding: options.encoding || 'utf8',
};
const stream = fs.createWriteStream(filename, streamOptions);
return this.write(stream, options);
}
async writeBuffer(options) {
const stream = new StreamBuf();
await this.write(stream, options);
return stream.read();
}
}
module.exports = CSV;

View File

@@ -0,0 +1,74 @@
const {EventEmitter} = require('events');
class LineBuffer extends EventEmitter {
constructor(options) {
super();
this.encoding = options.encoding;
this.buffer = null;
// part of cork/uncork
this.corked = false;
this.queue = [];
}
// Events:
// line: here is a line
// done: all lines emitted
write(chunk) {
// find line or lines in chunk and emit them if not corked
// or queue them if corked
const data = this.buffer ? this.buffer + chunk : chunk;
const lines = data.split(/\r?\n/g);
// save the last line
this.buffer = lines.pop();
lines.forEach(function(line) {
if (this.corked) {
this.queue.push(line);
} else {
this.emit('line', line);
}
});
return !this.corked;
}
cork() {
this.corked = true;
}
uncork() {
this.corked = false;
this._flush();
// tell the source I'm ready again
this.emit('drain');
}
setDefaultEncoding() {
// ?
}
end() {
if (this.buffer) {
this.emit('line', this.buffer);
this.buffer = null;
}
this.emit('done');
}
_flush() {
if (!this.corked) {
this.queue.forEach(line => {
this.emit('line', line);
});
this.queue = [];
}
}
}
module.exports = LineBuffer;

View File

@@ -0,0 +1,135 @@
// =======================================================================================================
// StreamConverter
//
// convert between encoding schemes in a stream
// Work in Progress - Will complete this at some point
let jconv;
class StreamConverter {
constructor(inner, options) {
this.inner = inner;
options = options || {};
this.innerEncoding = (options.innerEncoding || 'UTF8').toUpperCase();
this.outerEncoding = (options.outerEncoding || 'UTF8').toUpperCase();
this.innerBOM = options.innerBOM || null;
this.outerBOM = options.outerBOM || null;
this.writeStarted = false;
}
convertInwards(data) {
if (data) {
if (typeof data === 'string') {
data = Buffer.from(data, this.outerEncoding);
}
if (this.innerEncoding !== this.outerEncoding) {
data = jconv.convert(data, this.outerEncoding, this.innerEncoding);
}
}
return data;
}
convertOutwards(data) {
if (typeof data === 'string') {
data = Buffer.from(data, this.innerEncoding);
}
if (this.innerEncoding !== this.outerEncoding) {
data = jconv.convert(data, this.innerEncoding, this.outerEncoding);
}
return data;
}
addListener(event, handler) {
this.inner.addListener(event, handler);
}
removeListener(event, handler) {
this.inner.removeListener(event, handler);
}
write(data, encoding, callback) {
if (encoding instanceof Function) {
callback = encoding;
encoding = undefined;
}
if (!this.writeStarted) {
// if inner encoding has BOM, write it now
if (this.innerBOM) {
this.inner.write(this.innerBOM);
}
// if outer encoding has BOM, delete it now
if (this.outerBOM) {
if (data.length <= this.outerBOM.length) {
if (callback) {
callback();
}
return;
}
const bomless = Buffer.alloc(data.length - this.outerBOM.length);
data.copy(bomless, 0, this.outerBOM.length, data.length);
data = bomless;
}
this.writeStarted = true;
}
this.inner.write(
this.convertInwards(data),
encoding ? this.innerEncoding : undefined,
callback
);
}
read() {
// TBD
}
pipe(destination, options) {
const reverseConverter = new StreamConverter(destination, {
innerEncoding: this.outerEncoding,
outerEncoding: this.innerEncoding,
innerBOM: this.outerBOM,
outerBOM: this.innerBOM,
});
this.inner.pipe(reverseConverter, options);
}
close() {
this.inner.close();
}
on(type, callback) {
switch (type) {
case 'data':
this.inner.on('data', chunk => {
callback(this.convertOutwards(chunk));
});
return this;
default:
this.inner.on(type, callback);
return this;
}
}
once(type, callback) {
this.inner.once(type, callback);
}
end(chunk, encoding, callback) {
this.inner.end(this.convertInwards(chunk), this.innerEncoding, callback);
}
emit(type, value) {
this.inner.emit(type, value);
}
}
module.exports = StreamConverter;

View File

@@ -0,0 +1,91 @@
'use strict';
const colCache = require('../utils/col-cache');
class Anchor {
constructor(worksheet, address, offset = 0) {
this.worksheet = worksheet;
if (!address) {
this.nativeCol = 0;
this.nativeColOff = 0;
this.nativeRow = 0;
this.nativeRowOff = 0;
} else if (typeof address === 'string') {
const decoded = colCache.decodeAddress(address);
this.nativeCol = decoded.col + offset;
this.nativeColOff = 0;
this.nativeRow = decoded.row + offset;
this.nativeRowOff = 0;
} else if (address.nativeCol !== undefined) {
this.nativeCol = address.nativeCol || 0;
this.nativeColOff = address.nativeColOff || 0;
this.nativeRow = address.nativeRow || 0;
this.nativeRowOff = address.nativeRowOff || 0;
} else if (address.col !== undefined) {
this.col = address.col + offset;
this.row = address.row + offset;
} else {
this.nativeCol = 0;
this.nativeColOff = 0;
this.nativeRow = 0;
this.nativeRowOff = 0;
}
}
static asInstance(model) {
return model instanceof Anchor || model == null ? model : new Anchor(model);
}
get col() {
return this.nativeCol + (Math.min(this.colWidth - 1, this.nativeColOff) / this.colWidth);
}
set col(v) {
this.nativeCol = Math.floor(v);
this.nativeColOff = Math.floor((v - this.nativeCol) * this.colWidth);
}
get row() {
return this.nativeRow + (Math.min(this.rowHeight - 1, this.nativeRowOff) / this.rowHeight);
}
set row(v) {
this.nativeRow = Math.floor(v);
this.nativeRowOff = Math.floor((v - this.nativeRow) * this.rowHeight);
}
get colWidth() {
return this.worksheet &&
this.worksheet.getColumn(this.nativeCol + 1) &&
this.worksheet.getColumn(this.nativeCol + 1).isCustomWidth
? Math.floor(this.worksheet.getColumn(this.nativeCol + 1).width * 10000)
: 640000;
}
get rowHeight() {
return this.worksheet &&
this.worksheet.getRow(this.nativeRow + 1) &&
this.worksheet.getRow(this.nativeRow + 1).height
? Math.floor(this.worksheet.getRow(this.nativeRow + 1).height * 10000)
: 180000;
}
get model() {
return {
nativeCol: this.nativeCol,
nativeColOff: this.nativeColOff,
nativeRow: this.nativeRow,
nativeRowOff: this.nativeRowOff,
};
}
set model(value) {
this.nativeCol = value.nativeCol;
this.nativeColOff = value.nativeColOff;
this.nativeRow = value.nativeRow;
this.nativeRowOff = value.nativeRowOff;
}
}
module.exports = Anchor;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,320 @@
'use strict';
const _ = require('../utils/under-dash');
const Enums = require('./enums');
const colCache = require('../utils/col-cache');
const DEFAULT_COLUMN_WIDTH = 9;
// Column defines the column properties for 1 column.
// This includes header rows, widths, key, (style), etc.
// Worksheet will condense the columns as appropriate during serialization
class Column {
constructor(worksheet, number, defn) {
this._worksheet = worksheet;
this._number = number;
if (defn !== false) {
// sometimes defn will follow
this.defn = defn;
}
}
get number() {
return this._number;
}
get worksheet() {
return this._worksheet;
}
get letter() {
return colCache.n2l(this._number);
}
get isCustomWidth() {
return this.width !== undefined && this.width !== DEFAULT_COLUMN_WIDTH;
}
get defn() {
return {
header: this._header,
key: this.key,
width: this.width,
style: this.style,
hidden: this.hidden,
outlineLevel: this.outlineLevel,
};
}
set defn(value) {
if (value) {
this.key = value.key;
this.width = value.width !== undefined ? value.width : DEFAULT_COLUMN_WIDTH;
this.outlineLevel = value.outlineLevel;
if (value.style) {
this.style = value.style;
} else {
this.style = {};
}
// headers must be set after style
this.header = value.header;
this._hidden = !!value.hidden;
} else {
delete this._header;
delete this._key;
delete this.width;
this.style = {};
this.outlineLevel = 0;
}
}
get headers() {
return this._header && this._header instanceof Array ? this._header : [this._header];
}
get header() {
return this._header;
}
set header(value) {
if (value !== undefined) {
this._header = value;
this.headers.forEach((text, index) => {
this._worksheet.getCell(index + 1, this.number).value = text;
});
} else {
this._header = undefined;
}
}
get key() {
return this._key;
}
set key(value) {
const column = this._key && this._worksheet.getColumnKey(this._key);
if (column === this) {
this._worksheet.deleteColumnKey(this._key);
}
this._key = value;
if (value) {
this._worksheet.setColumnKey(this._key, this);
}
}
get hidden() {
return !!this._hidden;
}
set hidden(value) {
this._hidden = value;
}
get outlineLevel() {
return this._outlineLevel || 0;
}
set outlineLevel(value) {
this._outlineLevel = value;
}
get collapsed() {
return !!(
this._outlineLevel && this._outlineLevel >= this._worksheet.properties.outlineLevelCol
);
}
toString() {
return JSON.stringify({
key: this.key,
width: this.width,
headers: this.headers.length ? this.headers : undefined,
});
}
equivalentTo(other) {
return (
this.width === other.width &&
this.hidden === other.hidden &&
this.outlineLevel === other.outlineLevel &&
_.isEqual(this.style, other.style)
);
}
get isDefault() {
if (this.isCustomWidth) {
return false;
}
if (this.hidden) {
return false;
}
if (this.outlineLevel) {
return false;
}
const s = this.style;
if (s && (s.font || s.numFmt || s.alignment || s.border || s.fill || s.protection)) {
return false;
}
return true;
}
get headerCount() {
return this.headers.length;
}
eachCell(options, iteratee) {
const colNumber = this.number;
if (!iteratee) {
iteratee = options;
options = null;
}
this._worksheet.eachRow(options, (row, rowNumber) => {
iteratee(row.getCell(colNumber), rowNumber);
});
}
get values() {
const v = [];
this.eachCell((cell, rowNumber) => {
if (cell && cell.type !== Enums.ValueType.Null) {
v[rowNumber] = cell.value;
}
});
return v;
}
set values(v) {
if (!v) {
return;
}
const colNumber = this.number;
let offset = 0;
if (v.hasOwnProperty('0')) {
// assume contiguous array, start at row 1
offset = 1;
}
v.forEach((value, index) => {
this._worksheet.getCell(index + offset, colNumber).value = value;
});
}
// =========================================================================
// styles
_applyStyle(name, value) {
this.style[name] = value;
this.eachCell(cell => {
cell[name] = value;
});
return value;
}
get numFmt() {
return this.style.numFmt;
}
set numFmt(value) {
this._applyStyle('numFmt', value);
}
get font() {
return this.style.font;
}
set font(value) {
this._applyStyle('font', value);
}
get alignment() {
return this.style.alignment;
}
set alignment(value) {
this._applyStyle('alignment', value);
}
get protection() {
return this.style.protection;
}
set protection(value) {
this._applyStyle('protection', value);
}
get border() {
return this.style.border;
}
set border(value) {
this._applyStyle('border', value);
}
get fill() {
return this.style.fill;
}
set fill(value) {
this._applyStyle('fill', value);
}
// =============================================================================
// static functions
static toModel(columns) {
// Convert array of Column into compressed list cols
const cols = [];
let col = null;
if (columns) {
columns.forEach((column, index) => {
if (column.isDefault) {
if (col) {
col = null;
}
} else if (!col || !column.equivalentTo(col)) {
col = {
min: index + 1,
max: index + 1,
width: column.width !== undefined ? column.width : DEFAULT_COLUMN_WIDTH,
style: column.style,
isCustomWidth: column.isCustomWidth,
hidden: column.hidden,
outlineLevel: column.outlineLevel,
collapsed: column.collapsed,
};
cols.push(col);
} else {
col.max = index + 1;
}
});
}
return cols.length ? cols : undefined;
}
static fromModel(worksheet, cols) {
cols = cols || [];
const columns = [];
let count = 1;
let index = 0;
/**
* sort cols by min
* If it is not sorted, the subsequent column configuration will be overwritten
* */
cols = cols.sort(function(pre, next) {
return pre.min - next.min;
});
while (index < cols.length) {
const col = cols[index++];
while (count < col.min) {
columns.push(new Column(worksheet, count++));
}
while (count <= col.max) {
columns.push(new Column(worksheet, count++, col));
}
}
return columns.length ? columns : null;
}
}
module.exports = Column;

View File

@@ -0,0 +1,19 @@
class DataValidations {
constructor(model) {
this.model = model || {};
}
add(address, validation) {
return (this.model[address] = validation);
}
find(address) {
return this.model[address];
}
remove(address) {
this.model[address] = undefined;
}
}
module.exports = DataValidations;

View File

@@ -0,0 +1,234 @@
{
"TODO:": "This file is for analysis only. Not ready for release",
"name": "Office Theme",
"themeElements": {
"colorScheme": {
"name": "Office",
"dk1": { "type": "sys", "val": "windowText", "lastClr": "000000" },
"lt1": { "type": "sys", "val": "window", "lastClr": "FFFFFF" },
"dk2": { "type": "srgb", "val": "44546A" },
"lt2": { "type": "srgb", "val": "E7E6E6" },
"accent1": { "type": "srgb", "val": "5B9BD5" },
"accent2": { "type": "srgb", "val": "ED7D31" },
"accent3": { "type": "srgb", "val": "A5A5A5" },
"accent4": { "type": "srgb", "val": "FFC000" },
"accent5": { "type": "srgb", "val": "4472C4" },
"accent6": { "type": "srgb", "val": "70AD47" },
"hlink": { "type": "srgb", "val": "0563C1" },
"folHlink": { "type": "srgb", "val": "954F72" }
},
"fontScheme": {
"name": "Office",
"majorFont": {
"latin": { "typeface": "Calibri Light", "panose": "020F0302020204030204" },
"ea": { "typeface": "" },
"cs": { "typeface": "" },
"fonts": [
{ "script": "Jpan", "typeface": "游ゴシック Light" },
{ "script": "Hang", "typeface": "맑은 고딕" },
{ "script": "Hans", "typeface": "等线 Light" },
{ "script": "Hant", "typeface": "新細明體" },
{ "script": "Arab", "typeface": "Times New Roman" },
{ "script": "Hebr", "typeface": "Times New Roman" },
{ "script": "Thai", "typeface": "Tahoma" },
{ "script": "Ethi", "typeface": "Nyala" },
{ "script": "Beng", "typeface": "Vrinda" },
{ "script": "Gujr", "typeface": "Shruti" },
{ "script": "Khmr", "typeface": "MoolBoran" },
{ "script": "Knda", "typeface": "Tunga" },
{ "script": "Guru", "typeface": "Raavi" },
{ "script": "Cans", "typeface": "Euphemia" },
{ "script": "Cher", "typeface": "Plantagenet Cherokee" },
{ "script": "Yiii", "typeface": "Microsoft Yi Baiti" },
{ "script": "Tibt", "typeface": "Microsoft Himalaya" },
{ "script": "Thaa", "typeface": "MV Boli" },
{ "script": "Deva", "typeface": "Mangal" },
{ "script": "Telu", "typeface": "Gautami" },
{ "script": "Taml", "typeface": "Latha" },
{ "script": "Syrc", "typeface": "Estrangelo Edessa" },
{ "script": "Orya", "typeface": "Kalinga" },
{ "script": "Mlym", "typeface": "Kartika" },
{ "script": "Laoo", "typeface": "DokChampa" },
{ "script": "Sinh", "typeface": "Iskoola Pota" },
{ "script": "Mong", "typeface": "Mongolian Baiti" },
{ "script": "Viet", "typeface": "Times New Roman" },
{ "script": "Uigh", "typeface": "Microsoft Uighur" },
{ "script": "Geor", "typeface": "Sylfaen" }
]
},
"minorFont": {
"latin": { "typeface": "Calibri Light", "panose": "020F0302020204030204" },
"ea": { "typeface": "" },
"cs": { "typeface": "" },
"fonts": [
{ "script":"Jpan", "typeface": "游ゴシック" },
{ "script":"Hang", "typeface": "맑은 고딕" },
{ "script":"Hans", "typeface": "等线" },
{ "script":"Hant", "typeface": "新細明體" },
{ "script":"Arab", "typeface": "Arial" },
{ "script":"Hebr", "typeface": "Arial" },
{ "script":"Thai", "typeface": "Tahoma" },
{ "script":"Ethi", "typeface": "Nyala" },
{ "script":"Beng", "typeface": "Vrinda" },
{ "script":"Gujr", "typeface": "Shruti" },
{ "script":"Khmr", "typeface": "DaunPenh" },
{ "script":"Knda", "typeface": "Tunga" },
{ "script":"Guru", "typeface": "Raavi" },
{ "script":"Cans", "typeface": "Euphemia" },
{ "script":"Cher", "typeface": "Plantagenet Cherokee" },
{ "script":"Yiii", "typeface": "Microsoft Yi Baiti" },
{ "script":"Tibt", "typeface": "Microsoft Himalaya" },
{ "script":"Thaa", "typeface": "MV Boli" },
{ "script":"Deva", "typeface": "Mangal" },
{ "script":"Telu", "typeface": "Gautami" },
{ "script":"Taml", "typeface": "Latha" },
{ "script":"Syrc", "typeface": "Estrangelo Edessa" },
{ "script":"Orya", "typeface": "Kalinga" },
{ "script":"Mlym", "typeface": "Kartika" },
{ "script":"Laoo", "typeface": "DokChampa" },
{ "script":"Sinh", "typeface": "Iskoola Pota" },
{ "script":"Mong", "typeface": "Mongolian Baiti" },
{ "script":"Viet", "typeface": "Arial" },
{ "script":"Uigh", "typeface": "Microsoft Uighur" },
{ "script":"Geor", "typeface": "Sylfaen" }
]
}
},
"fmtScheme": {
"name": "Office",
"fillStyleLst": [
{ "type": "solidFill", "val": "phClr" },
{
"type": "gradFill",
"rotWithShape": true,
"gsLst": [
{ "pos":0, "schemeClr": { "val": "phClr", "lumMod": 110000, "satMod": 105000, "tint": 67000 } },
{ "pos":50000, "schemeClr": { "val": "phClr", "lumMod": 105000, "satMod": 103000, "tint": 73000 } },
{ "pos":100000, "schemeClr": { "val": "phClr", "lumMod": 105000, "satMod": 109000, "tint": 81000 } }
],
"lin": { "ang": 5400000, "scaled": false }
},
{
"type": "gradFill",
"rotWithShape": true,
"gsLst": [
{ "pos":0, "schemeClr": { "val": "phClr", "satMod": 103000, "lumMod": 102000, "tint": 94000 } },
{ "pos":50000, "schemeClr": { "val": "phClr", "satMod": 110000, "lumMod": 100000, "shade": 100000 } },
{ "pos":100000, "schemeClr": { "val": "phClr", "lumMod": 99000, "satMod": 120000, "tint": 78000 } }
],
"lin": { "ang": 5400000, "scaled": false }
}
],
"lnStyleLst": [
{
"w": 6350, "cap": "flat", "cmpd": "sng", "algn": "ctr",
"solidFill": { "schemeClr": { "val": "phClr" } },
"prstDash": { "val": "solid" },
"miter": { "lim": 800000 }
},
{
"w": 12700, "cap": "flat", "cmpd": "sng", "algn": "ctr",
"solidFill": { "schemeClr": { "val": "phClr" } },
"prstDash": { "val": "solid" },
"miter": { "lim": 800000 }
},
{
"w": 19050, "cap": "flat", "cmpd": "sng", "algn": "ctr",
"solidFill": { "schemeClr": { "val": "phClr" } },
"prstDash": { "val": "solid" },
"miter": { "lim": 800000 }
}
],
"effectStyleLst": [
{ "effectLst": [] },
{ "effectLst": [] },
{
"effectLst": [
{
"type": "outerShdw",
"blurRad": 57150, "dist": 19050, "dir": 5400000, "algn": "ctr", "rotWithShape": false,
"srgbClr": {
"val": "000000",
"alpha": {"val": 63000 }
}
}
]
}
],
"bgFillStyleLst": [
{ "type": "solidFill", "schemeClr": { "val": "phClr" } },
{ "type": "solidFill", "schemeClr": { "val": "phClr", "tint": 95000, "satMod": 170000 } },
{
"type": "gradFill",
"rotWithShape": true,
"gsLst": [
{ "pos":0, "schemeClr": { "val": "phClr", "tint": 93000, "satMod": 150000, "shade": 90000, "lumMod": 102000 } },
{ "pos":50000, "schemeClr": { "val": "phClr", "tint": 98000, "satMod": 130000, "shade": 73000, "lumMod": 103000 } },
{ "pos":100000, "schemeClr": { "val": "phClr", "shade": 63000, "satMod": 120000 } }
],
"lin": { "ang": 5400000, "scaled": false }
}
]
}
},
"objectDefaults": {
"spDef": {
"spPr": {},
"bodyPr": {},
"lstStyle": {},
"style": {
"lnRef": {
"idx": 1,
"schemeClr": "accent1"
},
"effectRef": {
"idx": 2,
"schemeClr": "accent1"
},
"fillRef": {
"idx": 3,
"schemeClr": "accent1"
},
"fontRef": {
"idx": "minor",
"schemeClr": "lt1"
}
}
},
"lnDef": {
"spPr": {},
"bodyPr": {},
"lstStyle": {},
"style": {
"lnRef": {
"idx": 2,
"schemeClr": "accent1"
},
"effectRef": {
"idx": 0,
"schemeClr": "accent1"
},
"fillRef": {
"idx": 1,
"schemeClr": "accent1"
},
"fontRef": {
"idx": "minor",
"schemeClr": "tx1"
}
}
}
},
"extraClrSchemeLst": [],
"extLst": [
{
"uri": "{05A4C25C-085E-4340-85A3-A5531E510DB2}",
"themeFamily": {
"name": "Office Theme",
"id": "{62F939B6-93AF-4DB8-9C6B-D6C7DFDC589F}",
"vid": "{4A3C46E8-61CC-4603-A589-7422A47A8E4A}",
"thm15": "http://schemas.microsoft.com/office/thememl/2012/main"
}
}
]
}

View File

@@ -0,0 +1,196 @@
'use strict';
const _ = require('../utils/under-dash');
const colCache = require('../utils/col-cache');
const CellMatrix = require('../utils/cell-matrix');
const Range = require('./range');
const rangeRegexp = /[$](\w+)[$](\d+)(:[$](\w+)[$](\d+))?/;
class DefinedNames {
constructor() {
this.matrixMap = {};
}
getMatrix(name) {
const matrix = this.matrixMap[name] || (this.matrixMap[name] = new CellMatrix());
return matrix;
}
// add a name to a cell. locStr in the form SheetName!$col$row or SheetName!$c1$r1:$c2:$r2
add(locStr, name) {
const location = colCache.decodeEx(locStr);
this.addEx(location, name);
}
addEx(location, name) {
const matrix = this.getMatrix(name);
if (location.top) {
for (let col = location.left; col <= location.right; col++) {
for (let row = location.top; row <= location.bottom; row++) {
const address = {
sheetName: location.sheetName,
address: colCache.n2l(col) + row,
row,
col,
};
matrix.addCellEx(address);
}
}
} else {
matrix.addCellEx(location);
}
}
remove(locStr, name) {
const location = colCache.decodeEx(locStr);
this.removeEx(location, name);
}
removeEx(location, name) {
const matrix = this.getMatrix(name);
matrix.removeCellEx(location);
}
removeAllNames(location) {
_.each(this.matrixMap, matrix => {
matrix.removeCellEx(location);
});
}
forEach(callback) {
_.each(this.matrixMap, (matrix, name) => {
matrix.forEach(cell => {
callback(name, cell);
});
});
}
// get all the names of a cell
getNames(addressStr) {
return this.getNamesEx(colCache.decodeEx(addressStr));
}
getNamesEx(address) {
return _.map(this.matrixMap, (matrix, name) => matrix.findCellEx(address) && name).filter(
Boolean
);
}
_explore(matrix, cell) {
cell.mark = false;
const {sheetName} = cell;
const range = new Range(cell.row, cell.col, cell.row, cell.col, sheetName);
let x;
let y;
// grow vertical - only one col to worry about
function vGrow(yy, edge) {
const c = matrix.findCellAt(sheetName, yy, cell.col);
if (!c || !c.mark) {
return false;
}
range[edge] = yy;
c.mark = false;
return true;
}
for (y = cell.row - 1; vGrow(y, 'top'); y--);
for (y = cell.row + 1; vGrow(y, 'bottom'); y++);
// grow horizontal - ensure all rows can grow
function hGrow(xx, edge) {
const cells = [];
for (y = range.top; y <= range.bottom; y++) {
const c = matrix.findCellAt(sheetName, y, xx);
if (c && c.mark) {
cells.push(c);
} else {
return false;
}
}
range[edge] = xx;
for (let i = 0; i < cells.length; i++) {
cells[i].mark = false;
}
return true;
}
for (x = cell.col - 1; hGrow(x, 'left'); x--);
for (x = cell.col + 1; hGrow(x, 'right'); x++);
return range;
}
getRanges(name, matrix) {
matrix = matrix || this.matrixMap[name];
if (!matrix) {
return {name, ranges: []};
}
// mark and sweep!
matrix.forEach(cell => {
cell.mark = true;
});
const ranges = matrix
.map(cell => cell.mark && this._explore(matrix, cell))
.filter(Boolean)
.map(range => range.$shortRange);
return {
name,
ranges,
};
}
normaliseMatrix(matrix, sheetName) {
// some of the cells might have shifted on specified sheet
// need to reassign rows, cols
matrix.forEachInSheet(sheetName, (cell, row, col) => {
if (cell) {
if (cell.row !== row || cell.col !== col) {
cell.row = row;
cell.col = col;
cell.address = colCache.n2l(col) + row;
}
}
});
}
spliceRows(sheetName, start, numDelete, numInsert) {
_.each(this.matrixMap, matrix => {
matrix.spliceRows(sheetName, start, numDelete, numInsert);
this.normaliseMatrix(matrix, sheetName);
});
}
spliceColumns(sheetName, start, numDelete, numInsert) {
_.each(this.matrixMap, matrix => {
matrix.spliceColumns(sheetName, start, numDelete, numInsert);
this.normaliseMatrix(matrix, sheetName);
});
}
get model() {
// To get names per cell - just iterate over all names finding cells if they exist
return _.map(this.matrixMap, (matrix, name) => this.getRanges(name, matrix)).filter(
definedName => definedName.ranges.length
);
}
set model(value) {
// value is [ { name, ranges }, ... ]
const matrixMap = (this.matrixMap = {});
value.forEach(definedName => {
const matrix = (matrixMap[definedName.name] = new CellMatrix());
definedName.ranges.forEach(rangeStr => {
if (rangeRegexp.test(rangeStr.split('!').pop() || '')) {
matrix.addCell(rangeStr);
}
});
});
}
}
module.exports = DefinedNames;

View File

@@ -0,0 +1,48 @@
'use strict';
module.exports = {
ValueType: {
Null: 0,
Merge: 1,
Number: 2,
String: 3,
Date: 4,
Hyperlink: 5,
Formula: 6,
SharedString: 7,
RichText: 8,
Boolean: 9,
Error: 10,
},
FormulaType: {
None: 0,
Master: 1,
Shared: 2,
},
RelationshipType: {
None: 0,
OfficeDocument: 1,
Worksheet: 2,
CalcChain: 3,
SharedStrings: 4,
Styles: 5,
Theme: 6,
Hyperlink: 7,
},
DocumentType: {
Xlsx: 1,
},
ReadingOrder: {
LeftToRight: 1,
RightToLeft: 2,
},
ErrorValue: {
NotApplicable: '#N/A',
Ref: '#REF!',
Name: '#NAME?',
DivZero: '#DIV/0!',
Null: '#NULL!',
Value: '#VALUE!',
Num: '#NUM!',
},
};

View File

@@ -0,0 +1,59 @@
const colCache = require('../utils/col-cache');
const Anchor = require('./anchor');
class Image {
constructor(worksheet, model) {
this.worksheet = worksheet;
this.model = model;
}
get model() {
switch (this.type) {
case 'background':
return {
type: this.type,
imageId: this.imageId,
};
case 'image':
return {
type: this.type,
imageId: this.imageId,
hyperlinks: this.range.hyperlinks,
range: {
tl: this.range.tl.model,
br: this.range.br && this.range.br.model,
ext: this.range.ext,
editAs: this.range.editAs,
},
};
default:
throw new Error('Invalid Image Type');
}
}
set model({type, imageId, range, hyperlinks}) {
this.type = type;
this.imageId = imageId;
if (type === 'image') {
if (typeof range === 'string') {
const decoded = colCache.decode(range);
this.range = {
tl: new Anchor(this.worksheet, {col: decoded.left, row: decoded.top}, -1),
br: new Anchor(this.worksheet, {col: decoded.right, row: decoded.bottom}, 0),
editAs: 'oneCell',
};
} else {
this.range = {
tl: new Anchor(this.worksheet, range.tl, 0),
br: range.br && new Anchor(this.worksheet, range.br, 0),
ext: range.ext,
editAs: range.editAs,
hyperlinks: hyperlinks || range.hyperlinks,
};
}
}
}
}
module.exports = Image;

View File

@@ -0,0 +1,18 @@
'use strict';
const XLSX = require('../xlsx/xlsx');
class ModelContainer {
constructor(model) {
this.model = model;
}
get xlsx() {
if (!this._xlsx) {
this._xlsx = new XLSX(this);
}
return this._xlsx;
}
}
module.exports = ModelContainer;

View File

@@ -0,0 +1,65 @@
const _ = require('../utils/under-dash');
class Note {
constructor(note) {
this.note = note;
}
get model() {
let value = null;
switch (typeof this.note) {
case 'string':
value = {
type: 'note',
note: {
texts: [
{
text: this.note,
},
],
},
};
break;
default:
value = {
type: 'note',
note: this.note,
};
break;
}
// Suitable for all cell comments
return _.deepMerge({}, Note.DEFAULT_CONFIGS, value);
}
set model(value) {
const {note} = value;
const {texts} = note;
if (texts.length === 1 && Object.keys(texts[0]).length === 1) {
this.note = texts[0].text;
} else {
this.note = note;
}
}
static fromModel(model) {
const note = new Note();
note.model = model;
return note;
}
}
Note.DEFAULT_CONFIGS = {
note: {
margins: {
insetmode: 'auto',
inset: [0.13, 0.13, 0.25, 0.25],
},
protection: {
locked: 'True',
lockText: 'True',
},
editAs: 'absolute',
},
};
module.exports = Note;

View File

@@ -0,0 +1,257 @@
const colCache = require('../utils/col-cache');
// used by worksheet to calculate sheet dimensions
class Range {
constructor() {
this.decode(arguments);
}
setTLBR(t, l, b, r, s) {
if (arguments.length < 4) {
// setTLBR(tl, br, s)
const tl = colCache.decodeAddress(t);
const br = colCache.decodeAddress(l);
this.model = {
top: Math.min(tl.row, br.row),
left: Math.min(tl.col, br.col),
bottom: Math.max(tl.row, br.row),
right: Math.max(tl.col, br.col),
sheetName: b,
};
this.setTLBR(tl.row, tl.col, br.row, br.col, s);
} else {
// setTLBR(t, l, b, r, s)
this.model = {
top: Math.min(t, b),
left: Math.min(l, r),
bottom: Math.max(t, b),
right: Math.max(l, r),
sheetName: s,
};
}
}
decode(argv) {
switch (argv.length) {
case 5: // [t,l,b,r,s]
this.setTLBR(argv[0], argv[1], argv[2], argv[3], argv[4]);
break;
case 4: // [t,l,b,r]
this.setTLBR(argv[0], argv[1], argv[2], argv[3]);
break;
case 3: // [tl,br,s]
this.setTLBR(argv[0], argv[1], argv[2]);
break;
case 2: // [tl,br]
this.setTLBR(argv[0], argv[1]);
break;
case 1: {
const value = argv[0];
if (value instanceof Range) {
// copy constructor
this.model = {
top: value.model.top,
left: value.model.left,
bottom: value.model.bottom,
right: value.model.right,
sheetName: value.sheetName,
};
} else if (value instanceof Array) {
// an arguments array
this.decode(value);
} else if (value.top && value.left && value.bottom && value.right) {
// a model
this.model = {
top: value.top,
left: value.left,
bottom: value.bottom,
right: value.right,
sheetName: value.sheetName,
};
} else {
// [sheetName!]tl:br
const tlbr = colCache.decodeEx(value);
if (tlbr.top) {
this.model = {
top: tlbr.top,
left: tlbr.left,
bottom: tlbr.bottom,
right: tlbr.right,
sheetName: tlbr.sheetName,
};
} else {
this.model = {
top: tlbr.row,
left: tlbr.col,
bottom: tlbr.row,
right: tlbr.col,
sheetName: tlbr.sheetName,
};
}
}
break;
}
case 0:
this.model = {
top: 0,
left: 0,
bottom: 0,
right: 0,
};
break;
default:
throw new Error(`Invalid number of arguments to _getDimensions() - ${argv.length}`);
}
}
get top() {
return this.model.top || 1;
}
set top(value) {
this.model.top = value;
}
get left() {
return this.model.left || 1;
}
set left(value) {
this.model.left = value;
}
get bottom() {
return this.model.bottom || 1;
}
set bottom(value) {
this.model.bottom = value;
}
get right() {
return this.model.right || 1;
}
set right(value) {
this.model.right = value;
}
get sheetName() {
return this.model.sheetName;
}
set sheetName(value) {
this.model.sheetName = value;
}
get _serialisedSheetName() {
const {sheetName} = this.model;
if (sheetName) {
if (/^[a-zA-Z0-9]*$/.test(sheetName)) {
return `${sheetName}!`;
}
return `'${sheetName}'!`;
}
return '';
}
expand(top, left, bottom, right) {
if (!this.model.top || top < this.top) this.top = top;
if (!this.model.left || left < this.left) this.left = left;
if (!this.model.bottom || bottom > this.bottom) this.bottom = bottom;
if (!this.model.right || right > this.right) this.right = right;
}
expandRow(row) {
if (row) {
const {dimensions, number} = row;
if (dimensions) {
this.expand(number, dimensions.min, number, dimensions.max);
}
}
}
expandToAddress(addressStr) {
const address = colCache.decodeEx(addressStr);
this.expand(address.row, address.col, address.row, address.col);
}
get tl() {
return colCache.n2l(this.left) + this.top;
}
get $t$l() {
return `$${colCache.n2l(this.left)}$${this.top}`;
}
get br() {
return colCache.n2l(this.right) + this.bottom;
}
get $b$r() {
return `$${colCache.n2l(this.right)}$${this.bottom}`;
}
get range() {
return `${this._serialisedSheetName + this.tl}:${this.br}`;
}
get $range() {
return `${this._serialisedSheetName + this.$t$l}:${this.$b$r}`;
}
get shortRange() {
return this.count > 1 ? this.range : this._serialisedSheetName + this.tl;
}
get $shortRange() {
return this.count > 1 ? this.$range : this._serialisedSheetName + this.$t$l;
}
get count() {
return (1 + this.bottom - this.top) * (1 + this.right - this.left);
}
toString() {
return this.range;
}
intersects(other) {
if (other.sheetName && this.sheetName && other.sheetName !== this.sheetName) return false;
if (other.bottom < this.top) return false;
if (other.top > this.bottom) return false;
if (other.right < this.left) return false;
if (other.left > this.right) return false;
return true;
}
contains(addressStr) {
const address = colCache.decodeEx(addressStr);
return this.containsEx(address);
}
containsEx(address) {
if (address.sheetName && this.sheetName && address.sheetName !== this.sheetName) return false;
return (
address.row >= this.top &&
address.row <= this.bottom &&
address.col >= this.left &&
address.col <= this.right
);
}
forEachAddress(cb) {
for (let col = this.left; col <= this.right; col++) {
for (let row = this.top; row <= this.bottom; row++) {
cb(colCache.encodeAddress(row, col), row, col);
}
}
}
}
module.exports = Range;

View File

@@ -0,0 +1,415 @@
'use strict';
const _ = require('../utils/under-dash');
const Enums = require('./enums');
const colCache = require('../utils/col-cache');
const Cell = require('./cell');
class Row {
constructor(worksheet, number) {
this._worksheet = worksheet;
this._number = number;
this._cells = [];
this.style = {};
this.outlineLevel = 0;
}
// return the row number
get number() {
return this._number;
}
get worksheet() {
return this._worksheet;
}
// Inform Streaming Writer that this row (and all rows before it) are complete
// and ready to write. Has no effect on Worksheet document
commit() {
this._worksheet._commitRow(this); // eslint-disable-line no-underscore-dangle
}
// helps GC by breaking cyclic references
destroy() {
delete this._worksheet;
delete this._cells;
delete this.style;
}
findCell(colNumber) {
return this._cells[colNumber - 1];
}
// given {address, row, col}, find or create new cell
getCellEx(address) {
let cell = this._cells[address.col - 1];
if (!cell) {
const column = this._worksheet.getColumn(address.col);
cell = new Cell(this, column, address.address);
this._cells[address.col - 1] = cell;
}
return cell;
}
// get cell by key, letter or column number
getCell(col) {
if (typeof col === 'string') {
// is it a key?
const column = this._worksheet.getColumnKey(col);
if (column) {
col = column.number;
} else {
col = colCache.l2n(col);
}
}
return (
this._cells[col - 1] ||
this.getCellEx({
address: colCache.encodeAddress(this._number, col),
row: this._number,
col,
})
);
}
// remove cell(s) and shift all higher cells down by count
splice(start, count, ...inserts) {
const nKeep = start + count;
const nExpand = inserts.length - count;
const nEnd = this._cells.length;
let i;
let cSrc;
let cDst;
if (nExpand < 0) {
// remove cells
for (i = start + inserts.length; i <= nEnd; i++) {
cDst = this._cells[i - 1];
cSrc = this._cells[i - nExpand - 1];
if (cSrc) {
cDst = this.getCell(i);
cDst.value = cSrc.value;
cDst.style = cSrc.style;
// eslint-disable-next-line no-underscore-dangle
cDst._comment = cSrc._comment;
} else if (cDst) {
cDst.value = null;
cDst.style = {};
// eslint-disable-next-line no-underscore-dangle
cDst._comment = undefined;
}
}
} else if (nExpand > 0) {
// insert new cells
for (i = nEnd; i >= nKeep; i--) {
cSrc = this._cells[i - 1];
if (cSrc) {
cDst = this.getCell(i + nExpand);
cDst.value = cSrc.value;
cDst.style = cSrc.style;
// eslint-disable-next-line no-underscore-dangle
cDst._comment = cSrc._comment;
} else {
this._cells[i + nExpand - 1] = undefined;
}
}
}
// now add the new values
for (i = 0; i < inserts.length; i++) {
cDst = this.getCell(start + i);
cDst.value = inserts[i];
cDst.style = {};
// eslint-disable-next-line no-underscore-dangle
cDst._comment = undefined;
}
}
// Iterate over all non-null cells in this row
eachCell(options, iteratee) {
if (!iteratee) {
iteratee = options;
options = null;
}
if (options && options.includeEmpty) {
const n = this._cells.length;
for (let i = 1; i <= n; i++) {
iteratee(this.getCell(i), i);
}
} else {
this._cells.forEach((cell, index) => {
if (cell && cell.type !== Enums.ValueType.Null) {
iteratee(cell, index + 1);
}
});
}
}
// ===========================================================================
// Page Breaks
addPageBreak(lft, rght) {
const ws = this._worksheet;
const left = Math.max(0, lft - 1) || 0;
const right = Math.max(0, rght - 1) || 16838;
const pb = {
id: this._number,
max: right,
man: 1,
};
if (left) pb.min = left;
ws.rowBreaks.push(pb);
}
// return a sparse array of cell values
get values() {
const values = [];
this._cells.forEach(cell => {
if (cell && cell.type !== Enums.ValueType.Null) {
values[cell.col] = cell.value;
}
});
return values;
}
// set the values by contiguous or sparse array, or by key'd object literal
set values(value) {
// this operation is not additive - any prior cells are removed
this._cells = [];
if (!value) {
// empty row
} else if (value instanceof Array) {
let offset = 0;
if (value.hasOwnProperty('0')) {
// contiguous array - start at column 1
offset = 1;
}
value.forEach((item, index) => {
if (item !== undefined) {
this.getCellEx({
address: colCache.encodeAddress(this._number, index + offset),
row: this._number,
col: index + offset,
}).value = item;
}
});
} else {
// assume object with column keys
this._worksheet.eachColumnKey((column, key) => {
if (value[key] !== undefined) {
this.getCellEx({
address: colCache.encodeAddress(this._number, column.number),
row: this._number,
col: column.number,
}).value = value[key];
}
});
}
}
// returns true if the row includes at least one cell with a value
get hasValues() {
return _.some(this._cells, cell => cell && cell.type !== Enums.ValueType.Null);
}
get cellCount() {
return this._cells.length;
}
get actualCellCount() {
let count = 0;
this.eachCell(() => {
count++;
});
return count;
}
// get the min and max column number for the non-null cells in this row or null
get dimensions() {
let min = 0;
let max = 0;
this._cells.forEach(cell => {
if (cell && cell.type !== Enums.ValueType.Null) {
if (!min || min > cell.col) {
min = cell.col;
}
if (max < cell.col) {
max = cell.col;
}
}
});
return min > 0
? {
min,
max,
}
: null;
}
// =========================================================================
// styles
_applyStyle(name, value) {
this.style[name] = value;
this._cells.forEach(cell => {
if (cell) {
cell[name] = value;
}
});
return value;
}
get numFmt() {
return this.style.numFmt;
}
set numFmt(value) {
this._applyStyle('numFmt', value);
}
get font() {
return this.style.font;
}
set font(value) {
this._applyStyle('font', value);
}
get alignment() {
return this.style.alignment;
}
set alignment(value) {
this._applyStyle('alignment', value);
}
get protection() {
return this.style.protection;
}
set protection(value) {
this._applyStyle('protection', value);
}
get border() {
return this.style.border;
}
set border(value) {
this._applyStyle('border', value);
}
get fill() {
return this.style.fill;
}
set fill(value) {
this._applyStyle('fill', value);
}
get hidden() {
return !!this._hidden;
}
set hidden(value) {
this._hidden = value;
}
get outlineLevel() {
return this._outlineLevel || 0;
}
set outlineLevel(value) {
this._outlineLevel = value;
}
get collapsed() {
return !!(
this._outlineLevel && this._outlineLevel >= this._worksheet.properties.outlineLevelRow
);
}
// =========================================================================
get model() {
const cells = [];
let min = 0;
let max = 0;
this._cells.forEach(cell => {
if (cell) {
const cellModel = cell.model;
if (cellModel) {
if (!min || min > cell.col) {
min = cell.col;
}
if (max < cell.col) {
max = cell.col;
}
cells.push(cellModel);
}
}
});
return this.height || cells.length
? {
cells,
number: this.number,
min,
max,
height: this.height,
style: this.style,
hidden: this.hidden,
outlineLevel: this.outlineLevel,
collapsed: this.collapsed,
}
: null;
}
set model(value) {
if (value.number !== this._number) {
throw new Error('Invalid row number in model');
}
this._cells = [];
let previousAddress;
value.cells.forEach(cellModel => {
switch (cellModel.type) {
case Cell.Types.Merge:
// special case - don't add this types
break;
default: {
let address;
if (cellModel.address) {
address = colCache.decodeAddress(cellModel.address);
} else if (previousAddress) {
// This is a <c> element without an r attribute
// Assume that it's the cell for the next column
const {row} = previousAddress;
const col = previousAddress.col + 1;
address = {
row,
col,
address: colCache.encodeAddress(row, col),
$col$row: `$${colCache.n2l(col)}$${row}`,
};
}
previousAddress = address;
const cell = this.getCellEx(address);
cell.model = cellModel;
break;
}
}
});
if (value.height) {
this.height = value.height;
} else {
delete this.height;
}
this.hidden = value.hidden;
this.outlineLevel = value.outlineLevel || 0;
this.style = (value.style && JSON.parse(JSON.stringify(value.style))) || {};
}
}
module.exports = Row;

View File

@@ -0,0 +1,465 @@
/* eslint-disable max-classes-per-file */
const colCache = require('../utils/col-cache');
class Column {
// wrapper around column model, allowing access and manipulation
constructor(table, column, index) {
this.table = table;
this.column = column;
this.index = index;
}
_set(name, value) {
this.table.cacheState();
this.column[name] = value;
}
/* eslint-disable lines-between-class-members */
get name() {
return this.column.name;
}
set name(value) {
this._set('name', value);
}
get filterButton() {
return this.column.filterButton;
}
set filterButton(value) {
this.column.filterButton = value;
}
get style() {
return this.column.style;
}
set style(value) {
this.column.style = value;
}
get totalsRowLabel() {
return this.column.totalsRowLabel;
}
set totalsRowLabel(value) {
this._set('totalsRowLabel', value);
}
get totalsRowFunction() {
return this.column.totalsRowFunction;
}
set totalsRowFunction(value) {
this._set('totalsRowFunction', value);
}
get totalsRowResult() {
return this.column.totalsRowResult;
}
set totalsRowResult(value) {
this._set('totalsRowResult', value);
}
get totalsRowFormula() {
return this.column.totalsRowFormula;
}
set totalsRowFormula(value) {
this._set('totalsRowFormula', value);
}
/* eslint-enable lines-between-class-members */
}
class Table {
constructor(worksheet, table) {
this.worksheet = worksheet;
if (table) {
this.table = table;
// check things are ok first
this.validate();
this.store();
}
}
getFormula(column) {
// get the correct formula to apply to the totals row
switch (column.totalsRowFunction) {
case 'none':
return null;
case 'average':
return `SUBTOTAL(101,${this.table.name}[${column.name}])`;
case 'countNums':
return `SUBTOTAL(102,${this.table.name}[${column.name}])`;
case 'count':
return `SUBTOTAL(103,${this.table.name}[${column.name}])`;
case 'max':
return `SUBTOTAL(104,${this.table.name}[${column.name}])`;
case 'min':
return `SUBTOTAL(105,${this.table.name}[${column.name}])`;
case 'stdDev':
return `SUBTOTAL(106,${this.table.name}[${column.name}])`;
case 'var':
return `SUBTOTAL(107,${this.table.name}[${column.name}])`;
case 'sum':
return `SUBTOTAL(109,${this.table.name}[${column.name}])`;
case 'custom':
return column.totalsRowFormula;
default:
throw new Error(`Invalid Totals Row Function: ${column.totalsRowFunction}`);
}
}
get width() {
// width of the table
return this.table.columns.length;
}
get height() {
// height of the table data
return this.table.rows.length;
}
get filterHeight() {
// height of the table data plus optional header row
return this.height + (this.table.headerRow ? 1 : 0);
}
get tableHeight() {
// full height of the table on the sheet
return this.filterHeight + (this.table.totalsRow ? 1 : 0);
}
validate() {
const {table} = this;
// set defaults and check is valid
const assign = (o, name, dflt) => {
if (o[name] === undefined) {
o[name] = dflt;
}
};
assign(table, 'headerRow', true);
assign(table, 'totalsRow', false);
assign(table, 'style', {});
assign(table.style, 'theme', 'TableStyleMedium2');
assign(table.style, 'showFirstColumn', false);
assign(table.style, 'showLastColumn', false);
assign(table.style, 'showRowStripes', false);
assign(table.style, 'showColumnStripes', false);
const assert = (test, message) => {
if (!test) {
throw new Error(message);
}
};
assert(table.ref, 'Table must have ref');
assert(table.columns, 'Table must have column definitions');
assert(table.rows, 'Table must have row definitions');
table.tl = colCache.decodeAddress(table.ref);
const {row, col} = table.tl;
assert(row > 0, 'Table must be on valid row');
assert(col > 0, 'Table must be on valid col');
const {width, filterHeight, tableHeight} = this;
// autoFilterRef is a range that includes optional headers only
table.autoFilterRef = colCache.encode(row, col, row + filterHeight - 1, col + width - 1);
// tableRef is a range that includes optional headers and totals
table.tableRef = colCache.encode(row, col, row + tableHeight - 1, col + width - 1);
table.columns.forEach((column, i) => {
assert(column.name, `Column ${i} must have a name`);
if (i === 0) {
assign(column, 'totalsRowLabel', 'Total');
} else {
assign(column, 'totalsRowFunction', 'none');
column.totalsRowFormula = this.getFormula(column);
}
});
}
store() {
// where the table needs to store table data, headers, footers in
// the sheet...
const assignStyle = (cell, style) => {
if (style) {
Object.keys(style).forEach(key => {
cell[key] = style[key];
});
}
};
const {worksheet, table} = this;
const {row, col} = table.tl;
let count = 0;
if (table.headerRow) {
const r = worksheet.getRow(row + count++);
table.columns.forEach((column, j) => {
const {style, name} = column;
const cell = r.getCell(col + j);
cell.value = name;
assignStyle(cell, style);
});
}
table.rows.forEach(data => {
const r = worksheet.getRow(row + count++);
data.forEach((value, j) => {
const cell = r.getCell(col + j);
cell.value = value;
assignStyle(cell, table.columns[j].style);
});
});
if (table.totalsRow) {
const r = worksheet.getRow(row + count++);
table.columns.forEach((column, j) => {
const cell = r.getCell(col + j);
if (j === 0) {
cell.value = column.totalsRowLabel;
} else {
const formula = this.getFormula(column);
if (formula) {
cell.value = {
formula: column.totalsRowFormula,
result: column.totalsRowResult,
};
} else {
cell.value = null;
}
}
assignStyle(cell, column.style);
});
}
}
load(worksheet) {
// where the table will read necessary features from a loaded sheet
const {table} = this;
const {row, col} = table.tl;
let count = 0;
if (table.headerRow) {
const r = worksheet.getRow(row + count++);
table.columns.forEach((column, j) => {
const cell = r.getCell(col + j);
cell.value = column.name;
});
}
table.rows.forEach(data => {
const r = worksheet.getRow(row + count++);
data.forEach((value, j) => {
const cell = r.getCell(col + j);
cell.value = value;
});
});
if (table.totalsRow) {
const r = worksheet.getRow(row + count++);
table.columns.forEach((column, j) => {
const cell = r.getCell(col + j);
if (j === 0) {
cell.value = column.totalsRowLabel;
} else {
const formula = this.getFormula(column);
if (formula) {
cell.value = {
formula: column.totalsRowFormula,
result: column.totalsRowResult,
};
}
}
});
}
}
get model() {
return this.table;
}
set model(value) {
this.table = value;
}
// ================================================================
// TODO: Mutating methods
cacheState() {
if (!this._cache) {
this._cache = {
ref: this.ref,
width: this.width,
tableHeight: this.tableHeight,
};
}
}
commit() {
// changes may have been made that might have on-sheet effects
if (!this._cache) {
return;
}
// check things are ok first
this.validate();
const ref = colCache.decodeAddress(this._cache.ref);
if (this.ref !== this._cache.ref) {
// wipe out whole table footprint at previous location
for (let i = 0; i < this._cache.tableHeight; i++) {
const row = this.worksheet.getRow(ref.row + i);
for (let j = 0; j < this._cache.width; j++) {
const cell = row.getCell(ref.col + j);
cell.value = null;
}
}
} else {
// clear out below table if it has shrunk
for (let i = this.tableHeight; i < this._cache.tableHeight; i++) {
const row = this.worksheet.getRow(ref.row + i);
for (let j = 0; j < this._cache.width; j++) {
const cell = row.getCell(ref.col + j);
cell.value = null;
}
}
// clear out to right of table if it has lost columns
for (let i = 0; i < this.tableHeight; i++) {
const row = this.worksheet.getRow(ref.row + i);
for (let j = this.width; j < this._cache.width; j++) {
const cell = row.getCell(ref.col + j);
cell.value = null;
}
}
}
this.store();
}
addRow(values, rowNumber) {
// Add a row of data, either insert at rowNumber or append
this.cacheState();
if (rowNumber === undefined) {
this.table.rows.push(values);
} else {
this.table.rows.splice(rowNumber, 0, values);
}
}
removeRows(rowIndex, count = 1) {
// Remove a rows of data
this.cacheState();
this.table.rows.splice(rowIndex, count);
}
getColumn(colIndex) {
const column = this.table.columns[colIndex];
return new Column(this, column, colIndex);
}
addColumn(column, values, colIndex) {
// Add a new column, including column defn and values
// Inserts at colNumber or adds to the right
this.cacheState();
if (colIndex === undefined) {
this.table.columns.push(column);
this.table.rows.forEach((row, i) => {
row.push(values[i]);
});
} else {
this.table.columns.splice(colIndex, 0, column);
this.table.rows.forEach((row, i) => {
row.splice(colIndex, 0, values[i]);
});
}
}
removeColumns(colIndex, count = 1) {
// Remove a column with data
this.cacheState();
this.table.columns.splice(colIndex, count);
this.table.rows.forEach(row => {
row.splice(colIndex, count);
});
}
_assign(target, prop, value) {
this.cacheState();
target[prop] = value;
}
/* eslint-disable lines-between-class-members */
get ref() {
return this.table.ref;
}
set ref(value) {
this._assign(this.table, 'ref', value);
}
get name() {
return this.table.name;
}
set name(value) {
this.table.name = value;
}
get displayName() {
return this.table.displyName || this.table.name;
}
set displayNamename(value) {
this.table.displayName = value;
}
get headerRow() {
return this.table.headerRow;
}
set headerRow(value) {
this._assign(this.table, 'headerRow', value);
}
get totalsRow() {
return this.table.totalsRow;
}
set totalsRow(value) {
this._assign(this.table, 'totalsRow', value);
}
get theme() {
return this.table.style.name;
}
set theme(value) {
this.table.style.name = value;
}
get showFirstColumn() {
return this.table.style.showFirstColumn;
}
set showFirstColumn(value) {
this.table.style.showFirstColumn = value;
}
get showLastColumn() {
return this.table.style.showLastColumn;
}
set showLastColumn(value) {
this.table.style.showLastColumn = value;
}
get showRowStripes() {
return this.table.style.showRowStripes;
}
set showRowStripes(value) {
this.table.style.showRowStripes = value;
}
get showColumnStripes() {
return this.table.style.showColumnStripes;
}
set showColumnStripes(value) {
this.table.style.showColumnStripes = value;
}
/* eslint-enable lines-between-class-members */
}
module.exports = Table;

View File

@@ -0,0 +1,221 @@
'use strict';
const Worksheet = require('./worksheet');
const DefinedNames = require('./defined-names');
const XLSX = require('../xlsx/xlsx');
const CSV = require('../csv/csv');
// Workbook requirements
// Load and Save from file and stream
// Access/Add/Delete individual worksheets
// Manage String table, Hyperlink table, etc.
// Manage scaffolding for contained objects to write to/read from
class Workbook {
constructor() {
this.category = '';
this.company = '';
this.created = new Date();
this.description = '';
this.keywords = '';
this.manager = '';
this.modified = this.created;
this.properties = {};
this.calcProperties = {};
this._worksheets = [];
this.subject = '';
this.title = '';
this.views = [];
this.media = [];
this._definedNames = new DefinedNames();
}
get xlsx() {
if (!this._xlsx) this._xlsx = new XLSX(this);
return this._xlsx;
}
get csv() {
if (!this._csv) this._csv = new CSV(this);
return this._csv;
}
get nextId() {
// find the next unique spot to add worksheet
for (let i = 1; i < this._worksheets.length; i++) {
if (!this._worksheets[i]) {
return i;
}
}
return this._worksheets.length || 1;
}
addWorksheet(name, options) {
const id = this.nextId;
// if options is a color, call it tabColor (and signal deprecated message)
if (options) {
if (typeof options === 'string') {
// eslint-disable-next-line no-console
console.trace(
'tabColor argument is now deprecated. Please use workbook.addWorksheet(name, {properties: { tabColor: { argb: "rbg value" } }'
);
options = {
properties: {
tabColor: {argb: options},
},
};
} else if (options.argb || options.theme || options.indexed) {
// eslint-disable-next-line no-console
console.trace(
'tabColor argument is now deprecated. Please use workbook.addWorksheet(name, {properties: { tabColor: { ... } }'
);
options = {
properties: {
tabColor: options,
},
};
}
}
const lastOrderNo = this._worksheets.reduce((acc, ws) => ((ws && ws.orderNo) > acc ? ws.orderNo : acc), 0);
const worksheetOptions = Object.assign({}, options, {
id,
name,
orderNo: lastOrderNo + 1,
workbook: this,
});
const worksheet = new Worksheet(worksheetOptions);
this._worksheets[id] = worksheet;
return worksheet;
}
removeWorksheetEx(worksheet) {
delete this._worksheets[worksheet.id];
}
removeWorksheet(id) {
const worksheet = this.getWorksheet(id);
if (worksheet) {
worksheet.destroy();
}
}
getWorksheet(id) {
if (id === undefined) {
return this._worksheets.find(Boolean);
}
if (typeof id === 'number') {
return this._worksheets[id];
}
if (typeof id === 'string') {
return this._worksheets.find(worksheet => worksheet && worksheet.name === id);
}
return undefined;
}
get worksheets() {
// return a clone of _worksheets
return this._worksheets
.slice(1)
.sort((a, b) => a.orderNo - b.orderNo)
.filter(Boolean);
}
eachSheet(iteratee) {
this.worksheets.forEach(sheet => {
iteratee(sheet, sheet.id);
});
}
get definedNames() {
return this._definedNames;
}
clearThemes() {
// Note: themes are not an exposed feature, meddle at your peril!
this._themes = undefined;
}
addImage(image) {
// TODO: validation?
const id = this.media.length;
this.media.push(Object.assign({}, image, {type: 'image'}));
return id;
}
getImage(id) {
return this.media[id];
}
get model() {
return {
creator: this.creator || 'Unknown',
lastModifiedBy: this.lastModifiedBy || 'Unknown',
lastPrinted: this.lastPrinted,
created: this.created,
modified: this.modified,
properties: this.properties,
worksheets: this.worksheets.map(worksheet => worksheet.model),
sheets: this.worksheets.map(ws => ws.model).filter(Boolean),
definedNames: this._definedNames.model,
views: this.views,
company: this.company,
manager: this.manager,
title: this.title,
subject: this.subject,
keywords: this.keywords,
category: this.category,
description: this.description,
language: this.language,
revision: this.revision,
contentStatus: this.contentStatus,
themes: this._themes,
media: this.media,
calcProperties: this.calcProperties,
};
}
set model(value) {
this.creator = value.creator;
this.lastModifiedBy = value.lastModifiedBy;
this.lastPrinted = value.lastPrinted;
this.created = value.created;
this.modified = value.modified;
this.company = value.company;
this.manager = value.manager;
this.title = value.title;
this.subject = value.subject;
this.keywords = value.keywords;
this.category = value.category;
this.description = value.description;
this.language = value.language;
this.revision = value.revision;
this.contentStatus = value.contentStatus;
this.properties = value.properties;
this.calcProperties = value.calcProperties;
this._worksheets = [];
value.worksheets.forEach(worksheetModel => {
const {id, name, state} = worksheetModel;
const orderNo = value.sheets && value.sheets.findIndex(ws => ws.id === id);
const worksheet = (this._worksheets[id] = new Worksheet({
id,
name,
orderNo,
state,
workbook: this,
}));
worksheet.model = worksheetModel;
});
this._definedNames.model = value.definedNames;
this.views = value.views;
this._themes = value.themes;
this.media = value.media || [];
}
}
module.exports = Workbook;

View File

@@ -0,0 +1,927 @@
const _ = require('../utils/under-dash');
const colCache = require('../utils/col-cache');
const Range = require('./range');
const Row = require('./row');
const Column = require('./column');
const Enums = require('./enums');
const Image = require('./image');
const Table = require('./table');
const DataValidations = require('./data-validations');
const Encryptor = require('../utils/encryptor');
const {copyStyle} = require('../utils/copy-style');
// Worksheet requirements
// Operate as sheet inside workbook or standalone
// Load and Save from file and stream
// Access/Add/Delete individual cells
// Manage column widths and row heights
class Worksheet {
constructor(options) {
options = options || {};
this._workbook = options.workbook;
// in a workbook, each sheet will have a number
this.id = options.id;
this.orderNo = options.orderNo;
// and a name
this.name = options.name;
// add a state
this.state = options.state || 'visible';
// rows allows access organised by row. Sparse array of arrays indexed by row-1, col
// Note: _rows is zero based. Must subtract 1 to go from cell.row to index
this._rows = [];
// column definitions
this._columns = null;
// column keys (addRow convenience): key ==> this._collumns index
this._keys = {};
// keep record of all merges
this._merges = {};
// record of all row and column pageBreaks
this.rowBreaks = [];
// for tabColor, default row height, outline levels, etc
this.properties = Object.assign(
{},
{
defaultRowHeight: 15,
dyDescent: 55,
outlineLevelCol: 0,
outlineLevelRow: 0,
},
options.properties
);
// for all things printing
this.pageSetup = Object.assign(
{},
{
margins: {left: 0.7, right: 0.7, top: 0.75, bottom: 0.75, header: 0.3, footer: 0.3},
orientation: 'portrait',
horizontalDpi: 4294967295,
verticalDpi: 4294967295,
fitToPage: !!(
options.pageSetup &&
(options.pageSetup.fitToWidth || options.pageSetup.fitToHeight) &&
!options.pageSetup.scale
),
pageOrder: 'downThenOver',
blackAndWhite: false,
draft: false,
cellComments: 'None',
errors: 'displayed',
scale: 100,
fitToWidth: 1,
fitToHeight: 1,
paperSize: undefined,
showRowColHeaders: false,
showGridLines: false,
firstPageNumber: undefined,
horizontalCentered: false,
verticalCentered: false,
rowBreaks: null,
colBreaks: null,
},
options.pageSetup
);
this.headerFooter = Object.assign(
{},
{
differentFirst: false,
differentOddEven: false,
oddHeader: null,
oddFooter: null,
evenHeader: null,
evenFooter: null,
firstHeader: null,
firstFooter: null,
},
options.headerFooter
);
this.dataValidations = new DataValidations();
// for freezepanes, split, zoom, gridlines, etc
this.views = options.views || [];
this.autoFilter = options.autoFilter || null;
// for images, etc
this._media = [];
// worksheet protection
this.sheetProtection = null;
// for tables
this.tables = {};
this.conditionalFormattings = [];
}
get name() {
return this._name;
}
set name(name) {
if (name === undefined) {
name = `sheet${this.id}`;
}
if (this._name === name) return;
if (typeof name !== 'string') {
throw new Error('The name has to be a string.');
}
if (name === '') {
throw new Error('The name can\'t be empty.');
}
if (name === 'History') {
throw new Error('The name "History" is protected. Please use a different name.');
}
// Illegal character in worksheet name: asterisk (*), question mark (?),
// colon (:), forward slash (/ \), or bracket ([])
if (/[*?:/\\[\]]/.test(name)) {
throw new Error(`Worksheet name ${name} cannot include any of the following characters: * ? : \\ / [ ]`);
}
if (/(^')|('$)/.test(name)) {
throw new Error(`The first or last character of worksheet name cannot be a single quotation mark: ${name}`);
}
if (name && name.length > 31) {
// eslint-disable-next-line no-console
console.warn(`Worksheet name ${name} exceeds 31 chars. This will be truncated`);
name = name.substring(0, 31);
}
if (this._workbook._worksheets.find(ws => ws && ws.name.toLowerCase() === name.toLowerCase())) {
throw new Error(`Worksheet name already exists: ${name}`);
}
this._name = name;
}
get workbook() {
return this._workbook;
}
// when you're done with this worksheet, call this to remove from workbook
destroy() {
this._workbook.removeWorksheetEx(this);
}
// Get the bounding range of the cells in this worksheet
get dimensions() {
const dimensions = new Range();
this._rows.forEach(row => {
if (row) {
const rowDims = row.dimensions;
if (rowDims) {
dimensions.expand(row.number, rowDims.min, row.number, rowDims.max);
}
}
});
return dimensions;
}
// =========================================================================
// Columns
// get the current columns array.
get columns() {
return this._columns;
}
// set the columns from an array of column definitions.
// Note: any headers defined will overwrite existing values.
set columns(value) {
// calculate max header row count
this._headerRowCount = value.reduce((pv, cv) => {
const headerCount = (cv.header && 1) || (cv.headers && cv.headers.length) || 0;
return Math.max(pv, headerCount);
}, 0);
// construct Column objects
let count = 1;
const columns = (this._columns = []);
value.forEach(defn => {
const column = new Column(this, count++, false);
columns.push(column);
column.defn = defn;
});
}
getColumnKey(key) {
return this._keys[key];
}
setColumnKey(key, value) {
this._keys[key] = value;
}
deleteColumnKey(key) {
delete this._keys[key];
}
eachColumnKey(f) {
_.each(this._keys, f);
}
// get a single column by col number. If it doesn't exist, create it and any gaps before it
getColumn(c) {
if (typeof c === 'string') {
// if it matches a key'd column, return that
const col = this._keys[c];
if (col) return col;
// otherwise, assume letter
c = colCache.l2n(c);
}
if (!this._columns) {
this._columns = [];
}
if (c > this._columns.length) {
let n = this._columns.length + 1;
while (n <= c) {
this._columns.push(new Column(this, n++));
}
}
return this._columns[c - 1];
}
spliceColumns(start, count, ...inserts) {
const rows = this._rows;
const nRows = rows.length;
if (inserts.length > 0) {
// must iterate over all rows whether they exist yet or not
for (let i = 0; i < nRows; i++) {
const rowArguments = [start, count];
// eslint-disable-next-line no-loop-func
inserts.forEach(insert => {
rowArguments.push(insert[i] || null);
});
const row = this.getRow(i + 1);
// eslint-disable-next-line prefer-spread
row.splice.apply(row, rowArguments);
}
} else {
// nothing to insert, so just splice all rows
this._rows.forEach(r => {
if (r) {
r.splice(start, count);
}
});
}
// splice column definitions
const nExpand = inserts.length - count;
const nKeep = start + count;
const nEnd = this._columns.length;
if (nExpand < 0) {
for (let i = start + inserts.length; i <= nEnd; i++) {
this.getColumn(i).defn = this.getColumn(i - nExpand).defn;
}
} else if (nExpand > 0) {
for (let i = nEnd; i >= nKeep; i--) {
this.getColumn(i + nExpand).defn = this.getColumn(i).defn;
}
}
for (let i = start; i < start + inserts.length; i++) {
this.getColumn(i).defn = null;
}
// account for defined names
this.workbook.definedNames.spliceColumns(this.name, start, count, inserts.length);
}
get lastColumn() {
return this.getColumn(this.columnCount);
}
get columnCount() {
let maxCount = 0;
this.eachRow(row => {
maxCount = Math.max(maxCount, row.cellCount);
});
return maxCount;
}
get actualColumnCount() {
// performance nightmare - for each row, counts all the columns used
const counts = [];
let count = 0;
this.eachRow(row => {
row.eachCell(({col}) => {
if (!counts[col]) {
counts[col] = true;
count++;
}
});
});
return count;
}
// =========================================================================
// Rows
_commitRow() {
// nop - allows streaming reader to fill a document
}
get _lastRowNumber() {
// need to cope with results of splice
const rows = this._rows;
let n = rows.length;
while (n > 0 && rows[n - 1] === undefined) {
n--;
}
return n;
}
get _nextRow() {
return this._lastRowNumber + 1;
}
get lastRow() {
if (this._rows.length) {
return this._rows[this._rows.length - 1];
}
return undefined;
}
// find a row (if exists) by row number
findRow(r) {
return this._rows[r - 1];
}
// find multiple rows (if exists) by row number
findRows(start, length) {
return this._rows.slice(start - 1, start - 1 + length);
}
get rowCount() {
return this._lastRowNumber;
}
get actualRowCount() {
// counts actual rows that have actual data
let count = 0;
this.eachRow(() => {
count++;
});
return count;
}
// get a row by row number.
getRow(r) {
let row = this._rows[r - 1];
if (!row) {
row = this._rows[r - 1] = new Row(this, r);
}
return row;
}
// get multiple rows by row number.
getRows(start, length) {
if (length < 1) return undefined;
const rows = [];
for (let i = start; i < start + length; i++) {
rows.push(this.getRow(i));
}
return rows;
}
addRow(value, style = 'n') {
const rowNo = this._nextRow;
const row = this.getRow(rowNo);
row.values = value;
this._setStyleOption(rowNo, style[0] === 'i' ? style : 'n');
return row;
}
addRows(value, style = 'n') {
const rows = [];
value.forEach(row => {
rows.push(this.addRow(row, style));
});
return rows;
}
insertRow(pos, value, style = 'n') {
this.spliceRows(pos, 0, value);
this._setStyleOption(pos, style);
return this.getRow(pos);
}
insertRows(pos, values, style = 'n') {
this.spliceRows(pos, 0, ...values);
if (style !== 'n') {
// copy over the styles
for (let i = 0; i < values.length; i++) {
if (style[0] === 'o' && this.findRow(values.length + pos + i) !== undefined) {
this._copyStyle(values.length + pos + i, pos + i, style[1] === '+');
} else if (style[0] === 'i' && this.findRow(pos - 1) !== undefined) {
this._copyStyle(pos - 1, pos + i, style[1] === '+');
}
}
}
return this.getRows(pos, values.length);
}
// set row at position to same style as of either pervious row (option 'i') or next row (option 'o')
_setStyleOption(pos, style = 'n') {
if (style[0] === 'o' && this.findRow(pos + 1) !== undefined) {
this._copyStyle(pos + 1, pos, style[1] === '+');
} else if (style[0] === 'i' && this.findRow(pos - 1) !== undefined) {
this._copyStyle(pos - 1, pos, style[1] === '+');
}
}
_copyStyle(src, dest, styleEmpty = false) {
const rSrc = this.getRow(src);
const rDst = this.getRow(dest);
rDst.style = copyStyle(rSrc.style);
// eslint-disable-next-line no-loop-func
rSrc.eachCell({includeEmpty: styleEmpty}, (cell, colNumber) => {
rDst.getCell(colNumber).style = copyStyle(cell.style);
});
rDst.height = rSrc.height;
}
duplicateRow(rowNum, count, insert = false) {
// create count duplicates of rowNum
// either inserting new or overwriting existing rows
const rSrc = this._rows[rowNum - 1];
const inserts = new Array(count).fill(rSrc.values);
this.spliceRows(rowNum + 1, insert ? 0 : count, ...inserts);
// now copy styles...
for (let i = 0; i < count; i++) {
const rDst = this._rows[rowNum + i];
rDst.style = rSrc.style;
rDst.height = rSrc.height;
// eslint-disable-next-line no-loop-func
rSrc.eachCell({includeEmpty: true}, (cell, colNumber) => {
rDst.getCell(colNumber).style = cell.style;
});
}
}
spliceRows(start, count, ...inserts) {
// same problem as row.splice, except worse.
const nKeep = start + count;
const nInserts = inserts.length;
const nExpand = nInserts - count;
const nEnd = this._rows.length;
let i;
let rSrc;
if (nExpand < 0) {
// remove rows
if (start === nEnd) {
this._rows[nEnd - 1] = undefined;
}
for (i = nKeep; i <= nEnd; i++) {
rSrc = this._rows[i - 1];
if (rSrc) {
const rDst = this.getRow(i + nExpand);
rDst.values = rSrc.values;
rDst.style = rSrc.style;
rDst.height = rSrc.height;
// eslint-disable-next-line no-loop-func
rSrc.eachCell({includeEmpty: true}, (cell, colNumber) => {
rDst.getCell(colNumber).style = cell.style;
});
this._rows[i - 1] = undefined;
} else {
this._rows[i + nExpand - 1] = undefined;
}
}
} else if (nExpand > 0) {
// insert new cells
for (i = nEnd; i >= nKeep; i--) {
rSrc = this._rows[i - 1];
if (rSrc) {
const rDst = this.getRow(i + nExpand);
rDst.values = rSrc.values;
rDst.style = rSrc.style;
rDst.height = rSrc.height;
// eslint-disable-next-line no-loop-func
rSrc.eachCell({includeEmpty: true}, (cell, colNumber) => {
rDst.getCell(colNumber).style = cell.style;
// remerge cells accounting for insert offset
if (cell._value.constructor.name === 'MergeValue') {
const cellToBeMerged = this.getRow(cell._row._number + nInserts).getCell(colNumber);
const prevMaster = cell._value._master;
const newMaster = this.getRow(prevMaster._row._number + nInserts).getCell(prevMaster._column._number);
cellToBeMerged.merge(newMaster);
}
});
} else {
this._rows[i + nExpand - 1] = undefined;
}
}
}
// now copy over the new values
for (i = 0; i < nInserts; i++) {
const rDst = this.getRow(start + i);
rDst.style = {};
rDst.values = inserts[i];
}
// account for defined names
this.workbook.definedNames.spliceRows(this.name, start, count, nInserts);
}
// iterate over every row in the worksheet, including maybe empty rows
eachRow(options, iteratee) {
if (!iteratee) {
iteratee = options;
options = undefined;
}
if (options && options.includeEmpty) {
const n = this._rows.length;
for (let i = 1; i <= n; i++) {
iteratee(this.getRow(i), i);
}
} else {
this._rows.forEach(row => {
if (row && row.hasValues) {
iteratee(row, row.number);
}
});
}
}
// return all rows as sparse array
getSheetValues() {
const rows = [];
this._rows.forEach(row => {
if (row) {
rows[row.number] = row.values;
}
});
return rows;
}
// =========================================================================
// Cells
// returns the cell at [r,c] or address given by r. If not found, return undefined
findCell(r, c) {
const address = colCache.getAddress(r, c);
const row = this._rows[address.row - 1];
return row ? row.findCell(address.col) : undefined;
}
// return the cell at [r,c] or address given by r. If not found, create a new one.
getCell(r, c) {
const address = colCache.getAddress(r, c);
const row = this.getRow(address.row);
return row.getCellEx(address);
}
// =========================================================================
// Merge
// convert the range defined by ['tl:br'], [tl,br] or [t,l,b,r] into a single 'merged' cell
mergeCells(...cells) {
const dimensions = new Range(cells);
this._mergeCellsInternal(dimensions);
}
mergeCellsWithoutStyle(...cells) {
const dimensions = new Range(cells);
this._mergeCellsInternal(dimensions, true);
}
_mergeCellsInternal(dimensions, ignoreStyle) {
// check cells aren't already merged
_.each(this._merges, merge => {
if (merge.intersects(dimensions)) {
throw new Error('Cannot merge already merged cells');
}
});
// apply merge
const master = this.getCell(dimensions.top, dimensions.left);
for (let i = dimensions.top; i <= dimensions.bottom; i++) {
for (let j = dimensions.left; j <= dimensions.right; j++) {
// merge all but the master cell
if (i > dimensions.top || j > dimensions.left) {
this.getCell(i, j).merge(master, ignoreStyle);
}
}
}
// index merge
this._merges[master.address] = dimensions;
}
_unMergeMaster(master) {
// master is always top left of a rectangle
const merge = this._merges[master.address];
if (merge) {
for (let i = merge.top; i <= merge.bottom; i++) {
for (let j = merge.left; j <= merge.right; j++) {
this.getCell(i, j).unmerge();
}
}
delete this._merges[master.address];
}
}
get hasMerges() {
// return true if this._merges has a merge object
return _.some(this._merges, Boolean);
}
// scan the range defined by ['tl:br'], [tl,br] or [t,l,b,r] and if any cell is part of a merge,
// un-merge the group. Note this function can affect multiple merges and merge-blocks are
// atomic - either they're all merged or all un-merged.
unMergeCells(...cells) {
const dimensions = new Range(cells);
// find any cells in that range and unmerge them
for (let i = dimensions.top; i <= dimensions.bottom; i++) {
for (let j = dimensions.left; j <= dimensions.right; j++) {
const cell = this.findCell(i, j);
if (cell) {
if (cell.type === Enums.ValueType.Merge) {
// this cell merges to another master
this._unMergeMaster(cell.master);
} else if (this._merges[cell.address]) {
// this cell is a master
this._unMergeMaster(cell);
}
}
}
}
}
// ===========================================================================
// Shared/Array Formula
fillFormula(range, formula, results, shareType = 'shared') {
// Define formula for top-left cell and share to rest
const decoded = colCache.decode(range);
const {top, left, bottom, right} = decoded;
const width = right - left + 1;
const masterAddress = colCache.encodeAddress(top, left);
const isShared = shareType === 'shared';
// work out result accessor
let getResult;
if (typeof results === 'function') {
getResult = results;
} else if (Array.isArray(results)) {
if (Array.isArray(results[0])) {
getResult = (row, col) => results[row - top][col - left];
} else {
// eslint-disable-next-line no-mixed-operators
getResult = (row, col) => results[(row - top) * width + (col - left)];
}
} else {
getResult = () => undefined;
}
let first = true;
for (let r = top; r <= bottom; r++) {
for (let c = left; c <= right; c++) {
if (first) {
this.getCell(r, c).value = {
shareType,
formula,
ref: range,
result: getResult(r, c),
};
first = false;
} else {
this.getCell(r, c).value = isShared
? {
sharedFormula: masterAddress,
result: getResult(r, c),
}
: getResult(r, c);
}
}
}
}
// =========================================================================
// Images
addImage(imageId, range) {
const model = {
type: 'image',
imageId,
range,
};
this._media.push(new Image(this, model));
}
getImages() {
return this._media.filter(m => m.type === 'image');
}
addBackgroundImage(imageId) {
const model = {
type: 'background',
imageId,
};
this._media.push(new Image(this, model));
}
getBackgroundImageId() {
const image = this._media.find(m => m.type === 'background');
return image && image.imageId;
}
// =========================================================================
// Worksheet Protection
protect(password, options) {
// TODO: make this function truly async
// perhaps marshal to worker thread or something
return new Promise(resolve => {
this.sheetProtection = {
sheet: true,
};
if (options && 'spinCount' in options) {
// force spinCount to be integer >= 0
options.spinCount = Number.isFinite(options.spinCount) ? Math.round(Math.max(0, options.spinCount)) : 100000;
}
if (password) {
this.sheetProtection.algorithmName = 'SHA-512';
this.sheetProtection.saltValue = Encryptor.randomBytes(16).toString('base64');
this.sheetProtection.spinCount = options && 'spinCount' in options ? options.spinCount : 100000; // allow user specified spinCount
this.sheetProtection.hashValue = Encryptor.convertPasswordToHash(
password,
'SHA512',
this.sheetProtection.saltValue,
this.sheetProtection.spinCount
);
}
if (options) {
this.sheetProtection = Object.assign(this.sheetProtection, options);
if (!password && 'spinCount' in options) {
delete this.sheetProtection.spinCount;
}
}
resolve();
});
}
unprotect() {
this.sheetProtection = null;
}
// =========================================================================
// Tables
addTable(model) {
const table = new Table(this, model);
this.tables[model.name] = table;
return table;
}
getTable(name) {
return this.tables[name];
}
removeTable(name) {
delete this.tables[name];
}
getTables() {
return Object.values(this.tables);
}
// ===========================================================================
// Conditional Formatting
addConditionalFormatting(cf) {
this.conditionalFormattings.push(cf);
}
removeConditionalFormatting(filter) {
if (typeof filter === 'number') {
this.conditionalFormattings.splice(filter, 1);
} else if (filter instanceof Function) {
this.conditionalFormattings = this.conditionalFormattings.filter(filter);
} else {
this.conditionalFormattings = [];
}
}
// ===========================================================================
// Deprecated
get tabColor() {
// eslint-disable-next-line no-console
console.trace('worksheet.tabColor property is now deprecated. Please use worksheet.properties.tabColor');
return this.properties.tabColor;
}
set tabColor(value) {
// eslint-disable-next-line no-console
console.trace('worksheet.tabColor property is now deprecated. Please use worksheet.properties.tabColor');
this.properties.tabColor = value;
}
// ===========================================================================
// Model
get model() {
const model = {
id: this.id,
name: this.name,
dataValidations: this.dataValidations.model,
properties: this.properties,
state: this.state,
pageSetup: this.pageSetup,
headerFooter: this.headerFooter,
rowBreaks: this.rowBreaks,
views: this.views,
autoFilter: this.autoFilter,
media: this._media.map(medium => medium.model),
sheetProtection: this.sheetProtection,
tables: Object.values(this.tables).map(table => table.model),
conditionalFormattings: this.conditionalFormattings,
};
// =================================================
// columns
model.cols = Column.toModel(this.columns);
// ==========================================================
// Rows
const rows = (model.rows = []);
const dimensions = (model.dimensions = new Range());
this._rows.forEach(row => {
const rowModel = row && row.model;
if (rowModel) {
dimensions.expand(rowModel.number, rowModel.min, rowModel.number, rowModel.max);
rows.push(rowModel);
}
});
// ==========================================================
// Merges
model.merges = [];
_.each(this._merges, merge => {
model.merges.push(merge.range);
});
return model;
}
_parseRows(model) {
this._rows = [];
model.rows.forEach(rowModel => {
const row = new Row(this, rowModel.number);
this._rows[row.number - 1] = row;
row.model = rowModel;
});
}
_parseMergeCells(model) {
_.each(model.mergeCells, merge => {
// Do not merge styles when importing an Excel file
// since each cell may have different styles intentionally.
this.mergeCellsWithoutStyle(merge);
});
}
set model(value) {
this.name = value.name;
this._columns = Column.fromModel(this, value.cols);
this._parseRows(value);
this._parseMergeCells(value);
this.dataValidations = new DataValidations(value.dataValidations);
this.properties = value.properties;
this.pageSetup = value.pageSetup;
this.headerFooter = value.headerFooter;
this.views = value.views;
this.autoFilter = value.autoFilter;
this._media = value.media.map(medium => new Image(this, medium));
this.sheetProtection = value.sheetProtection;
this.tables = value.tables.reduce((tables, table) => {
const t = new Table();
t.model = table;
tables[table.name] = t;
return tables;
}, {});
this.conditionalFormattings = value.conditionalFormattings;
}
}
module.exports = Worksheet;

View File

@@ -0,0 +1,13 @@
// this bundle is built without polyfill leaving apps the freedom to add their own
const ExcelJS = {
Workbook: require('./doc/workbook'),
};
// Object.assign mono-fill
const Enums = require('./doc/enums');
Object.keys(Enums).forEach(key => {
ExcelJS[key] = Enums[key];
});
module.exports = ExcelJS;

View File

@@ -0,0 +1,36 @@
/* eslint-disable import/no-extraneous-dependencies,node/no-unpublished-require */
require('core-js/modules/es.promise');
require('core-js/modules/es.promise.finally');
require('core-js/modules/es.object.assign');
require('core-js/modules/es.object.keys');
require('core-js/modules/es.object.values');
require('core-js/modules/es.symbol');
require('core-js/modules/es.symbol.async-iterator');
// required by core-js/modules/es.promise Promise.all
require('core-js/modules/es.array.iterator');
// required by node_modules/saxes/saxes.js SaxesParser.captureTo
require('core-js/modules/es.array.includes');
// required by lib/doc/workbook.js Workbook.model
require('core-js/modules/es.array.find-index');
// required by lib/doc/workbook.js Workbook.addWorksheet and Workbook.getWorksheet
require('core-js/modules/es.array.find');
// required by node_modules/saxes/saxes.js SaxesParser.getCode10
require('core-js/modules/es.string.from-code-point');
// required by lib/xlsx/xform/sheet/data-validations-xform.js DataValidationsXform.parseClose
require('core-js/modules/es.string.includes');
// required by lib/utils/utils.js utils.validInt and lib/csv/csv.js CSV.read
require('core-js/modules/es.number.is-nan');
require('regenerator-runtime/runtime');
const ExcelJS = {
Workbook: require('./doc/workbook'),
};
// Object.assign mono-fill
const Enums = require('./doc/enums');
Object.keys(Enums).forEach(key => {
ExcelJS[key] = Enums[key];
});
module.exports = ExcelJS;

View File

@@ -0,0 +1,14 @@
const ExcelJS = {
Workbook: require('./doc/workbook'),
ModelContainer: require('./doc/modelcontainer'),
stream: {
xlsx: {
WorkbookWriter: require('./stream/xlsx/workbook-writer'),
WorkbookReader: require('./stream/xlsx/workbook-reader'),
},
},
};
Object.assign(ExcelJS, require('./doc/enums'));
module.exports = ExcelJS;

View File

@@ -0,0 +1,83 @@
const {EventEmitter} = require('events');
const parseSax = require('../../utils/parse-sax');
const Enums = require('../../doc/enums');
const RelType = require('../../xlsx/rel-type');
class HyperlinkReader extends EventEmitter {
constructor({workbook, id, iterator, options}) {
super();
this.workbook = workbook;
this.id = id;
this.iterator = iterator;
this.options = options;
}
get count() {
return (this.hyperlinks && this.hyperlinks.length) || 0;
}
each(fn) {
return this.hyperlinks.forEach(fn);
}
async read() {
const {iterator, options} = this;
let emitHyperlinks = false;
let hyperlinks = null;
switch (options.hyperlinks) {
case 'emit':
emitHyperlinks = true;
break;
case 'cache':
this.hyperlinks = hyperlinks = {};
break;
default:
break;
}
if (!emitHyperlinks && !hyperlinks) {
this.emit('finished');
return;
}
try {
for await (const events of parseSax(iterator)) {
for (const {eventType, value} of events) {
if (eventType === 'opentag') {
const node = value;
if (node.name === 'Relationship') {
const rId = node.attributes.Id;
switch (node.attributes.Type) {
case RelType.Hyperlink:
{
const relationship = {
type: Enums.RelationshipType.Styles,
rId,
target: node.attributes.Target,
targetMode: node.attributes.TargetMode,
};
if (emitHyperlinks) {
this.emit('hyperlink', relationship);
} else {
hyperlinks[relationship.rId] = relationship;
}
}
break;
default:
break;
}
}
}
}
}
this.emit('finished');
} catch (error) {
this.emit('error', error);
}
}
}
module.exports = HyperlinkReader;

View File

@@ -0,0 +1,121 @@
const XmlStream = require('../../utils/xml-stream');
const RelType = require('../../xlsx/rel-type');
const colCache = require('../../utils/col-cache');
const CommentXform = require('../../xlsx/xform/comment/comment-xform');
const VmlShapeXform = require('../../xlsx/xform/comment/vml-shape-xform');
class SheetCommentsWriter {
constructor(worksheet, sheetRelsWriter, options) {
// in a workbook, each sheet will have a number
this.id = options.id;
this.count = 0;
this._worksheet = worksheet;
this._workbook = options.workbook;
this._sheetRelsWriter = sheetRelsWriter;
}
get commentsStream() {
if (!this._commentsStream) {
// eslint-disable-next-line no-underscore-dangle
this._commentsStream = this._workbook._openStream(`/xl/comments${this.id}.xml`);
}
return this._commentsStream;
}
get vmlStream() {
if (!this._vmlStream) {
// eslint-disable-next-line no-underscore-dangle
this._vmlStream = this._workbook._openStream(`xl/drawings/vmlDrawing${this.id}.vml`);
}
return this._vmlStream;
}
_addRelationships() {
const commentRel = {
Type: RelType.Comments,
Target: `../comments${this.id}.xml`,
};
this._sheetRelsWriter.addRelationship(commentRel);
const vmlDrawingRel = {
Type: RelType.VmlDrawing,
Target: `../drawings/vmlDrawing${this.id}.vml`,
};
this.vmlRelId = this._sheetRelsWriter.addRelationship(vmlDrawingRel);
}
_addCommentRefs() {
this._workbook.commentRefs.push({
commentName: `comments${this.id}`,
vmlDrawing: `vmlDrawing${this.id}`,
});
}
_writeOpen() {
this.commentsStream.write(
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' +
'<comments xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">' +
'<authors><author>Author</author></authors>' +
'<commentList>'
);
this.vmlStream.write(
'<?xml version="1.0" encoding="UTF-8"?>' +
'<xml xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:x="urn:schemas-microsoft-com:office:excel">' +
'<o:shapelayout v:ext="edit">' +
'<o:idmap v:ext="edit" data="1" />' +
'</o:shapelayout>' +
'<v:shapetype id="_x0000_t202" coordsize="21600,21600" o:spt="202" path="m,l,21600r21600,l21600,xe">' +
'<v:stroke joinstyle="miter" />' +
'<v:path gradientshapeok="t" o:connecttype="rect" />' +
'</v:shapetype>'
);
}
_writeComment(comment, index) {
const commentXform = new CommentXform();
const commentsXmlStream = new XmlStream();
commentXform.render(commentsXmlStream, comment);
this.commentsStream.write(commentsXmlStream.xml);
const vmlShapeXform = new VmlShapeXform();
const vmlXmlStream = new XmlStream();
vmlShapeXform.render(vmlXmlStream, comment, index);
this.vmlStream.write(vmlXmlStream.xml);
}
_writeClose() {
this.commentsStream.write('</commentList></comments>');
this.vmlStream.write('</xml>');
}
addComments(comments) {
if (comments && comments.length) {
if (!this.startedData) {
this._worksheet.comments = [];
this._writeOpen();
this._addRelationships();
this._addCommentRefs();
this.startedData = true;
}
comments.forEach(item => {
item.refAddress = colCache.decodeAddress(item.ref);
});
comments.forEach(comment => {
this._writeComment(comment, this.count);
this.count += 1;
});
}
}
commit() {
if (this.count) {
this._writeClose();
this.commentsStream.end();
this.vmlStream.end();
}
}
}
module.exports = SheetCommentsWriter;

View File

@@ -0,0 +1,119 @@
/* eslint-disable max-classes-per-file */
const utils = require('../../utils/utils');
const RelType = require('../../xlsx/rel-type');
class HyperlinksProxy {
constructor(sheetRelsWriter) {
this.writer = sheetRelsWriter;
}
push(hyperlink) {
this.writer.addHyperlink(hyperlink);
}
}
class SheetRelsWriter {
constructor(options) {
// in a workbook, each sheet will have a number
this.id = options.id;
// count of all relationships
this.count = 0;
// keep record of all hyperlinks
this._hyperlinks = [];
this._workbook = options.workbook;
}
get stream() {
if (!this._stream) {
// eslint-disable-next-line no-underscore-dangle
this._stream = this._workbook._openStream(`/xl/worksheets/_rels/sheet${this.id}.xml.rels`);
}
return this._stream;
}
get length() {
return this._hyperlinks.length;
}
each(fn) {
return this._hyperlinks.forEach(fn);
}
get hyperlinksProxy() {
return this._hyperlinksProxy || (this._hyperlinksProxy = new HyperlinksProxy(this));
}
addHyperlink(hyperlink) {
// Write to stream
const relationship = {
Target: hyperlink.target,
Type: RelType.Hyperlink,
TargetMode: 'External',
};
const rId = this._writeRelationship(relationship);
// store sheet stuff for later
this._hyperlinks.push({
rId,
address: hyperlink.address,
});
}
addMedia(media) {
return this._writeRelationship(media);
}
addRelationship(rel) {
return this._writeRelationship(rel);
}
commit() {
if (this.count) {
// write xml utro
this._writeClose();
// and close stream
this.stream.end();
}
}
// ================================================================================
_writeOpen() {
this.stream.write(
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">`
);
}
_writeRelationship(relationship) {
if (!this.count) {
this._writeOpen();
}
const rId = `rId${++this.count}`;
if (relationship.TargetMode) {
this.stream.write(
`<Relationship Id="${rId}"` +
` Type="${relationship.Type}"` +
` Target="${utils.xmlEncode(relationship.Target)}"` +
` TargetMode="${relationship.TargetMode}"` +
'/>'
);
} else {
this.stream.write(
`<Relationship Id="${rId}" Type="${relationship.Type}" Target="${relationship.Target}"/>`
);
}
return rId;
}
_writeClose() {
this.stream.write('</Relationships>');
}
}
module.exports = SheetRelsWriter;

View File

@@ -0,0 +1,337 @@
const fs = require('fs');
const {EventEmitter} = require('events');
const {PassThrough, Readable} = require('readable-stream');
const nodeStream = require('stream');
const unzip = require('unzipper');
const tmp = require('tmp');
const iterateStream = require('../../utils/iterate-stream');
const parseSax = require('../../utils/parse-sax');
const StyleManager = require('../../xlsx/xform/style/styles-xform');
const WorkbookXform = require('../../xlsx/xform/book/workbook-xform');
const RelationshipsXform = require('../../xlsx/xform/core/relationships-xform');
const WorksheetReader = require('./worksheet-reader');
const HyperlinkReader = require('./hyperlink-reader');
tmp.setGracefulCleanup();
class WorkbookReader extends EventEmitter {
constructor(input, options = {}) {
super();
this.input = input;
this.options = {
worksheets: 'emit',
sharedStrings: 'cache',
hyperlinks: 'ignore',
styles: 'ignore',
entries: 'ignore',
...options,
};
this.styles = new StyleManager();
this.styles.init();
}
_getStream(input) {
if (input instanceof nodeStream.Readable || input instanceof Readable) {
return input;
}
if (typeof input === 'string') {
return fs.createReadStream(input);
}
throw new Error(`Could not recognise input: ${input}`);
}
async read(input, options) {
try {
for await (const {eventType, value} of this.parse(input, options)) {
switch (eventType) {
case 'shared-strings':
this.emit(eventType, value);
break;
case 'worksheet':
this.emit(eventType, value);
await value.read();
break;
case 'hyperlinks':
this.emit(eventType, value);
break;
}
}
this.emit('end');
this.emit('finished');
} catch (error) {
this.emit('error', error);
}
}
async *[Symbol.asyncIterator]() {
for await (const {eventType, value} of this.parse()) {
if (eventType === 'worksheet') {
yield value;
}
}
}
async *parse(input, options) {
if (options) this.options = options;
const stream = (this.stream = this._getStream(input || this.input));
const zip = unzip.Parse({forceStream: true});
stream.pipe(zip);
// worksheets, deferred for parsing after shared strings reading
const waitingWorkSheets = [];
for await (const entry of iterateStream(zip)) {
let match;
let sheetNo;
switch (entry.path) {
case '_rels/.rels':
break;
case 'xl/_rels/workbook.xml.rels':
await this._parseRels(entry);
break;
case 'xl/workbook.xml':
await this._parseWorkbook(entry);
break;
case 'xl/sharedStrings.xml':
yield* this._parseSharedStrings(entry);
break;
case 'xl/styles.xml':
await this._parseStyles(entry);
break;
default:
if (entry.path.match(/xl\/worksheets\/sheet\d+[.]xml/)) {
match = entry.path.match(/xl\/worksheets\/sheet(\d+)[.]xml/);
sheetNo = match[1];
if (this.sharedStrings && this.workbookRels) {
yield* this._parseWorksheet(iterateStream(entry), sheetNo);
} else {
// create temp file for each worksheet
await new Promise((resolve, reject) => {
tmp.file((err, path, fd, tempFileCleanupCallback) => {
if (err) {
return reject(err);
}
waitingWorkSheets.push({sheetNo, path, tempFileCleanupCallback});
const tempStream = fs.createWriteStream(path);
tempStream.on('error', reject);
entry.pipe(tempStream);
return tempStream.on('finish', () => {
return resolve();
});
});
});
}
} else if (entry.path.match(/xl\/worksheets\/_rels\/sheet\d+[.]xml.rels/)) {
match = entry.path.match(/xl\/worksheets\/_rels\/sheet(\d+)[.]xml.rels/);
sheetNo = match[1];
yield* this._parseHyperlinks(iterateStream(entry), sheetNo);
}
break;
}
entry.autodrain();
}
for (const {sheetNo, path, tempFileCleanupCallback} of waitingWorkSheets) {
let fileStream = fs.createReadStream(path);
// TODO: Remove once node v8 is deprecated
// Detect and upgrade old fileStreams
if (!fileStream[Symbol.asyncIterator]) {
fileStream = fileStream.pipe(new PassThrough());
}
yield* this._parseWorksheet(fileStream, sheetNo);
tempFileCleanupCallback();
}
}
_emitEntry(payload) {
if (this.options.entries === 'emit') {
this.emit('entry', payload);
}
}
async _parseRels(entry) {
const xform = new RelationshipsXform();
this.workbookRels = await xform.parseStream(iterateStream(entry));
}
async _parseWorkbook(entry) {
this._emitEntry({type: 'workbook'});
const workbook = new WorkbookXform();
await workbook.parseStream(iterateStream(entry));
this.properties = workbook.map.workbookPr;
this.model = workbook.model;
}
async *_parseSharedStrings(entry) {
this._emitEntry({type: 'shared-strings'});
switch (this.options.sharedStrings) {
case 'cache':
this.sharedStrings = [];
break;
case 'emit':
break;
default:
return;
}
let text = null;
let richText = [];
let index = 0;
let font = null;
for await (const events of parseSax(iterateStream(entry))) {
for (const {eventType, value} of events) {
if (eventType === 'opentag') {
const node = value;
switch (node.name) {
case 'b':
font = font || {};
font.bold = true;
break;
case 'charset':
font = font || {};
font.charset = parseInt(node.attributes.charset, 10);
break;
case 'color':
font = font || {};
font.color = {};
if (node.attributes.rgb) {
font.color.argb = node.attributes.argb;
}
if (node.attributes.val) {
font.color.argb = node.attributes.val;
}
if (node.attributes.theme) {
font.color.theme = node.attributes.theme;
}
break;
case 'family':
font = font || {};
font.family = parseInt(node.attributes.val, 10);
break;
case 'i':
font = font || {};
font.italic = true;
break;
case 'outline':
font = font || {};
font.outline = true;
break;
case 'rFont':
font = font || {};
font.name = node.value;
break;
case 'si':
font = null;
richText = [];
text = null;
break;
case 'sz':
font = font || {};
font.size = parseInt(node.attributes.val, 10);
break;
case 'strike':
break;
case 't':
text = null;
break;
case 'u':
font = font || {};
font.underline = true;
break;
case 'vertAlign':
font = font || {};
font.vertAlign = node.attributes.val;
break;
}
} else if (eventType === 'text') {
text = text ? text + value : value;
} else if (eventType === 'closetag') {
const node = value;
switch (node.name) {
case 'r':
richText.push({
font,
text,
});
font = null;
text = null;
break;
case 'si':
if (this.options.sharedStrings === 'cache') {
this.sharedStrings.push(richText.length ? {richText} : text);
} else if (this.options.sharedStrings === 'emit') {
yield {index: index++, text: richText.length ? {richText} : text};
}
richText = [];
font = null;
text = null;
break;
}
}
}
}
}
async _parseStyles(entry) {
this._emitEntry({type: 'styles'});
if (this.options.styles === 'cache') {
this.styles = new StyleManager();
await this.styles.parseStream(iterateStream(entry));
}
}
*_parseWorksheet(iterator, sheetNo) {
this._emitEntry({type: 'worksheet', id: sheetNo});
const worksheetReader = new WorksheetReader({
workbook: this,
id: sheetNo,
iterator,
options: this.options,
});
const matchingRel = (this.workbookRels || []).find(rel => rel.Target === `worksheets/sheet${sheetNo}.xml`);
const matchingSheet = matchingRel && (this.model.sheets || []).find(sheet => sheet.rId === matchingRel.Id);
if (matchingSheet) {
worksheetReader.id = matchingSheet.id;
worksheetReader.name = matchingSheet.name;
worksheetReader.state = matchingSheet.state;
}
if (this.options.worksheets === 'emit') {
yield {eventType: 'worksheet', value: worksheetReader};
}
}
*_parseHyperlinks(iterator, sheetNo) {
this._emitEntry({type: 'hyperlinks', id: sheetNo});
const hyperlinksReader = new HyperlinkReader({
workbook: this,
id: sheetNo,
iterator,
options: this.options,
});
if (this.options.hyperlinks === 'emit') {
yield {eventType: 'hyperlinks', value: hyperlinksReader};
}
}
}
// for reference - these are the valid values for options
WorkbookReader.Options = {
worksheets: ['emit', 'ignore'],
sharedStrings: ['cache', 'emit', 'ignore'],
hyperlinks: ['cache', 'emit', 'ignore'],
styles: ['cache', 'ignore'],
entries: ['emit', 'ignore'],
};
module.exports = WorkbookReader;

View File

@@ -0,0 +1,347 @@
const fs = require('fs');
const Archiver = require('archiver');
const StreamBuf = require('../../utils/stream-buf');
const RelType = require('../../xlsx/rel-type');
const StylesXform = require('../../xlsx/xform/style/styles-xform');
const SharedStrings = require('../../utils/shared-strings');
const DefinedNames = require('../../doc/defined-names');
const CoreXform = require('../../xlsx/xform/core/core-xform');
const RelationshipsXform = require('../../xlsx/xform/core/relationships-xform');
const ContentTypesXform = require('../../xlsx/xform/core/content-types-xform');
const AppXform = require('../../xlsx/xform/core/app-xform');
const WorkbookXform = require('../../xlsx/xform/book/workbook-xform');
const SharedStringsXform = require('../../xlsx/xform/strings/shared-strings-xform');
const WorksheetWriter = require('./worksheet-writer');
const theme1Xml = require('../../xlsx/xml/theme1.js');
class WorkbookWriter {
constructor(options) {
options = options || {};
this.created = options.created || new Date();
this.modified = options.modified || this.created;
this.creator = options.creator || 'ExcelJS';
this.lastModifiedBy = options.lastModifiedBy || 'ExcelJS';
this.lastPrinted = options.lastPrinted;
// using shared strings creates a smaller xlsx file but may use more memory
this.useSharedStrings = options.useSharedStrings || false;
this.sharedStrings = new SharedStrings();
// style manager
this.styles = options.useStyles ? new StylesXform(true) : new StylesXform.Mock(true);
// defined names
this._definedNames = new DefinedNames();
this._worksheets = [];
this.views = [];
this.zipOptions = options.zip;
this.media = [];
this.commentRefs = [];
this.zip = Archiver('zip', this.zipOptions);
if (options.stream) {
this.stream = options.stream;
} else if (options.filename) {
this.stream = fs.createWriteStream(options.filename);
} else {
this.stream = new StreamBuf();
}
this.zip.pipe(this.stream);
// these bits can be added right now
this.promise = Promise.all([this.addThemes(), this.addOfficeRels()]);
}
get definedNames() {
return this._definedNames;
}
_openStream(path) {
const stream = new StreamBuf({bufSize: 65536, batch: true});
this.zip.append(stream, {name: path});
stream.on('finish', () => {
stream.emit('zipped');
});
return stream;
}
_commitWorksheets() {
const commitWorksheet = function(worksheet) {
if (!worksheet.committed) {
return new Promise(resolve => {
worksheet.stream.on('zipped', () => {
resolve();
});
worksheet.commit();
});
}
return Promise.resolve();
};
// if there are any uncommitted worksheets, commit them now and wait
const promises = this._worksheets.map(commitWorksheet);
if (promises.length) {
return Promise.all(promises);
}
return Promise.resolve();
}
async commit() {
// commit all worksheets, then add suplimentary files
await this.promise;
await this.addMedia();
await this._commitWorksheets();
await Promise.all([
this.addContentTypes(),
this.addApp(),
this.addCore(),
this.addSharedStrings(),
this.addStyles(),
this.addWorkbookRels(),
]);
await this.addWorkbook();
return this._finalize();
}
get nextId() {
// find the next unique spot to add worksheet
let i;
for (i = 1; i < this._worksheets.length; i++) {
if (!this._worksheets[i]) {
return i;
}
}
return this._worksheets.length || 1;
}
addImage(image) {
const id = this.media.length;
const medium = Object.assign({}, image, {type: 'image', name: `image${id}.${image.extension}`});
this.media.push(medium);
return id;
}
getImage(id) {
return this.media[id];
}
addWorksheet(name, options) {
// it's possible to add a worksheet with different than default
// shared string handling
// in fact, it's even possible to switch it mid-sheet
options = options || {};
const useSharedStrings =
options.useSharedStrings !== undefined ? options.useSharedStrings : this.useSharedStrings;
if (options.tabColor) {
// eslint-disable-next-line no-console
console.trace('tabColor option has moved to { properties: tabColor: {...} }');
options.properties = Object.assign(
{
tabColor: options.tabColor,
},
options.properties
);
}
const id = this.nextId;
name = name || `sheet${id}`;
const worksheet = new WorksheetWriter({
id,
name,
workbook: this,
useSharedStrings,
properties: options.properties,
state: options.state,
pageSetup: options.pageSetup,
views: options.views,
autoFilter: options.autoFilter,
headerFooter: options.headerFooter,
});
this._worksheets[id] = worksheet;
return worksheet;
}
getWorksheet(id) {
if (id === undefined) {
return this._worksheets.find(() => true);
}
if (typeof id === 'number') {
return this._worksheets[id];
}
if (typeof id === 'string') {
return this._worksheets.find(worksheet => worksheet && worksheet.name === id);
}
return undefined;
}
addStyles() {
return new Promise(resolve => {
this.zip.append(this.styles.xml, {name: 'xl/styles.xml'});
resolve();
});
}
addThemes() {
return new Promise(resolve => {
this.zip.append(theme1Xml, {name: 'xl/theme/theme1.xml'});
resolve();
});
}
addOfficeRels() {
return new Promise(resolve => {
const xform = new RelationshipsXform();
const xml = xform.toXml([
{Id: 'rId1', Type: RelType.OfficeDocument, Target: 'xl/workbook.xml'},
{Id: 'rId2', Type: RelType.CoreProperties, Target: 'docProps/core.xml'},
{Id: 'rId3', Type: RelType.ExtenderProperties, Target: 'docProps/app.xml'},
]);
this.zip.append(xml, {name: '/_rels/.rels'});
resolve();
});
}
addContentTypes() {
return new Promise(resolve => {
const model = {
worksheets: this._worksheets.filter(Boolean),
sharedStrings: this.sharedStrings,
commentRefs: this.commentRefs,
media: this.media,
};
const xform = new ContentTypesXform();
const xml = xform.toXml(model);
this.zip.append(xml, {name: '[Content_Types].xml'});
resolve();
});
}
addMedia() {
return Promise.all(
this.media.map(medium => {
if (medium.type === 'image') {
const filename = `xl/media/${medium.name}`;
if (medium.filename) {
return this.zip.file(medium.filename, {name: filename});
}
if (medium.buffer) {
return this.zip.append(medium.buffer, {name: filename});
}
if (medium.base64) {
const dataimg64 = medium.base64;
const content = dataimg64.substring(dataimg64.indexOf(',') + 1);
return this.zip.append(content, {name: filename, base64: true});
}
}
throw new Error('Unsupported media');
})
);
}
addApp() {
return new Promise(resolve => {
const model = {
worksheets: this._worksheets.filter(Boolean),
};
const xform = new AppXform();
const xml = xform.toXml(model);
this.zip.append(xml, {name: 'docProps/app.xml'});
resolve();
});
}
addCore() {
return new Promise(resolve => {
const coreXform = new CoreXform();
const xml = coreXform.toXml(this);
this.zip.append(xml, {name: 'docProps/core.xml'});
resolve();
});
}
addSharedStrings() {
if (this.sharedStrings.count) {
return new Promise(resolve => {
const sharedStringsXform = new SharedStringsXform();
const xml = sharedStringsXform.toXml(this.sharedStrings);
this.zip.append(xml, {name: '/xl/sharedStrings.xml'});
resolve();
});
}
return Promise.resolve();
}
addWorkbookRels() {
let count = 1;
const relationships = [
{Id: `rId${count++}`, Type: RelType.Styles, Target: 'styles.xml'},
{Id: `rId${count++}`, Type: RelType.Theme, Target: 'theme/theme1.xml'},
];
if (this.sharedStrings.count) {
relationships.push({
Id: `rId${count++}`,
Type: RelType.SharedStrings,
Target: 'sharedStrings.xml',
});
}
this._worksheets.forEach(worksheet => {
if (worksheet) {
worksheet.rId = `rId${count++}`;
relationships.push({
Id: worksheet.rId,
Type: RelType.Worksheet,
Target: `worksheets/sheet${worksheet.id}.xml`,
});
}
});
return new Promise(resolve => {
const xform = new RelationshipsXform();
const xml = xform.toXml(relationships);
this.zip.append(xml, {name: '/xl/_rels/workbook.xml.rels'});
resolve();
});
}
addWorkbook() {
const {zip} = this;
const model = {
worksheets: this._worksheets.filter(Boolean),
definedNames: this._definedNames.model,
views: this.views,
properties: {},
calcProperties: {},
};
return new Promise(resolve => {
const xform = new WorkbookXform();
xform.prepare(model);
zip.append(xform.toXml(model), {name: '/xl/workbook.xml'});
resolve();
});
}
_finalize() {
return new Promise((resolve, reject) => {
this.stream.on('error', reject);
this.stream.on('finish', () => {
resolve(this);
});
this.zip.on('error', reject);
this.zip.finalize();
});
}
}
module.exports = WorkbookWriter;

View File

@@ -0,0 +1,374 @@
const {EventEmitter} = require('events');
const parseSax = require('../../utils/parse-sax');
const _ = require('../../utils/under-dash');
const utils = require('../../utils/utils');
const colCache = require('../../utils/col-cache');
const Dimensions = require('../../doc/range');
const Row = require('../../doc/row');
const Column = require('../../doc/column');
class WorksheetReader extends EventEmitter {
constructor({workbook, id, iterator, options}) {
super();
this.workbook = workbook;
this.id = id;
this.iterator = iterator;
this.options = options || {};
// and a name
this.name = `Sheet${this.id}`;
// column definitions
this._columns = null;
this._keys = {};
// keep a record of dimensions
this._dimensions = new Dimensions();
}
// destroy - not a valid operation for a streaming writer
// even though some streamers might be able to, it's a bad idea.
destroy() {
throw new Error('Invalid Operation: destroy');
}
// return the current dimensions of the writer
get dimensions() {
return this._dimensions;
}
// =========================================================================
// Columns
// get the current columns array.
get columns() {
return this._columns;
}
// get a single column by col number. If it doesn't exist, it and any gaps before it
// are created.
getColumn(c) {
if (typeof c === 'string') {
// if it matches a key'd column, return that
const col = this._keys[c];
if (col) {
return col;
}
// otherise, assume letter
c = colCache.l2n(c);
}
if (!this._columns) {
this._columns = [];
}
if (c > this._columns.length) {
let n = this._columns.length + 1;
while (n <= c) {
this._columns.push(new Column(this, n++));
}
}
return this._columns[c - 1];
}
getColumnKey(key) {
return this._keys[key];
}
setColumnKey(key, value) {
this._keys[key] = value;
}
deleteColumnKey(key) {
delete this._keys[key];
}
eachColumnKey(f) {
_.each(this._keys, f);
}
async read() {
try {
for await (const events of this.parse()) {
for (const {eventType, value} of events) {
this.emit(eventType, value);
}
}
this.emit('finished');
} catch (error) {
this.emit('error', error);
}
}
async *[Symbol.asyncIterator]() {
for await (const events of this.parse()) {
for (const {eventType, value} of events) {
if (eventType === 'row') {
yield value;
}
}
}
}
async *parse() {
const {iterator, options} = this;
let emitSheet = false;
let emitHyperlinks = false;
let hyperlinks = null;
switch (options.worksheets) {
case 'emit':
emitSheet = true;
break;
case 'prep':
break;
default:
break;
}
switch (options.hyperlinks) {
case 'emit':
emitHyperlinks = true;
break;
case 'cache':
this.hyperlinks = hyperlinks = {};
break;
default:
break;
}
if (!emitSheet && !emitHyperlinks && !hyperlinks) {
return;
}
// references
const {sharedStrings, styles, properties} = this.workbook;
// xml position
let inCols = false;
let inRows = false;
let inHyperlinks = false;
// parse state
let cols = null;
let row = null;
let c = null;
let current = null;
for await (const events of parseSax(iterator)) {
const worksheetEvents = [];
for (const {eventType, value} of events) {
if (eventType === 'opentag') {
const node = value;
if (emitSheet) {
switch (node.name) {
case 'cols':
inCols = true;
cols = [];
break;
case 'sheetData':
inRows = true;
break;
case 'col':
if (inCols) {
cols.push({
min: parseInt(node.attributes.min, 10),
max: parseInt(node.attributes.max, 10),
width: parseFloat(node.attributes.width),
styleId: parseInt(node.attributes.style || '0', 10),
});
}
break;
case 'row':
if (inRows) {
const r = parseInt(node.attributes.r, 10);
row = new Row(this, r);
if (node.attributes.ht) {
row.height = parseFloat(node.attributes.ht);
}
if (node.attributes.s) {
const styleId = parseInt(node.attributes.s, 10);
const style = styles.getStyleModel(styleId);
if (style) {
row.style = style;
}
}
}
break;
case 'c':
if (row) {
c = {
ref: node.attributes.r,
s: parseInt(node.attributes.s, 10),
t: node.attributes.t,
};
}
break;
case 'f':
if (c) {
current = c.f = {text: ''};
}
break;
case 'v':
if (c) {
current = c.v = {text: ''};
}
break;
case 'is':
case 't':
if (c) {
current = c.v = {text: ''};
}
break;
case 'mergeCell':
break;
default:
break;
}
}
// =================================================================
//
if (emitHyperlinks || hyperlinks) {
switch (node.name) {
case 'hyperlinks':
inHyperlinks = true;
break;
case 'hyperlink':
if (inHyperlinks) {
const hyperlink = {
ref: node.attributes.ref,
rId: node.attributes['r:id'],
};
if (emitHyperlinks) {
worksheetEvents.push({eventType: 'hyperlink', value: hyperlink});
} else {
hyperlinks[hyperlink.ref] = hyperlink;
}
}
break;
default:
break;
}
}
} else if (eventType === 'text') {
// only text data is for sheet values
if (emitSheet) {
if (current) {
current.text += value;
}
}
} else if (eventType === 'closetag') {
const node = value;
if (emitSheet) {
switch (node.name) {
case 'cols':
inCols = false;
this._columns = Column.fromModel(cols);
break;
case 'sheetData':
inRows = false;
break;
case 'row':
this._dimensions.expandRow(row);
worksheetEvents.push({eventType: 'row', value: row});
row = null;
break;
case 'c':
if (row && c) {
const address = colCache.decodeAddress(c.ref);
const cell = row.getCell(address.col);
if (c.s) {
const style = styles.getStyleModel(c.s);
if (style) {
cell.style = style;
}
}
if (c.f) {
const cellValue = {
formula: c.f.text,
};
if (c.v) {
if (c.t === 'str') {
cellValue.result = utils.xmlDecode(c.v.text);
} else {
cellValue.result = parseFloat(c.v.text);
}
}
cell.value = cellValue;
} else if (c.v) {
switch (c.t) {
case 's': {
const index = parseInt(c.v.text, 10);
if (sharedStrings) {
cell.value = sharedStrings[index];
} else {
cell.value = {
sharedString: index,
};
}
break;
}
case 'inlineStr':
case 'str':
cell.value = utils.xmlDecode(c.v.text);
break;
case 'e':
cell.value = {error: c.v.text};
break;
case 'b':
cell.value = parseInt(c.v.text, 10) !== 0;
break;
default:
if (utils.isDateFmt(cell.numFmt)) {
cell.value = utils.excelToDate(
parseFloat(c.v.text),
properties.model && properties.model.date1904
);
} else {
cell.value = parseFloat(c.v.text);
}
break;
}
}
if (hyperlinks) {
const hyperlink = hyperlinks[c.ref];
if (hyperlink) {
cell.text = cell.value;
cell.value = undefined;
cell.hyperlink = hyperlink;
}
}
c = null;
}
break;
default:
break;
}
}
if (emitHyperlinks || hyperlinks) {
switch (node.name) {
case 'hyperlinks':
inHyperlinks = false;
break;
default:
break;
}
}
}
}
if (worksheetEvents.length > 0) {
yield worksheetEvents;
}
}
}
}
module.exports = WorksheetReader;

View File

@@ -0,0 +1,717 @@
const _ = require('../../utils/under-dash');
const RelType = require('../../xlsx/rel-type');
const colCache = require('../../utils/col-cache');
const Encryptor = require('../../utils/encryptor');
const Dimensions = require('../../doc/range');
const StringBuf = require('../../utils/string-buf');
const Row = require('../../doc/row');
const Column = require('../../doc/column');
const SheetRelsWriter = require('./sheet-rels-writer');
const SheetCommentsWriter = require('./sheet-comments-writer');
const DataValidations = require('../../doc/data-validations');
const xmlBuffer = new StringBuf();
// ============================================================================================
// Xforms
const ListXform = require('../../xlsx/xform/list-xform');
const DataValidationsXform = require('../../xlsx/xform/sheet/data-validations-xform');
const SheetPropertiesXform = require('../../xlsx/xform/sheet/sheet-properties-xform');
const SheetFormatPropertiesXform = require('../../xlsx/xform/sheet/sheet-format-properties-xform');
const ColXform = require('../../xlsx/xform/sheet/col-xform');
const RowXform = require('../../xlsx/xform/sheet/row-xform');
const HyperlinkXform = require('../../xlsx/xform/sheet/hyperlink-xform');
const SheetViewXform = require('../../xlsx/xform/sheet/sheet-view-xform');
const SheetProtectionXform = require('../../xlsx/xform/sheet/sheet-protection-xform');
const PageMarginsXform = require('../../xlsx/xform/sheet/page-margins-xform');
const PageSetupXform = require('../../xlsx/xform/sheet/page-setup-xform');
const AutoFilterXform = require('../../xlsx/xform/sheet/auto-filter-xform');
const PictureXform = require('../../xlsx/xform/sheet/picture-xform');
const ConditionalFormattingsXform = require('../../xlsx/xform/sheet/cf/conditional-formattings-xform');
const HeaderFooterXform = require('../../xlsx/xform/sheet/header-footer-xform');
const RowBreaksXform = require('../../xlsx/xform/sheet/row-breaks-xform');
// since prepare and render are functional, we can use singletons
const xform = {
dataValidations: new DataValidationsXform(),
sheetProperties: new SheetPropertiesXform(),
sheetFormatProperties: new SheetFormatPropertiesXform(),
columns: new ListXform({tag: 'cols', length: false, childXform: new ColXform()}),
row: new RowXform(),
hyperlinks: new ListXform({tag: 'hyperlinks', length: false, childXform: new HyperlinkXform()}),
sheetViews: new ListXform({tag: 'sheetViews', length: false, childXform: new SheetViewXform()}),
sheetProtection: new SheetProtectionXform(),
pageMargins: new PageMarginsXform(),
pageSeteup: new PageSetupXform(),
autoFilter: new AutoFilterXform(),
picture: new PictureXform(),
conditionalFormattings: new ConditionalFormattingsXform(),
headerFooter: new HeaderFooterXform(),
rowBreaks: new RowBreaksXform(),
};
// ============================================================================================
class WorksheetWriter {
constructor(options) {
// in a workbook, each sheet will have a number
this.id = options.id;
// and a name
this.name = options.name || `Sheet${this.id}`;
// add a state
this.state = options.state || 'visible';
// rows are stored here while they need to be worked on.
// when they are committed, they will be deleted.
this._rows = [];
// column definitions
this._columns = null;
// column keys (addRow convenience): key ==> this._columns index
this._keys = {};
// keep a record of all row and column pageBreaks
this._merges = [];
this._merges.add = function() {}; // ignore cell instruction
// keep record of all hyperlinks
this._sheetRelsWriter = new SheetRelsWriter(options);
this._sheetCommentsWriter = new SheetCommentsWriter(this, this._sheetRelsWriter, options);
// keep a record of dimensions
this._dimensions = new Dimensions();
// first uncommitted row
this._rowZero = 1;
// committed flag
this.committed = false;
// for data validations
this.dataValidations = new DataValidations();
// for sharing formulae
this._formulae = {};
this._siFormulae = 0;
// keep a record of conditionalFormattings
this.conditionalFormatting = [];
// keep a record of all row and column pageBreaks
this.rowBreaks = [];
// for default row height, outline levels, etc
this.properties = Object.assign(
{},
{
defaultRowHeight: 15,
dyDescent: 55,
outlineLevelCol: 0,
outlineLevelRow: 0,
},
options.properties
);
this.headerFooter = Object.assign(
{},
{
differentFirst: false,
differentOddEven: false,
oddHeader: null,
oddFooter: null,
evenHeader: null,
evenFooter: null,
firstHeader: null,
firstFooter: null,
},
options.headerFooter
);
// for all things printing
this.pageSetup = Object.assign(
{},
{
margins: {left: 0.7, right: 0.7, top: 0.75, bottom: 0.75, header: 0.3, footer: 0.3},
orientation: 'portrait',
horizontalDpi: 4294967295,
verticalDpi: 4294967295,
fitToPage: !!(
options.pageSetup &&
(options.pageSetup.fitToWidth || options.pageSetup.fitToHeight) &&
!options.pageSetup.scale
),
pageOrder: 'downThenOver',
blackAndWhite: false,
draft: false,
cellComments: 'None',
errors: 'displayed',
scale: 100,
fitToWidth: 1,
fitToHeight: 1,
paperSize: undefined,
showRowColHeaders: false,
showGridLines: false,
horizontalCentered: false,
verticalCentered: false,
rowBreaks: null,
colBreaks: null,
},
options.pageSetup
);
// using shared strings creates a smaller xlsx file but may use more memory
this.useSharedStrings = options.useSharedStrings || false;
this._workbook = options.workbook;
this.hasComments = false;
// views
this._views = options.views || [];
// auto filter
this.autoFilter = options.autoFilter || null;
this._media = [];
// worksheet protection
this.sheetProtection = null;
// start writing to stream now
this._writeOpenWorksheet();
this.startedData = false;
}
get workbook() {
return this._workbook;
}
get stream() {
if (!this._stream) {
// eslint-disable-next-line no-underscore-dangle
this._stream = this._workbook._openStream(`/xl/worksheets/sheet${this.id}.xml`);
// pause stream to prevent 'data' events
this._stream.pause();
}
return this._stream;
}
// destroy - not a valid operation for a streaming writer
// even though some streamers might be able to, it's a bad idea.
destroy() {
throw new Error('Invalid Operation: destroy');
}
commit() {
if (this.committed) {
return;
}
// commit all rows
this._rows.forEach(cRow => {
if (cRow) {
// write the row to the stream
this._writeRow(cRow);
}
});
// we _cannot_ accept new rows from now on
this._rows = null;
if (!this.startedData) {
this._writeOpenSheetData();
}
this._writeCloseSheetData();
this._writeAutoFilter();
this._writeMergeCells();
// for some reason, Excel can't handle dimensions at the bottom of the file
// this._writeDimensions();
this._writeHyperlinks();
this._writeConditionalFormatting();
this._writeDataValidations();
this._writeSheetProtection();
this._writePageMargins();
this._writePageSetup();
this._writeBackground();
this._writeHeaderFooter();
this._writeRowBreaks();
// Legacy Data tag for comments
this._writeLegacyData();
this._writeCloseWorksheet();
// signal end of stream to workbook
this.stream.end();
this._sheetCommentsWriter.commit();
// also commit the hyperlinks if any
this._sheetRelsWriter.commit();
this.committed = true;
}
// return the current dimensions of the writer
get dimensions() {
return this._dimensions;
}
get views() {
return this._views;
}
// =========================================================================
// Columns
// get the current columns array.
get columns() {
return this._columns;
}
// set the columns from an array of column definitions.
// Note: any headers defined will overwrite existing values.
set columns(value) {
// calculate max header row count
this._headerRowCount = value.reduce((pv, cv) => {
const headerCount = (cv.header && 1) || (cv.headers && cv.headers.length) || 0;
return Math.max(pv, headerCount);
}, 0);
// construct Column objects
let count = 1;
const columns = (this._columns = []);
value.forEach(defn => {
const column = new Column(this, count++, false);
columns.push(column);
column.defn = defn;
});
}
getColumnKey(key) {
return this._keys[key];
}
setColumnKey(key, value) {
this._keys[key] = value;
}
deleteColumnKey(key) {
delete this._keys[key];
}
eachColumnKey(f) {
_.each(this._keys, f);
}
// get a single column by col number. If it doesn't exist, it and any gaps before it
// are created.
getColumn(c) {
if (typeof c === 'string') {
// if it matches a key'd column, return that
const col = this._keys[c];
if (col) return col;
// otherwise, assume letter
c = colCache.l2n(c);
}
if (!this._columns) {
this._columns = [];
}
if (c > this._columns.length) {
let n = this._columns.length + 1;
while (n <= c) {
this._columns.push(new Column(this, n++));
}
}
return this._columns[c - 1];
}
// =========================================================================
// Rows
get _nextRow() {
return this._rowZero + this._rows.length;
}
// iterate over every uncommitted row in the worksheet, including maybe empty rows
eachRow(options, iteratee) {
if (!iteratee) {
iteratee = options;
options = undefined;
}
if (options && options.includeEmpty) {
const n = this._nextRow;
for (let i = this._rowZero; i < n; i++) {
iteratee(this.getRow(i), i);
}
} else {
this._rows.forEach(row => {
if (row.hasValues) {
iteratee(row, row.number);
}
});
}
}
_commitRow(cRow) {
// since rows must be written in order, we commit all rows up till and including cRow
let found = false;
while (this._rows.length && !found) {
const row = this._rows.shift();
this._rowZero++;
if (row) {
this._writeRow(row);
found = row.number === cRow.number;
this._rowZero = row.number + 1;
}
}
}
get lastRow() {
// returns last uncommitted row
if (this._rows.length) {
return this._rows[this._rows.length - 1];
}
return undefined;
}
// find a row (if exists) by row number
findRow(rowNumber) {
const index = rowNumber - this._rowZero;
return this._rows[index];
}
getRow(rowNumber) {
const index = rowNumber - this._rowZero;
// may fail if rows have been comitted
if (index < 0) {
throw new Error('Out of bounds: this row has been committed');
}
let row = this._rows[index];
if (!row) {
this._rows[index] = row = new Row(this, rowNumber);
}
return row;
}
addRow(value) {
const row = new Row(this, this._nextRow);
this._rows[row.number - this._rowZero] = row;
row.values = value;
return row;
}
// ================================================================================
// Cells
// returns the cell at [r,c] or address given by r. If not found, return undefined
findCell(r, c) {
const address = colCache.getAddress(r, c);
const row = this.findRow(address.row);
return row ? row.findCell(address.column) : undefined;
}
// return the cell at [r,c] or address given by r. If not found, create a new one.
getCell(r, c) {
const address = colCache.getAddress(r, c);
const row = this.getRow(address.row);
return row.getCellEx(address);
}
mergeCells(...cells) {
// may fail if rows have been comitted
const dimensions = new Dimensions(cells);
// check cells aren't already merged
this._merges.forEach(merge => {
if (merge.intersects(dimensions)) {
throw new Error('Cannot merge already merged cells');
}
});
// apply merge
const master = this.getCell(dimensions.top, dimensions.left);
for (let i = dimensions.top; i <= dimensions.bottom; i++) {
for (let j = dimensions.left; j <= dimensions.right; j++) {
if (i > dimensions.top || j > dimensions.left) {
this.getCell(i, j).merge(master);
}
}
}
// index merge
this._merges.push(dimensions);
}
// ===========================================================================
// Conditional Formatting
addConditionalFormatting(cf) {
this.conditionalFormatting.push(cf);
}
removeConditionalFormatting(filter) {
if (typeof filter === 'number') {
this.conditionalFormatting.splice(filter, 1);
} else if (filter instanceof Function) {
this.conditionalFormatting = this.conditionalFormatting.filter(filter);
} else {
this.conditionalFormatting = [];
}
}
// =========================================================================
addBackgroundImage(imageId) {
this._background = {
imageId,
};
}
getBackgroundImageId() {
return this._background && this._background.imageId;
}
// =========================================================================
// Worksheet Protection
protect(password, options) {
// TODO: make this function truly async
// perhaps marshal to worker thread or something
return new Promise(resolve => {
this.sheetProtection = {
sheet: true,
};
if (options && 'spinCount' in options) {
// force spinCount to be integer >= 0
options.spinCount = Number.isFinite(options.spinCount) ? Math.round(Math.max(0, options.spinCount)) : 100000;
}
if (password) {
this.sheetProtection.algorithmName = 'SHA-512';
this.sheetProtection.saltValue = Encryptor.randomBytes(16).toString('base64');
this.sheetProtection.spinCount = options && 'spinCount' in options ? options.spinCount : 100000; // allow user specified spinCount
this.sheetProtection.hashValue = Encryptor.convertPasswordToHash(
password,
'SHA512',
this.sheetProtection.saltValue,
this.sheetProtection.spinCount
);
}
if (options) {
this.sheetProtection = Object.assign(this.sheetProtection, options);
if (!password && 'spinCount' in options) {
delete this.sheetProtection.spinCount;
}
}
resolve();
});
}
unprotect() {
this.sheetProtection = null;
}
// ================================================================================
_write(text) {
xmlBuffer.reset();
xmlBuffer.addText(text);
this.stream.write(xmlBuffer);
}
_writeSheetProperties(xmlBuf, properties, pageSetup) {
const sheetPropertiesModel = {
outlineProperties: properties && properties.outlineProperties,
tabColor: properties && properties.tabColor,
pageSetup:
pageSetup && pageSetup.fitToPage
? {
fitToPage: pageSetup.fitToPage,
}
: undefined,
};
xmlBuf.addText(xform.sheetProperties.toXml(sheetPropertiesModel));
}
_writeSheetFormatProperties(xmlBuf, properties) {
const sheetFormatPropertiesModel = properties
? {
defaultRowHeight: properties.defaultRowHeight,
dyDescent: properties.dyDescent,
outlineLevelCol: properties.outlineLevelCol,
outlineLevelRow: properties.outlineLevelRow,
}
: undefined;
if (properties.defaultColWidth) {
sheetFormatPropertiesModel.defaultColWidth = properties.defaultColWidth;
}
xmlBuf.addText(xform.sheetFormatProperties.toXml(sheetFormatPropertiesModel));
}
_writeOpenWorksheet() {
xmlBuffer.reset();
xmlBuffer.addText('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>');
xmlBuffer.addText(
'<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"' +
' xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"' +
' xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"' +
' mc:Ignorable="x14ac"' +
' xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac">'
);
this._writeSheetProperties(xmlBuffer, this.properties, this.pageSetup);
xmlBuffer.addText(xform.sheetViews.toXml(this.views));
this._writeSheetFormatProperties(xmlBuffer, this.properties);
this.stream.write(xmlBuffer);
}
_writeColumns() {
const cols = Column.toModel(this.columns);
if (cols) {
xform.columns.prepare(cols, {styles: this._workbook.styles});
this.stream.write(xform.columns.toXml(cols));
}
}
_writeOpenSheetData() {
this._write('<sheetData>');
}
_writeRow(row) {
if (!this.startedData) {
this._writeColumns();
this._writeOpenSheetData();
this.startedData = true;
}
if (row.hasValues || row.height) {
const {model} = row;
const options = {
styles: this._workbook.styles,
sharedStrings: this.useSharedStrings ? this._workbook.sharedStrings : undefined,
hyperlinks: this._sheetRelsWriter.hyperlinksProxy,
merges: this._merges,
formulae: this._formulae,
siFormulae: this._siFormulae,
comments: [],
};
xform.row.prepare(model, options);
this.stream.write(xform.row.toXml(model));
if (options.comments.length) {
this.hasComments = true;
this._sheetCommentsWriter.addComments(options.comments);
}
}
}
_writeCloseSheetData() {
this._write('</sheetData>');
}
_writeMergeCells() {
if (this._merges.length) {
xmlBuffer.reset();
xmlBuffer.addText(`<mergeCells count="${this._merges.length}">`);
this._merges.forEach(merge => {
xmlBuffer.addText(`<mergeCell ref="${merge}"/>`);
});
xmlBuffer.addText('</mergeCells>');
this.stream.write(xmlBuffer);
}
}
_writeHyperlinks() {
// eslint-disable-next-line no-underscore-dangle
this.stream.write(xform.hyperlinks.toXml(this._sheetRelsWriter._hyperlinks));
}
_writeConditionalFormatting() {
const options = {
styles: this._workbook.styles,
};
xform.conditionalFormattings.prepare(this.conditionalFormatting, options);
this.stream.write(xform.conditionalFormattings.toXml(this.conditionalFormatting));
}
_writeRowBreaks() {
this.stream.write(xform.rowBreaks.toXml(this.rowBreaks));
}
_writeDataValidations() {
this.stream.write(xform.dataValidations.toXml(this.dataValidations.model));
}
_writeSheetProtection() {
this.stream.write(xform.sheetProtection.toXml(this.sheetProtection));
}
_writePageMargins() {
this.stream.write(xform.pageMargins.toXml(this.pageSetup.margins));
}
_writePageSetup() {
this.stream.write(xform.pageSeteup.toXml(this.pageSetup));
}
_writeHeaderFooter() {
this.stream.write(xform.headerFooter.toXml(this.headerFooter));
}
_writeAutoFilter() {
this.stream.write(xform.autoFilter.toXml(this.autoFilter));
}
_writeBackground() {
if (this._background) {
if (this._background.imageId !== undefined) {
const image = this._workbook.getImage(this._background.imageId);
const pictureId = this._sheetRelsWriter.addMedia({
Target: `../media/${image.name}`,
Type: RelType.Image,
});
this._background = {
...this._background,
rId: pictureId,
};
}
this.stream.write(xform.picture.toXml({rId: this._background.rId}));
}
}
_writeLegacyData() {
if (this.hasComments) {
xmlBuffer.reset();
xmlBuffer.addText(`<legacyDrawing r:id="${this._sheetCommentsWriter.vmlRelId}"/>`);
this.stream.write(xmlBuffer);
}
}
_writeDimensions() {
// for some reason, Excel can't handle dimensions at the bottom of the file
// and we don't know the dimensions until the commit, so don't write them.
// this._write('<dimension ref="' + this._dimensions + '"/>');
}
_writeCloseWorksheet() {
this._write('</worksheet>');
}
}
module.exports = WorksheetWriter;

View File

@@ -0,0 +1,15 @@
const {EventEmitter} = require('events');
// =============================================================================
// AutoDrain - kind of /dev/null
class AutoDrain extends EventEmitter {
write(chunk) {
this.emit('data', chunk);
}
end() {
this.emit('end');
}
}
module.exports = AutoDrain;

View File

@@ -0,0 +1,14 @@
// eslint-disable-next-line node/no-unsupported-features/node-builtins
const textDecoder = typeof TextDecoder === 'undefined' ? null : new TextDecoder('utf-8');
function bufferToString(chunk) {
if (typeof chunk === 'string') {
return chunk;
}
if (textDecoder) {
return textDecoder.decode(chunk);
}
return chunk.toString();
}
exports.bufferToString = bufferToString;

View File

@@ -0,0 +1,15 @@
// eslint-disable-next-line node/no-unsupported-features/node-builtins
const textEncoder = typeof TextEncoder === 'undefined' ? null : new TextEncoder('utf-8');
const {Buffer} = require('buffer');
function stringToBuffer(str) {
if (typeof str !== 'string') {
return str;
}
if (textEncoder) {
return Buffer.from(textEncoder.encode(str).buffer);
}
return Buffer.from(str);
}
exports.stringToBuffer = stringToBuffer;

View File

@@ -0,0 +1,165 @@
const _ = require('./under-dash');
const colCache = require('./col-cache');
class CellMatrix {
constructor(template) {
this.template = template;
this.sheets = {};
}
addCell(addressStr) {
this.addCellEx(colCache.decodeEx(addressStr));
}
getCell(addressStr) {
return this.findCellEx(colCache.decodeEx(addressStr), true);
}
findCell(addressStr) {
return this.findCellEx(colCache.decodeEx(addressStr), false);
}
findCellAt(sheetName, rowNumber, colNumber) {
const sheet = this.sheets[sheetName];
const row = sheet && sheet[rowNumber];
return row && row[colNumber];
}
addCellEx(address) {
if (address.top) {
for (let row = address.top; row <= address.bottom; row++) {
for (let col = address.left; col <= address.right; col++) {
this.getCellAt(address.sheetName, row, col);
}
}
} else {
this.findCellEx(address, true);
}
}
getCellEx(address) {
return this.findCellEx(address, true);
}
findCellEx(address, create) {
const sheet = this.findSheet(address, create);
const row = this.findSheetRow(sheet, address, create);
return this.findRowCell(row, address, create);
}
getCellAt(sheetName, rowNumber, colNumber) {
const sheet = this.sheets[sheetName] || (this.sheets[sheetName] = []);
const row = sheet[rowNumber] || (sheet[rowNumber] = []);
const cell =
row[colNumber] ||
(row[colNumber] = {
sheetName,
address: colCache.n2l(colNumber) + rowNumber,
row: rowNumber,
col: colNumber,
});
return cell;
}
removeCellEx(address) {
const sheet = this.findSheet(address);
if (!sheet) {
return;
}
const row = this.findSheetRow(sheet, address);
if (!row) {
return;
}
delete row[address.col];
}
forEachInSheet(sheetName, callback) {
const sheet = this.sheets[sheetName];
if (sheet) {
sheet.forEach((row, rowNumber) => {
if (row) {
row.forEach((cell, colNumber) => {
if (cell) {
callback(cell, rowNumber, colNumber);
}
});
}
});
}
}
forEach(callback) {
_.each(this.sheets, (sheet, sheetName) => {
this.forEachInSheet(sheetName, callback);
});
}
map(callback) {
const results = [];
this.forEach(cell => {
results.push(callback(cell));
});
return results;
}
findSheet(address, create) {
const name = address.sheetName;
if (this.sheets[name]) {
return this.sheets[name];
}
if (create) {
return (this.sheets[name] = []);
}
return undefined;
}
findSheetRow(sheet, address, create) {
const {row} = address;
if (sheet && sheet[row]) {
return sheet[row];
}
if (create) {
return (sheet[row] = []);
}
return undefined;
}
findRowCell(row, address, create) {
const {col} = address;
if (row && row[col]) {
return row[col];
}
if (create) {
return (row[col] = this.template
? Object.assign(address, JSON.parse(JSON.stringify(this.template)))
: address);
}
return undefined;
}
spliceRows(sheetName, start, numDelete, numInsert) {
const sheet = this.sheets[sheetName];
if (sheet) {
const inserts = [];
for (let i = 0; i < numInsert; i++) {
inserts.push([]);
}
sheet.splice(start, numDelete, ...inserts);
}
}
spliceColumns(sheetName, start, numDelete, numInsert) {
const sheet = this.sheets[sheetName];
if (sheet) {
const inserts = [];
for (let i = 0; i < numInsert; i++) {
inserts.push(null);
}
_.each(sheet, row => {
row.splice(start, numDelete, ...inserts);
});
}
}
}
module.exports = CellMatrix;

View File

@@ -0,0 +1,287 @@
const addressRegex = /^[A-Z]+\d+$/;
// =========================================================================
// Column Letter to Number conversion
const colCache = {
_dictionary: [
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
],
_l2nFill: 0,
_l2n: {},
_n2l: [],
_level(n) {
if (n <= 26) {
return 1;
}
if (n <= 26 * 26) {
return 2;
}
return 3;
},
_fill(level) {
let c;
let v;
let l1;
let l2;
let l3;
let n = 1;
if (level >= 4) {
throw new Error('Out of bounds. Excel supports columns from 1 to 16384');
}
if (this._l2nFill < 1 && level >= 1) {
while (n <= 26) {
c = this._dictionary[n - 1];
this._n2l[n] = c;
this._l2n[c] = n;
n++;
}
this._l2nFill = 1;
}
if (this._l2nFill < 2 && level >= 2) {
n = 27;
while (n <= 26 + (26 * 26)) {
v = n - (26 + 1);
l1 = v % 26;
l2 = Math.floor(v / 26);
c = this._dictionary[l2] + this._dictionary[l1];
this._n2l[n] = c;
this._l2n[c] = n;
n++;
}
this._l2nFill = 2;
}
if (this._l2nFill < 3 && level >= 3) {
n = 26 + (26 * 26) + 1;
while (n <= 16384) {
v = n - ((26 * 26) + 26 + 1);
l1 = v % 26;
l2 = Math.floor(v / 26) % 26;
l3 = Math.floor(v / (26 * 26));
c = this._dictionary[l3] + this._dictionary[l2] + this._dictionary[l1];
this._n2l[n] = c;
this._l2n[c] = n;
n++;
}
this._l2nFill = 3;
}
},
l2n(l) {
if (!this._l2n[l]) {
this._fill(l.length);
}
if (!this._l2n[l]) {
throw new Error(`Out of bounds. Invalid column letter: ${l}`);
}
return this._l2n[l];
},
n2l(n) {
if (n < 1 || n > 16384) {
throw new Error(`${n} is out of bounds. Excel supports columns from 1 to 16384`);
}
if (!this._n2l[n]) {
this._fill(this._level(n));
}
return this._n2l[n];
},
// =========================================================================
// Address processing
_hash: {},
// check if value looks like an address
validateAddress(value) {
if (!addressRegex.test(value)) {
throw new Error(`Invalid Address: ${value}`);
}
return true;
},
// convert address string into structure
decodeAddress(value) {
const addr = value.length < 5 && this._hash[value];
if (addr) {
return addr;
}
let hasCol = false;
let col = '';
let colNumber = 0;
let hasRow = false;
let row = '';
let rowNumber = 0;
for (let i = 0, char; i < value.length; i++) {
char = value.charCodeAt(i);
// col should before row
if (!hasRow && char >= 65 && char <= 90) {
// 65 = 'A'.charCodeAt(0)
// 90 = 'Z'.charCodeAt(0)
hasCol = true;
col += value[i];
// colNumber starts from 1
colNumber = (colNumber * 26) + char - 64;
} else if (char >= 48 && char <= 57) {
// 48 = '0'.charCodeAt(0)
// 57 = '9'.charCodeAt(0)
hasRow = true;
row += value[i];
// rowNumber starts from 0
rowNumber = (rowNumber * 10) + char - 48;
} else if (hasRow && hasCol && char !== 36) {
// 36 = '$'.charCodeAt(0)
break;
}
}
if (!hasCol) {
colNumber = undefined;
} else if (colNumber > 16384) {
throw new Error(`Out of bounds. Invalid column letter: ${col}`);
}
if (!hasRow) {
rowNumber = undefined;
}
// in case $row$col
value = col + row;
const address = {
address: value,
col: colNumber,
row: rowNumber,
$col$row: `$${col}$${row}`,
};
// mem fix - cache only the tl 100x100 square
if (colNumber <= 100 && rowNumber <= 100) {
this._hash[value] = address;
this._hash[address.$col$row] = address;
}
return address;
},
// convert r,c into structure (if only 1 arg, assume r is address string)
getAddress(r, c) {
if (c) {
const address = this.n2l(c) + r;
return this.decodeAddress(address);
}
return this.decodeAddress(r);
},
// convert [address], [tl:br] into address structures
decode(value) {
const parts = value.split(':');
if (parts.length === 2) {
const tl = this.decodeAddress(parts[0]);
const br = this.decodeAddress(parts[1]);
const result = {
top: Math.min(tl.row, br.row),
left: Math.min(tl.col, br.col),
bottom: Math.max(tl.row, br.row),
right: Math.max(tl.col, br.col),
};
// reconstruct tl, br and dimensions
result.tl = this.n2l(result.left) + result.top;
result.br = this.n2l(result.right) + result.bottom;
result.dimensions = `${result.tl}:${result.br}`;
return result;
}
return this.decodeAddress(value);
},
// convert [sheetName!][$]col[$]row[[$]col[$]row] into address or range structures
decodeEx(value) {
const groups = value.match(/(?:(?:(?:'((?:[^']|'')*)')|([^'^ !]*))!)?(.*)/);
const sheetName = groups[1] || groups[2]; // Qouted and unqouted groups
const reference = groups[3]; // Remaining address
const parts = reference.split(':');
if (parts.length > 1) {
let tl = this.decodeAddress(parts[0]);
let br = this.decodeAddress(parts[1]);
const top = Math.min(tl.row, br.row);
const left = Math.min(tl.col, br.col);
const bottom = Math.max(tl.row, br.row);
const right = Math.max(tl.col, br.col);
tl = this.n2l(left) + top;
br = this.n2l(right) + bottom;
return {
top,
left,
bottom,
right,
sheetName,
tl: {address: tl, col: left, row: top, $col$row: `$${this.n2l(left)}$${top}`, sheetName},
br: {
address: br,
col: right,
row: bottom,
$col$row: `$${this.n2l(right)}$${bottom}`,
sheetName,
},
dimensions: `${tl}:${br}`,
};
}
if (reference.startsWith('#')) {
return sheetName ? {sheetName, error: reference} : {error: reference};
}
const address = this.decodeAddress(reference);
return sheetName ? {sheetName, ...address} : address;
},
// convert row,col into address string
encodeAddress(row, col) {
return colCache.n2l(col) + row;
},
// convert row,col into string address or t,l,b,r into range
encode() {
switch (arguments.length) {
case 2:
return colCache.encodeAddress(arguments[0], arguments[1]);
case 4:
return `${colCache.encodeAddress(arguments[0], arguments[1])}:${colCache.encodeAddress(
arguments[2],
arguments[3]
)}`;
default:
throw new Error('Can only encode with 2 or 4 arguments');
}
},
// return true if address is contained within range
inRange(range, address) {
const [left, top, , right, bottom] = range;
const [col, row] = address;
return col >= left && col <= right && row >= top && row <= bottom;
},
};
module.exports = colCache;

View File

@@ -0,0 +1,43 @@
const oneDepthCopy = (obj, nestKeys) => ({
...obj,
...nestKeys.reduce((memo, key) => {
if (obj[key]) memo[key] = {...obj[key]};
return memo;
}, {}),
});
const setIfExists = (src, dst, key, nestKeys = []) => {
if (src[key]) dst[key] = oneDepthCopy(src[key], nestKeys);
};
const isEmptyObj = obj => Object.keys(obj).length === 0;
const copyStyle = style => {
if (!style) return style;
if (isEmptyObj(style)) return {};
const copied = {...style};
setIfExists(style, copied, 'font', ['color']);
setIfExists(style, copied, 'alignment');
setIfExists(style, copied, 'protection');
if (style.border) {
setIfExists(style, copied, 'border');
setIfExists(style.border, copied.border, 'top', ['color']);
setIfExists(style.border, copied.border, 'left', ['color']);
setIfExists(style.border, copied.border, 'bottom', ['color']);
setIfExists(style.border, copied.border, 'right', ['color']);
setIfExists(style.border, copied.border, 'diagonal', ['color']);
}
if (style.fill) {
setIfExists(style, copied, 'fill', ['fgColor', 'bgColor', 'center']);
if (style.fill.stops) {
copied.fill.stops = style.fill.stops.map(s => oneDepthCopy(s, ['color']));
}
}
return copied;
};
exports.copyStyle = copyStyle;

View File

@@ -0,0 +1,55 @@
'use strict';
const crypto = require('crypto');
const Encryptor = {
/**
* Calculate a hash of the concatenated buffers with the given algorithm.
* @param {string} algorithm - The hash algorithm.
* @returns {Buffer} The hash
*/
hash(algorithm, ...buffers) {
const hash = crypto.createHash(algorithm);
hash.update(Buffer.concat(buffers));
return hash.digest();
},
/**
* Convert a password into an encryption key
* @param {string} password - The password
* @param {string} hashAlgorithm - The hash algoritm
* @param {string} saltValue - The salt value
* @param {number} spinCount - The spin count
* @param {number} keyBits - The length of the key in bits
* @param {Buffer} blockKey - The block key
* @returns {Buffer} The encryption key
*/
convertPasswordToHash(password, hashAlgorithm, saltValue, spinCount) {
hashAlgorithm = hashAlgorithm.toLowerCase();
const hashes = crypto.getHashes();
if (hashes.indexOf(hashAlgorithm) < 0) {
throw new Error(`Hash algorithm '${hashAlgorithm}' not supported!`);
}
// Password must be in unicode buffer
const passwordBuffer = Buffer.from(password, 'utf16le');
// Generate the initial hash
let key = this.hash(hashAlgorithm, Buffer.from(saltValue, 'base64'), passwordBuffer);
// Now regenerate until spin count
for (let i = 0; i < spinCount; i++) {
const iterator = Buffer.alloc(4);
// this is the 'special' element of Excel password hashing
// that stops us from using crypto.pbkdf2()
iterator.writeUInt32LE(i, 0);
key = this.hash(hashAlgorithm, key, iterator);
}
return key.toString('base64');
},
/**
* Generates cryptographically strong pseudo-random data.
* @param size The size argument is a number indicating the number of bytes to generate.
*/
randomBytes(size) {
return crypto.randomBytes(size);
},
};
module.exports = Encryptor;

View File

@@ -0,0 +1,48 @@
module.exports = async function* iterateStream(stream) {
const contents = [];
stream.on('data', data => contents.push(data));
let resolveStreamEndedPromise;
const streamEndedPromise = new Promise(resolve => (resolveStreamEndedPromise = resolve));
let ended = false;
stream.on('end', () => {
ended = true;
resolveStreamEndedPromise();
});
let error = false;
stream.on('error', err => {
error = err;
resolveStreamEndedPromise();
});
while (!ended || contents.length > 0) {
if (contents.length === 0) {
stream.resume();
// eslint-disable-next-line no-await-in-loop
await Promise.race([once(stream, 'data'), streamEndedPromise]);
} else {
stream.pause();
const data = contents.shift();
yield data;
}
if (error) throw error;
}
resolveStreamEndedPromise();
};
function once(eventEmitter, type) {
// TODO: Use require('events').once when node v10 is dropped
return new Promise(resolve => {
let fired = false;
const handler = () => {
if (!fired) {
fired = true;
eventEmitter.removeListener(type, handler);
resolve();
}
};
eventEmitter.addListener(type, handler);
});
}

View File

@@ -0,0 +1,30 @@
const {SaxesParser} = require('saxes');
const {PassThrough} = require('readable-stream');
const {bufferToString} = require('./browser-buffer-decode');
module.exports = async function* (iterable) {
// TODO: Remove once node v8 is deprecated
// Detect and upgrade old streams
if (iterable.pipe && !iterable[Symbol.asyncIterator]) {
iterable = iterable.pipe(new PassThrough());
}
const saxesParser = new SaxesParser();
let error;
saxesParser.on('error', err => {
error = err;
});
let events = [];
saxesParser.on('opentag', value => events.push({eventType: 'opentag', value}));
saxesParser.on('text', value => events.push({eventType: 'text', value}));
saxesParser.on('closetag', value => events.push({eventType: 'closetag', value}));
for await (const chunk of iterable) {
saxesParser.write(bufferToString(chunk));
// saxesParser.write and saxesParser.on() are synchronous,
// so we can only reach the below line once all events have been emitted
if (error) throw error;
// As a performance optimization, we gather all events instead of passing
// them one by one, which would cause each event to go through the event queue
yield events;
events = [];
}
};

View File

@@ -0,0 +1,44 @@
const colCache = require('./col-cache');
// const cellRefRegex = /(([a-z_\-0-9]*)!)?[$]?([a-z]+)[$]?([1-9][0-9]*)/i;
const replacementCandidateRx = /(([a-z_\-0-9]*)!)?([a-z0-9_$]{2,})([(])?/gi;
const CRrx = /^([$])?([a-z]+)([$])?([1-9][0-9]*)$/i;
function slideFormula(formula, fromCell, toCell) {
const offset = colCache.decode(fromCell);
const to = colCache.decode(toCell);
return formula.replace(
replacementCandidateRx,
(refMatch, sheet, sheetMaybe, addrPart, trailingParen) => {
if (trailingParen) {
return refMatch;
}
const match = CRrx.exec(addrPart);
if (match) {
const colDollar = match[1];
const colStr = match[2].toUpperCase();
const rowDollar = match[3];
const rowStr = match[4];
if (colStr.length > 3 || (colStr.length === 3 && colStr > 'XFD')) {
// > XFD is the highest col number in excel 2007 and beyond, so this is a named range
return refMatch;
}
let col = colCache.l2n(colStr);
let row = parseInt(rowStr, 10);
if (!colDollar) {
col += to.col - offset.col;
}
if (!rowDollar) {
row += to.row - offset.row;
}
const res = (sheet || '') + (colDollar || '') + colCache.n2l(col) + (rowDollar || '') + row;
return res;
}
return refMatch;
}
);
}
module.exports = {
slideFormula,
};

View File

@@ -0,0 +1,35 @@
class SharedStrings {
constructor() {
this._values = [];
this._totalRefs = 0;
this._hash = Object.create(null);
}
get count() {
return this._values.length;
}
get values() {
return this._values;
}
get totalRefs() {
return this._totalRefs;
}
getString(index) {
return this._values[index];
}
add(value) {
let index = this._hash[value];
if (index === undefined) {
index = this._hash[value] = this._values.length;
this._values.push(value);
}
this._totalRefs++;
return index;
}
}
module.exports = SharedStrings;

View File

@@ -0,0 +1,72 @@
const Stream = require('readable-stream');
// =============================================================================
// StreamBase64 - A utility to convert to/from base64 stream
// Note: does not buffer data, must be piped
class StreamBase64 extends Stream.Duplex {
constructor() {
super();
// consuming pipe streams go here
this.pipes = [];
}
// writable
// event drain - if write returns false (which it won't), indicates when safe to write again.
// finish - end() has been called
// pipe(src) - pipe() has been called on readable
// unpipe(src) - unpipe() has been called on readable
// error - duh
write(/* data, encoding */) {
return true;
}
cork() {}
uncork() {}
end(/* chunk, encoding, callback */) {}
// readable
// event readable - some data is now available
// event data - switch to flowing mode - feeds chunks to handler
// event end - no more data
// event close - optional, indicates upstream close
// event error - duh
read(/* size */) {}
setEncoding(encoding) {
// causes stream.read or stream.on('data) to return strings of encoding instead of Buffer objects
this.encoding = encoding;
}
pause() {}
resume() {}
isPaused() {}
pipe(destination) {
// add destination to pipe list & write current buffer
this.pipes.push(destination);
}
unpipe(destination) {
// remove destination from pipe list
this.pipes = this.pipes.filter(pipe => pipe !== destination);
}
unshift(/* chunk */) {
// some numpty has read some data that's not for them and they want to put it back!
// Might implement this some day
throw new Error('Not Implemented');
}
wrap(/* stream */) {
// not implemented
throw new Error('Not Implemented');
}
}
module.exports = StreamBase64;

View File

@@ -0,0 +1,364 @@
/* eslint-disable max-classes-per-file */
const Stream = require('readable-stream');
const utils = require('./utils');
const StringBuf = require('./string-buf');
// =============================================================================
// data chunks - encapsulating incoming data
class StringChunk {
constructor(data, encoding) {
this._data = data;
this._encoding = encoding;
}
get length() {
return this.toBuffer().length;
}
// copy to target buffer
copy(target, targetOffset, offset, length) {
return this.toBuffer().copy(target, targetOffset, offset, length);
}
toBuffer() {
if (!this._buffer) {
this._buffer = Buffer.from(this._data, this._encoding);
}
return this._buffer;
}
}
class StringBufChunk {
constructor(data) {
this._data = data;
}
get length() {
return this._data.length;
}
// copy to target buffer
copy(target, targetOffset, offset, length) {
// eslint-disable-next-line no-underscore-dangle
return this._data._buf.copy(target, targetOffset, offset, length);
}
toBuffer() {
return this._data.toBuffer();
}
}
class BufferChunk {
constructor(data) {
this._data = data;
}
get length() {
return this._data.length;
}
// copy to target buffer
copy(target, targetOffset, offset, length) {
this._data.copy(target, targetOffset, offset, length);
}
toBuffer() {
return this._data;
}
}
// =============================================================================
// ReadWriteBuf - a single buffer supporting simple read-write
class ReadWriteBuf {
constructor(size) {
this.size = size;
// the buffer
this.buffer = Buffer.alloc(size);
// read index
this.iRead = 0;
// write index
this.iWrite = 0;
}
toBuffer() {
if (this.iRead === 0 && this.iWrite === this.size) {
return this.buffer;
}
const buf = Buffer.alloc(this.iWrite - this.iRead);
this.buffer.copy(buf, 0, this.iRead, this.iWrite);
return buf;
}
get length() {
return this.iWrite - this.iRead;
}
get eod() {
return this.iRead === this.iWrite;
}
get full() {
return this.iWrite === this.size;
}
read(size) {
let buf;
// read size bytes from buffer and return buffer
if (size === 0) {
// special case - return null if no data requested
return null;
}
if (size === undefined || size >= this.length) {
// if no size specified or size is at least what we have then return all of the bytes
buf = this.toBuffer();
this.iRead = this.iWrite;
return buf;
}
// otherwise return a chunk
buf = Buffer.alloc(size);
this.buffer.copy(buf, 0, this.iRead, size);
this.iRead += size;
return buf;
}
write(chunk, offset, length) {
// write as many bytes from data from optional source offset
// and return number of bytes written
const size = Math.min(length, this.size - this.iWrite);
chunk.copy(this.buffer, this.iWrite, offset, offset + size);
this.iWrite += size;
return size;
}
}
// =============================================================================
// StreamBuf - a multi-purpose read-write stream
// As MemBuf - write as much data as you like. Then call toBuffer() to consolidate
// As StreamHub - pipe to multiple writables
// As readable stream - feed data into the writable part and have some other code read from it.
// Note: Not sure why but StreamBuf does not like JS "class" sugar. It fails the
// integration tests
const StreamBuf = function(options) {
options = options || {};
this.bufSize = options.bufSize || 1024 * 1024;
this.buffers = [];
// batch mode fills a buffer completely before passing the data on
// to pipes or 'readable' event listeners
this.batch = options.batch || false;
this.corked = false;
// where in the current writable buffer we're up to
this.inPos = 0;
// where in the current readable buffer we've read up to
this.outPos = 0;
// consuming pipe streams go here
this.pipes = [];
// controls emit('data')
this.paused = false;
this.encoding = null;
};
utils.inherits(StreamBuf, Stream.Duplex, {
toBuffer() {
switch (this.buffers.length) {
case 0:
return null;
case 1:
return this.buffers[0].toBuffer();
default:
return Buffer.concat(this.buffers.map(rwBuf => rwBuf.toBuffer()));
}
},
// writable
// event drain - if write returns false (which it won't), indicates when safe to write again.
// finish - end() has been called
// pipe(src) - pipe() has been called on readable
// unpipe(src) - unpipe() has been called on readable
// error - duh
_getWritableBuffer() {
if (this.buffers.length) {
const last = this.buffers[this.buffers.length - 1];
if (!last.full) {
return last;
}
}
const buf = new ReadWriteBuf(this.bufSize);
this.buffers.push(buf);
return buf;
},
async _pipe(chunk) {
const write = function(pipe) {
return new Promise(resolve => {
pipe.write(chunk.toBuffer(), () => {
resolve();
});
});
};
await Promise.all(this.pipes.map(write));
},
_writeToBuffers(chunk) {
let inPos = 0;
const inLen = chunk.length;
while (inPos < inLen) {
// find writable buffer
const buffer = this._getWritableBuffer();
// write some data
inPos += buffer.write(chunk, inPos, inLen - inPos);
}
},
async write(data, encoding, callback) {
if (encoding instanceof Function) {
callback = encoding;
encoding = 'utf8';
}
callback = callback || utils.nop;
// encapsulate data into a chunk
let chunk;
if (data instanceof StringBuf) {
chunk = new StringBufChunk(data);
} else if (data instanceof Buffer) {
chunk = new BufferChunk(data);
} else if (typeof data === 'string' || data instanceof String || data instanceof ArrayBuffer) {
chunk = new StringChunk(data, encoding);
} else {
throw new Error('Chunk must be one of type String, Buffer or StringBuf.');
}
// now, do something with the chunk
if (this.pipes.length) {
if (this.batch) {
this._writeToBuffers(chunk);
while (!this.corked && this.buffers.length > 1) {
this._pipe(this.buffers.shift());
}
} else if (!this.corked) {
await this._pipe(chunk);
callback();
} else {
this._writeToBuffers(chunk);
process.nextTick(callback);
}
} else {
if (!this.paused) {
this.emit('data', chunk.toBuffer());
}
this._writeToBuffers(chunk);
this.emit('readable');
}
return true;
},
cork() {
this.corked = true;
},
_flush(/* destination */) {
// if we have comsumers...
if (this.pipes.length) {
// and there's stuff not written
while (this.buffers.length) {
this._pipe(this.buffers.shift());
}
}
},
uncork() {
this.corked = false;
this._flush();
},
end(chunk, encoding, callback) {
const writeComplete = error => {
if (error) {
callback(error);
} else {
this._flush();
this.pipes.forEach(pipe => {
pipe.end();
});
this.emit('finish');
}
};
if (chunk) {
this.write(chunk, encoding, writeComplete);
} else {
writeComplete();
}
},
// readable
// event readable - some data is now available
// event data - switch to flowing mode - feeds chunks to handler
// event end - no more data
// event close - optional, indicates upstream close
// event error - duh
read(size) {
let buffers;
// read min(buffer, size || infinity)
if (size) {
buffers = [];
while (size && this.buffers.length && !this.buffers[0].eod) {
const first = this.buffers[0];
const buffer = first.read(size);
size -= buffer.length;
buffers.push(buffer);
if (first.eod && first.full) {
this.buffers.shift();
}
}
return Buffer.concat(buffers);
}
buffers = this.buffers.map(buf => buf.toBuffer()).filter(Boolean);
this.buffers = [];
return Buffer.concat(buffers);
},
setEncoding(encoding) {
// causes stream.read or stream.on('data) to return strings of encoding instead of Buffer objects
this.encoding = encoding;
},
pause() {
this.paused = true;
},
resume() {
this.paused = false;
},
isPaused() {
return !!this.paused;
},
pipe(destination) {
// add destination to pipe list & write current buffer
this.pipes.push(destination);
if (!this.paused && this.buffers.length) {
this.end();
}
},
unpipe(destination) {
// remove destination from pipe list
this.pipes = this.pipes.filter(pipe => pipe !== destination);
},
unshift(/* chunk */) {
// some numpty has read some data that's not for them and they want to put it back!
// Might implement this some day
throw new Error('Not Implemented');
},
wrap(/* stream */) {
// not implemented
throw new Error('Not Implemented');
},
});
module.exports = StreamBuf;

View File

@@ -0,0 +1,82 @@
// StringBuf - a way to keep string memory operations to a minimum
// while building the strings for the xml files
class StringBuf {
constructor(options) {
this._buf = Buffer.alloc((options && options.size) || 16384);
this._encoding = (options && options.encoding) || 'utf8';
// where in the buffer we are at
this._inPos = 0;
// for use by toBuffer()
this._buffer = undefined;
}
get length() {
return this._inPos;
}
get capacity() {
return this._buf.length;
}
get buffer() {
return this._buf;
}
toBuffer() {
// return the current data as a single enclosing buffer
if (!this._buffer) {
this._buffer = Buffer.alloc(this.length);
this._buf.copy(this._buffer, 0, 0, this.length);
}
return this._buffer;
}
reset(position) {
position = position || 0;
this._buffer = undefined;
this._inPos = position;
}
_grow(min) {
let size = this._buf.length * 2;
while (size < min) {
size *= 2;
}
const buf = Buffer.alloc(size);
this._buf.copy(buf, 0);
this._buf = buf;
}
addText(text) {
this._buffer = undefined;
let inPos = this._inPos + this._buf.write(text, this._inPos, this._encoding);
// if we've hit (or nearing capacity), grow the buf
while (inPos >= this._buf.length - 4) {
this._grow(this._inPos + text.length);
// keep trying to write until we've completely written the text
inPos = this._inPos + this._buf.write(text, this._inPos, this._encoding);
}
this._inPos = inPos;
}
addStringBuf(inBuf) {
if (inBuf.length) {
this._buffer = undefined;
if (this.length + inBuf.length > this.capacity) {
this._grow(this.length + inBuf.length);
}
// eslint-disable-next-line no-underscore-dangle
inBuf._buf.copy(this._buf, this._inPos, 0, inBuf.length);
this._inPos += inBuf.length;
}
}
}
module.exports = StringBuf;

View File

@@ -0,0 +1,35 @@
// StringBuilder - a way to keep string memory operations to a minimum
// while building the strings for the xml files
class StringBuilder {
constructor() {
this.reset();
}
get length() {
return this._buf.length;
}
toString() {
return this._buf.join('');
}
reset(position) {
if (position) {
while (this._buf.length > position) {
this._buf.pop();
}
} else {
this._buf = [];
}
}
addText(text) {
this._buf.push(text);
}
addStringBuf(inBuf) {
this._buf.push(inBuf.toString());
}
}
module.exports = StringBuilder;

View File

@@ -0,0 +1,67 @@
const events = require('events');
// =============================================================================
// StutteredPipe - Used to slow down streaming so GC can get a look in
class StutteredPipe extends events.EventEmitter {
constructor(readable, writable, options) {
super();
options = options || {};
this.readable = readable;
this.writable = writable;
this.bufSize = options.bufSize || 16384;
this.autoPause = options.autoPause || false;
this.paused = false;
this.eod = false;
this.scheduled = null;
readable.on('end', () => {
this.eod = true;
writable.end();
});
// need to have some way to communicate speed of stream
// back from the consumer
readable.on('readable', () => {
if (!this.paused) {
this.resume();
}
});
this._schedule();
}
pause() {
this.paused = true;
}
resume() {
if (!this.eod) {
if (this.scheduled !== null) {
clearImmediate(this.scheduled);
}
this._schedule();
}
}
_schedule() {
this.scheduled = setImmediate(() => {
this.scheduled = null;
if (!this.eod && !this.paused) {
const data = this.readable.read(this.bufSize);
if (data && data.length) {
this.writable.write(data);
if (!this.paused && !this.autoPause) {
this._schedule();
}
} else if (!this.paused) {
this._schedule();
}
}
});
}
}
module.exports = StutteredPipe;

View File

@@ -0,0 +1,24 @@
class TypedStack {
constructor(type) {
this._type = type;
this._stack = [];
}
get size() {
return this._stack.length;
}
pop() {
const tos = this._stack.pop();
return tos || new this._type();
}
push(instance) {
if (!(instance instanceof this._type)) {
throw new Error('Invalid type pushed to TypedStack');
}
this._stack.push(instance);
}
}
module.exports = TypedStack;

View File

@@ -0,0 +1,184 @@
const {toString} = Object.prototype;
const escapeHtmlRegex = /["&<>]/;
const _ = {
each: function each(obj, cb) {
if (obj) {
if (Array.isArray(obj)) {
obj.forEach(cb);
} else {
Object.keys(obj).forEach(key => {
cb(obj[key], key);
});
}
}
},
some: function some(obj, cb) {
if (obj) {
if (Array.isArray(obj)) {
return obj.some(cb);
}
return Object.keys(obj).some(key => cb(obj[key], key));
}
return false;
},
every: function every(obj, cb) {
if (obj) {
if (Array.isArray(obj)) {
return obj.every(cb);
}
return Object.keys(obj).every(key => cb(obj[key], key));
}
return true;
},
map: function map(obj, cb) {
if (obj) {
if (Array.isArray(obj)) {
return obj.map(cb);
}
return Object.keys(obj).map(key => cb(obj[key], key));
}
return [];
},
keyBy(a, p) {
return a.reduce((o, v) => {
o[v[p]] = v;
return o;
}, {});
},
isEqual: function isEqual(a, b) {
const aType = typeof a;
const bType = typeof b;
const aArray = Array.isArray(a);
const bArray = Array.isArray(b);
let keys;
if (aType !== bType) {
return false;
}
switch (typeof a) {
case 'object':
if (aArray || bArray) {
if (aArray && bArray) {
return (
a.length === b.length &&
a.every((aValue, index) => {
const bValue = b[index];
return _.isEqual(aValue, bValue);
})
);
}
return false;
}
if (a === null || b === null) {
return a === b;
}
// Compare object keys and values
keys = Object.keys(a);
if (Object.keys(b).length !== keys.length) {
return false;
}
for (const key of keys) {
if (!b.hasOwnProperty(key)) {
return false;
}
}
return _.every(a, (aValue, key) => {
const bValue = b[key];
return _.isEqual(aValue, bValue);
});
default:
return a === b;
}
},
escapeHtml(html) {
const regexResult = escapeHtmlRegex.exec(html);
if (!regexResult) return html;
let result = '';
let escape = '';
let lastIndex = 0;
let i = regexResult.index;
for (; i < html.length; i++) {
switch (html.charAt(i)) {
case '"':
escape = '&quot;';
break;
case '&':
escape = '&amp;';
break;
case '\'':
escape = '&apos;';
break;
case '<':
escape = '&lt;';
break;
case '>':
escape = '&gt;';
break;
default:
continue;
}
if (lastIndex !== i) result += html.substring(lastIndex, i);
lastIndex = i + 1;
result += escape;
}
if (lastIndex !== i) return result + html.substring(lastIndex, i);
return result;
},
strcmp(a, b) {
if (a < b) return -1;
if (a > b) return 1;
return 0;
},
isUndefined(val) {
return toString.call(val) === '[object Undefined]';
},
isObject(val) {
return toString.call(val) === '[object Object]';
},
deepMerge() {
const target = arguments[0] || {};
const {length} = arguments;
// eslint-disable-next-line one-var
let src, clone, copyIsArray;
function assignValue(val, key) {
src = target[key];
copyIsArray = Array.isArray(val);
if (_.isObject(val) || copyIsArray) {
if (copyIsArray) {
copyIsArray = false;
clone = src && Array.isArray(src) ? src : [];
} else {
clone = src && _.isObject(src) ? src : {};
}
target[key] = _.deepMerge(clone, val);
} else if (!_.isUndefined(val)) {
target[key] = val;
}
}
for (let i = 0; i < length; i++) {
_.each(arguments[i], assignValue);
}
return target;
},
};
module.exports = _;

View File

@@ -0,0 +1,172 @@
const fs = require('fs');
// useful stuff
const inherits = function(cls, superCtor, statics, prototype) {
// eslint-disable-next-line no-underscore-dangle
cls.super_ = superCtor;
if (!prototype) {
prototype = statics;
statics = null;
}
if (statics) {
Object.keys(statics).forEach(i => {
Object.defineProperty(cls, i, Object.getOwnPropertyDescriptor(statics, i));
});
}
const properties = {
constructor: {
value: cls,
enumerable: false,
writable: false,
configurable: true,
},
};
if (prototype) {
Object.keys(prototype).forEach(i => {
properties[i] = Object.getOwnPropertyDescriptor(prototype, i);
});
}
cls.prototype = Object.create(superCtor.prototype, properties);
};
// eslint-disable-next-line no-control-regex
const xmlDecodeRegex = /[<>&'"\x7F\x00-\x08\x0B-\x0C\x0E-\x1F]/;
const utils = {
nop() {},
promiseImmediate(value) {
return new Promise(resolve => {
if (global.setImmediate) {
setImmediate(() => {
resolve(value);
});
} else {
// poorman's setImmediate - must wait at least 1ms
setTimeout(() => {
resolve(value);
}, 1);
}
});
},
inherits,
dateToExcel(d, date1904) {
return 25569 + ( d.getTime() / (24 * 3600 * 1000) ) - (date1904 ? 1462 : 0);
},
excelToDate(v, date1904) {
const millisecondSinceEpoch = Math.round((v - 25569 + (date1904 ? 1462 : 0)) * 24 * 3600 * 1000);
return new Date(millisecondSinceEpoch);
},
parsePath(filepath) {
const last = filepath.lastIndexOf('/');
return {
path: filepath.substring(0, last),
name: filepath.substring(last + 1),
};
},
getRelsPath(filepath) {
const path = utils.parsePath(filepath);
return `${path.path}/_rels/${path.name}.rels`;
},
xmlEncode(text) {
const regexResult = xmlDecodeRegex.exec(text);
if (!regexResult) return text;
let result = '';
let escape = '';
let lastIndex = 0;
let i = regexResult.index;
for (; i < text.length; i++) {
const charCode = text.charCodeAt(i);
switch (charCode) {
case 34: // "
escape = '&quot;';
break;
case 38: // &
escape = '&amp;';
break;
case 39: // '
escape = '&apos;';
break;
case 60: // <
escape = '&lt;';
break;
case 62: // >
escape = '&gt;';
break;
case 127:
escape = '';
break;
default: {
if (charCode <= 31 && (charCode <= 8 || (charCode >= 11 && charCode !== 13))) {
escape = '';
break;
}
continue;
}
}
if (lastIndex !== i) result += text.substring(lastIndex, i);
lastIndex = i + 1;
if (escape) result += escape;
}
if (lastIndex !== i) return result + text.substring(lastIndex, i);
return result;
},
xmlDecode(text) {
return text.replace(/&([a-z]*);/g, c => {
switch (c) {
case '&lt;':
return '<';
case '&gt;':
return '>';
case '&amp;':
return '&';
case '&apos;':
return '\'';
case '&quot;':
return '"';
default:
return c;
}
});
},
validInt(value) {
const i = parseInt(value, 10);
return !Number.isNaN(i) ? i : 0;
},
isDateFmt(fmt) {
if (!fmt) {
return false;
}
// must remove all chars inside quotes and []
fmt = fmt.replace(/\[[^\]]*]/g, '');
fmt = fmt.replace(/"[^"]*"/g, '');
// then check for date formatting chars
const result = fmt.match(/[ymdhMsb]+/) !== null;
return result;
},
fs: {
exists(path) {
return new Promise(resolve => {
fs.access(path, fs.constants.F_OK, err => {
resolve(!err);
});
});
},
},
toIsoDateString(dt) {
return dt.toIsoString().subsstr(0, 10);
},
parseBoolean(value) {
return value === true || value === 'true' || value === 1 || value === '1';
},
};
module.exports = utils;

View File

@@ -0,0 +1,169 @@
const _ = require('./under-dash');
const utils = require('./utils');
// constants
const OPEN_ANGLE = '<';
const CLOSE_ANGLE = '>';
const OPEN_ANGLE_SLASH = '</';
const CLOSE_SLASH_ANGLE = '/>';
function pushAttribute(xml, name, value) {
xml.push(` ${name}="${utils.xmlEncode(value.toString())}"`);
}
function pushAttributes(xml, attributes) {
if (attributes) {
const tmp = [];
_.each(attributes, (value, name) => {
if (value !== undefined) {
pushAttribute(tmp, name, value);
}
});
xml.push(tmp.join(""));
}
}
class XmlStream {
constructor() {
this._xml = [];
this._stack = [];
this._rollbacks = [];
}
get tos() {
return this._stack.length ? this._stack[this._stack.length - 1] : undefined;
}
get cursor() {
// handy way to track whether anything has been added
return this._xml.length;
}
openXml(docAttributes) {
const xml = this._xml;
// <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
xml.push('<?xml');
pushAttributes(xml, docAttributes);
xml.push('?>\n');
}
openNode(name, attributes) {
const parent = this.tos;
const xml = this._xml;
if (parent && this.open) {
xml.push(CLOSE_ANGLE);
}
this._stack.push(name);
// start streaming node
xml.push(OPEN_ANGLE);
xml.push(name);
pushAttributes(xml, attributes);
this.leaf = true;
this.open = true;
}
addAttribute(name, value) {
if (!this.open) {
throw new Error('Cannot write attributes to node if it is not open');
}
if (value !== undefined) {
pushAttribute(this._xml, name, value);
}
}
addAttributes(attrs) {
if (!this.open) {
throw new Error('Cannot write attributes to node if it is not open');
}
pushAttributes(this._xml, attrs);
}
writeText(text) {
const xml = this._xml;
if (this.open) {
xml.push(CLOSE_ANGLE);
this.open = false;
}
this.leaf = false;
xml.push(utils.xmlEncode(text.toString()));
}
writeXml(xml) {
if (this.open) {
this._xml.push(CLOSE_ANGLE);
this.open = false;
}
this.leaf = false;
this._xml.push(xml);
}
closeNode() {
const node = this._stack.pop();
const xml = this._xml;
if (this.leaf) {
xml.push(CLOSE_SLASH_ANGLE);
} else {
xml.push(OPEN_ANGLE_SLASH);
xml.push(node);
xml.push(CLOSE_ANGLE);
}
this.open = false;
this.leaf = false;
}
leafNode(name, attributes, text) {
this.openNode(name, attributes);
if (text !== undefined) {
// zeros need to be written
this.writeText(text);
}
this.closeNode();
}
closeAll() {
while (this._stack.length) {
this.closeNode();
}
}
addRollback() {
this._rollbacks.push({
xml: this._xml.length,
stack: this._stack.length,
leaf: this.leaf,
open: this.open,
});
return this.cursor;
}
commit() {
this._rollbacks.pop();
}
rollback() {
const r = this._rollbacks.pop();
if (this._xml.length > r.xml) {
this._xml.splice(r.xml, this._xml.length - r.xml);
}
if (this._stack.length > r.stack) {
this._stack.splice(r.stack, this._stack.length - r.stack);
}
this.leaf = r.leaf;
this.open = r.open;
}
get xml() {
this.closeAll();
return this._xml.join('');
}
}
XmlStream.StdDocAttributes = {
version: '1.0',
encoding: 'UTF-8',
standalone: 'yes',
};
module.exports = XmlStream;

View File

@@ -0,0 +1,87 @@
const events = require('events');
const JSZip = require('jszip');
const StreamBuf = require('./stream-buf');
const {stringToBuffer} = require('./browser-buffer-encode');
// =============================================================================
// The ZipWriter class
// Packs streamed data into an output zip stream
class ZipWriter extends events.EventEmitter {
constructor(options) {
super();
this.options = Object.assign(
{
type: 'nodebuffer',
compression: 'DEFLATE',
},
options
);
this.zip = new JSZip();
this.stream = new StreamBuf();
}
append(data, options) {
if (options.hasOwnProperty('base64') && options.base64) {
this.zip.file(options.name, data, {base64: true});
} else {
// https://www.npmjs.com/package/process
if (process.browser && typeof data === 'string') {
// use TextEncoder in browser
data = stringToBuffer(data);
}
this.zip.file(options.name, data);
}
}
async finalize() {
const content = await this.zip.generateAsync(this.options);
this.stream.end(content);
this.emit('finish');
}
// ==========================================================================
// Stream.Readable interface
read(size) {
return this.stream.read(size);
}
setEncoding(encoding) {
return this.stream.setEncoding(encoding);
}
pause() {
return this.stream.pause();
}
resume() {
return this.stream.resume();
}
isPaused() {
return this.stream.isPaused();
}
pipe(destination, options) {
return this.stream.pipe(destination, options);
}
unpipe(destination) {
return this.stream.unpipe(destination);
}
unshift(chunk) {
return this.stream.unshift(chunk);
}
wrap(stream) {
return this.stream.wrap(stream);
}
}
// =============================================================================
module.exports = {
ZipWriter,
};

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<%relationships.forEach(function(r) {%>
<Relationship
Id="<%=r.rId%>"
Type="<%=r.type%>"
Target="<%=r.target%>" <% if (r.targetMode) {%>
TargetMode="<%=r.targetMode%>"<%}%>
/>
<%});%>
</Relationships>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<calcChain xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<% formulae.forEach(function(formula) { %>
<c r="<%=formula.address%>" i="<%=formula.i%>" <% if(formula.l) { %>l="<%=formula.l%>"<% } %>/>
<% }); %>
</calcChain>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dc:creator><%=creator%></dc:creator>
<cp:lastModifiedBy><%=lastModifiedBy%></cp:lastModifiedBy>
<dcterms:created xsi:type="dcterms:W3CDTF"><%=created.toISOString().replace(/\.\d{3}/,"")%></dcterms:created>
<dcterms:modified xsi:type="dcterms:W3CDTF"><%=modified.toISOString().replace(/\.\d{3}/,"")%></dcterms:modified>
</cp:coreProperties>

View File

@@ -0,0 +1,153 @@
module.exports = {
0: {f: 'General'},
1: {f: '0'},
2: {f: '0.00'},
3: {f: '#,##0'},
4: {f: '#,##0.00'},
9: {f: '0%'},
10: {f: '0.00%'},
11: {f: '0.00E+00'},
12: {f: '# ?/?'},
13: {f: '# ??/??'},
14: {f: 'mm-dd-yy'},
15: {f: 'd-mmm-yy'},
16: {f: 'd-mmm'},
17: {f: 'mmm-yy'},
18: {f: 'h:mm AM/PM'},
19: {f: 'h:mm:ss AM/PM'},
20: {f: 'h:mm'},
21: {f: 'h:mm:ss'},
22: {f: 'm/d/yy "h":mm'},
27: {
'zh-tw': '[$-404]e/m/d',
'zh-cn': 'yyyy"年"m"月"',
'ja-jp': '[$-411]ge.m.d',
'ko-kr': 'yyyy"年" mm"月" dd"日"',
},
28: {
'zh-tw': '[$-404]e"年"m"月"d"日"',
'zh-cn': 'm"月"d"日"',
'ja-jp': '[$-411]ggge"年"m"月"d"日"',
'ko-kr': 'mm-dd',
},
29: {
'zh-tw': '[$-404]e"年"m"月"d"日"',
'zh-cn': 'm"月"d"日"',
'ja-jp': '[$-411]ggge"年"m"月"d"日"',
'ko-kr': 'mm-dd',
},
30: {'zh-tw': 'm/d/yy ', 'zh-cn': 'm-d-yy', 'ja-jp': 'm/d/yy', 'ko-kr': 'mm-dd-yy'},
31: {
'zh-tw': 'yyyy"年"m"月"d"日"',
'zh-cn': 'yyyy"年"m"月"d"日"',
'ja-jp': 'yyyy"年"m"月"d"日"',
'ko-kr': 'yyyy"년" mm"월" dd"일"',
},
32: {
'zh-tw': 'hh"時"mm"分"',
'zh-cn': 'h"时"mm"分"',
'ja-jp': 'h"時"mm"分"',
'ko-kr': 'h"시" mm"분"',
},
33: {
'zh-tw': 'hh"時"mm"分"ss"秒"',
'zh-cn': 'h"时"mm"分"ss"秒"',
'ja-jp': 'h"時"mm"分"ss"秒"',
'ko-kr': 'h"시" mm"분" ss"초"',
},
34: {
'zh-tw': '上午/下午 hh"時"mm"分"',
'zh-cn': '上午/下午 h"时"mm"分"',
'ja-jp': 'yyyy"年"m"月"',
'ko-kr': 'yyyy-mm-dd',
},
35: {
'zh-tw': '上午/下午 hh"時"mm"分"ss"秒"',
'zh-cn': '上午/下午 h"时"mm"分"ss"秒"',
'ja-jp': 'm"月"d"日"',
'ko-kr': 'yyyy-mm-dd',
},
36: {
'zh-tw': '[$-404]e/m/d',
'zh-cn': 'yyyy"年"m"月"',
'ja-jp': '[$-411]ge.m.d',
'ko-kr': 'yyyy"年" mm"月" dd"日"',
},
37: {f: '#,##0 ;(#,##0)'},
38: {f: '#,##0 ;[Red](#,##0)'},
39: {f: '#,##0.00 ;(#,##0.00)'},
40: {f: '#,##0.00 ;[Red](#,##0.00)'},
45: {f: 'mm:ss'},
46: {f: '[h]:mm:ss'},
47: {f: 'mmss.0'},
48: {f: '##0.0E+0'},
49: {f: '@'},
50: {
'zh-tw': '[$-404]e/m/d',
'zh-cn': 'yyyy"年"m"月"',
'ja-jp': '[$-411]ge.m.d',
'ko-kr': 'yyyy"年" mm"月" dd"日"',
},
51: {
'zh-tw': '[$-404]e"年"m"月"d"日"',
'zh-cn': 'm"月"d"日"',
'ja-jp': '[$-411]ggge"年"m"月"d"日"',
'ko-kr': 'mm-dd',
},
52: {
'zh-tw': '上午/下午 hh"時"mm"分"',
'zh-cn': 'yyyy"年"m"月"',
'ja-jp': 'yyyy"年"m"月"',
'ko-kr': 'yyyy-mm-dd',
},
53: {
'zh-tw': '上午/下午 hh"時"mm"分"ss"秒"',
'zh-cn': 'm"月"d"日"',
'ja-jp': 'm"月"d"日"',
'ko-kr': 'yyyy-mm-dd',
},
54: {
'zh-tw': '[$-404]e"年"m"月"d"日"',
'zh-cn': 'm"月"d"日"',
'ja-jp': '[$-411]ggge"年"m"月"d"日"',
'ko-kr': 'mm-dd',
},
55: {
'zh-tw': '上午/下午 hh"時"mm"分"',
'zh-cn': '上午/下午 h"时"mm"分"',
'ja-jp': 'yyyy"年"m"月"',
'ko-kr': 'yyyy-mm-dd',
},
56: {
'zh-tw': '上午/下午 hh"時"mm"分"ss"秒"',
'zh-cn': '上午/下午 h"时"mm"分"ss"秒"',
'ja-jp': 'm"月"d"日"',
'ko-kr': 'yyyy-mm-dd',
},
57: {
'zh-tw': '[$-404]e/m/d',
'zh-cn': 'yyyy"年"m"月"',
'ja-jp': '[$-411]ge.m.d',
'ko-kr': 'yyyy"年" mm"月" dd"日"',
},
58: {
'zh-tw': '[$-404]e"年"m"月"d"日"',
'zh-cn': 'm"月"d"日"',
'ja-jp': '[$-411]ggge"年"m"月"d"日"',
'ko-kr': 'mm-dd',
},
59: {'th-th': 't0'},
60: {'th-th': 't0.00'},
61: {'th-th': 't#,##0'},
62: {'th-th': 't#,##0.00'},
67: {'th-th': 't0%'},
68: {'th-th': 't0.00%'},
69: {'th-th': 't# ?/?'},
70: {'th-th': 't# ??/??'},
81: {'th-th': 'd/m/bb'},
};

View File

@@ -0,0 +1,21 @@
'use strict';
module.exports = {
OfficeDocument:
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument',
Worksheet: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet',
CalcChain: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/calcChain',
SharedStrings:
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings',
Styles: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles',
Theme: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme',
Hyperlink: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink',
Image: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image',
CoreProperties:
'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties',
ExtenderProperties:
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties',
Comments: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments',
VmlDrawing: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing',
Table: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table',
};

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<styleSheet
xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="x14ac"
xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac">
<% if (numFmts.length) { %><numFmts count="<%=numFmts.length%>">
<%numFmts.forEach(function(nf) {%><%=nf.xml%>
<%});%>
</numFmts>
<% } %>
<fonts count="<%=fonts.length%>" x14ac:knownFonts="1">
<%fonts.forEach(function(font) {%><%=font.xml%>
<%})%>
</fonts>
<fills count="<%=fills.length%>">
<%fills.forEach(function(fill) {%><%=fill.xml%>
<%});%>
</fills>
<borders count="<%=borders.length%>">
<%borders.forEach(function(border) {%><%=border.xml%>
<%});%>
</borders>
<cellStyleXfs count="1">
<xf numFmtId="0" fontId="0" fillId="0" borderId="0"/>
</cellStyleXfs>
<cellXfs count="<%=styles.length%>">
<%styles.forEach(function(style) {%><%=style.xml%>
<%});%>
</cellXfs>
<cellStyles count="1">
<cellStyle name="Normal" xfId="0" builtinId="0"/>
</cellStyles>
<dxfs count="0"/>
<tableStyles count="0" defaultTableStyle="TableStyleMedium2" defaultPivotStyle="PivotStyleLight16"/>
<extLst>
<ext uri="{EB79DEF2-80B8-43e5-95BD-54CBDDF9020C}" xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main">
<x14:slicerStyles defaultSlicerStyle="SlicerStyleLight1"/>
</ext>
</extLst>
</styleSheet>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook
xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<fileVersion appName="xl" lastEdited="5" lowestEdited="5" rupBuild="9303"/>
<workbookPr defaultThemeVersion="124226"/>
<bookViews>
<workbookView xWindow="480" yWindow="60" windowWidth="18195" windowHeight="8505"/>
</bookViews>
<sheets>
<% worksheets.forEach(function(worksheet) { %><sheet name="<%=worksheet.name%>" state="<%=worksheet.state%>" sheetId="<%=worksheet.id%>" r:id="<%=worksheet.rId%>"/>
<% }); %>
</sheets>
<%=definedNames.xml%>
<calcPr calcId="145621"/>
</workbook>

View File

@@ -0,0 +1,145 @@
const parseSax = require('../../utils/parse-sax');
const XmlStream = require('../../utils/xml-stream');
/* 'virtual' methods used as a form of documentation */
/* eslint-disable class-methods-use-this */
// Base class for Xforms
class BaseXform {
// constructor(/* model, name */) {}
// ============================================================
// Virtual Interface
prepare(/* model, options */) {
// optional preparation (mutation) of model so it is ready for write
}
render(/* xmlStream, model */) {
// convert model to xml
}
parseOpen(node) {
// XML node opened
}
parseText(text) {
// chunk of text encountered for current node
}
parseClose(name) {
// XML node closed
}
reconcile(model, options) {
// optional post-parse step (opposite to prepare)
}
// ============================================================
reset() {
// to make sure parses don't bleed to next iteration
this.model = null;
// if we have a map - reset them too
if (this.map) {
Object.values(this.map).forEach(xform => {
if (xform instanceof BaseXform) {
xform.reset();
} else if (xform.xform) {
xform.xform.reset();
}
});
}
}
mergeModel(obj) {
// set obj's props to this.model
this.model = Object.assign(this.model || {}, obj);
}
async parse(saxParser) {
for await (const events of saxParser) {
for (const {eventType, value} of events) {
if (eventType === 'opentag') {
this.parseOpen(value);
} else if (eventType === 'text') {
this.parseText(value);
} else if (eventType === 'closetag') {
if (!this.parseClose(value.name)) {
return this.model;
}
}
}
}
return this.model;
}
async parseStream(stream) {
return this.parse(parseSax(stream));
}
get xml() {
// convenience function to get the xml of this.model
// useful for manager types that are built during the prepare phase
return this.toXml(this.model);
}
toXml(model) {
const xmlStream = new XmlStream();
this.render(xmlStream, model);
return xmlStream.xml;
}
// ============================================================
// Useful Utilities
static toAttribute(value, dflt, always = false) {
if (value === undefined) {
if (always) {
return dflt;
}
} else if (always || value !== dflt) {
return value.toString();
}
return undefined;
}
static toStringAttribute(value, dflt, always = false) {
return BaseXform.toAttribute(value, dflt, always);
}
static toStringValue(attr, dflt) {
return attr === undefined ? dflt : attr;
}
static toBoolAttribute(value, dflt, always = false) {
if (value === undefined) {
if (always) {
return dflt;
}
} else if (always || value !== dflt) {
return value ? '1' : '0';
}
return undefined;
}
static toBoolValue(attr, dflt) {
return attr === undefined ? dflt : attr === '1';
}
static toIntAttribute(value, dflt, always = false) {
return BaseXform.toAttribute(value, dflt, always);
}
static toIntValue(attr, dflt) {
return attr === undefined ? dflt : parseInt(attr, 10);
}
static toFloatAttribute(value, dflt, always = false) {
return BaseXform.toAttribute(value, dflt, always);
}
static toFloatValue(attr, dflt) {
return attr === undefined ? dflt : parseFloat(attr);
}
}
module.exports = BaseXform;

View File

@@ -0,0 +1,91 @@
const BaseXform = require('../base-xform');
const colCache = require('../../../utils/col-cache');
class DefinedNamesXform extends BaseXform {
render(xmlStream, model) {
// <definedNames>
// <definedName name="name">name.ranges.join(',')</definedName>
// <definedName name="_xlnm.Print_Area" localSheetId="0">name.ranges.join(',')</definedName>
// </definedNames>
xmlStream.openNode('definedName', {
name: model.name,
localSheetId: model.localSheetId,
});
xmlStream.writeText(model.ranges.join(','));
xmlStream.closeNode();
}
parseOpen(node) {
switch (node.name) {
case 'definedName':
this._parsedName = node.attributes.name;
this._parsedLocalSheetId = node.attributes.localSheetId;
this._parsedText = [];
return true;
default:
return false;
}
}
parseText(text) {
this._parsedText.push(text);
}
parseClose() {
this.model = {
name: this._parsedName,
ranges: extractRanges(this._parsedText.join('')),
};
if (this._parsedLocalSheetId !== undefined) {
this.model.localSheetId = parseInt(this._parsedLocalSheetId, 10);
}
return false;
}
}
function isValidRange(range) {
try {
colCache.decodeEx(range);
return true;
} catch (err) {
return false;
}
}
function extractRanges(parsedText) {
const ranges = [];
let quotesOpened = false;
let last = '';
parsedText.split(',').forEach(item => {
if (!item) {
return;
}
const quotes = (item.match(/'/g) || []).length;
if (!quotes) {
if (quotesOpened) {
last += `${item},`;
} else if (isValidRange(item)) {
ranges.push(item);
}
return;
}
const quotesEven = quotes % 2 === 0;
if (!quotesOpened && quotesEven && isValidRange(item)) {
ranges.push(item);
} else if (quotesOpened && !quotesEven) {
quotesOpened = false;
if (isValidRange(last + item)) {
ranges.push(last + item);
}
last = '';
} else {
quotesOpened = true;
last += `${item},`;
}
});
return ranges;
}
module.exports = DefinedNamesXform;

View File

@@ -0,0 +1,34 @@
const utils = require('../../../utils/utils');
const BaseXform = require('../base-xform');
class WorksheetXform extends BaseXform {
render(xmlStream, model) {
xmlStream.leafNode('sheet', {
sheetId: model.id,
name: model.name,
state: model.state,
'r:id': model.rId,
});
}
parseOpen(node) {
if (node.name === 'sheet') {
this.model = {
name: utils.xmlDecode(node.attributes.name),
id: parseInt(node.attributes.sheetId, 10),
state: node.attributes.state,
rId: node.attributes['r:id'],
};
return true;
}
return false;
}
parseText() {}
parseClose() {
return false;
}
}
module.exports = WorksheetXform;

View File

@@ -0,0 +1,26 @@
const BaseXform = require('../base-xform');
class WorkbookCalcPropertiesXform extends BaseXform {
render(xmlStream, model) {
xmlStream.leafNode('calcPr', {
calcId: 171027,
fullCalcOnLoad: model.fullCalcOnLoad ? 1 : undefined,
});
}
parseOpen(node) {
if (node.name === 'calcPr') {
this.model = {};
return true;
}
return false;
}
parseText() {}
parseClose() {
return false;
}
}
module.exports = WorkbookCalcPropertiesXform;

View File

@@ -0,0 +1,29 @@
const BaseXform = require('../base-xform');
class WorksheetPropertiesXform extends BaseXform {
render(xmlStream, model) {
xmlStream.leafNode('workbookPr', {
date1904: model.date1904 ? 1 : undefined,
defaultThemeVersion: 164011,
filterPrivacy: 1,
});
}
parseOpen(node) {
if (node.name === 'workbookPr') {
this.model = {
date1904: node.attributes.date1904 === '1',
};
return true;
}
return false;
}
parseText() {}
parseClose() {
return false;
}
}
module.exports = WorksheetPropertiesXform;

View File

@@ -0,0 +1,53 @@
const BaseXform = require('../base-xform');
class WorkbookViewXform extends BaseXform {
render(xmlStream, model) {
const attributes = {
xWindow: model.x || 0,
yWindow: model.y || 0,
windowWidth: model.width || 12000,
windowHeight: model.height || 24000,
firstSheet: model.firstSheet,
activeTab: model.activeTab,
};
if (model.visibility && model.visibility !== 'visible') {
attributes.visibility = model.visibility;
}
xmlStream.leafNode('workbookView', attributes);
}
parseOpen(node) {
if (node.name === 'workbookView') {
const model = (this.model = {});
const addS = function(name, value, dflt) {
const s = value !== undefined ? (model[name] = value) : dflt;
if (s !== undefined) {
model[name] = s;
}
};
const addN = function(name, value, dflt) {
const n = value !== undefined ? (model[name] = parseInt(value, 10)) : dflt;
if (n !== undefined) {
model[name] = n;
}
};
addN('x', node.attributes.xWindow, 0);
addN('y', node.attributes.yWindow, 0);
addN('width', node.attributes.windowWidth, 25000);
addN('height', node.attributes.windowHeight, 10000);
addS('visibility', node.attributes.visibility, 'visible');
addN('activeTab', node.attributes.activeTab, undefined);
addN('firstSheet', node.attributes.firstSheet, undefined);
return true;
}
return false;
}
parseText() {}
parseClose() {
return false;
}
}
module.exports = WorkbookViewXform;

View File

@@ -0,0 +1,255 @@
const _ = require('../../../utils/under-dash');
const colCache = require('../../../utils/col-cache');
const XmlStream = require('../../../utils/xml-stream');
const BaseXform = require('../base-xform');
const StaticXform = require('../static-xform');
const ListXform = require('../list-xform');
const DefinedNameXform = require('./defined-name-xform');
const SheetXform = require('./sheet-xform');
const WorkbookViewXform = require('./workbook-view-xform');
const WorkbookPropertiesXform = require('./workbook-properties-xform');
const WorkbookCalcPropertiesXform = require('./workbook-calc-properties-xform');
class WorkbookXform extends BaseXform {
constructor() {
super();
this.map = {
fileVersion: WorkbookXform.STATIC_XFORMS.fileVersion,
workbookPr: new WorkbookPropertiesXform(),
bookViews: new ListXform({
tag: 'bookViews',
count: false,
childXform: new WorkbookViewXform(),
}),
sheets: new ListXform({tag: 'sheets', count: false, childXform: new SheetXform()}),
definedNames: new ListXform({
tag: 'definedNames',
count: false,
childXform: new DefinedNameXform(),
}),
calcPr: new WorkbookCalcPropertiesXform(),
};
}
prepare(model) {
model.sheets = model.worksheets;
// collate all the print areas from all of the sheets and add them to the defined names
const printAreas = [];
let index = 0; // sheets is sparse array - calc index manually
model.sheets.forEach(sheet => {
if (sheet.pageSetup && sheet.pageSetup.printArea) {
sheet.pageSetup.printArea.split('&&').forEach(printArea => {
const printAreaComponents = printArea.split(':');
const definedName = {
name: '_xlnm.Print_Area',
ranges: [`'${sheet.name}'!$${printAreaComponents[0]}:$${printAreaComponents[1]}`],
localSheetId: index,
};
printAreas.push(definedName);
});
}
if (
sheet.pageSetup &&
(sheet.pageSetup.printTitlesRow || sheet.pageSetup.printTitlesColumn)
) {
const ranges = [];
if (sheet.pageSetup.printTitlesColumn) {
const titlesColumns = sheet.pageSetup.printTitlesColumn.split(':');
ranges.push(`'${sheet.name}'!$${titlesColumns[0]}:$${titlesColumns[1]}`);
}
if (sheet.pageSetup.printTitlesRow) {
const titlesRows = sheet.pageSetup.printTitlesRow.split(':');
ranges.push(`'${sheet.name}'!$${titlesRows[0]}:$${titlesRows[1]}`);
}
const definedName = {
name: '_xlnm.Print_Titles',
ranges,
localSheetId: index,
};
printAreas.push(definedName);
}
index++;
});
if (printAreas.length) {
model.definedNames = model.definedNames.concat(printAreas);
}
(model.media || []).forEach((medium, i) => {
// assign name
medium.name = medium.type + (i + 1);
});
}
render(xmlStream, model) {
xmlStream.openXml(XmlStream.StdDocAttributes);
xmlStream.openNode('workbook', WorkbookXform.WORKBOOK_ATTRIBUTES);
this.map.fileVersion.render(xmlStream);
this.map.workbookPr.render(xmlStream, model.properties);
this.map.bookViews.render(xmlStream, model.views);
this.map.sheets.render(xmlStream, model.sheets);
this.map.definedNames.render(xmlStream, model.definedNames);
this.map.calcPr.render(xmlStream, model.calcProperties);
xmlStream.closeNode();
}
parseOpen(node) {
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
switch (node.name) {
case 'workbook':
return true;
default:
this.parser = this.map[node.name];
if (this.parser) {
this.parser.parseOpen(node);
}
return true;
}
}
parseText(text) {
if (this.parser) {
this.parser.parseText(text);
}
}
parseClose(name) {
if (this.parser) {
if (!this.parser.parseClose(name)) {
this.parser = undefined;
}
return true;
}
switch (name) {
case 'workbook':
this.model = {
sheets: this.map.sheets.model,
properties: this.map.workbookPr.model || {},
views: this.map.bookViews.model,
calcProperties: {},
};
if (this.map.definedNames.model) {
this.model.definedNames = this.map.definedNames.model;
}
return false;
default:
// not quite sure how we get here!
return true;
}
}
reconcile(model) {
const rels = (model.workbookRels || []).reduce((map, rel) => {
map[rel.Id] = rel;
return map;
}, {});
// reconcile sheet ids, rIds and names
const worksheets = [];
let worksheet;
let index = 0;
(model.sheets || []).forEach(sheet => {
const rel = rels[sheet.rId];
if (!rel) {
return;
}
// if rel.Target start with `[space]/xl/` or `/xl/` , then it will be replaced with `''` and spliced behind `xl/`,
// otherwise it will be spliced directly behind `xl/`. i.g.
worksheet = model.worksheetHash[`xl/${rel.Target.replace(/^(\s|\/xl\/)+/, '')}`];
// If there are "chartsheets" in the file, rel.Target will
// come out as chartsheets/sheet1.xml or similar here, and
// that won't be in model.worksheetHash.
// As we don't have the infrastructure to support chartsheets,
// we will ignore them for now:
if (worksheet) {
worksheet.name = sheet.name;
worksheet.id = sheet.id;
worksheet.state = sheet.state;
worksheets[index++] = worksheet;
}
});
// reconcile print areas
const definedNames = [];
_.each(model.definedNames, definedName => {
if (definedName.name === '_xlnm.Print_Area') {
worksheet = worksheets[definedName.localSheetId];
if (worksheet) {
if (!worksheet.pageSetup) {
worksheet.pageSetup = {};
}
const range = colCache.decodeEx(definedName.ranges[0]);
worksheet.pageSetup.printArea = worksheet.pageSetup.printArea
? `${worksheet.pageSetup.printArea}&&${range.dimensions}`
: range.dimensions;
}
} else if (definedName.name === '_xlnm.Print_Titles') {
worksheet = worksheets[definedName.localSheetId];
if (worksheet) {
if (!worksheet.pageSetup) {
worksheet.pageSetup = {};
}
const rangeString = definedName.ranges.join(',');
const dollarRegex = /\$/g;
const rowRangeRegex = /\$\d+:\$\d+/;
const rowRangeMatches = rangeString.match(rowRangeRegex);
if (rowRangeMatches && rowRangeMatches.length) {
const range = rowRangeMatches[0];
worksheet.pageSetup.printTitlesRow = range.replace(dollarRegex, '');
}
const columnRangeRegex = /\$[A-Z]+:\$[A-Z]+/;
const columnRangeMatches = rangeString.match(columnRangeRegex);
if (columnRangeMatches && columnRangeMatches.length) {
const range = columnRangeMatches[0];
worksheet.pageSetup.printTitlesColumn = range.replace(dollarRegex, '');
}
}
} else {
definedNames.push(definedName);
}
});
model.definedNames = definedNames;
// used by sheets to build their image models
model.media.forEach((media, i) => {
media.index = i;
});
}
}
WorkbookXform.WORKBOOK_ATTRIBUTES = {
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
'xmlns:mc': 'http://schemas.openxmlformats.org/markup-compatibility/2006',
'mc:Ignorable': 'x15',
'xmlns:x15': 'http://schemas.microsoft.com/office/spreadsheetml/2010/11/main',
};
WorkbookXform.STATIC_XFORMS = {
fileVersion: new StaticXform({
tag: 'fileVersion',
$: {appName: 'xl', lastEdited: 5, lowestEdited: 5, rupBuild: 9303},
}),
};
module.exports = WorkbookXform;

View File

@@ -0,0 +1,105 @@
const RichTextXform = require('../strings/rich-text-xform');
const utils = require('../../../utils/utils');
const BaseXform = require('../base-xform');
/**
<comment ref="B1" authorId="0">
<text>
<r>
<rPr>
<b/>
<sz val="9"/>
<rFont val="宋体"/>
<charset val="134"/>
</rPr>
<t>51422:</t>
</r>
<r>
<rPr>
<sz val="9"/>
<rFont val="宋体"/>
<charset val="134"/>
</rPr>
<t xml:space="preserve">&#10;test</t>
</r>
</text>
</comment>
*/
const CommentXform = (module.exports = function(model) {
this.model = model;
});
utils.inherits(CommentXform, BaseXform, {
get tag() {
return 'r';
},
get richTextXform() {
if (!this._richTextXform) {
this._richTextXform = new RichTextXform();
}
return this._richTextXform;
},
render(xmlStream, model) {
model = model || this.model;
xmlStream.openNode('comment', {
ref: model.ref,
authorId: 0,
});
xmlStream.openNode('text');
if (model && model.note && model.note.texts) {
model.note.texts.forEach(text => {
this.richTextXform.render(xmlStream, text);
});
}
xmlStream.closeNode();
xmlStream.closeNode();
},
parseOpen(node) {
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
switch (node.name) {
case 'comment':
this.model = {
type: 'note',
note: {
texts: [],
},
...node.attributes,
};
return true;
case 'r':
this.parser = this.richTextXform;
this.parser.parseOpen(node);
return true;
default:
return false;
}
},
parseText(text) {
if (this.parser) {
this.parser.parseText(text);
}
},
parseClose(name) {
switch (name) {
case 'comment':
return false;
case 'r':
this.model.note.texts.push(this.parser.model);
this.parser = undefined;
return true;
default:
if (this.parser) {
this.parser.parseClose(name);
}
return true;
}
},
});

View File

@@ -0,0 +1,82 @@
const XmlStream = require('../../../utils/xml-stream');
const utils = require('../../../utils/utils');
const BaseXform = require('../base-xform');
const CommentXform = require('./comment-xform');
const CommentsXform = (module.exports = function() {
this.map = {
comment: new CommentXform(),
};
});
utils.inherits(
CommentsXform,
BaseXform,
{
COMMENTS_ATTRIBUTES: {
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
},
},
{
render(xmlStream, model) {
model = model || this.model;
xmlStream.openXml(XmlStream.StdDocAttributes);
xmlStream.openNode('comments', CommentsXform.COMMENTS_ATTRIBUTES);
// authors
// TODO: support authors properly
xmlStream.openNode('authors');
xmlStream.leafNode('author', null, 'Author');
xmlStream.closeNode();
// comments
xmlStream.openNode('commentList');
model.comments.forEach(comment => {
this.map.comment.render(xmlStream, comment);
});
xmlStream.closeNode();
xmlStream.closeNode();
},
parseOpen(node) {
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
switch (node.name) {
case 'commentList':
this.model = {
comments: [],
};
return true;
case 'comment':
this.parser = this.map.comment;
this.parser.parseOpen(node);
return true;
default:
return false;
}
},
parseText(text) {
if (this.parser) {
this.parser.parseText(text);
}
},
parseClose(name) {
switch (name) {
case 'commentList':
return false;
case 'comment':
this.model.comments.push(this.parser.model);
this.parser = undefined;
return true;
default:
if (this.parser) {
this.parser.parseClose(name);
}
return true;
}
},
}
);

View File

@@ -0,0 +1,39 @@
const BaseXform = require('../../base-xform');
class VmlPositionXform extends BaseXform {
constructor(model) {
super();
this._model = model;
}
get tag() {
return this._model && this._model.tag;
}
render(xmlStream, model, type) {
if (model === type[2]) {
xmlStream.leafNode(this.tag);
} else if (this.tag === 'x:SizeWithCells' && model === type[1]) {
xmlStream.leafNode(this.tag);
}
}
parseOpen(node) {
switch (node.name) {
case this.tag:
this.model = {};
this.model[this.tag] = true;
return true;
default:
return false;
}
}
parseText() {}
parseClose() {
return false;
}
}
module.exports = VmlPositionXform;

View File

@@ -0,0 +1,36 @@
const BaseXform = require('../../base-xform');
class VmlProtectionXform extends BaseXform {
constructor(model) {
super();
this._model = model;
}
get tag() {
return this._model && this._model.tag;
}
render(xmlStream, model) {
xmlStream.leafNode(this.tag, null, model);
}
parseOpen(node) {
switch (node.name) {
case this.tag:
this.text = '';
return true;
default:
return false;
}
}
parseText(text) {
this.text = text;
}
parseClose() {
return false;
}
}
module.exports = VmlProtectionXform;

View File

@@ -0,0 +1,60 @@
const BaseXform = require('../base-xform');
// render the triangle in the cell for the comment
class VmlAnchorXform extends BaseXform {
get tag() {
return 'x:Anchor';
}
getAnchorRect(anchor) {
const l = Math.floor(anchor.left);
const lf = Math.floor((anchor.left - l) * 68);
const t = Math.floor(anchor.top);
const tf = Math.floor((anchor.top - t) * 18);
const r = Math.floor(anchor.right);
const rf = Math.floor((anchor.right - r) * 68);
const b = Math.floor(anchor.bottom);
const bf = Math.floor((anchor.bottom - b) * 18);
return [l, lf, t, tf, r, rf, b, bf];
}
getDefaultRect(ref) {
const l = ref.col;
const lf = 6;
const t = Math.max(ref.row - 2, 0);
const tf = 14;
const r = l + 2;
const rf = 2;
const b = t + 4;
const bf = 16;
return [l, lf, t, tf, r, rf, b, bf];
}
render(xmlStream, model) {
const rect = model.anchor
? this.getAnchorRect(model.anchor)
: this.getDefaultRect(model.refAddress);
xmlStream.leafNode('x:Anchor', null, rect.join(', '));
}
parseOpen(node) {
switch (node.name) {
case this.tag:
this.text = '';
return true;
default:
return false;
}
}
parseText(text) {
this.text = text;
}
parseClose() {
return false;
}
}
module.exports = VmlAnchorXform;

View File

@@ -0,0 +1,95 @@
const BaseXform = require('../base-xform');
const VmlAnchorXform = require('./vml-anchor-xform');
const VmlProtectionXform = require('./style/vml-protection-xform');
const VmlPositionXform = require('./style/vml-position-xform');
const POSITION_TYPE = ['twoCells', 'oneCells', 'absolute'];
class VmlClientDataXform extends BaseXform {
constructor() {
super();
this.map = {
'x:Anchor': new VmlAnchorXform(),
'x:Locked': new VmlProtectionXform({tag: 'x:Locked'}),
'x:LockText': new VmlProtectionXform({tag: 'x:LockText'}),
'x:SizeWithCells': new VmlPositionXform({tag: 'x:SizeWithCells'}),
'x:MoveWithCells': new VmlPositionXform({tag: 'x:MoveWithCells'}),
};
}
get tag() {
return 'x:ClientData';
}
render(xmlStream, model) {
const {protection, editAs} = model.note;
xmlStream.openNode(this.tag, {ObjectType: 'Note'});
this.map['x:MoveWithCells'].render(xmlStream, editAs, POSITION_TYPE);
this.map['x:SizeWithCells'].render(xmlStream, editAs, POSITION_TYPE);
this.map['x:Anchor'].render(xmlStream, model);
this.map['x:Locked'].render(xmlStream, protection.locked);
xmlStream.leafNode('x:AutoFill', null, 'False');
this.map['x:LockText'].render(xmlStream, protection.lockText);
xmlStream.leafNode('x:Row', null, model.refAddress.row - 1);
xmlStream.leafNode('x:Column', null, model.refAddress.col - 1);
xmlStream.closeNode();
}
parseOpen(node) {
switch (node.name) {
case this.tag:
this.reset();
this.model = {
anchor: [],
protection: {},
editAs: '',
};
break;
default:
this.parser = this.map[node.name];
if (this.parser) {
this.parser.parseOpen(node);
}
break;
}
return true;
}
parseText(text) {
if (this.parser) {
this.parser.parseText(text);
}
}
parseClose(name) {
if (this.parser) {
if (!this.parser.parseClose(name)) {
this.parser = undefined;
}
return true;
}
switch (name) {
case this.tag:
this.normalizeModel();
return false;
default:
return true;
}
}
normalizeModel() {
const position = Object.assign(
{},
this.map['x:MoveWithCells'].model,
this.map['x:SizeWithCells'].model
);
const len = Object.keys(position).length;
this.model.editAs = POSITION_TYPE[len];
this.model.anchor = this.map['x:Anchor'].text;
this.model.protection.locked = this.map['x:Locked'].text;
this.model.protection.lockText = this.map['x:LockText'].text;
}
}
module.exports = VmlClientDataXform;

View File

@@ -0,0 +1,107 @@
const XmlStream = require('../../../utils/xml-stream');
const BaseXform = require('../base-xform');
const VmlShapeXform = require('./vml-shape-xform');
// This class is (currently) single purposed to insert the triangle
// drawing icons on commented cells
class VmlNotesXform extends BaseXform {
constructor() {
super();
this.map = {
'v:shape': new VmlShapeXform(),
};
}
get tag() {
return 'xml';
}
render(xmlStream, model) {
xmlStream.openXml(XmlStream.StdDocAttributes);
xmlStream.openNode(this.tag, VmlNotesXform.DRAWING_ATTRIBUTES);
xmlStream.openNode('o:shapelayout', {'v:ext': 'edit'});
xmlStream.leafNode('o:idmap', {'v:ext': 'edit', data: 1});
xmlStream.closeNode();
xmlStream.openNode('v:shapetype', {
id: '_x0000_t202',
coordsize: '21600,21600',
'o:spt': 202,
path: 'm,l,21600r21600,l21600,xe',
});
xmlStream.leafNode('v:stroke', {joinstyle: 'miter'});
xmlStream.leafNode('v:path', {gradientshapeok: 't', 'o:connecttype': 'rect'});
xmlStream.closeNode();
model.comments.forEach((item, index) => {
this.map['v:shape'].render(xmlStream, item, index);
});
xmlStream.closeNode();
}
parseOpen(node) {
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
switch (node.name) {
case this.tag:
this.reset();
this.model = {
comments: [],
};
break;
default:
this.parser = this.map[node.name];
if (this.parser) {
this.parser.parseOpen(node);
}
break;
}
return true;
}
parseText(text) {
if (this.parser) {
this.parser.parseText(text);
}
}
parseClose(name) {
if (this.parser) {
if (!this.parser.parseClose(name)) {
this.model.comments.push(this.parser.model);
this.parser = undefined;
}
return true;
}
switch (name) {
case this.tag:
return false;
default:
// could be some unrecognised tags
return true;
}
}
reconcile(model, options) {
model.anchors.forEach(anchor => {
if (anchor.br) {
this.map['xdr:twoCellAnchor'].reconcile(anchor, options);
} else {
this.map['xdr:oneCellAnchor'].reconcile(anchor, options);
}
});
}
}
VmlNotesXform.DRAWING_ATTRIBUTES = {
'xmlns:v': 'urn:schemas-microsoft-com:vml',
'xmlns:o': 'urn:schemas-microsoft-com:office:office',
'xmlns:x': 'urn:schemas-microsoft-com:office:excel',
};
module.exports = VmlNotesXform;

View File

@@ -0,0 +1,95 @@
const BaseXform = require('../base-xform');
const VmlTextboxXform = require('./vml-textbox-xform');
const VmlClientDataXform = require('./vml-client-data-xform');
class VmlShapeXform extends BaseXform {
constructor() {
super();
this.map = {
'v:textbox': new VmlTextboxXform(),
'x:ClientData': new VmlClientDataXform(),
};
}
get tag() {
return 'v:shape';
}
render(xmlStream, model, index) {
xmlStream.openNode('v:shape', VmlShapeXform.V_SHAPE_ATTRIBUTES(model, index));
xmlStream.leafNode('v:fill', {color2: 'infoBackground [80]'});
xmlStream.leafNode('v:shadow', {color: 'none [81]', obscured: 't'});
xmlStream.leafNode('v:path', {'o:connecttype': 'none'});
this.map['v:textbox'].render(xmlStream, model);
this.map['x:ClientData'].render(xmlStream, model);
xmlStream.closeNode();
}
parseOpen(node) {
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
switch (node.name) {
case this.tag:
this.reset();
this.model = {
margins: {
insetmode: node.attributes['o:insetmode'],
},
anchor: '',
editAs: '',
protection: {},
};
break;
default:
this.parser = this.map[node.name];
if (this.parser) {
this.parser.parseOpen(node);
}
break;
}
return true;
}
parseText(text) {
if (this.parser) {
this.parser.parseText(text);
}
}
parseClose(name) {
if (this.parser) {
if (!this.parser.parseClose(name)) {
this.parser = undefined;
}
return true;
}
switch (name) {
case this.tag:
this.model.margins.inset = this.map['v:textbox'].model && this.map['v:textbox'].model.inset;
this.model.protection =
this.map['x:ClientData'].model && this.map['x:ClientData'].model.protection;
this.model.anchor = this.map['x:ClientData'].model && this.map['x:ClientData'].model.anchor;
this.model.editAs = this.map['x:ClientData'].model && this.map['x:ClientData'].model.editAs;
return false;
default:
return true;
}
}
}
VmlShapeXform.V_SHAPE_ATTRIBUTES = (model, index) => ({
id: `_x0000_s${1025 + index}`,
type: '#_x0000_t202',
style:
'position:absolute; margin-left:105.3pt;margin-top:10.5pt;width:97.8pt;height:59.1pt;z-index:1;visibility:hidden',
fillcolor: 'infoBackground [80]',
strokecolor: 'none [81]',
'o:insetmode': model.note.margins && model.note.margins.insetmode,
});
module.exports = VmlShapeXform;

View File

@@ -0,0 +1,64 @@
const BaseXform = require('../base-xform');
class VmlTextboxXform extends BaseXform {
get tag() {
return 'v:textbox';
}
conversionUnit(value, multiple, unit) {
return `${parseFloat(value) * multiple.toFixed(2)}${unit}`;
}
reverseConversionUnit(inset) {
return (inset || '').split(',').map(margin => {
return Number(parseFloat(this.conversionUnit(parseFloat(margin), 0.1, '')).toFixed(2));
});
}
render(xmlStream, model) {
const attributes = {
style: 'mso-direction-alt:auto',
};
if (model && model.note) {
let {inset} = model.note && model.note.margins;
if (Array.isArray(inset)) {
inset = inset
.map(margin => {
return this.conversionUnit(margin, 10, 'mm');
})
.join(',');
}
if (inset) {
attributes.inset = inset;
}
}
xmlStream.openNode('v:textbox', attributes);
xmlStream.leafNode('div', {style: 'text-align:left'});
xmlStream.closeNode();
}
parseOpen(node) {
switch (node.name) {
case this.tag:
this.model = {
inset: this.reverseConversionUnit(node.attributes.inset),
};
return true;
default:
return true;
}
}
parseText() {}
parseClose(name) {
switch (name) {
case this.tag:
return false;
default:
return true;
}
}
}
module.exports = VmlTextboxXform;

View File

@@ -0,0 +1,56 @@
const BaseXform = require('./base-xform');
/* 'virtual' methods used as a form of documentation */
/* eslint-disable class-methods-use-this */
// base class for xforms that are composed of other xforms
// offers some default implementations
class CompositeXform extends BaseXform {
createNewModel(node) {
return {};
}
parseOpen(node) {
// Typical pattern for composite xform
this.parser = this.parser || this.map[node.name];
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
if (node.name === this.tag) {
this.model = this.createNewModel(node);
return true;
}
return false;
}
parseText(text) {
// Default implementation. Send text to child parser
if (this.parser) {
this.parser.parseText(text);
}
}
onParserClose(name, parser) {
// parseClose has seen a child parser close
// now need to incorporate into this.model somehow
this.model[name] = parser.model;
}
parseClose(name) {
// Default implementation
if (this.parser) {
if (!this.parser.parseClose(name)) {
this.onParserClose(name, this.parser);
this.parser = undefined;
}
return true;
}
return name !== this.tag;
}
}
module.exports = CompositeXform;

View File

@@ -0,0 +1,32 @@
const BaseXform = require('../base-xform');
class AppHeadingPairsXform extends BaseXform {
render(xmlStream, model) {
xmlStream.openNode('HeadingPairs');
xmlStream.openNode('vt:vector', {size: 2, baseType: 'variant'});
xmlStream.openNode('vt:variant');
xmlStream.leafNode('vt:lpstr', undefined, 'Worksheets');
xmlStream.closeNode();
xmlStream.openNode('vt:variant');
xmlStream.leafNode('vt:i4', undefined, model.length);
xmlStream.closeNode();
xmlStream.closeNode();
xmlStream.closeNode();
}
parseOpen(node) {
// no parsing
return node.name === 'HeadingPairs';
}
parseText() {}
parseClose(name) {
return name !== 'HeadingPairs';
}
}
module.exports = AppHeadingPairsXform;

View File

@@ -0,0 +1,28 @@
const BaseXform = require('../base-xform');
class AppTitlesOfPartsXform extends BaseXform {
render(xmlStream, model) {
xmlStream.openNode('TitlesOfParts');
xmlStream.openNode('vt:vector', {size: model.length, baseType: 'lpstr'});
model.forEach(sheet => {
xmlStream.leafNode('vt:lpstr', undefined, sheet.name);
});
xmlStream.closeNode();
xmlStream.closeNode();
}
parseOpen(node) {
// no parsing
return node.name === 'TitlesOfParts';
}
parseText() {}
parseClose(name) {
return name !== 'TitlesOfParts';
}
}
module.exports = AppTitlesOfPartsXform;

View File

@@ -0,0 +1,100 @@
const XmlStream = require('../../../utils/xml-stream');
const BaseXform = require('../base-xform');
const StringXform = require('../simple/string-xform');
const AppHeadingPairsXform = require('./app-heading-pairs-xform');
const AppTitleOfPartsXform = require('./app-titles-of-parts-xform');
class AppXform extends BaseXform {
constructor() {
super();
this.map = {
Company: new StringXform({tag: 'Company'}),
Manager: new StringXform({tag: 'Manager'}),
HeadingPairs: new AppHeadingPairsXform(),
TitleOfParts: new AppTitleOfPartsXform(),
};
}
render(xmlStream, model) {
xmlStream.openXml(XmlStream.StdDocAttributes);
xmlStream.openNode('Properties', AppXform.PROPERTY_ATTRIBUTES);
xmlStream.leafNode('Application', undefined, 'Microsoft Excel');
xmlStream.leafNode('DocSecurity', undefined, '0');
xmlStream.leafNode('ScaleCrop', undefined, 'false');
this.map.HeadingPairs.render(xmlStream, model.worksheets);
this.map.TitleOfParts.render(xmlStream, model.worksheets);
this.map.Company.render(xmlStream, model.company || '');
this.map.Manager.render(xmlStream, model.manager);
xmlStream.leafNode('LinksUpToDate', undefined, 'false');
xmlStream.leafNode('SharedDoc', undefined, 'false');
xmlStream.leafNode('HyperlinksChanged', undefined, 'false');
xmlStream.leafNode('AppVersion', undefined, '16.0300');
xmlStream.closeNode();
}
parseOpen(node) {
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
switch (node.name) {
case 'Properties':
return true;
default:
this.parser = this.map[node.name];
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
// there's a lot we don't bother to parse
return false;
}
}
parseText(text) {
if (this.parser) {
this.parser.parseText(text);
}
}
parseClose(name) {
if (this.parser) {
if (!this.parser.parseClose(name)) {
this.parser = undefined;
}
return true;
}
switch (name) {
case 'Properties':
this.model = {
worksheets: this.map.TitleOfParts.model,
company: this.map.Company.model,
manager: this.map.Manager.model,
};
return false;
default:
return true;
}
}
}
AppXform.DateFormat = function(dt) {
return dt.toISOString().replace(/[.]\d{3,6}/, '');
};
AppXform.DateAttrs = {'xsi:type': 'dcterms:W3CDTF'};
AppXform.PROPERTY_ATTRIBUTES = {
xmlns: 'http://schemas.openxmlformats.org/officeDocument/2006/extended-properties',
'xmlns:vt': 'http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes',
};
module.exports = AppXform;

View File

@@ -0,0 +1,120 @@
const XmlStream = require('../../../utils/xml-stream');
const BaseXform = require('../base-xform');
// used for rendering the [Content_Types].xml file
// not used for parsing
class ContentTypesXform extends BaseXform {
render(xmlStream, model) {
xmlStream.openXml(XmlStream.StdDocAttributes);
xmlStream.openNode('Types', ContentTypesXform.PROPERTY_ATTRIBUTES);
const mediaHash = {};
(model.media || []).forEach(medium => {
if (medium.type === 'image') {
const imageType = medium.extension;
if (!mediaHash[imageType]) {
mediaHash[imageType] = true;
xmlStream.leafNode('Default', {Extension: imageType, ContentType: `image/${imageType}`});
}
}
});
xmlStream.leafNode('Default', {
Extension: 'rels',
ContentType: 'application/vnd.openxmlformats-package.relationships+xml',
});
xmlStream.leafNode('Default', {Extension: 'xml', ContentType: 'application/xml'});
xmlStream.leafNode('Override', {
PartName: '/xl/workbook.xml',
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml',
});
model.worksheets.forEach(worksheet => {
const name = `/xl/worksheets/sheet${worksheet.id}.xml`;
xmlStream.leafNode('Override', {
PartName: name,
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml',
});
});
xmlStream.leafNode('Override', {
PartName: '/xl/theme/theme1.xml',
ContentType: 'application/vnd.openxmlformats-officedocument.theme+xml',
});
xmlStream.leafNode('Override', {
PartName: '/xl/styles.xml',
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml',
});
const hasSharedStrings = model.sharedStrings && model.sharedStrings.count;
if (hasSharedStrings) {
xmlStream.leafNode('Override', {
PartName: '/xl/sharedStrings.xml',
ContentType:
'application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml',
});
}
if (model.tables) {
model.tables.forEach(table => {
xmlStream.leafNode('Override', {
PartName: `/xl/tables/${table.target}`,
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml',
});
});
}
if (model.drawings) {
model.drawings.forEach(drawing => {
xmlStream.leafNode('Override', {
PartName: `/xl/drawings/${drawing.name}.xml`,
ContentType: 'application/vnd.openxmlformats-officedocument.drawing+xml',
});
});
}
if (model.commentRefs) {
xmlStream.leafNode('Default', {
Extension: 'vml',
ContentType: 'application/vnd.openxmlformats-officedocument.vmlDrawing',
});
model.commentRefs.forEach(({commentName}) => {
xmlStream.leafNode('Override', {
PartName: `/xl/${commentName}.xml`,
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml',
});
});
}
xmlStream.leafNode('Override', {
PartName: '/docProps/core.xml',
ContentType: 'application/vnd.openxmlformats-package.core-properties+xml',
});
xmlStream.leafNode('Override', {
PartName: '/docProps/app.xml',
ContentType: 'application/vnd.openxmlformats-officedocument.extended-properties+xml',
});
xmlStream.closeNode();
}
parseOpen() {
return false;
}
parseText() {}
parseClose() {
return false;
}
}
ContentTypesXform.PROPERTY_ATTRIBUTES = {
xmlns: 'http://schemas.openxmlformats.org/package/2006/content-types',
};
module.exports = ContentTypesXform;

View File

@@ -0,0 +1,136 @@
const XmlStream = require('../../../utils/xml-stream');
const BaseXform = require('../base-xform');
const DateXform = require('../simple/date-xform');
const StringXform = require('../simple/string-xform');
const IntegerXform = require('../simple/integer-xform');
class CoreXform extends BaseXform {
constructor() {
super();
this.map = {
'dc:creator': new StringXform({tag: 'dc:creator'}),
'dc:title': new StringXform({tag: 'dc:title'}),
'dc:subject': new StringXform({tag: 'dc:subject'}),
'dc:description': new StringXform({tag: 'dc:description'}),
'dc:identifier': new StringXform({tag: 'dc:identifier'}),
'dc:language': new StringXform({tag: 'dc:language'}),
'cp:keywords': new StringXform({tag: 'cp:keywords'}),
'cp:category': new StringXform({tag: 'cp:category'}),
'cp:lastModifiedBy': new StringXform({tag: 'cp:lastModifiedBy'}),
'cp:lastPrinted': new DateXform({tag: 'cp:lastPrinted', format: CoreXform.DateFormat}),
'cp:revision': new IntegerXform({tag: 'cp:revision'}),
'cp:version': new StringXform({tag: 'cp:version'}),
'cp:contentStatus': new StringXform({tag: 'cp:contentStatus'}),
'cp:contentType': new StringXform({tag: 'cp:contentType'}),
'dcterms:created': new DateXform({
tag: 'dcterms:created',
attrs: CoreXform.DateAttrs,
format: CoreXform.DateFormat,
}),
'dcterms:modified': new DateXform({
tag: 'dcterms:modified',
attrs: CoreXform.DateAttrs,
format: CoreXform.DateFormat,
}),
};
}
render(xmlStream, model) {
xmlStream.openXml(XmlStream.StdDocAttributes);
xmlStream.openNode('cp:coreProperties', CoreXform.CORE_PROPERTY_ATTRIBUTES);
this.map['dc:creator'].render(xmlStream, model.creator);
this.map['dc:title'].render(xmlStream, model.title);
this.map['dc:subject'].render(xmlStream, model.subject);
this.map['dc:description'].render(xmlStream, model.description);
this.map['dc:identifier'].render(xmlStream, model.identifier);
this.map['dc:language'].render(xmlStream, model.language);
this.map['cp:keywords'].render(xmlStream, model.keywords);
this.map['cp:category'].render(xmlStream, model.category);
this.map['cp:lastModifiedBy'].render(xmlStream, model.lastModifiedBy);
this.map['cp:lastPrinted'].render(xmlStream, model.lastPrinted);
this.map['cp:revision'].render(xmlStream, model.revision);
this.map['cp:version'].render(xmlStream, model.version);
this.map['cp:contentStatus'].render(xmlStream, model.contentStatus);
this.map['cp:contentType'].render(xmlStream, model.contentType);
this.map['dcterms:created'].render(xmlStream, model.created);
this.map['dcterms:modified'].render(xmlStream, model.modified);
xmlStream.closeNode();
}
parseOpen(node) {
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
switch (node.name) {
case 'cp:coreProperties':
case 'coreProperties':
return true;
default:
this.parser = this.map[node.name];
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
throw new Error(`Unexpected xml node in parseOpen: ${JSON.stringify(node)}`);
}
}
parseText(text) {
if (this.parser) {
this.parser.parseText(text);
}
}
parseClose(name) {
if (this.parser) {
if (!this.parser.parseClose(name)) {
this.parser = undefined;
}
return true;
}
switch (name) {
case 'cp:coreProperties':
case 'coreProperties':
this.model = {
creator: this.map['dc:creator'].model,
title: this.map['dc:title'].model,
subject: this.map['dc:subject'].model,
description: this.map['dc:description'].model,
identifier: this.map['dc:identifier'].model,
language: this.map['dc:language'].model,
keywords: this.map['cp:keywords'].model,
category: this.map['cp:category'].model,
lastModifiedBy: this.map['cp:lastModifiedBy'].model,
lastPrinted: this.map['cp:lastPrinted'].model,
revision: this.map['cp:revision'].model,
contentStatus: this.map['cp:contentStatus'].model,
contentType: this.map['cp:contentType'].model,
created: this.map['dcterms:created'].model,
modified: this.map['dcterms:modified'].model,
};
return false;
default:
throw new Error(`Unexpected xml node in parseClose: ${name}`);
}
}
}
CoreXform.DateFormat = function(dt) {
return dt.toISOString().replace(/[.]\d{3}/, '');
};
CoreXform.DateAttrs = {'xsi:type': 'dcterms:W3CDTF'};
CoreXform.CORE_PROPERTY_ATTRIBUTES = {
'xmlns:cp': 'http://schemas.openxmlformats.org/package/2006/metadata/core-properties',
'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
'xmlns:dcterms': 'http://purl.org/dc/terms/',
'xmlns:dcmitype': 'http://purl.org/dc/dcmitype/',
'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
};
module.exports = CoreXform;

View File

@@ -0,0 +1,25 @@
const BaseXform = require('../base-xform');
class RelationshipXform extends BaseXform {
render(xmlStream, model) {
xmlStream.leafNode('Relationship', model);
}
parseOpen(node) {
switch (node.name) {
case 'Relationship':
this.model = node.attributes;
return true;
default:
return false;
}
}
parseText() {}
parseClose() {
return false;
}
}
module.exports = RelationshipXform;

View File

@@ -0,0 +1,73 @@
const XmlStream = require('../../../utils/xml-stream');
const BaseXform = require('../base-xform');
const RelationshipXform = require('./relationship-xform');
class RelationshipsXform extends BaseXform {
constructor() {
super();
this.map = {
Relationship: new RelationshipXform(),
};
}
render(xmlStream, model) {
model = model || this._values;
xmlStream.openXml(XmlStream.StdDocAttributes);
xmlStream.openNode('Relationships', RelationshipsXform.RELATIONSHIPS_ATTRIBUTES);
model.forEach(relationship => {
this.map.Relationship.render(xmlStream, relationship);
});
xmlStream.closeNode();
}
parseOpen(node) {
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
switch (node.name) {
case 'Relationships':
this.model = [];
return true;
default:
this.parser = this.map[node.name];
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
throw new Error(`Unexpected xml node in parseOpen: ${JSON.stringify(node)}`);
}
}
parseText(text) {
if (this.parser) {
this.parser.parseText(text);
}
}
parseClose(name) {
if (this.parser) {
if (!this.parser.parseClose(name)) {
this.model.push(this.parser.model);
this.parser = undefined;
}
return true;
}
switch (name) {
case 'Relationships':
return false;
default:
throw new Error(`Unexpected xml node in parseClose: ${name}`);
}
}
}
RelationshipsXform.RELATIONSHIPS_ATTRIBUTES = {
xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships',
};
module.exports = RelationshipsXform;

View File

@@ -0,0 +1,48 @@
const BaseXform = require('../base-xform');
class BaseCellAnchorXform extends BaseXform {
parseOpen(node) {
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
switch (node.name) {
case this.tag:
this.reset();
this.model = {
range: {
editAs: node.attributes.editAs || 'oneCell',
},
};
break;
default:
this.parser = this.map[node.name];
if (this.parser) {
this.parser.parseOpen(node);
}
break;
}
return true;
}
parseText(text) {
if (this.parser) {
this.parser.parseText(text);
}
}
reconcilePicture(model, options) {
if (model && model.rId) {
const rel = options.rels[model.rId];
const match = rel.Target.match(/.*\/media\/(.+[.][a-zA-Z]{3,4})/);
if (match) {
const name = match[1];
const mediaId = options.mediaIndex[name];
return options.media[mediaId];
}
}
return undefined;
}
}
module.exports = BaseCellAnchorXform;

View File

@@ -0,0 +1,71 @@
const BaseXform = require('../base-xform');
const BlipXform = require('./blip-xform');
class BlipFillXform extends BaseXform {
constructor() {
super();
this.map = {
'a:blip': new BlipXform(),
};
}
get tag() {
return 'xdr:blipFill';
}
render(xmlStream, model) {
xmlStream.openNode(this.tag);
this.map['a:blip'].render(xmlStream, model);
// TODO: options for this + parsing
xmlStream.openNode('a:stretch');
xmlStream.leafNode('a:fillRect');
xmlStream.closeNode();
xmlStream.closeNode();
}
parseOpen(node) {
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
switch (node.name) {
case this.tag:
this.reset();
break;
default:
this.parser = this.map[node.name];
if (this.parser) {
this.parser.parseOpen(node);
}
break;
}
return true;
}
parseText() {}
parseClose(name) {
if (this.parser) {
if (!this.parser.parseClose(name)) {
this.parser = undefined;
}
return true;
}
switch (name) {
case this.tag:
this.model = this.map['a:blip'].model;
return false;
default:
return true;
}
}
}
module.exports = BlipFillXform;

View File

@@ -0,0 +1,42 @@
const BaseXform = require('../base-xform');
class BlipXform extends BaseXform {
get tag() {
return 'a:blip';
}
render(xmlStream, model) {
xmlStream.leafNode(this.tag, {
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
'r:embed': model.rId,
cstate: 'print',
});
// TODO: handle children (e.g. a:extLst=>a:ext=>a14:useLocalDpi
}
parseOpen(node) {
switch (node.name) {
case this.tag:
this.model = {
rId: node.attributes['r:embed'],
};
return true;
default:
return true;
}
}
parseText() {}
parseClose(name) {
switch (name) {
case this.tag:
return false;
default:
// unprocessed internal nodes
return true;
}
}
}
module.exports = BlipXform;

View File

@@ -0,0 +1,38 @@
const BaseXform = require('../base-xform');
class CNvPicPrXform extends BaseXform {
get tag() {
return 'xdr:cNvPicPr';
}
render(xmlStream) {
xmlStream.openNode(this.tag);
xmlStream.leafNode('a:picLocks', {
noChangeAspect: '1',
});
xmlStream.closeNode();
}
parseOpen(node) {
switch (node.name) {
case this.tag:
return true;
default:
return true;
}
}
parseText() {}
parseClose(name) {
switch (name) {
case this.tag:
return false;
default:
// unprocessed internal nodes
return true;
}
}
}
module.exports = CNvPicPrXform;

View File

@@ -0,0 +1,68 @@
const BaseXform = require('../base-xform');
const HlickClickXform = require('./hlink-click-xform');
const ExtLstXform = require('./ext-lst-xform');
class CNvPrXform extends BaseXform {
constructor() {
super();
this.map = {
'a:hlinkClick': new HlickClickXform(),
'a:extLst': new ExtLstXform(),
};
}
get tag() {
return 'xdr:cNvPr';
}
render(xmlStream, model) {
xmlStream.openNode(this.tag, {
id: model.index,
name: `Picture ${model.index}`,
});
this.map['a:hlinkClick'].render(xmlStream, model);
this.map['a:extLst'].render(xmlStream, model);
xmlStream.closeNode();
}
parseOpen(node) {
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
switch (node.name) {
case this.tag:
this.reset();
break;
default:
this.parser = this.map[node.name];
if (this.parser) {
this.parser.parseOpen(node);
}
break;
}
return true;
}
parseText() {}
parseClose(name) {
if (this.parser) {
if (!this.parser.parseClose(name)) {
this.parser = undefined;
}
return true;
}
switch (name) {
case this.tag:
this.model = this.map['a:hlinkClick'].model;
return false;
default:
return true;
}
}
}
module.exports = CNvPrXform;

View File

@@ -0,0 +1,77 @@
const BaseXform = require('../base-xform');
const IntegerXform = require('../simple/integer-xform');
class CellPositionXform extends BaseXform {
constructor(options) {
super();
this.tag = options.tag;
this.map = {
'xdr:col': new IntegerXform({tag: 'xdr:col', zero: true}),
'xdr:colOff': new IntegerXform({tag: 'xdr:colOff', zero: true}),
'xdr:row': new IntegerXform({tag: 'xdr:row', zero: true}),
'xdr:rowOff': new IntegerXform({tag: 'xdr:rowOff', zero: true}),
};
}
render(xmlStream, model) {
xmlStream.openNode(this.tag);
this.map['xdr:col'].render(xmlStream, model.nativeCol);
this.map['xdr:colOff'].render(xmlStream, model.nativeColOff);
this.map['xdr:row'].render(xmlStream, model.nativeRow);
this.map['xdr:rowOff'].render(xmlStream, model.nativeRowOff);
xmlStream.closeNode();
}
parseOpen(node) {
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
switch (node.name) {
case this.tag:
this.reset();
break;
default:
this.parser = this.map[node.name];
if (this.parser) {
this.parser.parseOpen(node);
}
break;
}
return true;
}
parseText(text) {
if (this.parser) {
this.parser.parseText(text);
}
}
parseClose(name) {
if (this.parser) {
if (!this.parser.parseClose(name)) {
this.parser = undefined;
}
return true;
}
switch (name) {
case this.tag:
this.model = {
nativeCol: this.map['xdr:col'].model,
nativeColOff: this.map['xdr:colOff'].model,
nativeRow: this.map['xdr:row'].model,
nativeRowOff: this.map['xdr:rowOff'].model,
};
return false;
default:
// not quite sure how we get here!
return true;
}
}
}
module.exports = CellPositionXform;

View File

@@ -0,0 +1,109 @@
const colCache = require('../../../utils/col-cache');
const XmlStream = require('../../../utils/xml-stream');
const BaseXform = require('../base-xform');
const TwoCellAnchorXform = require('./two-cell-anchor-xform');
const OneCellAnchorXform = require('./one-cell-anchor-xform');
function getAnchorType(model) {
const range = typeof model.range === 'string' ? colCache.decode(model.range) : model.range;
return range.br ? 'xdr:twoCellAnchor' : 'xdr:oneCellAnchor';
}
class DrawingXform extends BaseXform {
constructor() {
super();
this.map = {
'xdr:twoCellAnchor': new TwoCellAnchorXform(),
'xdr:oneCellAnchor': new OneCellAnchorXform(),
};
}
prepare(model) {
model.anchors.forEach((item, index) => {
item.anchorType = getAnchorType(item);
const anchor = this.map[item.anchorType];
anchor.prepare(item, {index});
});
}
get tag() {
return 'xdr:wsDr';
}
render(xmlStream, model) {
xmlStream.openXml(XmlStream.StdDocAttributes);
xmlStream.openNode(this.tag, DrawingXform.DRAWING_ATTRIBUTES);
model.anchors.forEach(item => {
const anchor = this.map[item.anchorType];
anchor.render(xmlStream, item);
});
xmlStream.closeNode();
}
parseOpen(node) {
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
switch (node.name) {
case this.tag:
this.reset();
this.model = {
anchors: [],
};
break;
default:
this.parser = this.map[node.name];
if (this.parser) {
this.parser.parseOpen(node);
}
break;
}
return true;
}
parseText(text) {
if (this.parser) {
this.parser.parseText(text);
}
}
parseClose(name) {
if (this.parser) {
if (!this.parser.parseClose(name)) {
this.model.anchors.push(this.parser.model);
this.parser = undefined;
}
return true;
}
switch (name) {
case this.tag:
return false;
default:
// could be some unrecognised tags
return true;
}
}
reconcile(model, options) {
model.anchors.forEach(anchor => {
if (anchor.br) {
this.map['xdr:twoCellAnchor'].reconcile(anchor, options);
} else {
this.map['xdr:oneCellAnchor'].reconcile(anchor, options);
}
});
}
}
DrawingXform.DRAWING_ATTRIBUTES = {
'xmlns:xdr': 'http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing',
'xmlns:a': 'http://schemas.openxmlformats.org/drawingml/2006/main',
};
module.exports = DrawingXform;

View File

@@ -0,0 +1,43 @@
const BaseXform = require('../base-xform');
class ExtLstXform extends BaseXform {
get tag() {
return 'a:extLst';
}
render(xmlStream) {
xmlStream.openNode(this.tag);
xmlStream.openNode('a:ext', {
uri: '{FF2B5EF4-FFF2-40B4-BE49-F238E27FC236}',
});
xmlStream.leafNode('a16:creationId', {
'xmlns:a16': 'http://schemas.microsoft.com/office/drawing/2014/main',
id: '{00000000-0008-0000-0000-000002000000}',
});
xmlStream.closeNode();
xmlStream.closeNode();
}
parseOpen(node) {
switch (node.name) {
case this.tag:
return true;
default:
return true;
}
}
parseText() {}
parseClose(name) {
switch (name) {
case this.tag:
return false;
default:
// unprocessed internal nodes
return true;
}
}
}
module.exports = ExtLstXform;

View File

@@ -0,0 +1,44 @@
const BaseXform = require('../base-xform');
/** https://en.wikipedia.org/wiki/Office_Open_XML_file_formats#DrawingML */
const EMU_PER_PIXEL_AT_96_DPI = 9525;
class ExtXform extends BaseXform {
constructor(options) {
super();
this.tag = options.tag;
this.map = {};
}
render(xmlStream, model) {
xmlStream.openNode(this.tag);
const width = Math.floor(model.width * EMU_PER_PIXEL_AT_96_DPI);
const height = Math.floor(model.height * EMU_PER_PIXEL_AT_96_DPI);
xmlStream.addAttribute('cx', width);
xmlStream.addAttribute('cy', height);
xmlStream.closeNode();
}
parseOpen(node) {
if (node.name === this.tag) {
this.model = {
width: parseInt(node.attributes.cx || '0', 10) / EMU_PER_PIXEL_AT_96_DPI,
height: parseInt(node.attributes.cy || '0', 10) / EMU_PER_PIXEL_AT_96_DPI,
};
return true;
}
return false;
}
parseText(/* text */) {}
parseClose(/* name */) {
return false;
}
}
module.exports = ExtXform;

View File

@@ -0,0 +1,41 @@
const BaseXform = require('../base-xform');
class HLinkClickXform extends BaseXform {
get tag() {
return 'a:hlinkClick';
}
render(xmlStream, model) {
if (!(model.hyperlinks && model.hyperlinks.rId)) {
return;
}
xmlStream.leafNode(this.tag, {
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
'r:id': model.hyperlinks.rId,
tooltip: model.hyperlinks.tooltip,
});
}
parseOpen(node) {
switch (node.name) {
case this.tag:
this.model = {
hyperlinks: {
rId: node.attributes['r:id'],
tooltip: node.attributes.tooltip,
},
};
return true;
default:
return true;
}
}
parseText() {}
parseClose() {
return false;
}
}
module.exports = HLinkClickXform;

View File

@@ -0,0 +1,65 @@
const BaseXform = require('../base-xform');
const CNvPrXform = require('./c-nv-pr-xform');
const CNvPicPrXform = require('./c-nv-pic-pr-xform');
class NvPicPrXform extends BaseXform {
constructor() {
super();
this.map = {
'xdr:cNvPr': new CNvPrXform(),
'xdr:cNvPicPr': new CNvPicPrXform(),
};
}
get tag() {
return 'xdr:nvPicPr';
}
render(xmlStream, model) {
xmlStream.openNode(this.tag);
this.map['xdr:cNvPr'].render(xmlStream, model);
this.map['xdr:cNvPicPr'].render(xmlStream, model);
xmlStream.closeNode();
}
parseOpen(node) {
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
switch (node.name) {
case this.tag:
this.reset();
break;
default:
this.parser = this.map[node.name];
if (this.parser) {
this.parser.parseOpen(node);
}
break;
}
return true;
}
parseText() {}
parseClose(name) {
if (this.parser) {
if (!this.parser.parseClose(name)) {
this.parser = undefined;
}
return true;
}
switch (name) {
case this.tag:
this.model = this.map['xdr:cNvPr'].model;
return false;
default:
return true;
}
}
}
module.exports = NvPicPrXform;

View File

@@ -0,0 +1,63 @@
const BaseCellAnchorXform = require('./base-cell-anchor-xform');
const StaticXform = require('../static-xform');
const CellPositionXform = require('./cell-position-xform');
const ExtXform = require('./ext-xform');
const PicXform = require('./pic-xform');
class OneCellAnchorXform extends BaseCellAnchorXform {
constructor() {
super();
this.map = {
'xdr:from': new CellPositionXform({tag: 'xdr:from'}),
'xdr:ext': new ExtXform({tag: 'xdr:ext'}),
'xdr:pic': new PicXform(),
'xdr:clientData': new StaticXform({tag: 'xdr:clientData'}),
};
}
get tag() {
return 'xdr:oneCellAnchor';
}
prepare(model, options) {
this.map['xdr:pic'].prepare(model.picture, options);
}
render(xmlStream, model) {
xmlStream.openNode(this.tag, {editAs: model.range.editAs || 'oneCell'});
this.map['xdr:from'].render(xmlStream, model.range.tl);
this.map['xdr:ext'].render(xmlStream, model.range.ext);
this.map['xdr:pic'].render(xmlStream, model.picture);
this.map['xdr:clientData'].render(xmlStream, {});
xmlStream.closeNode();
}
parseClose(name) {
if (this.parser) {
if (!this.parser.parseClose(name)) {
this.parser = undefined;
}
return true;
}
switch (name) {
case this.tag:
this.model.range.tl = this.map['xdr:from'].model;
this.model.range.ext = this.map['xdr:ext'].model;
this.model.picture = this.map['xdr:pic'].model;
return false;
default:
// could be some unrecognised tags
return true;
}
}
reconcile(model, options) {
model.medium = this.reconcilePicture(model.picture, options);
}
}
module.exports = OneCellAnchorXform;

View File

@@ -0,0 +1,77 @@
const BaseXform = require('../base-xform');
const StaticXform = require('../static-xform');
const BlipFillXform = require('./blip-fill-xform');
const NvPicPrXform = require('./nv-pic-pr-xform');
const spPrJSON = require('./sp-pr');
class PicXform extends BaseXform {
constructor() {
super();
this.map = {
'xdr:nvPicPr': new NvPicPrXform(),
'xdr:blipFill': new BlipFillXform(),
'xdr:spPr': new StaticXform(spPrJSON),
};
}
get tag() {
return 'xdr:pic';
}
prepare(model, options) {
model.index = options.index + 1;
}
render(xmlStream, model) {
xmlStream.openNode(this.tag);
this.map['xdr:nvPicPr'].render(xmlStream, model);
this.map['xdr:blipFill'].render(xmlStream, model);
this.map['xdr:spPr'].render(xmlStream, model);
xmlStream.closeNode();
}
parseOpen(node) {
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
switch (node.name) {
case this.tag:
this.reset();
break;
default:
this.parser = this.map[node.name];
if (this.parser) {
this.parser.parseOpen(node);
}
break;
}
return true;
}
parseText() {}
parseClose(name) {
if (this.parser) {
if (!this.parser.parseClose(name)) {
this.mergeModel(this.parser.model);
this.parser = undefined;
}
return true;
}
switch (name) {
case this.tag:
return false;
default:
// not quite sure how we get here!
return true;
}
}
}
module.exports = PicXform;

View File

@@ -0,0 +1,17 @@
module.exports = {
tag: 'xdr:spPr',
c: [
{
tag: 'a:xfrm',
c: [
{tag: 'a:off', $: {x: '0', y: '0'}},
{tag: 'a:ext', $: {cx: '0', cy: '0'}},
],
},
{
tag: 'a:prstGeom',
$: {prst: 'rect'},
c: [{tag: 'a:avLst'}],
},
],
};

View File

@@ -0,0 +1,62 @@
const BaseCellAnchorXform = require('./base-cell-anchor-xform');
const StaticXform = require('../static-xform');
const CellPositionXform = require('./cell-position-xform');
const PicXform = require('./pic-xform');
class TwoCellAnchorXform extends BaseCellAnchorXform {
constructor() {
super();
this.map = {
'xdr:from': new CellPositionXform({tag: 'xdr:from'}),
'xdr:to': new CellPositionXform({tag: 'xdr:to'}),
'xdr:pic': new PicXform(),
'xdr:clientData': new StaticXform({tag: 'xdr:clientData'}),
};
}
get tag() {
return 'xdr:twoCellAnchor';
}
prepare(model, options) {
this.map['xdr:pic'].prepare(model.picture, options);
}
render(xmlStream, model) {
xmlStream.openNode(this.tag, {editAs: model.range.editAs || 'oneCell'});
this.map['xdr:from'].render(xmlStream, model.range.tl);
this.map['xdr:to'].render(xmlStream, model.range.br);
this.map['xdr:pic'].render(xmlStream, model.picture);
this.map['xdr:clientData'].render(xmlStream, {});
xmlStream.closeNode();
}
parseClose(name) {
if (this.parser) {
if (!this.parser.parseClose(name)) {
this.parser = undefined;
}
return true;
}
switch (name) {
case this.tag:
this.model.range.tl = this.map['xdr:from'].model;
this.model.range.br = this.map['xdr:to'].model;
this.model.picture = this.map['xdr:pic'].model;
return false;
default:
// could be some unrecognised tags
return true;
}
}
reconcile(model, options) {
model.medium = this.reconcilePicture(model.picture, options);
}
}
module.exports = TwoCellAnchorXform;

View File

@@ -0,0 +1,95 @@
const BaseXform = require('./base-xform');
class ListXform extends BaseXform {
constructor(options) {
super();
this.tag = options.tag;
this.always = !!options.always;
this.count = options.count;
this.empty = options.empty;
this.$count = options.$count || 'count';
this.$ = options.$;
this.childXform = options.childXform;
this.maxItems = options.maxItems;
}
prepare(model, options) {
const {childXform} = this;
if (model) {
model.forEach((childModel, index) => {
options.index = index;
childXform.prepare(childModel, options);
});
}
}
render(xmlStream, model) {
if (this.always || (model && model.length)) {
xmlStream.openNode(this.tag, this.$);
if (this.count) {
xmlStream.addAttribute(this.$count, (model && model.length) || 0);
}
const {childXform} = this;
(model || []).forEach((childModel, index) => {
childXform.render(xmlStream, childModel, index);
});
xmlStream.closeNode();
} else if (this.empty) {
xmlStream.leafNode(this.tag);
}
}
parseOpen(node) {
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
switch (node.name) {
case this.tag:
this.model = [];
return true;
default:
if (this.childXform.parseOpen(node)) {
this.parser = this.childXform;
return true;
}
return false;
}
}
parseText(text) {
if (this.parser) {
this.parser.parseText(text);
}
}
parseClose(name) {
if (this.parser) {
if (!this.parser.parseClose(name)) {
this.model.push(this.parser.model);
this.parser = undefined;
if (this.maxItems && this.model.length > this.maxItems) {
throw new Error(`Max ${this.childXform.tag} count (${this.maxItems}) exceeded`);
}
}
return true;
}
return false;
}
reconcile(model, options) {
if (model) {
const {childXform} = this;
model.forEach(childModel => {
childXform.reconcile(childModel, options);
});
}
}
}
module.exports = ListXform;

View File

@@ -0,0 +1,38 @@
const colCache = require('../../../utils/col-cache');
const BaseXform = require('../base-xform');
class AutoFilterXform extends BaseXform {
get tag() {
return 'autoFilter';
}
render(xmlStream, model) {
if (model) {
if (typeof model === 'string') {
// assume range
xmlStream.leafNode('autoFilter', {ref: model});
} else {
const getAddress = function(addr) {
if (typeof addr === 'string') {
return addr;
}
return colCache.getAddress(addr.row, addr.column).address;
};
const firstAddress = getAddress(model.from);
const secondAddress = getAddress(model.to);
if (firstAddress && secondAddress) {
xmlStream.leafNode('autoFilter', {ref: `${firstAddress}:${secondAddress}`});
}
}
}
}
parseOpen(node) {
if (node.name === 'autoFilter') {
this.model = node.attributes.ref;
}
}
}
module.exports = AutoFilterXform;

View File

@@ -0,0 +1,498 @@
const utils = require('../../../utils/utils');
const BaseXform = require('../base-xform');
const Range = require('../../../doc/range');
const Enums = require('../../../doc/enums');
const RichTextXform = require('../strings/rich-text-xform');
function getValueType(v) {
if (v === null || v === undefined) {
return Enums.ValueType.Null;
}
if (v instanceof String || typeof v === 'string') {
return Enums.ValueType.String;
}
if (typeof v === 'number') {
return Enums.ValueType.Number;
}
if (typeof v === 'boolean') {
return Enums.ValueType.Boolean;
}
if (v instanceof Date) {
return Enums.ValueType.Date;
}
if (v.text && v.hyperlink) {
return Enums.ValueType.Hyperlink;
}
if (v.formula) {
return Enums.ValueType.Formula;
}
if (v.error) {
return Enums.ValueType.Error;
}
throw new Error('I could not understand type of value');
}
function getEffectiveCellType(cell) {
switch (cell.type) {
case Enums.ValueType.Formula:
return getValueType(cell.result);
default:
return cell.type;
}
}
class CellXform extends BaseXform {
constructor() {
super();
this.richTextXForm = new RichTextXform();
}
get tag() {
return 'c';
}
prepare(model, options) {
const styleId = options.styles.addStyleModel(model.style || {}, getEffectiveCellType(model));
if (styleId) {
model.styleId = styleId;
}
if (model.comment) {
options.comments.push({...model.comment, ref: model.address});
}
switch (model.type) {
case Enums.ValueType.String:
case Enums.ValueType.RichText:
if (options.sharedStrings) {
model.ssId = options.sharedStrings.add(model.value);
}
break;
case Enums.ValueType.Date:
if (options.date1904) {
model.date1904 = true;
}
break;
case Enums.ValueType.Hyperlink:
if (options.sharedStrings && model.text !== undefined && model.text !== null) {
model.ssId = options.sharedStrings.add(model.text);
}
options.hyperlinks.push({
address: model.address,
target: model.hyperlink,
tooltip: model.tooltip,
});
break;
case Enums.ValueType.Merge:
options.merges.add(model);
break;
case Enums.ValueType.Formula:
if (options.date1904) {
// in case valueType is date
model.date1904 = true;
}
if (model.shareType === 'shared') {
model.si = options.siFormulae++;
}
if (model.formula) {
options.formulae[model.address] = model;
} else if (model.sharedFormula) {
const master = options.formulae[model.sharedFormula];
if (!master) {
throw new Error(
`Shared Formula master must exist above and or left of clone for cell ${model.address}`
);
}
if (master.si === undefined) {
master.shareType = 'shared';
master.si = options.siFormulae++;
master.range = new Range(master.address, model.address);
} else if (master.range) {
master.range.expandToAddress(model.address);
}
model.si = master.si;
}
break;
default:
break;
}
}
renderFormula(xmlStream, model) {
let attrs = null;
switch (model.shareType) {
case 'shared':
attrs = {
t: 'shared',
ref: model.ref || model.range.range,
si: model.si,
};
break;
case 'array':
attrs = {
t: 'array',
ref: model.ref,
};
break;
default:
if (model.si !== undefined) {
attrs = {
t: 'shared',
si: model.si,
};
}
break;
}
switch (getValueType(model.result)) {
case Enums.ValueType.Null: // ?
xmlStream.leafNode('f', attrs, model.formula);
break;
case Enums.ValueType.String:
// oddly, formula results don't ever use shared strings
xmlStream.addAttribute('t', 'str');
xmlStream.leafNode('f', attrs, model.formula);
xmlStream.leafNode('v', null, model.result);
break;
case Enums.ValueType.Number:
xmlStream.leafNode('f', attrs, model.formula);
xmlStream.leafNode('v', null, model.result);
break;
case Enums.ValueType.Boolean:
xmlStream.addAttribute('t', 'b');
xmlStream.leafNode('f', attrs, model.formula);
xmlStream.leafNode('v', null, model.result ? 1 : 0);
break;
case Enums.ValueType.Error:
xmlStream.addAttribute('t', 'e');
xmlStream.leafNode('f', attrs, model.formula);
xmlStream.leafNode('v', null, model.result.error);
break;
case Enums.ValueType.Date:
xmlStream.leafNode('f', attrs, model.formula);
xmlStream.leafNode('v', null, utils.dateToExcel(model.result, model.date1904));
break;
// case Enums.ValueType.Hyperlink: // ??
// case Enums.ValueType.Formula:
default:
throw new Error('I could not understand type of value');
}
}
render(xmlStream, model) {
if (model.type === Enums.ValueType.Null && !model.styleId) {
// if null and no style, exit
return;
}
xmlStream.openNode('c');
xmlStream.addAttribute('r', model.address);
if (model.styleId) {
xmlStream.addAttribute('s', model.styleId);
}
switch (model.type) {
case Enums.ValueType.Null:
break;
case Enums.ValueType.Number:
xmlStream.leafNode('v', null, model.value);
break;
case Enums.ValueType.Boolean:
xmlStream.addAttribute('t', 'b');
xmlStream.leafNode('v', null, model.value ? '1' : '0');
break;
case Enums.ValueType.Error:
xmlStream.addAttribute('t', 'e');
xmlStream.leafNode('v', null, model.value.error);
break;
case Enums.ValueType.String:
case Enums.ValueType.RichText:
if (model.ssId !== undefined) {
xmlStream.addAttribute('t', 's');
xmlStream.leafNode('v', null, model.ssId);
} else if (model.value && model.value.richText) {
xmlStream.addAttribute('t', 'inlineStr');
xmlStream.openNode('is');
model.value.richText.forEach(text => {
this.richTextXForm.render(xmlStream, text);
});
xmlStream.closeNode('is');
} else {
xmlStream.addAttribute('t', 'str');
xmlStream.leafNode('v', null, model.value);
}
break;
case Enums.ValueType.Date:
xmlStream.leafNode('v', null, utils.dateToExcel(model.value, model.date1904));
break;
case Enums.ValueType.Hyperlink:
if (model.ssId !== undefined) {
xmlStream.addAttribute('t', 's');
xmlStream.leafNode('v', null, model.ssId);
} else {
xmlStream.addAttribute('t', 'str');
xmlStream.leafNode('v', null, model.text);
}
break;
case Enums.ValueType.Formula:
this.renderFormula(xmlStream, model);
break;
case Enums.ValueType.Merge:
// nothing to add
break;
default:
break;
}
xmlStream.closeNode(); // </c>
}
parseOpen(node) {
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
switch (node.name) {
case 'c':
// const address = colCache.decodeAddress(node.attributes.r);
this.model = {
address: node.attributes.r,
};
this.t = node.attributes.t;
if (node.attributes.s) {
this.model.styleId = parseInt(node.attributes.s, 10);
}
return true;
case 'f':
this.currentNode = 'f';
this.model.si = node.attributes.si;
this.model.shareType = node.attributes.t;
this.model.ref = node.attributes.ref;
return true;
case 'v':
this.currentNode = 'v';
return true;
case 't':
this.currentNode = 't';
return true;
case 'r':
this.parser = this.richTextXForm;
this.parser.parseOpen(node);
return true;
default:
return false;
}
}
parseText(text) {
if (this.parser) {
this.parser.parseText(text);
return;
}
switch (this.currentNode) {
case 'f':
this.model.formula = this.model.formula ? this.model.formula + text : text;
break;
case 'v':
case 't':
if (this.model.value && this.model.value.richText) {
this.model.value.richText.text = this.model.value.richText.text
? this.model.value.richText.text + text
: text;
} else {
this.model.value = this.model.value ? this.model.value + text : text;
}
break;
default:
break;
}
}
parseClose(name) {
switch (name) {
case 'c': {
const {model} = this;
// first guess on cell type
if (model.formula || model.shareType) {
model.type = Enums.ValueType.Formula;
if (model.value) {
if (this.t === 'str') {
model.result = utils.xmlDecode(model.value);
} else if (this.t === 'b') {
model.result = parseInt(model.value, 10) !== 0;
} else if (this.t === 'e') {
model.result = {error: model.value};
} else {
model.result = parseFloat(model.value);
}
model.value = undefined;
}
} else if (model.value !== undefined) {
switch (this.t) {
case 's':
model.type = Enums.ValueType.String;
model.value = parseInt(model.value, 10);
break;
case 'str':
model.type = Enums.ValueType.String;
model.value = utils.xmlDecode(model.value);
break;
case 'inlineStr':
model.type = Enums.ValueType.String;
break;
case 'b':
model.type = Enums.ValueType.Boolean;
model.value = parseInt(model.value, 10) !== 0;
break;
case 'e':
model.type = Enums.ValueType.Error;
model.value = {error: model.value};
break;
default:
model.type = Enums.ValueType.Number;
model.value = parseFloat(model.value);
break;
}
} else if (model.styleId) {
model.type = Enums.ValueType.Null;
} else {
model.type = Enums.ValueType.Merge;
}
return false;
}
case 'f':
case 'v':
case 'is':
this.currentNode = undefined;
return true;
case 't':
if (this.parser) {
this.parser.parseClose(name);
return true;
}
this.currentNode = undefined;
return true;
case 'r':
this.model.value = this.model.value || {};
this.model.value.richText = this.model.value.richText || [];
this.model.value.richText.push(this.parser.model);
this.parser = undefined;
this.currentNode = undefined;
return true;
default:
if (this.parser) {
this.parser.parseClose(name);
return true;
}
return false;
}
}
reconcile(model, options) {
const style = model.styleId && options.styles && options.styles.getStyleModel(model.styleId);
if (style) {
model.style = style;
}
if (model.styleId !== undefined) {
model.styleId = undefined;
}
switch (model.type) {
case Enums.ValueType.String:
if (typeof model.value === 'number') {
if (options.sharedStrings) {
model.value = options.sharedStrings.getString(model.value);
}
}
if (model.value.richText) {
model.type = Enums.ValueType.RichText;
}
break;
case Enums.ValueType.Number:
if (style && utils.isDateFmt(style.numFmt)) {
model.type = Enums.ValueType.Date;
model.value = utils.excelToDate(model.value, options.date1904);
}
break;
case Enums.ValueType.Formula:
if (model.result !== undefined && style && utils.isDateFmt(style.numFmt)) {
model.result = utils.excelToDate(model.result, options.date1904);
}
if (model.shareType === 'shared') {
if (model.ref) {
// master
options.formulae[model.si] = model.address;
} else {
// slave
model.sharedFormula = options.formulae[model.si];
delete model.shareType;
}
delete model.si;
}
break;
default:
break;
}
// look for hyperlink
const hyperlink = options.hyperlinkMap[model.address];
if (hyperlink) {
if (model.type === Enums.ValueType.Formula) {
model.text = model.result;
model.result = undefined;
} else {
model.text = model.value;
model.value = undefined;
}
model.type = Enums.ValueType.Hyperlink;
model.hyperlink = hyperlink;
}
const comment = options.commentsMap && options.commentsMap[model.address];
if (comment) {
model.comment = comment;
}
}
}
module.exports = CellXform;

View File

@@ -0,0 +1,27 @@
const BaseXform = require('../../base-xform');
class CfIconExtXform extends BaseXform {
get tag() {
return 'x14:cfIcon';
}
render(xmlStream, model) {
xmlStream.leafNode(this.tag, {
iconSet: model.iconSet,
iconId: model.iconId,
});
}
parseOpen({attributes}) {
this.model = {
iconSet: attributes.iconSet,
iconId: BaseXform.toIntValue(attributes.iconId),
};
}
parseClose(name) {
return name !== this.tag;
}
}
module.exports = CfIconExtXform;

View File

@@ -0,0 +1,98 @@
const {v4: uuidv4} = require('uuid');
const BaseXform = require('../../base-xform');
const CompositeXform = require('../../composite-xform');
const DatabarExtXform = require('./databar-ext-xform');
const IconSetExtXform = require('./icon-set-ext-xform');
const extIcons = {
'3Triangles': true,
'3Stars': true,
'5Boxes': true,
};
class CfRuleExtXform extends CompositeXform {
constructor() {
super();
this.map = {
'x14:dataBar': (this.databarXform = new DatabarExtXform()),
'x14:iconSet': (this.iconSetXform = new IconSetExtXform()),
};
}
get tag() {
return 'x14:cfRule';
}
static isExt(rule) {
// is this rule primitive?
if (rule.type === 'dataBar') {
return DatabarExtXform.isExt(rule);
}
if (rule.type === 'iconSet') {
if (rule.custom || extIcons[rule.iconSet]) {
return true;
}
}
return false;
}
prepare(model) {
if (CfRuleExtXform.isExt(model)) {
model.x14Id = `{${uuidv4()}}`.toUpperCase();
}
}
render(xmlStream, model) {
if (!CfRuleExtXform.isExt(model)) {
return;
}
switch (model.type) {
case 'dataBar':
this.renderDataBar(xmlStream, model);
break;
case 'iconSet':
this.renderIconSet(xmlStream, model);
break;
}
}
renderDataBar(xmlStream, model) {
xmlStream.openNode(this.tag, {
type: 'dataBar',
id: model.x14Id,
});
this.databarXform.render(xmlStream, model);
xmlStream.closeNode();
}
renderIconSet(xmlStream, model) {
xmlStream.openNode(this.tag, {
type: 'iconSet',
priority: model.priority,
id: model.x14Id || `{${uuidv4()}}`,
});
this.iconSetXform.render(xmlStream, model);
xmlStream.closeNode();
}
createNewModel({attributes}) {
return {
type: attributes.type,
x14Id: attributes.id,
priority: BaseXform.toIntValue(attributes.priority),
};
}
onParserClose(name, parser) {
Object.assign(this.model, parser.model);
}
}
module.exports = CfRuleExtXform;

Some files were not shown because too many files have changed in this diff Show More