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,22 @@
The MIT License (MIT)
Copyright (c) 2014-2019 Guyon Roche
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,13 @@
/**
* Copyright (c) 2014-2019 Guyon Roche
* LICENCE: MIT - please refer to LICENSE file included with this module
* or https://github.com/exceljs/exceljs/blob/master/LICENSE
*/
if (parseInt(process.versions.node.split('.')[0], 10) < 10) {
throw new Error(
'For node versions older than 10, please use the ES5 Import: https://github.com/exceljs/exceljs#es5-imports'
);
}
module.exports = require('./lib/exceljs.nodejs.js');

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
import * as ExcelJS from './dist/es5';
export default ExcelJS;

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'}],
},
],
};

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