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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,43 @@
const CompositeXform = require('../../composite-xform');
const FExtXform = require('./f-ext-xform');
class CfvoExtXform extends CompositeXform {
constructor() {
super();
this.map = {
'xm:f': (this.fExtXform = new FExtXform()),
};
}
get tag() {
return 'x14:cfvo';
}
render(xmlStream, model) {
xmlStream.openNode(this.tag, {
type: model.type,
});
if (model.value !== undefined) {
this.fExtXform.render(xmlStream, model.value);
}
xmlStream.closeNode();
}
createNewModel(node) {
return {
type: node.attributes.type,
};
}
onParserClose(name, parser) {
switch (name) {
case 'xm:f':
this.model.value = parser.model ? parseFloat(parser.model) : 0;
break;
}
}
}
module.exports = CfvoExtXform;

View File

@@ -0,0 +1,62 @@
const CompositeXform = require('../../composite-xform');
const SqRefExtXform = require('./sqref-ext-xform');
const CfRuleExtXform = require('./cf-rule-ext-xform');
class ConditionalFormattingExtXform extends CompositeXform {
constructor() {
super();
this.map = {
'xm:sqref': (this.sqRef = new SqRefExtXform()),
'x14:cfRule': (this.cfRule = new CfRuleExtXform()),
};
}
get tag() {
return 'x14:conditionalFormatting';
}
prepare(model, options) {
model.rules.forEach(rule => {
this.cfRule.prepare(rule, options);
});
}
render(xmlStream, model) {
if (!model.rules.some(CfRuleExtXform.isExt)) {
return;
}
xmlStream.openNode(this.tag, {
'xmlns:xm': 'http://schemas.microsoft.com/office/excel/2006/main',
});
model.rules.filter(CfRuleExtXform.isExt).forEach(rule => this.cfRule.render(xmlStream, rule));
// for some odd reason, Excel needs the <xm:sqref> node to be after the rules
this.sqRef.render(xmlStream, model.ref);
xmlStream.closeNode();
}
createNewModel() {
return {
rules: [],
};
}
onParserClose(name, parser) {
switch (name) {
case 'xm:sqref':
this.model.ref = parser.model;
break;
case 'x14:cfRule':
this.model.rules.push(parser.model);
break;
}
}
}
module.exports = ConditionalFormattingExtXform;

View File

@@ -0,0 +1,50 @@
const CompositeXform = require('../../composite-xform');
const CfRuleExtXform = require('./cf-rule-ext-xform');
const ConditionalFormattingExtXform = require('./conditional-formatting-ext-xform');
class ConditionalFormattingsExtXform extends CompositeXform {
constructor() {
super();
this.map = {
'x14:conditionalFormatting': (this.cfXform = new ConditionalFormattingExtXform()),
};
}
get tag() {
return 'x14:conditionalFormattings';
}
hasContent(model) {
if (model.hasExtContent === undefined) {
model.hasExtContent = model.some(cf => cf.rules.some(CfRuleExtXform.isExt));
}
return model.hasExtContent;
}
prepare(model, options) {
model.forEach(cf => {
this.cfXform.prepare(cf, options);
});
}
render(xmlStream, model) {
if (this.hasContent(model)) {
xmlStream.openNode(this.tag);
model.forEach(cf => this.cfXform.render(xmlStream, cf));
xmlStream.closeNode();
}
}
createNewModel() {
return [];
}
onParserClose(name, parser) {
// model is array of conditional formatting objects
this.model.push(parser.model);
}
}
module.exports = ConditionalFormattingsExtXform;

View File

@@ -0,0 +1,98 @@
const BaseXform = require('../../base-xform');
const CompositeXform = require('../../composite-xform');
const ColorXform = require('../../style/color-xform');
const CfvoExtXform = require('./cfvo-ext-xform');
class DatabarExtXform extends CompositeXform {
constructor() {
super();
this.map = {
'x14:cfvo': (this.cfvoXform = new CfvoExtXform()),
'x14:borderColor': (this.borderColorXform = new ColorXform('x14:borderColor')),
'x14:negativeBorderColor': (this.negativeBorderColorXform = new ColorXform(
'x14:negativeBorderColor'
)),
'x14:negativeFillColor': (this.negativeFillColorXform = new ColorXform(
'x14:negativeFillColor'
)),
'x14:axisColor': (this.axisColorXform = new ColorXform('x14:axisColor')),
};
}
static isExt(rule) {
// not all databars need ext
// TODO: refine this
return !rule.gradient;
}
get tag() {
return 'x14:dataBar';
}
render(xmlStream, model) {
xmlStream.openNode(this.tag, {
minLength: BaseXform.toIntAttribute(model.minLength, 0, true),
maxLength: BaseXform.toIntAttribute(model.maxLength, 100, true),
border: BaseXform.toBoolAttribute(model.border, false),
gradient: BaseXform.toBoolAttribute(model.gradient, true),
negativeBarColorSameAsPositive: BaseXform.toBoolAttribute(
model.negativeBarColorSameAsPositive,
true
),
negativeBarBorderColorSameAsPositive: BaseXform.toBoolAttribute(
model.negativeBarBorderColorSameAsPositive,
true
),
axisPosition: BaseXform.toAttribute(model.axisPosition, 'auto'),
direction: BaseXform.toAttribute(model.direction, 'leftToRight'),
});
model.cfvo.forEach(cfvo => {
this.cfvoXform.render(xmlStream, cfvo);
});
this.borderColorXform.render(xmlStream, model.borderColor);
this.negativeBorderColorXform.render(xmlStream, model.negativeBorderColor);
this.negativeFillColorXform.render(xmlStream, model.negativeFillColor);
this.axisColorXform.render(xmlStream, model.axisColor);
xmlStream.closeNode();
}
createNewModel({attributes}) {
return {
cfvo: [],
minLength: BaseXform.toIntValue(attributes.minLength, 0),
maxLength: BaseXform.toIntValue(attributes.maxLength, 100),
border: BaseXform.toBoolValue(attributes.border, false),
gradient: BaseXform.toBoolValue(attributes.gradient, true),
negativeBarColorSameAsPositive: BaseXform.toBoolValue(
attributes.negativeBarColorSameAsPositive,
true
),
negativeBarBorderColorSameAsPositive: BaseXform.toBoolValue(
attributes.negativeBarBorderColorSameAsPositive,
true
),
axisPosition: BaseXform.toStringValue(attributes.axisPosition, 'auto'),
direction: BaseXform.toStringValue(attributes.direction, 'leftToRight'),
};
}
onParserClose(name, parser) {
const [, prop] = name.split(':');
switch (prop) {
case 'cfvo':
this.model.cfvo.push(parser.model);
break;
default:
this.model[prop] = parser.model;
break;
}
}
}
module.exports = DatabarExtXform;

View File

@@ -0,0 +1,25 @@
const BaseXform = require('../../base-xform');
class FExtXform extends BaseXform {
get tag() {
return 'xm:f';
}
render(xmlStream, model) {
xmlStream.leafNode(this.tag, null, model);
}
parseOpen() {
this.model = '';
}
parseText(text) {
this.model += text;
}
parseClose(name) {
return name !== this.tag;
}
}
module.exports = FExtXform;

View File

@@ -0,0 +1,73 @@
const BaseXform = require('../../base-xform');
const CompositeXform = require('../../composite-xform');
const CfvoExtXform = require('./cfvo-ext-xform');
const CfIconExtXform = require('./cf-icon-ext-xform');
class IconSetExtXform extends CompositeXform {
constructor() {
super();
this.map = {
'x14:cfvo': (this.cfvoXform = new CfvoExtXform()),
'x14:cfIcon': (this.cfIconXform = new CfIconExtXform()),
};
}
get tag() {
return 'x14:iconSet';
}
render(xmlStream, model) {
xmlStream.openNode(this.tag, {
iconSet: BaseXform.toStringAttribute(model.iconSet),
reverse: BaseXform.toBoolAttribute(model.reverse, false),
showValue: BaseXform.toBoolAttribute(model.showValue, true),
custom: BaseXform.toBoolAttribute(model.icons, false),
});
model.cfvo.forEach(cfvo => {
this.cfvoXform.render(xmlStream, cfvo);
});
if (model.icons) {
model.icons.forEach((icon, i) => {
icon.iconId = i;
this.cfIconXform.render(xmlStream, icon);
});
}
xmlStream.closeNode();
}
createNewModel({attributes}) {
return {
cfvo: [],
iconSet: BaseXform.toStringValue(attributes.iconSet, '3TrafficLights'),
reverse: BaseXform.toBoolValue(attributes.reverse, false),
showValue: BaseXform.toBoolValue(attributes.showValue, true),
};
}
onParserClose(name, parser) {
const [, prop] = name.split(':');
switch (prop) {
case 'cfvo':
this.model.cfvo.push(parser.model);
break;
case 'cfIcon':
if (!this.model.icons) {
this.model.icons = [];
}
this.model.icons.push(parser.model);
break;
default:
this.model[prop] = parser.model;
break;
}
}
}
module.exports = IconSetExtXform;

View File

@@ -0,0 +1,25 @@
const BaseXform = require('../../base-xform');
class SqrefExtXform extends BaseXform {
get tag() {
return 'xm:sqref';
}
render(xmlStream, model) {
xmlStream.leafNode(this.tag, null, model);
}
parseOpen() {
this.model = '';
}
parseText(text) {
this.model += text;
}
parseClose(name) {
return name !== this.tag;
}
}
module.exports = SqrefExtXform;

View File

@@ -0,0 +1,301 @@
const BaseXform = require('../../base-xform');
const CompositeXform = require('../../composite-xform');
const Range = require('../../../../doc/range');
const DatabarXform = require('./databar-xform');
const ExtLstRefXform = require('./ext-lst-ref-xform');
const FormulaXform = require('./formula-xform');
const ColorScaleXform = require('./color-scale-xform');
const IconSetXform = require('./icon-set-xform');
const extIcons = {
'3Triangles': true,
'3Stars': true,
'5Boxes': true,
};
const getTextFormula = model => {
if (model.formulae && model.formulae[0]) {
return model.formulae[0];
}
const range = new Range(model.ref);
const {tl} = range;
switch (model.operator) {
case 'containsText':
return `NOT(ISERROR(SEARCH("${model.text}",${tl})))`;
case 'containsBlanks':
return `LEN(TRIM(${tl}))=0`;
case 'notContainsBlanks':
return `LEN(TRIM(${tl}))>0`;
case 'containsErrors':
return `ISERROR(${tl})`;
case 'notContainsErrors':
return `NOT(ISERROR(${tl}))`;
default:
return undefined;
}
};
const getTimePeriodFormula = model => {
if (model.formulae && model.formulae[0]) {
return model.formulae[0];
}
const range = new Range(model.ref);
const {tl} = range;
switch (model.timePeriod) {
case 'thisWeek':
return `AND(TODAY()-ROUNDDOWN(${tl},0)<=WEEKDAY(TODAY())-1,ROUNDDOWN(${tl},0)-TODAY()<=7-WEEKDAY(TODAY()))`;
case 'lastWeek':
return `AND(TODAY()-ROUNDDOWN(${tl},0)>=(WEEKDAY(TODAY())),TODAY()-ROUNDDOWN(${tl},0)<(WEEKDAY(TODAY())+7))`;
case 'nextWeek':
return `AND(ROUNDDOWN(${tl},0)-TODAY()>(7-WEEKDAY(TODAY())),ROUNDDOWN(${tl},0)-TODAY()<(15-WEEKDAY(TODAY())))`;
case 'yesterday':
return `FLOOR(${tl},1)=TODAY()-1`;
case 'today':
return `FLOOR(${tl},1)=TODAY()`;
case 'tomorrow':
return `FLOOR(${tl},1)=TODAY()+1`;
case 'last7Days':
return `AND(TODAY()-FLOOR(${tl},1)<=6,FLOOR(${tl},1)<=TODAY())`;
case 'lastMonth':
return `AND(MONTH(${tl})=MONTH(EDATE(TODAY(),0-1)),YEAR(${tl})=YEAR(EDATE(TODAY(),0-1)))`;
case 'thisMonth':
return `AND(MONTH(${tl})=MONTH(TODAY()),YEAR(${tl})=YEAR(TODAY()))`;
case 'nextMonth':
return `AND(MONTH(${tl})=MONTH(EDATE(TODAY(),0+1)),YEAR(${tl})=YEAR(EDATE(TODAY(),0+1)))`;
default:
return undefined;
}
};
const opType = attributes => {
const {type, operator} = attributes;
switch (type) {
case 'containsText':
case 'containsBlanks':
case 'notContainsBlanks':
case 'containsErrors':
case 'notContainsErrors':
return {
type: 'containsText',
operator: type,
};
default:
return {type, operator};
}
};
class CfRuleXform extends CompositeXform {
constructor() {
super();
this.map = {
dataBar: (this.databarXform = new DatabarXform()),
extLst: (this.extLstRefXform = new ExtLstRefXform()),
formula: (this.formulaXform = new FormulaXform()),
colorScale: (this.colorScaleXform = new ColorScaleXform()),
iconSet: (this.iconSetXform = new IconSetXform()),
};
}
get tag() {
return 'cfRule';
}
static isPrimitive(rule) {
// is this rule primitive?
if (rule.type === 'iconSet') {
if (rule.custom || extIcons[rule.iconSet]) {
return false;
}
}
return true;
}
render(xmlStream, model) {
switch (model.type) {
case 'expression':
this.renderExpression(xmlStream, model);
break;
case 'cellIs':
this.renderCellIs(xmlStream, model);
break;
case 'top10':
this.renderTop10(xmlStream, model);
break;
case 'aboveAverage':
this.renderAboveAverage(xmlStream, model);
break;
case 'dataBar':
this.renderDataBar(xmlStream, model);
break;
case 'colorScale':
this.renderColorScale(xmlStream, model);
break;
case 'iconSet':
this.renderIconSet(xmlStream, model);
break;
case 'containsText':
this.renderText(xmlStream, model);
break;
case 'timePeriod':
this.renderTimePeriod(xmlStream, model);
break;
}
}
renderExpression(xmlStream, model) {
xmlStream.openNode(this.tag, {
type: 'expression',
dxfId: model.dxfId,
priority: model.priority,
});
this.formulaXform.render(xmlStream, model.formulae[0]);
xmlStream.closeNode();
}
renderCellIs(xmlStream, model) {
xmlStream.openNode(this.tag, {
type: 'cellIs',
dxfId: model.dxfId,
priority: model.priority,
operator: model.operator,
});
model.formulae.forEach(formula => {
this.formulaXform.render(xmlStream, formula);
});
xmlStream.closeNode();
}
renderTop10(xmlStream, model) {
xmlStream.leafNode(this.tag, {
type: 'top10',
dxfId: model.dxfId,
priority: model.priority,
percent: BaseXform.toBoolAttribute(model.percent, false),
bottom: BaseXform.toBoolAttribute(model.bottom, false),
rank: BaseXform.toIntValue(model.rank, 10, true),
});
}
renderAboveAverage(xmlStream, model) {
xmlStream.leafNode(this.tag, {
type: 'aboveAverage',
dxfId: model.dxfId,
priority: model.priority,
aboveAverage: BaseXform.toBoolAttribute(model.aboveAverage, true),
});
}
renderDataBar(xmlStream, model) {
xmlStream.openNode(this.tag, {
type: 'dataBar',
priority: model.priority,
});
this.databarXform.render(xmlStream, model);
this.extLstRefXform.render(xmlStream, model);
xmlStream.closeNode();
}
renderColorScale(xmlStream, model) {
xmlStream.openNode(this.tag, {
type: 'colorScale',
priority: model.priority,
});
this.colorScaleXform.render(xmlStream, model);
xmlStream.closeNode();
}
renderIconSet(xmlStream, model) {
// iconset is all primitive or all extLst
if (!CfRuleXform.isPrimitive(model)) {
return;
}
xmlStream.openNode(this.tag, {
type: 'iconSet',
priority: model.priority,
});
this.iconSetXform.render(xmlStream, model);
xmlStream.closeNode();
}
renderText(xmlStream, model) {
xmlStream.openNode(this.tag, {
type: model.operator,
dxfId: model.dxfId,
priority: model.priority,
operator: BaseXform.toStringAttribute(model.operator, 'containsText'),
});
const formula = getTextFormula(model);
if (formula) {
this.formulaXform.render(xmlStream, formula);
}
xmlStream.closeNode();
}
renderTimePeriod(xmlStream, model) {
xmlStream.openNode(this.tag, {
type: 'timePeriod',
dxfId: model.dxfId,
priority: model.priority,
timePeriod: model.timePeriod,
});
const formula = getTimePeriodFormula(model);
if (formula) {
this.formulaXform.render(xmlStream, formula);
}
xmlStream.closeNode();
}
createNewModel({attributes}) {
return {
...opType(attributes),
dxfId: BaseXform.toIntValue(attributes.dxfId),
priority: BaseXform.toIntValue(attributes.priority),
timePeriod: attributes.timePeriod,
percent: BaseXform.toBoolValue(attributes.percent),
bottom: BaseXform.toBoolValue(attributes.bottom),
rank: BaseXform.toIntValue(attributes.rank),
aboveAverage: BaseXform.toBoolValue(attributes.aboveAverage),
};
}
onParserClose(name, parser) {
switch (name) {
case 'dataBar':
case 'extLst':
case 'colorScale':
case 'iconSet':
// merge parser model with ours
Object.assign(this.model, parser.model);
break;
case 'formula':
// except - formula is a string and appends to formulae
this.model.formulae = this.model.formulae || [];
this.model.formulae.push(parser.model);
break;
}
}
}
module.exports = CfRuleXform;

View File

@@ -0,0 +1,27 @@
const BaseXform = require('../../base-xform');
class CfvoXform extends BaseXform {
get tag() {
return 'cfvo';
}
render(xmlStream, model) {
xmlStream.leafNode(this.tag, {
type: model.type,
val: model.value,
});
}
parseOpen(node) {
this.model = {
type: node.attributes.type,
value: BaseXform.toFloatValue(node.attributes.val),
};
}
parseClose(name) {
return name !== this.tag;
}
}
module.exports = CfvoXform;

View File

@@ -0,0 +1,45 @@
const CompositeXform = require('../../composite-xform');
const ColorXform = require('../../style/color-xform');
const CfvoXform = require('./cfvo-xform');
class ColorScaleXform extends CompositeXform {
constructor() {
super();
this.map = {
cfvo: (this.cfvoXform = new CfvoXform()),
color: (this.colorXform = new ColorXform()),
};
}
get tag() {
return 'colorScale';
}
render(xmlStream, model) {
xmlStream.openNode(this.tag);
model.cfvo.forEach(cfvo => {
this.cfvoXform.render(xmlStream, cfvo);
});
model.color.forEach(color => {
this.colorXform.render(xmlStream, color);
});
xmlStream.closeNode();
}
createNewModel(node) {
return {
cfvo: [],
color: [],
};
}
onParserClose(name, parser) {
this.model[name].push(parser.model);
}
}
module.exports = ColorScaleXform;

View File

@@ -0,0 +1,48 @@
const CompositeXform = require('../../composite-xform');
const CfRuleXform = require('./cf-rule-xform');
class ConditionalFormattingXform extends CompositeXform {
constructor() {
super();
this.map = {
cfRule: new CfRuleXform(),
};
}
get tag() {
return 'conditionalFormatting';
}
render(xmlStream, model) {
// if there are no primitive rules, exit now
if (!model.rules.some(CfRuleXform.isPrimitive)) {
return;
}
xmlStream.openNode(this.tag, {sqref: model.ref});
model.rules.forEach(rule => {
if (CfRuleXform.isPrimitive(rule)) {
rule.ref = model.ref;
this.map.cfRule.render(xmlStream, rule);
}
});
xmlStream.closeNode();
}
createNewModel({attributes}) {
return {
ref: attributes.sqref,
rules: [],
};
}
onParserClose(name, parser) {
this.model.rules.push(parser.model);
}
}
module.exports = ConditionalFormattingXform;

View File

@@ -0,0 +1,92 @@
const BaseXform = require('../../base-xform');
const ConditionalFormattingXform = require('./conditional-formatting-xform');
class ConditionalFormattingsXform extends BaseXform {
constructor() {
super();
this.cfXform = new ConditionalFormattingXform();
}
get tag() {
return 'conditionalFormatting';
}
reset() {
this.model = [];
}
prepare(model, options) {
// ensure each rule has a priority value
let nextPriority = model.reduce(
(p, cf) => Math.max(p, ...cf.rules.map(rule => rule.priority || 0)),
1
);
model.forEach(cf => {
cf.rules.forEach(rule => {
if (!rule.priority) {
rule.priority = nextPriority++;
}
if (rule.style) {
rule.dxfId = options.styles.addDxfStyle(rule.style);
}
});
});
}
render(xmlStream, model) {
model.forEach(cf => {
this.cfXform.render(xmlStream, cf);
});
}
parseOpen(node) {
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
switch (node.name) {
case 'conditionalFormatting':
this.parser = this.cfXform;
this.parser.parseOpen(node);
return true;
default:
return false;
}
}
parseText(text) {
if (this.parser) {
this.parser.parseText(text);
}
}
parseClose(name) {
if (this.parser) {
if (!this.parser.parseClose(name)) {
this.model.push(this.parser.model);
this.parser = undefined;
return false;
}
return true;
}
return false;
}
reconcile(model, options) {
model.forEach(cf => {
cf.rules.forEach(rule => {
if (rule.dxfId !== undefined) {
rule.style = options.styles.getDxfStyle(rule.dxfId);
delete rule.dxfId;
}
});
});
}
}
module.exports = ConditionalFormattingsXform;

View File

@@ -0,0 +1,49 @@
const CompositeXform = require('../../composite-xform');
const ColorXform = require('../../style/color-xform');
const CfvoXform = require('./cfvo-xform');
class DatabarXform extends CompositeXform {
constructor() {
super();
this.map = {
cfvo: (this.cfvoXform = new CfvoXform()),
color: (this.colorXform = new ColorXform()),
};
}
get tag() {
return 'dataBar';
}
render(xmlStream, model) {
xmlStream.openNode(this.tag);
model.cfvo.forEach(cfvo => {
this.cfvoXform.render(xmlStream, cfvo);
});
this.colorXform.render(xmlStream, model.color);
xmlStream.closeNode();
}
createNewModel() {
return {
cfvo: [],
};
}
onParserClose(name, parser) {
switch (name) {
case 'cfvo':
this.model.cfvo.push(parser.model);
break;
case 'color':
this.model.color = parser.model;
break;
}
}
}
module.exports = DatabarXform;

View File

@@ -0,0 +1,87 @@
/* eslint-disable max-classes-per-file */
const BaseXform = require('../../base-xform');
const CompositeXform = require('../../composite-xform');
class X14IdXform extends BaseXform {
get tag() {
return 'x14:id';
}
render(xmlStream, model) {
xmlStream.leafNode(this.tag, null, model);
}
parseOpen() {
this.model = '';
}
parseText(text) {
this.model += text;
}
parseClose(name) {
return name !== this.tag;
}
}
class ExtXform extends CompositeXform {
constructor() {
super();
this.map = {
'x14:id': (this.idXform = new X14IdXform()),
};
}
get tag() {
return 'ext';
}
render(xmlStream, model) {
xmlStream.openNode(this.tag, {
uri: '{B025F937-C7B1-47D3-B67F-A62EFF666E3E}',
'xmlns:x14': 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/main',
});
this.idXform.render(xmlStream, model.x14Id);
xmlStream.closeNode();
}
createNewModel() {
return {};
}
onParserClose(name, parser) {
this.model.x14Id = parser.model;
}
}
class ExtLstRefXform extends CompositeXform {
constructor() {
super();
this.map = {
ext: new ExtXform(),
};
}
get tag() {
return 'extLst';
}
render(xmlStream, model) {
xmlStream.openNode(this.tag);
this.map.ext.render(xmlStream, model);
xmlStream.closeNode();
}
createNewModel() {
return {};
}
onParserClose(name, parser) {
Object.assign(this.model, parser.model);
}
}
module.exports = ExtLstRefXform;

View File

@@ -0,0 +1,25 @@
const BaseXform = require('../../base-xform');
class FormulaXform extends BaseXform {
get tag() {
return 'formula';
}
render(xmlStream, model) {
xmlStream.leafNode(this.tag, null, model);
}
parseOpen() {
this.model = '';
}
parseText(text) {
this.model += text;
}
parseClose(name) {
return name !== this.tag;
}
}
module.exports = FormulaXform;

View File

@@ -0,0 +1,47 @@
const BaseXform = require('../../base-xform');
const CompositeXform = require('../../composite-xform');
const CfvoXform = require('./cfvo-xform');
class IconSetXform extends CompositeXform {
constructor() {
super();
this.map = {
cfvo: (this.cfvoXform = new CfvoXform()),
};
}
get tag() {
return 'iconSet';
}
render(xmlStream, model) {
xmlStream.openNode(this.tag, {
iconSet: BaseXform.toStringAttribute(model.iconSet, '3TrafficLights'),
reverse: BaseXform.toBoolAttribute(model.reverse, false),
showValue: BaseXform.toBoolAttribute(model.showValue, true),
});
model.cfvo.forEach(cfvo => {
this.cfvoXform.render(xmlStream, cfvo);
});
xmlStream.closeNode();
}
createNewModel({attributes}) {
return {
iconSet: BaseXform.toStringValue(attributes.iconSet, '3TrafficLights'),
reverse: BaseXform.toBoolValue(attributes.reverse),
showValue: BaseXform.toBoolValue(attributes.showValue),
cfvo: [],
};
}
onParserClose(name, parser) {
this.model[name].push(parser.model);
}
}
module.exports = IconSetXform;

View File

@@ -0,0 +1,86 @@
const utils = require('../../../utils/utils');
const BaseXform = require('../base-xform');
class ColXform extends BaseXform {
get tag() {
return 'col';
}
prepare(model, options) {
const styleId = options.styles.addStyleModel(model.style || {});
if (styleId) {
model.styleId = styleId;
}
}
render(xmlStream, model) {
xmlStream.openNode('col');
xmlStream.addAttribute('min', model.min);
xmlStream.addAttribute('max', model.max);
if (model.width) {
xmlStream.addAttribute('width', model.width);
}
if (model.styleId) {
xmlStream.addAttribute('style', model.styleId);
}
if (model.hidden) {
xmlStream.addAttribute('hidden', '1');
}
if (model.bestFit) {
xmlStream.addAttribute('bestFit', '1');
}
if (model.outlineLevel) {
xmlStream.addAttribute('outlineLevel', model.outlineLevel);
}
if (model.collapsed) {
xmlStream.addAttribute('collapsed', '1');
}
xmlStream.addAttribute('customWidth', '1');
xmlStream.closeNode();
}
parseOpen(node) {
if (node.name === 'col') {
const model = (this.model = {
min: parseInt(node.attributes.min || '0', 10),
max: parseInt(node.attributes.max || '0', 10),
width:
node.attributes.width === undefined
? undefined
: parseFloat(node.attributes.width || '0'),
});
if (node.attributes.style) {
model.styleId = parseInt(node.attributes.style, 10);
}
if (utils.parseBoolean(node.attributes.hidden)) {
model.hidden = true;
}
if (utils.parseBoolean(node.attributes.bestFit)) {
model.bestFit = true;
}
if (node.attributes.outlineLevel) {
model.outlineLevel = parseInt(node.attributes.outlineLevel, 10);
}
if (utils.parseBoolean(node.attributes.collapsed)) {
model.collapsed = true;
}
return true;
}
return false;
}
parseText() {}
parseClose() {
return false;
}
reconcile(model, options) {
// reconcile column styles
if (model.styleId) {
model.style = options.styles.getStyleModel(model.styleId);
}
}
}
module.exports = ColXform;

View File

@@ -0,0 +1,257 @@
const _ = require('../../../utils/under-dash');
const utils = require('../../../utils/utils');
const colCache = require('../../../utils/col-cache');
const BaseXform = require('../base-xform');
const Range = require('../../../doc/range');
function assign(definedName, attributes, name, defaultValue) {
const value = attributes[name];
if (value !== undefined) {
definedName[name] = value;
} else if (defaultValue !== undefined) {
definedName[name] = defaultValue;
}
}
function assignBool(definedName, attributes, name, defaultValue) {
const value = attributes[name];
if (value !== undefined) {
definedName[name] = utils.parseBoolean(value);
} else if (defaultValue !== undefined) {
definedName[name] = defaultValue;
}
}
function optimiseDataValidations(model) {
// Squeeze alike data validations together into rectangular ranges
// to reduce file size and speed up Excel load time
const dvList = _.map(model, (dataValidation, address) => ({
address,
dataValidation,
marked: false,
})).sort((a, b) => _.strcmp(a.address, b.address));
const dvMap = _.keyBy(dvList, 'address');
const matchCol = (addr, height, col) => {
for (let i = 0; i < height; i++) {
const otherAddress = colCache.encodeAddress(addr.row + i, col);
if (!model[otherAddress] || !_.isEqual(model[addr.address], model[otherAddress])) {
return false;
}
}
return true;
};
return dvList
.map(dv => {
if (!dv.marked) {
const addr = colCache.decodeEx(dv.address);
if (addr.dimensions) {
dvMap[addr.dimensions].marked = true;
return {
...dv.dataValidation,
sqref: dv.address,
};
}
// iterate downwards - finding matching cells
let height = 1;
let otherAddress = colCache.encodeAddress(addr.row + height, addr.col);
while (model[otherAddress] && _.isEqual(dv.dataValidation, model[otherAddress])) {
height++;
otherAddress = colCache.encodeAddress(addr.row + height, addr.col);
}
// iterate rightwards...
let width = 1;
while (matchCol(addr, height, addr.col + width)) {
width++;
}
// mark all included addresses
for (let i = 0; i < height; i++) {
for (let j = 0; j < width; j++) {
otherAddress = colCache.encodeAddress(addr.row + i, addr.col + j);
dvMap[otherAddress].marked = true;
}
}
if (height > 1 || width > 1) {
const bottom = addr.row + (height - 1);
const right = addr.col + (width - 1);
return {
...dv.dataValidation,
sqref: `${dv.address}:${colCache.encodeAddress(bottom, right)}`,
};
}
return {
...dv.dataValidation,
sqref: dv.address,
};
}
return null;
})
.filter(Boolean);
}
class DataValidationsXform extends BaseXform {
get tag() {
return 'dataValidations';
}
render(xmlStream, model) {
const optimizedModel = optimiseDataValidations(model);
if (optimizedModel.length) {
xmlStream.openNode('dataValidations', {count: optimizedModel.length});
optimizedModel.forEach(value => {
xmlStream.openNode('dataValidation');
if (value.type !== 'any') {
xmlStream.addAttribute('type', value.type);
if (value.operator && value.type !== 'list' && value.operator !== 'between') {
xmlStream.addAttribute('operator', value.operator);
}
if (value.allowBlank) {
xmlStream.addAttribute('allowBlank', '1');
}
}
if (value.showInputMessage) {
xmlStream.addAttribute('showInputMessage', '1');
}
if (value.promptTitle) {
xmlStream.addAttribute('promptTitle', value.promptTitle);
}
if (value.prompt) {
xmlStream.addAttribute('prompt', value.prompt);
}
if (value.showErrorMessage) {
xmlStream.addAttribute('showErrorMessage', '1');
}
if (value.errorStyle) {
xmlStream.addAttribute('errorStyle', value.errorStyle);
}
if (value.errorTitle) {
xmlStream.addAttribute('errorTitle', value.errorTitle);
}
if (value.error) {
xmlStream.addAttribute('error', value.error);
}
xmlStream.addAttribute('sqref', value.sqref);
(value.formulae || []).forEach((formula, index) => {
xmlStream.openNode(`formula${index + 1}`);
if (value.type === 'date') {
xmlStream.writeText(utils.dateToExcel(new Date(formula)));
} else {
xmlStream.writeText(formula);
}
xmlStream.closeNode();
});
xmlStream.closeNode();
});
xmlStream.closeNode();
}
}
parseOpen(node) {
switch (node.name) {
case 'dataValidations':
this.model = {};
return true;
case 'dataValidation': {
this._address = node.attributes.sqref;
const dataValidation = {type: node.attributes.type || 'any', formulae: []};
if (node.attributes.type) {
assignBool(dataValidation, node.attributes, 'allowBlank');
}
assignBool(dataValidation, node.attributes, 'showInputMessage');
assignBool(dataValidation, node.attributes, 'showErrorMessage');
switch (dataValidation.type) {
case 'any':
case 'list':
case 'custom':
break;
default:
assign(dataValidation, node.attributes, 'operator', 'between');
break;
}
assign(dataValidation, node.attributes, 'promptTitle');
assign(dataValidation, node.attributes, 'prompt');
assign(dataValidation, node.attributes, 'errorStyle');
assign(dataValidation, node.attributes, 'errorTitle');
assign(dataValidation, node.attributes, 'error');
this._dataValidation = dataValidation;
return true;
}
case 'formula1':
case 'formula2':
this._formula = [];
return true;
default:
return false;
}
}
parseText(text) {
if (this._formula) {
this._formula.push(text);
}
}
parseClose(name) {
switch (name) {
case 'dataValidations':
return false;
case 'dataValidation': {
if (!this._dataValidation.formulae || !this._dataValidation.formulae.length) {
delete this._dataValidation.formulae;
delete this._dataValidation.operator;
}
// The four known cases: 1. E4:L9 N4:U9 2.E4 L9 3. N4:U9 4. E4
const list = this._address.split(/\s+/g) || [];
list.forEach(addr => {
if (addr.includes(':')) {
const range = new Range(addr);
range.forEachAddress(address => {
this.model[address] = this._dataValidation;
});
} else {
this.model[addr] = this._dataValidation;
}
});
return true;
}
case 'formula1':
case 'formula2': {
let formula = this._formula.join('');
switch (this._dataValidation.type) {
case 'whole':
case 'textLength':
formula = parseInt(formula, 10);
break;
case 'decimal':
formula = parseFloat(formula);
break;
case 'date':
formula = utils.excelToDate(parseFloat(formula));
break;
default:
break;
}
this._dataValidation.formulae.push(formula);
this._formula = undefined;
return true;
}
default:
return true;
}
}
}
module.exports = DataValidationsXform;

View File

@@ -0,0 +1,29 @@
const BaseXform = require('../base-xform');
class DimensionXform extends BaseXform {
get tag() {
return 'dimension';
}
render(xmlStream, model) {
if (model) {
xmlStream.leafNode('dimension', {ref: model});
}
}
parseOpen(node) {
if (node.name === 'dimension') {
this.model = node.attributes.ref;
return true;
}
return false;
}
parseText() {}
parseClose() {
return false;
}
}
module.exports = DimensionXform;

View File

@@ -0,0 +1,33 @@
const BaseXform = require('../base-xform');
class DrawingXform extends BaseXform {
get tag() {
return 'drawing';
}
render(xmlStream, model) {
if (model) {
xmlStream.leafNode(this.tag, {'r:id': model.rId});
}
}
parseOpen(node) {
switch (node.name) {
case this.tag:
this.model = {
rId: node.attributes['r:id'],
};
return true;
default:
return false;
}
}
parseText() {}
parseClose() {
return false;
}
}
module.exports = DrawingXform;

View File

@@ -0,0 +1,86 @@
/* eslint-disable max-classes-per-file */
const CompositeXform = require('../composite-xform');
const ConditionalFormattingsExt = require('./cf-ext/conditional-formattings-ext-xform');
class ExtXform extends CompositeXform {
constructor() {
super();
this.map = {
'x14:conditionalFormattings': (this.conditionalFormattings = new ConditionalFormattingsExt()),
};
}
get tag() {
return 'ext';
}
hasContent(model) {
return this.conditionalFormattings.hasContent(model.conditionalFormattings);
}
prepare(model, options) {
this.conditionalFormattings.prepare(model.conditionalFormattings, options);
}
render(xmlStream, model) {
xmlStream.openNode('ext', {
uri: '{78C0D931-6437-407d-A8EE-F0AAD7539E65}',
'xmlns:x14': 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/main',
});
this.conditionalFormattings.render(xmlStream, model.conditionalFormattings);
xmlStream.closeNode();
}
createNewModel() {
return {};
}
onParserClose(name, parser) {
this.model[name] = parser.model;
}
}
class ExtLstXform extends CompositeXform {
constructor() {
super();
this.map = {
ext: (this.ext = new ExtXform()),
};
}
get tag() {
return 'extLst';
}
prepare(model, options) {
this.ext.prepare(model, options);
}
hasContent(model) {
return this.ext.hasContent(model);
}
render(xmlStream, model) {
if (!this.hasContent(model)) {
return;
}
xmlStream.openNode('extLst');
this.ext.render(xmlStream, model);
xmlStream.closeNode();
}
createNewModel() {
return {};
}
onParserClose(name, parser) {
Object.assign(this.model, parser.model);
}
}
module.exports = ExtLstXform;

View File

@@ -0,0 +1,146 @@
const BaseXform = require('../base-xform');
class HeaderFooterXform extends BaseXform {
get tag() {
return 'headerFooter';
}
render(xmlStream, model) {
if (model) {
xmlStream.addRollback();
let createTag = false;
xmlStream.openNode('headerFooter');
if (model.differentFirst) {
xmlStream.addAttribute('differentFirst', '1');
createTag = true;
}
if (model.differentOddEven) {
xmlStream.addAttribute('differentOddEven', '1');
createTag = true;
}
if (model.oddHeader && typeof model.oddHeader === 'string') {
xmlStream.leafNode('oddHeader', null, model.oddHeader);
createTag = true;
}
if (model.oddFooter && typeof model.oddFooter === 'string') {
xmlStream.leafNode('oddFooter', null, model.oddFooter);
createTag = true;
}
if (model.evenHeader && typeof model.evenHeader === 'string') {
xmlStream.leafNode('evenHeader', null, model.evenHeader);
createTag = true;
}
if (model.evenFooter && typeof model.evenFooter === 'string') {
xmlStream.leafNode('evenFooter', null, model.evenFooter);
createTag = true;
}
if (model.firstHeader && typeof model.firstHeader === 'string') {
xmlStream.leafNode('firstHeader', null, model.firstHeader);
createTag = true;
}
if (model.firstFooter && typeof model.firstFooter === 'string') {
xmlStream.leafNode('firstFooter', null, model.firstFooter);
createTag = true;
}
if (createTag) {
xmlStream.closeNode();
xmlStream.commit();
} else {
xmlStream.rollback();
}
}
}
parseOpen(node) {
switch (node.name) {
case 'headerFooter':
this.model = {};
if (node.attributes.differentFirst) {
this.model.differentFirst = parseInt(node.attributes.differentFirst, 0) === 1;
}
if (node.attributes.differentOddEven) {
this.model.differentOddEven = parseInt(node.attributes.differentOddEven, 0) === 1;
}
return true;
case 'oddHeader':
this.currentNode = 'oddHeader';
return true;
case 'oddFooter':
this.currentNode = 'oddFooter';
return true;
case 'evenHeader':
this.currentNode = 'evenHeader';
return true;
case 'evenFooter':
this.currentNode = 'evenFooter';
return true;
case 'firstHeader':
this.currentNode = 'firstHeader';
return true;
case 'firstFooter':
this.currentNode = 'firstFooter';
return true;
default:
return false;
}
}
parseText(text) {
switch (this.currentNode) {
case 'oddHeader':
this.model.oddHeader = text;
break;
case 'oddFooter':
this.model.oddFooter = text;
break;
case 'evenHeader':
this.model.evenHeader = text;
break;
case 'evenFooter':
this.model.evenFooter = text;
break;
case 'firstHeader':
this.model.firstHeader = text;
break;
case 'firstFooter':
this.model.firstFooter = text;
break;
default:
break;
}
}
parseClose() {
switch (this.currentNode) {
case 'oddHeader':
case 'oddFooter':
case 'evenHeader':
case 'evenFooter':
case 'firstHeader':
case 'firstFooter':
this.currentNode = undefined;
return true;
default:
return false;
}
}
}
module.exports = HeaderFooterXform;

View File

@@ -0,0 +1,54 @@
const BaseXform = require('../base-xform');
class HyperlinkXform extends BaseXform {
get tag() {
return 'hyperlink';
}
render(xmlStream, model) {
if (this.isInternalLink(model)) {
xmlStream.leafNode('hyperlink', {
ref: model.address,
'r:id': model.rId,
tooltip: model.tooltip,
location: model.target,
});
} else {
xmlStream.leafNode('hyperlink', {
ref: model.address,
'r:id': model.rId,
tooltip: model.tooltip,
});
}
}
parseOpen(node) {
if (node.name === 'hyperlink') {
this.model = {
address: node.attributes.ref,
rId: node.attributes['r:id'],
tooltip: node.attributes.tooltip,
};
// This is an internal link
if (node.attributes.location) {
this.model.target = node.attributes.location;
}
return true;
}
return false;
}
parseText() {}
parseClose() {
return false;
}
isInternalLink(model) {
// @example: Sheet2!D3, return true
return model.target && /^[^!]+![a-zA-Z]+[\d]+$/.test(model.target);
}
}
module.exports = HyperlinkXform;

View File

@@ -0,0 +1,27 @@
const BaseXform = require('../base-xform');
class MergeCellXform extends BaseXform {
get tag() {
return 'mergeCell';
}
render(xmlStream, model) {
xmlStream.leafNode('mergeCell', {ref: model});
}
parseOpen(node) {
if (node.name === 'mergeCell') {
this.model = node.attributes.ref;
return true;
}
return false;
}
parseText() {}
parseClose() {
return false;
}
}
module.exports = MergeCellXform;

View File

@@ -0,0 +1,56 @@
const _ = require('../../../utils/under-dash');
const Range = require('../../../doc/range');
const colCache = require('../../../utils/col-cache');
const Enums = require('../../../doc/enums');
class Merges {
constructor() {
// optional mergeCells is array of ranges (like the xml)
this.merges = {};
}
add(merge) {
// merge is {address, master}
if (this.merges[merge.master]) {
this.merges[merge.master].expandToAddress(merge.address);
} else {
const range = `${merge.master}:${merge.address}`;
this.merges[merge.master] = new Range(range);
}
}
get mergeCells() {
return _.map(this.merges, merge => merge.range);
}
reconcile(mergeCells, rows) {
// reconcile merge list with merge cells
_.each(mergeCells, merge => {
const dimensions = colCache.decode(merge);
for (let i = dimensions.top; i <= dimensions.bottom; i++) {
const row = rows[i - 1];
for (let j = dimensions.left; j <= dimensions.right; j++) {
const cell = row.cells[j - 1];
if (!cell) {
// nulls are not included in document - so if master cell has no value - add a null one here
row.cells[j] = {
type: Enums.ValueType.Null,
address: colCache.encodeAddress(i, j),
};
} else if (cell.type === Enums.ValueType.Merge) {
cell.master = dimensions.tl;
}
}
}
});
}
getMasterAddress(address) {
// if address has been merged, return its master's address. Assumes reconcile has been called
const range = this.hash[address];
return range && range.tl;
}
}
module.exports = Merges;

View File

@@ -0,0 +1,43 @@
const BaseXform = require('../base-xform');
const isDefined = attr => typeof attr !== 'undefined';
class OutlinePropertiesXform extends BaseXform {
get tag() {
return 'outlinePr';
}
render(xmlStream, model) {
if (model && (isDefined(model.summaryBelow) || isDefined(model.summaryRight))) {
xmlStream.leafNode(this.tag, {
summaryBelow: isDefined(model.summaryBelow) ? Number(model.summaryBelow) : undefined,
summaryRight: isDefined(model.summaryRight) ? Number(model.summaryRight) : undefined,
});
return true;
}
return false;
}
parseOpen(node) {
if (node.name === this.tag) {
this.model = {
summaryBelow: isDefined(node.attributes.summaryBelow)
? Boolean(Number(node.attributes.summaryBelow))
: undefined,
summaryRight: isDefined(node.attributes.summaryRight)
? Boolean(Number(node.attributes.summaryRight))
: undefined,
};
return true;
}
return false;
}
parseText() {}
parseClose() {
return false;
}
}
module.exports = OutlinePropertiesXform;

View File

@@ -0,0 +1,27 @@
const BaseXform = require('../base-xform');
class PageBreaksXform extends BaseXform {
get tag() {
return 'brk';
}
render(xmlStream, model) {
xmlStream.leafNode('brk', model);
}
parseOpen(node) {
if (node.name === 'brk') {
this.model = node.attributes.ref;
return true;
}
return false;
}
parseText() {}
parseClose() {
return false;
}
}
module.exports = PageBreaksXform;

View File

@@ -0,0 +1,49 @@
const _ = require('../../../utils/under-dash');
const BaseXform = require('../base-xform');
class PageMarginsXform extends BaseXform {
get tag() {
return 'pageMargins';
}
render(xmlStream, model) {
if (model) {
const attributes = {
left: model.left,
right: model.right,
top: model.top,
bottom: model.bottom,
header: model.header,
footer: model.footer,
};
if (_.some(attributes, value => value !== undefined)) {
xmlStream.leafNode(this.tag, attributes);
}
}
}
parseOpen(node) {
switch (node.name) {
case this.tag:
this.model = {
left: parseFloat(node.attributes.left || 0.7),
right: parseFloat(node.attributes.right || 0.7),
top: parseFloat(node.attributes.top || 0.75),
bottom: parseFloat(node.attributes.bottom || 0.75),
header: parseFloat(node.attributes.header || 0.3),
footer: parseFloat(node.attributes.footer || 0.3),
};
return true;
default:
return false;
}
}
parseText() {}
parseClose() {
return false;
}
}
module.exports = PageMarginsXform;

View File

@@ -0,0 +1,35 @@
const BaseXform = require('../base-xform');
class PageSetupPropertiesXform extends BaseXform {
get tag() {
return 'pageSetUpPr';
}
render(xmlStream, model) {
if (model && model.fitToPage) {
xmlStream.leafNode(this.tag, {
fitToPage: model.fitToPage ? '1' : undefined,
});
return true;
}
return false;
}
parseOpen(node) {
if (node.name === this.tag) {
this.model = {
fitToPage: node.attributes.fitToPage === '1',
};
return true;
}
return false;
}
parseText() {}
parseClose() {
return false;
}
}
module.exports = PageSetupPropertiesXform;

View File

@@ -0,0 +1,103 @@
const _ = require('../../../utils/under-dash');
const BaseXform = require('../base-xform');
function booleanToXml(model) {
return model ? '1' : undefined;
}
function pageOrderToXml(model) {
switch (model) {
case 'overThenDown':
return model;
default:
return undefined;
}
}
function cellCommentsToXml(model) {
switch (model) {
case 'atEnd':
case 'asDisplyed':
return model;
default:
return undefined;
}
}
function errorsToXml(model) {
switch (model) {
case 'dash':
case 'blank':
case 'NA':
return model;
default:
return undefined;
}
}
function pageSizeToModel(value) {
return value !== undefined ? parseInt(value, 10) : undefined;
}
class PageSetupXform extends BaseXform {
get tag() {
return 'pageSetup';
}
render(xmlStream, model) {
if (model) {
const attributes = {
paperSize: model.paperSize,
orientation: model.orientation,
horizontalDpi: model.horizontalDpi,
verticalDpi: model.verticalDpi,
pageOrder: pageOrderToXml(model.pageOrder),
blackAndWhite: booleanToXml(model.blackAndWhite),
draft: booleanToXml(model.draft),
cellComments: cellCommentsToXml(model.cellComments),
errors: errorsToXml(model.errors),
scale: model.scale,
fitToWidth: model.fitToWidth,
fitToHeight: model.fitToHeight,
firstPageNumber: model.firstPageNumber,
useFirstPageNumber: booleanToXml(model.firstPageNumber),
usePrinterDefaults: booleanToXml(model.usePrinterDefaults),
copies: model.copies,
};
if (_.some(attributes, value => value !== undefined)) {
xmlStream.leafNode(this.tag, attributes);
}
}
}
parseOpen(node) {
switch (node.name) {
case this.tag:
this.model = {
paperSize: pageSizeToModel(node.attributes.paperSize),
orientation: node.attributes.orientation || 'portrait',
horizontalDpi: parseInt(node.attributes.horizontalDpi || '4294967295', 10),
verticalDpi: parseInt(node.attributes.verticalDpi || '4294967295', 10),
pageOrder: node.attributes.pageOrder || 'downThenOver',
blackAndWhite: node.attributes.blackAndWhite === '1',
draft: node.attributes.draft === '1',
cellComments: node.attributes.cellComments || 'None',
errors: node.attributes.errors || 'displayed',
scale: parseInt(node.attributes.scale || '100', 10),
fitToWidth: parseInt(node.attributes.fitToWidth || '1', 10),
fitToHeight: parseInt(node.attributes.fitToHeight || '1', 10),
firstPageNumber: parseInt(node.attributes.firstPageNumber || '1', 10),
useFirstPageNumber: node.attributes.useFirstPageNumber === '1',
usePrinterDefaults: node.attributes.usePrinterDefaults === '1',
copies: parseInt(node.attributes.copies || '1', 10),
};
return true;
default:
return false;
}
}
parseText() {}
parseClose() {
return false;
}
}
module.exports = PageSetupXform;

View File

@@ -0,0 +1,33 @@
const BaseXform = require('../base-xform');
class PictureXform extends BaseXform {
get tag() {
return 'picture';
}
render(xmlStream, model) {
if (model) {
xmlStream.leafNode(this.tag, {'r:id': model.rId});
}
}
parseOpen(node) {
switch (node.name) {
case this.tag:
this.model = {
rId: node.attributes['r:id'],
};
return true;
default:
return false;
}
}
parseText() {}
parseClose() {
return false;
}
}
module.exports = PictureXform;

View File

@@ -0,0 +1,49 @@
const _ = require('../../../utils/under-dash');
const BaseXform = require('../base-xform');
function booleanToXml(model) {
return model ? '1' : undefined;
}
class PrintOptionsXform extends BaseXform {
get tag() {
return 'printOptions';
}
render(xmlStream, model) {
if (model) {
const attributes = {
headings: booleanToXml(model.showRowColHeaders),
gridLines: booleanToXml(model.showGridLines),
horizontalCentered: booleanToXml(model.horizontalCentered),
verticalCentered: booleanToXml(model.verticalCentered),
};
if (_.some(attributes, value => value !== undefined)) {
xmlStream.leafNode(this.tag, attributes);
}
}
}
parseOpen(node) {
switch (node.name) {
case this.tag:
this.model = {
showRowColHeaders: node.attributes.headings === '1',
showGridLines: node.attributes.gridLines === '1',
horizontalCentered: node.attributes.horizontalCentered === '1',
verticalCentered: node.attributes.verticalCentered === '1',
};
return true;
default:
return false;
}
}
parseText() {}
parseClose() {
return false;
}
}
module.exports = PrintOptionsXform;

View File

@@ -0,0 +1,39 @@
'use strict';
const PageBreaksXform = require('./page-breaks-xform');
const ListXform = require('../list-xform');
class RowBreaksXform extends ListXform {
constructor() {
const options = {
tag: 'rowBreaks',
count: true,
childXform: new PageBreaksXform(),
};
super(options);
}
// get tag() { return 'rowBreaks'; }
render(xmlStream, model) {
if (model && model.length) {
xmlStream.openNode(this.tag, this.$);
if (this.count) {
xmlStream.addAttribute(this.$count, model.length);
xmlStream.addAttribute('manualBreakCount', model.length);
}
const {childXform} = this;
model.forEach(childModel => {
childXform.render(xmlStream, childModel);
});
xmlStream.closeNode();
} else if (this.empty) {
xmlStream.leafNode(this.tag);
}
}
}
module.exports = RowBreaksXform;

View File

@@ -0,0 +1,142 @@
const BaseXform = require('../base-xform');
const utils = require('../../../utils/utils');
const CellXform = require('./cell-xform');
class RowXform extends BaseXform {
constructor(options) {
super();
this.maxItems = options && options.maxItems;
this.map = {
c: new CellXform(),
};
}
get tag() {
return 'row';
}
prepare(model, options) {
const styleId = options.styles.addStyleModel(model.style);
if (styleId) {
model.styleId = styleId;
}
const cellXform = this.map.c;
model.cells.forEach(cellModel => {
cellXform.prepare(cellModel, options);
});
}
render(xmlStream, model, options) {
xmlStream.openNode('row');
xmlStream.addAttribute('r', model.number);
if (model.height) {
xmlStream.addAttribute('ht', model.height);
xmlStream.addAttribute('customHeight', '1');
}
if (model.hidden) {
xmlStream.addAttribute('hidden', '1');
}
if (model.min > 0 && model.max > 0 && model.min <= model.max) {
xmlStream.addAttribute('spans', `${model.min}:${model.max}`);
}
if (model.styleId) {
xmlStream.addAttribute('s', model.styleId);
xmlStream.addAttribute('customFormat', '1');
}
xmlStream.addAttribute('x14ac:dyDescent', '0.25');
if (model.outlineLevel) {
xmlStream.addAttribute('outlineLevel', model.outlineLevel);
}
if (model.collapsed) {
xmlStream.addAttribute('collapsed', '1');
}
const cellXform = this.map.c;
model.cells.forEach(cellModel => {
cellXform.render(xmlStream, cellModel, options);
});
xmlStream.closeNode();
}
parseOpen(node) {
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
if (node.name === 'row') {
this.numRowsSeen += 1;
const spans = node.attributes.spans
? node.attributes.spans.split(':').map(span => parseInt(span, 10))
: [undefined, undefined];
const model = (this.model = {
number: parseInt(node.attributes.r, 10),
min: spans[0],
max: spans[1],
cells: [],
});
if (node.attributes.s) {
model.styleId = parseInt(node.attributes.s, 10);
}
if (utils.parseBoolean(node.attributes.hidden)) {
model.hidden = true;
}
if (utils.parseBoolean(node.attributes.bestFit)) {
model.bestFit = true;
}
if (node.attributes.ht) {
model.height = parseFloat(node.attributes.ht);
}
if (node.attributes.outlineLevel) {
model.outlineLevel = parseInt(node.attributes.outlineLevel, 10);
}
if (utils.parseBoolean(node.attributes.collapsed)) {
model.collapsed = true;
}
return true;
}
this.parser = this.map[node.name];
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
return false;
}
parseText(text) {
if (this.parser) {
this.parser.parseText(text);
}
}
parseClose(name) {
if (this.parser) {
if (!this.parser.parseClose(name)) {
this.model.cells.push(this.parser.model);
if (this.maxItems && this.model.cells.length > this.maxItems) {
throw new Error(`Max column count (${this.maxItems}) exceeded`);
}
this.parser = undefined;
}
return true;
}
return false;
}
reconcile(model, options) {
model.style = model.styleId ? options.styles.getStyleModel(model.styleId) : {};
if (model.styleId !== undefined) {
model.styleId = undefined;
}
const cellXform = this.map.c;
model.cells.forEach(cellModel => {
cellXform.reconcile(cellModel, options);
});
}
}
module.exports = RowXform;

View File

@@ -0,0 +1,55 @@
const _ = require('../../../utils/under-dash');
const BaseXform = require('../base-xform');
class SheetFormatPropertiesXform extends BaseXform {
get tag() {
return 'sheetFormatPr';
}
render(xmlStream, model) {
if (model) {
const attributes = {
defaultRowHeight: model.defaultRowHeight,
outlineLevelRow: model.outlineLevelRow,
outlineLevelCol: model.outlineLevelCol,
'x14ac:dyDescent': model.dyDescent,
};
if (model.defaultColWidth) {
attributes.defaultColWidth = model.defaultColWidth;
}
// default value for 'defaultRowHeight' is 15, this should not be 'custom'
if (!model.defaultRowHeight || model.defaultRowHeight !== 15) {
attributes.customHeight = '1';
}
if (_.some(attributes, value => value !== undefined)) {
xmlStream.leafNode('sheetFormatPr', attributes);
}
}
}
parseOpen(node) {
if (node.name === 'sheetFormatPr') {
this.model = {
defaultRowHeight: parseFloat(node.attributes.defaultRowHeight || '0'),
dyDescent: parseFloat(node.attributes['x14ac:dyDescent'] || '0'),
outlineLevelRow: parseInt(node.attributes.outlineLevelRow || '0', 10),
outlineLevelCol: parseInt(node.attributes.outlineLevelCol || '0', 10),
};
if (node.attributes.defaultColWidth) {
this.model.defaultColWidth = parseFloat(node.attributes.defaultColWidth);
}
return true;
}
return false;
}
parseText() {}
parseClose() {
return false;
}
}
module.exports = SheetFormatPropertiesXform;

View File

@@ -0,0 +1,90 @@
const BaseXform = require('../base-xform');
const ColorXform = require('../style/color-xform');
const PageSetupPropertiesXform = require('./page-setup-properties-xform');
const OutlinePropertiesXform = require('./outline-properties-xform');
class SheetPropertiesXform extends BaseXform {
constructor() {
super();
this.map = {
tabColor: new ColorXform('tabColor'),
pageSetUpPr: new PageSetupPropertiesXform(),
outlinePr: new OutlinePropertiesXform(),
};
}
get tag() {
return 'sheetPr';
}
render(xmlStream, model) {
if (model) {
xmlStream.addRollback();
xmlStream.openNode('sheetPr');
let inner = false;
inner = this.map.tabColor.render(xmlStream, model.tabColor) || inner;
inner = this.map.pageSetUpPr.render(xmlStream, model.pageSetup) || inner;
inner = this.map.outlinePr.render(xmlStream, model.outlineProperties) || inner;
if (inner) {
xmlStream.closeNode();
xmlStream.commit();
} else {
xmlStream.rollback();
}
}
}
parseOpen(node) {
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
if (node.name === this.tag) {
this.reset();
return true;
}
if (this.map[node.name]) {
this.parser = this.map[node.name];
this.parser.parseOpen(node);
return true;
}
return false;
}
parseText(text) {
if (this.parser) {
this.parser.parseText(text);
return true;
}
return false;
}
parseClose(name) {
if (this.parser) {
if (!this.parser.parseClose(name)) {
this.parser = undefined;
}
return true;
}
if (this.map.tabColor.model || this.map.pageSetUpPr.model || this.map.outlinePr.model) {
this.model = {};
if (this.map.tabColor.model) {
this.model.tabColor = this.map.tabColor.model;
}
if (this.map.pageSetUpPr.model) {
this.model.pageSetup = this.map.pageSetUpPr.model;
}
if (this.map.outlinePr.model) {
this.model.outlineProperties = this.map.outlinePr.model;
}
} else {
this.model = null;
}
return false;
}
}
module.exports = SheetPropertiesXform;

View File

@@ -0,0 +1,89 @@
const _ = require('../../../utils/under-dash');
const BaseXform = require('../base-xform');
function booleanToXml(model, value) {
return model ? value : undefined;
}
function xmlToBoolean(value, equals) {
return value === equals ? true : undefined;
}
class SheetProtectionXform extends BaseXform {
get tag() {
return 'sheetProtection';
}
render(xmlStream, model) {
if (model) {
const attributes = {
sheet: booleanToXml(model.sheet, '1'),
selectLockedCells: model.selectLockedCells === false ? '1' : undefined,
selectUnlockedCells: model.selectUnlockedCells === false ? '1' : undefined,
formatCells: booleanToXml(model.formatCells, '0'),
formatColumns: booleanToXml(model.formatColumns, '0'),
formatRows: booleanToXml(model.formatRows, '0'),
insertColumns: booleanToXml(model.insertColumns, '0'),
insertRows: booleanToXml(model.insertRows, '0'),
insertHyperlinks: booleanToXml(model.insertHyperlinks, '0'),
deleteColumns: booleanToXml(model.deleteColumns, '0'),
deleteRows: booleanToXml(model.deleteRows, '0'),
sort: booleanToXml(model.sort, '0'),
autoFilter: booleanToXml(model.autoFilter, '0'),
pivotTables: booleanToXml(model.pivotTables, '0'),
};
if (model.sheet) {
attributes.algorithmName = model.algorithmName;
attributes.hashValue = model.hashValue;
attributes.saltValue = model.saltValue;
attributes.spinCount = model.spinCount;
attributes.objects = booleanToXml(model.objects === false, '1');
attributes.scenarios = booleanToXml(model.scenarios === false, '1');
}
if (_.some(attributes, value => value !== undefined)) {
xmlStream.leafNode(this.tag, attributes);
}
}
}
parseOpen(node) {
switch (node.name) {
case this.tag:
this.model = {
sheet: xmlToBoolean(node.attributes.sheet, '1'),
objects: node.attributes.objects === '1' ? false : undefined,
scenarios: node.attributes.scenarios === '1' ? false : undefined,
selectLockedCells: node.attributes.selectLockedCells === '1' ? false : undefined,
selectUnlockedCells: node.attributes.selectUnlockedCells === '1' ? false : undefined,
formatCells: xmlToBoolean(node.attributes.formatCells, '0'),
formatColumns: xmlToBoolean(node.attributes.formatColumns, '0'),
formatRows: xmlToBoolean(node.attributes.formatRows, '0'),
insertColumns: xmlToBoolean(node.attributes.insertColumns, '0'),
insertRows: xmlToBoolean(node.attributes.insertRows, '0'),
insertHyperlinks: xmlToBoolean(node.attributes.insertHyperlinks, '0'),
deleteColumns: xmlToBoolean(node.attributes.deleteColumns, '0'),
deleteRows: xmlToBoolean(node.attributes.deleteRows, '0'),
sort: xmlToBoolean(node.attributes.sort, '0'),
autoFilter: xmlToBoolean(node.attributes.autoFilter, '0'),
pivotTables: xmlToBoolean(node.attributes.pivotTables, '0'),
};
if (node.attributes.algorithmName) {
this.model.algorithmName = node.attributes.algorithmName;
this.model.hashValue = node.attributes.hashValue;
this.model.saltValue = node.attributes.saltValue;
this.model.spinCount = parseInt(node.attributes.spinCount, 10);
}
return true;
default:
return false;
}
}
parseText() {}
parseClose() {
return false;
}
}
module.exports = SheetProtectionXform;

View File

@@ -0,0 +1,202 @@
const colCache = require('../../../utils/col-cache');
const BaseXform = require('../base-xform');
const VIEW_STATES = {
frozen: 'frozen',
frozenSplit: 'frozen',
split: 'split',
};
class SheetViewXform extends BaseXform {
get tag() {
return 'sheetView';
}
prepare(model) {
switch (model.state) {
case 'frozen':
case 'split':
break;
default:
model.state = 'normal';
break;
}
}
render(xmlStream, model) {
xmlStream.openNode('sheetView', {
workbookViewId: model.workbookViewId || 0,
});
const add = function(name, value, included) {
if (included) {
xmlStream.addAttribute(name, value);
}
};
add('rightToLeft', '1', model.rightToLeft === true);
add('tabSelected', '1', model.tabSelected);
add('showRuler', '0', model.showRuler === false);
add('showRowColHeaders', '0', model.showRowColHeaders === false);
add('showGridLines', '0', model.showGridLines === false);
add('zoomScale', model.zoomScale, model.zoomScale);
add('zoomScaleNormal', model.zoomScaleNormal, model.zoomScaleNormal);
add('view', model.style, model.style);
let topLeftCell;
let xSplit;
let ySplit;
let activePane;
switch (model.state) {
case 'frozen':
xSplit = model.xSplit || 0;
ySplit = model.ySplit || 0;
topLeftCell = model.topLeftCell || colCache.getAddress(ySplit + 1, xSplit + 1).address;
activePane =
(model.xSplit && model.ySplit && 'bottomRight') ||
(model.xSplit && 'topRight') ||
'bottomLeft';
xmlStream.leafNode('pane', {
xSplit: model.xSplit || undefined,
ySplit: model.ySplit || undefined,
topLeftCell,
activePane,
state: 'frozen',
});
xmlStream.leafNode('selection', {
pane: activePane,
activeCell: model.activeCell,
sqref: model.activeCell,
});
break;
case 'split':
if (model.activePane === 'topLeft') {
model.activePane = undefined;
}
xmlStream.leafNode('pane', {
xSplit: model.xSplit || undefined,
ySplit: model.ySplit || undefined,
topLeftCell: model.topLeftCell,
activePane: model.activePane,
});
xmlStream.leafNode('selection', {
pane: model.activePane,
activeCell: model.activeCell,
sqref: model.activeCell,
});
break;
case 'normal':
if (model.activeCell) {
xmlStream.leafNode('selection', {
activeCell: model.activeCell,
sqref: model.activeCell,
});
}
break;
default:
break;
}
xmlStream.closeNode();
}
parseOpen(node) {
switch (node.name) {
case 'sheetView':
this.sheetView = {
workbookViewId: parseInt(node.attributes.workbookViewId, 10),
rightToLeft: node.attributes.rightToLeft === '1',
tabSelected: node.attributes.tabSelected === '1',
showRuler: !(node.attributes.showRuler === '0'),
showRowColHeaders: !(node.attributes.showRowColHeaders === '0'),
showGridLines: !(node.attributes.showGridLines === '0'),
zoomScale: parseInt(node.attributes.zoomScale || '100', 10),
zoomScaleNormal: parseInt(node.attributes.zoomScaleNormal || '100', 10),
style: node.attributes.view,
};
this.pane = undefined;
this.selections = {};
return true;
case 'pane':
this.pane = {
xSplit: parseInt(node.attributes.xSplit || '0', 10),
ySplit: parseInt(node.attributes.ySplit || '0', 10),
topLeftCell: node.attributes.topLeftCell,
activePane: node.attributes.activePane || 'topLeft',
state: node.attributes.state,
};
return true;
case 'selection': {
const name = node.attributes.pane || 'topLeft';
this.selections[name] = {
pane: name,
activeCell: node.attributes.activeCell,
};
return true;
}
default:
return false;
}
}
parseText() {}
parseClose(name) {
let model;
let selection;
switch (name) {
case 'sheetView':
if (this.sheetView && this.pane) {
model = this.model = {
workbookViewId: this.sheetView.workbookViewId,
rightToLeft: this.sheetView.rightToLeft,
state: VIEW_STATES[this.pane.state] || 'split', // split is default
xSplit: this.pane.xSplit,
ySplit: this.pane.ySplit,
topLeftCell: this.pane.topLeftCell,
showRuler: this.sheetView.showRuler,
showRowColHeaders: this.sheetView.showRowColHeaders,
showGridLines: this.sheetView.showGridLines,
zoomScale: this.sheetView.zoomScale,
zoomScaleNormal: this.sheetView.zoomScaleNormal,
};
if (this.model.state === 'split') {
model.activePane = this.pane.activePane;
}
selection = this.selections[this.pane.activePane];
if (selection && selection.activeCell) {
model.activeCell = selection.activeCell;
}
if (this.sheetView.style) {
model.style = this.sheetView.style;
}
} else {
model = this.model = {
workbookViewId: this.sheetView.workbookViewId,
rightToLeft: this.sheetView.rightToLeft,
state: 'normal',
showRuler: this.sheetView.showRuler,
showRowColHeaders: this.sheetView.showRowColHeaders,
showGridLines: this.sheetView.showGridLines,
zoomScale: this.sheetView.zoomScale,
zoomScaleNormal: this.sheetView.zoomScaleNormal,
};
selection = this.selections.topLeft;
if (selection && selection.activeCell) {
model.activeCell = selection.activeCell;
}
if (this.sheetView.style) {
model.style = this.sheetView.style;
}
}
return false;
default:
return true;
}
}
reconcile() {}
}
module.exports = SheetViewXform;

View File

@@ -0,0 +1,33 @@
const BaseXform = require('../base-xform');
class TablePartXform extends BaseXform {
get tag() {
return 'tablePart';
}
render(xmlStream, model) {
if (model) {
xmlStream.leafNode(this.tag, {'r:id': model.rId});
}
}
parseOpen(node) {
switch (node.name) {
case this.tag:
this.model = {
rId: node.attributes['r:id'],
};
return true;
default:
return false;
}
}
parseText() {}
parseClose() {
return false;
}
}
module.exports = TablePartXform;

View File

@@ -0,0 +1,539 @@
const _ = require('../../../utils/under-dash');
const colCache = require('../../../utils/col-cache');
const XmlStream = require('../../../utils/xml-stream');
const RelType = require('../../rel-type');
const Merges = require('./merges');
const BaseXform = require('../base-xform');
const ListXform = require('../list-xform');
const RowXform = require('./row-xform');
const ColXform = require('./col-xform');
const DimensionXform = require('./dimension-xform');
const HyperlinkXform = require('./hyperlink-xform');
const MergeCellXform = require('./merge-cell-xform');
const DataValidationsXform = require('./data-validations-xform');
const SheetPropertiesXform = require('./sheet-properties-xform');
const SheetFormatPropertiesXform = require('./sheet-format-properties-xform');
const SheetViewXform = require('./sheet-view-xform');
const SheetProtectionXform = require('./sheet-protection-xform');
const PageMarginsXform = require('./page-margins-xform');
const PageSetupXform = require('./page-setup-xform');
const PrintOptionsXform = require('./print-options-xform');
const AutoFilterXform = require('./auto-filter-xform');
const PictureXform = require('./picture-xform');
const DrawingXform = require('./drawing-xform');
const TablePartXform = require('./table-part-xform');
const RowBreaksXform = require('./row-breaks-xform');
const HeaderFooterXform = require('./header-footer-xform');
const ConditionalFormattingsXform = require('./cf/conditional-formattings-xform');
const ExtListXform = require('./ext-lst-xform');
const mergeRule = (rule, extRule) => {
Object.keys(extRule).forEach(key => {
const value = rule[key];
const extValue = extRule[key];
if (value === undefined && extValue !== undefined) {
rule[key] = extValue;
}
});
};
const mergeConditionalFormattings = (model, extModel) => {
// conditional formattings are rendered in worksheet.conditionalFormatting and also in
// worksheet.extLst.ext.x14:conditionalFormattings
// some (e.g. dataBar) are even spread across both!
if (!extModel || !extModel.length) {
return model;
}
if (!model || !model.length) {
return extModel;
}
// index model rules by x14Id
const cfMap = {};
const ruleMap = {};
model.forEach(cf => {
cfMap[cf.ref] = cf;
cf.rules.forEach(rule => {
const {x14Id} = rule;
if (x14Id) {
ruleMap[x14Id] = rule;
}
});
});
extModel.forEach(extCf => {
extCf.rules.forEach(extRule => {
const rule = ruleMap[extRule.x14Id];
if (rule) {
// merge with matching rule
mergeRule(rule, extRule);
} else if (cfMap[extCf.ref]) {
// reuse existing cf ref
cfMap[extCf.ref].rules.push(extRule);
} else {
// create new cf
model.push({
ref: extCf.ref,
rules: [extRule],
});
}
});
});
// need to cope with rules in extModel that don't exist in model
return model;
};
class WorkSheetXform extends BaseXform {
constructor(options) {
super();
const {maxRows, maxCols, ignoreNodes} = options || {};
this.ignoreNodes = ignoreNodes || [];
this.map = {
sheetPr: new SheetPropertiesXform(),
dimension: new DimensionXform(),
sheetViews: new ListXform({
tag: 'sheetViews',
count: false,
childXform: new SheetViewXform(),
}),
sheetFormatPr: new SheetFormatPropertiesXform(),
cols: new ListXform({tag: 'cols', count: false, childXform: new ColXform()}),
sheetData: new ListXform({
tag: 'sheetData',
count: false,
empty: true,
childXform: new RowXform({maxItems: maxCols}),
maxItems: maxRows,
}),
autoFilter: new AutoFilterXform(),
mergeCells: new ListXform({tag: 'mergeCells', count: true, childXform: new MergeCellXform()}),
rowBreaks: new RowBreaksXform(),
hyperlinks: new ListXform({
tag: 'hyperlinks',
count: false,
childXform: new HyperlinkXform(),
}),
pageMargins: new PageMarginsXform(),
dataValidations: new DataValidationsXform(),
pageSetup: new PageSetupXform(),
headerFooter: new HeaderFooterXform(),
printOptions: new PrintOptionsXform(),
picture: new PictureXform(),
drawing: new DrawingXform(),
sheetProtection: new SheetProtectionXform(),
tableParts: new ListXform({tag: 'tableParts', count: true, childXform: new TablePartXform()}),
conditionalFormatting: new ConditionalFormattingsXform(),
extLst: new ExtListXform(),
};
}
prepare(model, options) {
options.merges = new Merges();
model.hyperlinks = options.hyperlinks = [];
model.comments = options.comments = [];
options.formulae = {};
options.siFormulae = 0;
this.map.cols.prepare(model.cols, options);
this.map.sheetData.prepare(model.rows, options);
this.map.conditionalFormatting.prepare(model.conditionalFormattings, options);
model.mergeCells = options.merges.mergeCells;
// prepare relationships
const rels = (model.rels = []);
function nextRid(r) {
return `rId${r.length + 1}`;
}
model.hyperlinks.forEach(hyperlink => {
const rId = nextRid(rels);
hyperlink.rId = rId;
rels.push({
Id: rId,
Type: RelType.Hyperlink,
Target: hyperlink.target,
TargetMode: 'External',
});
});
// prepare comment relationships
if (model.comments.length > 0) {
const comment = {
Id: nextRid(rels),
Type: RelType.Comments,
Target: `../comments${model.id}.xml`,
};
rels.push(comment);
const vmlDrawing = {
Id: nextRid(rels),
Type: RelType.VmlDrawing,
Target: `../drawings/vmlDrawing${model.id}.vml`,
};
rels.push(vmlDrawing);
model.comments.forEach(item => {
item.refAddress = colCache.decodeAddress(item.ref);
});
options.commentRefs.push({
commentName: `comments${model.id}`,
vmlDrawing: `vmlDrawing${model.id}`,
});
}
const drawingRelsHash = [];
let bookImage;
model.media.forEach(medium => {
if (medium.type === 'background') {
const rId = nextRid(rels);
bookImage = options.media[medium.imageId];
rels.push({
Id: rId,
Type: RelType.Image,
Target: `../media/${bookImage.name}.${bookImage.extension}`,
});
model.background = {
rId,
};
model.image = options.media[medium.imageId];
} else if (medium.type === 'image') {
let {drawing} = model;
bookImage = options.media[medium.imageId];
if (!drawing) {
drawing = model.drawing = {
rId: nextRid(rels),
name: `drawing${++options.drawingsCount}`,
anchors: [],
rels: [],
};
options.drawings.push(drawing);
rels.push({
Id: drawing.rId,
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing',
Target: `../drawings/${drawing.name}.xml`,
});
}
let rIdImage =
this.preImageId === medium.imageId ? drawingRelsHash[medium.imageId] : drawingRelsHash[drawing.rels.length];
if (!rIdImage) {
rIdImage = nextRid(drawing.rels);
drawingRelsHash[drawing.rels.length] = rIdImage;
drawing.rels.push({
Id: rIdImage,
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image',
Target: `../media/${bookImage.name}.${bookImage.extension}`,
});
}
const anchor = {
picture: {
rId: rIdImage,
},
range: medium.range,
};
if (medium.hyperlinks && medium.hyperlinks.hyperlink) {
const rIdHyperLink = nextRid(drawing.rels);
drawingRelsHash[drawing.rels.length] = rIdHyperLink;
anchor.picture.hyperlinks = {
tooltip: medium.hyperlinks.tooltip,
rId: rIdHyperLink,
};
drawing.rels.push({
Id: rIdHyperLink,
Type: RelType.Hyperlink,
Target: medium.hyperlinks.hyperlink,
TargetMode: 'External',
});
}
this.preImageId = medium.imageId;
drawing.anchors.push(anchor);
}
});
// prepare tables
model.tables.forEach(table => {
// relationships
const rId = nextRid(rels);
table.rId = rId;
rels.push({
Id: rId,
Type: RelType.Table,
Target: `../tables/${table.target}`,
});
// dynamic styles
table.columns.forEach(column => {
const {style} = column;
if (style) {
column.dxfId = options.styles.addDxfStyle(style);
}
});
});
// prepare ext items
this.map.extLst.prepare(model, options);
}
render(xmlStream, model) {
xmlStream.openXml(XmlStream.StdDocAttributes);
xmlStream.openNode('worksheet', WorkSheetXform.WORKSHEET_ATTRIBUTES);
const sheetFormatPropertiesModel = model.properties
? {
defaultRowHeight: model.properties.defaultRowHeight,
dyDescent: model.properties.dyDescent,
outlineLevelCol: model.properties.outlineLevelCol,
outlineLevelRow: model.properties.outlineLevelRow,
}
: undefined;
if (model.properties && model.properties.defaultColWidth) {
sheetFormatPropertiesModel.defaultColWidth = model.properties.defaultColWidth;
}
const sheetPropertiesModel = {
outlineProperties: model.properties && model.properties.outlineProperties,
tabColor: model.properties && model.properties.tabColor,
pageSetup:
model.pageSetup && model.pageSetup.fitToPage
? {
fitToPage: model.pageSetup.fitToPage,
}
: undefined,
};
const pageMarginsModel = model.pageSetup && model.pageSetup.margins;
const printOptionsModel = {
showRowColHeaders: model.pageSetup && model.pageSetup.showRowColHeaders,
showGridLines: model.pageSetup && model.pageSetup.showGridLines,
horizontalCentered: model.pageSetup && model.pageSetup.horizontalCentered,
verticalCentered: model.pageSetup && model.pageSetup.verticalCentered,
};
const sheetProtectionModel = model.sheetProtection;
this.map.sheetPr.render(xmlStream, sheetPropertiesModel);
this.map.dimension.render(xmlStream, model.dimensions);
this.map.sheetViews.render(xmlStream, model.views);
this.map.sheetFormatPr.render(xmlStream, sheetFormatPropertiesModel);
this.map.cols.render(xmlStream, model.cols);
this.map.sheetData.render(xmlStream, model.rows);
this.map.sheetProtection.render(xmlStream, sheetProtectionModel); // Note: must be after sheetData and before autoFilter
this.map.autoFilter.render(xmlStream, model.autoFilter);
this.map.mergeCells.render(xmlStream, model.mergeCells);
this.map.conditionalFormatting.render(xmlStream, model.conditionalFormattings); // Note: must be before dataValidations
this.map.dataValidations.render(xmlStream, model.dataValidations);
// For some reason hyperlinks have to be after the data validations
this.map.hyperlinks.render(xmlStream, model.hyperlinks);
this.map.printOptions.render(xmlStream, printOptionsModel); // Note: must be before pageMargins
this.map.pageMargins.render(xmlStream, pageMarginsModel);
this.map.pageSetup.render(xmlStream, model.pageSetup);
this.map.headerFooter.render(xmlStream, model.headerFooter);
this.map.rowBreaks.render(xmlStream, model.rowBreaks);
this.map.drawing.render(xmlStream, model.drawing); // Note: must be after rowBreaks
this.map.picture.render(xmlStream, model.background); // Note: must be after drawing
this.map.tableParts.render(xmlStream, model.tables);
this.map.extLst.render(xmlStream, model);
if (model.rels) {
// add a <legacyDrawing /> node for each comment
model.rels.forEach(rel => {
if (rel.Type === RelType.VmlDrawing) {
xmlStream.leafNode('legacyDrawing', {'r:id': rel.Id});
}
});
}
xmlStream.closeNode();
}
parseOpen(node) {
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
if (node.name === 'worksheet') {
_.each(this.map, xform => {
xform.reset();
});
return true;
}
if (this.map[node.name] && !this.ignoreNodes.includes(node.name)) {
this.parser = this.map[node.name];
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 'worksheet': {
const properties = this.map.sheetFormatPr.model || {};
if (this.map.sheetPr.model && this.map.sheetPr.model.tabColor) {
properties.tabColor = this.map.sheetPr.model.tabColor;
}
if (this.map.sheetPr.model && this.map.sheetPr.model.outlineProperties) {
properties.outlineProperties = this.map.sheetPr.model.outlineProperties;
}
const sheetProperties = {
fitToPage:
(this.map.sheetPr.model &&
this.map.sheetPr.model.pageSetup &&
this.map.sheetPr.model.pageSetup.fitToPage) ||
false,
margins: this.map.pageMargins.model,
};
const pageSetup = Object.assign(sheetProperties, this.map.pageSetup.model, this.map.printOptions.model);
const conditionalFormattings = mergeConditionalFormattings(
this.map.conditionalFormatting.model,
this.map.extLst.model && this.map.extLst.model['x14:conditionalFormattings']
);
this.model = {
dimensions: this.map.dimension.model,
cols: this.map.cols.model,
rows: this.map.sheetData.model,
mergeCells: this.map.mergeCells.model,
hyperlinks: this.map.hyperlinks.model,
dataValidations: this.map.dataValidations.model,
properties,
views: this.map.sheetViews.model,
pageSetup,
headerFooter: this.map.headerFooter.model,
background: this.map.picture.model,
drawing: this.map.drawing.model,
tables: this.map.tableParts.model,
conditionalFormattings,
};
if (this.map.autoFilter.model) {
this.model.autoFilter = this.map.autoFilter.model;
}
if (this.map.sheetProtection.model) {
this.model.sheetProtection = this.map.sheetProtection.model;
}
return false;
}
default:
// not quite sure how we get here!
return true;
}
}
reconcile(model, options) {
// options.merges = new Merges();
// options.merges.reconcile(model.mergeCells, model.rows);
const rels = (model.relationships || []).reduce((h, rel) => {
h[rel.Id] = rel;
if (rel.Type === RelType.Comments) {
model.comments = options.comments[rel.Target].comments;
}
if (rel.Type === RelType.VmlDrawing && model.comments && model.comments.length) {
const vmlComment = options.vmlDrawings[rel.Target].comments;
model.comments.forEach((comment, index) => {
comment.note = Object.assign({}, comment.note, vmlComment[index]);
});
}
return h;
}, {});
options.commentsMap = (model.comments || []).reduce((h, comment) => {
if (comment.ref) {
h[comment.ref] = comment;
}
return h;
}, {});
options.hyperlinkMap = (model.hyperlinks || []).reduce((h, hyperlink) => {
if (hyperlink.rId) {
h[hyperlink.address] = rels[hyperlink.rId].Target;
}
return h;
}, {});
options.formulae = {};
// compact the rows and cells
model.rows = (model.rows && model.rows.filter(Boolean)) || [];
model.rows.forEach(row => {
row.cells = (row.cells && row.cells.filter(Boolean)) || [];
});
this.map.cols.reconcile(model.cols, options);
this.map.sheetData.reconcile(model.rows, options);
this.map.conditionalFormatting.reconcile(model.conditionalFormattings, options);
model.media = [];
if (model.drawing) {
const drawingRel = rels[model.drawing.rId];
const match = drawingRel.Target.match(/\/drawings\/([a-zA-Z0-9]+)[.][a-zA-Z]{3,4}$/);
if (match) {
const drawingName = match[1];
const drawing = options.drawings[drawingName];
drawing.anchors.forEach(anchor => {
if (anchor.medium) {
const image = {
type: 'image',
imageId: anchor.medium.index,
range: anchor.range,
hyperlinks: anchor.picture.hyperlinks,
};
model.media.push(image);
}
});
}
}
const backgroundRel = model.background && rels[model.background.rId];
if (backgroundRel) {
const target = backgroundRel.Target.split('/media/')[1];
const imageId = options.mediaIndex && options.mediaIndex[target];
if (imageId !== undefined) {
model.media.push({
type: 'background',
imageId,
});
}
}
model.tables = (model.tables || []).map(tablePart => {
const rel = rels[tablePart.rId];
return options.tables[rel.Target];
});
delete model.relationships;
delete model.hyperlinks;
delete model.comments;
}
}
WorkSheetXform.WORKSHEET_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': 'x14ac',
'xmlns:x14ac': 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac',
};
module.exports = WorkSheetXform;

View File

@@ -0,0 +1,31 @@
const BaseXform = require('../base-xform');
class BooleanXform extends BaseXform {
constructor(options) {
super();
this.tag = options.tag;
this.attr = options.attr;
}
render(xmlStream, model) {
if (model) {
xmlStream.openNode(this.tag);
xmlStream.closeNode();
}
}
parseOpen(node) {
if (node.name === this.tag) {
this.model = true;
}
}
parseText() {}
parseClose() {
return false;
}
}
module.exports = BooleanXform;

View File

@@ -0,0 +1,66 @@
const BaseXform = require('../base-xform');
class DateXform extends BaseXform {
constructor(options) {
super();
this.tag = options.tag;
this.attr = options.attr;
this.attrs = options.attrs;
this._format =
options.format ||
function(dt) {
try {
if (Number.isNaN(dt.getTime())) return '';
return dt.toISOString();
} catch (e) {
return '';
}
};
this._parse =
options.parse ||
function(str) {
return new Date(str);
};
}
render(xmlStream, model) {
if (model) {
xmlStream.openNode(this.tag);
if (this.attrs) {
xmlStream.addAttributes(this.attrs);
}
if (this.attr) {
xmlStream.addAttribute(this.attr, this._format(model));
} else {
xmlStream.writeText(this._format(model));
}
xmlStream.closeNode();
}
}
parseOpen(node) {
if (node.name === this.tag) {
if (this.attr) {
this.model = this._parse(node.attributes[this.attr]);
} else {
this.text = [];
}
}
}
parseText(text) {
if (!this.attr) {
this.text.push(text);
}
}
parseClose() {
if (!this.attr) {
this.model = this._parse(this.text.join(''));
}
return false;
}
}
module.exports = DateXform;

View File

@@ -0,0 +1,51 @@
const BaseXform = require('../base-xform');
class FloatXform extends BaseXform {
constructor(options) {
super();
this.tag = options.tag;
this.attr = options.attr;
this.attrs = options.attrs;
}
render(xmlStream, model) {
if (model !== undefined) {
xmlStream.openNode(this.tag);
if (this.attrs) {
xmlStream.addAttributes(this.attrs);
}
if (this.attr) {
xmlStream.addAttribute(this.attr, model);
} else {
xmlStream.writeText(model);
}
xmlStream.closeNode();
}
}
parseOpen(node) {
if (node.name === this.tag) {
if (this.attr) {
this.model = parseFloat(node.attributes[this.attr]);
} else {
this.text = [];
}
}
}
parseText(text) {
if (!this.attr) {
this.text.push(text);
}
}
parseClose() {
if (!this.attr) {
this.model = parseFloat(this.text.join(''));
}
return false;
}
}
module.exports = FloatXform;

View File

@@ -0,0 +1,57 @@
const BaseXform = require('../base-xform');
class IntegerXform extends BaseXform {
constructor(options) {
super();
this.tag = options.tag;
this.attr = options.attr;
this.attrs = options.attrs;
// option to render zero
this.zero = options.zero;
}
render(xmlStream, model) {
// int is different to float in that zero is not rendered
if (model || this.zero) {
xmlStream.openNode(this.tag);
if (this.attrs) {
xmlStream.addAttributes(this.attrs);
}
if (this.attr) {
xmlStream.addAttribute(this.attr, model);
} else {
xmlStream.writeText(model);
}
xmlStream.closeNode();
}
}
parseOpen(node) {
if (node.name === this.tag) {
if (this.attr) {
this.model = parseInt(node.attributes[this.attr], 10);
} else {
this.text = [];
}
return true;
}
return false;
}
parseText(text) {
if (!this.attr) {
this.text.push(text);
}
}
parseClose() {
if (!this.attr) {
this.model = parseInt(this.text.join('') || 0, 10);
}
return false;
}
}
module.exports = IntegerXform;

View File

@@ -0,0 +1,51 @@
const BaseXform = require('../base-xform');
class StringXform extends BaseXform {
constructor(options) {
super();
this.tag = options.tag;
this.attr = options.attr;
this.attrs = options.attrs;
}
render(xmlStream, model) {
if (model !== undefined) {
xmlStream.openNode(this.tag);
if (this.attrs) {
xmlStream.addAttributes(this.attrs);
}
if (this.attr) {
xmlStream.addAttribute(this.attr, model);
} else {
xmlStream.writeText(model);
}
xmlStream.closeNode();
}
}
parseOpen(node) {
if (node.name === this.tag) {
if (this.attr) {
this.model = node.attributes[this.attr];
} else {
this.text = [];
}
}
}
parseText(text) {
if (!this.attr) {
this.text.push(text);
}
}
parseClose() {
if (!this.attr) {
this.model = this.text.join('');
}
return false;
}
}
module.exports = StringXform;

View File

@@ -0,0 +1,64 @@
const BaseXform = require('./base-xform');
const XmlStream = require('../../utils/xml-stream');
// const model = {
// tag: 'name',
// $: {attr: 'value'},
// c: [
// { tag: 'child' }
// ],
// t: 'some text'
// };
function build(xmlStream, model) {
xmlStream.openNode(model.tag, model.$);
if (model.c) {
model.c.forEach(child => {
build(xmlStream, child);
});
}
if (model.t) {
xmlStream.writeText(model.t);
}
xmlStream.closeNode();
}
class StaticXform extends BaseXform {
constructor(model) {
super();
// This class is an optimisation for static (unimportant and unchanging) xml
// It is stateless - apart from its static model and so can be used as a singleton
// Being stateless - it will only track entry to and exit from it's root xml tag during parsing and nothing else
// Known issues:
// since stateless - parseOpen always returns true. Parent xform must know when to start using this xform
// if the root tag is recursive, the parsing will behave unpredictably
this._model = model;
}
render(xmlStream) {
if (!this._xml) {
const stream = new XmlStream();
build(stream, this._model);
this._xml = stream.xml;
}
xmlStream.writeXml(this._xml);
}
parseOpen() {
return true;
}
parseText() {}
parseClose(name) {
switch (name) {
case this._model.tag:
return false;
default:
return true;
}
}
}
module.exports = StaticXform;

View File

@@ -0,0 +1,98 @@
const TextXform = require('./text-xform');
const RichTextXform = require('./rich-text-xform');
const BaseXform = require('../base-xform');
// <rPh sb="0" eb="1">
// <t>(its pronounciation in KATAKANA)</t>
// </rPh>
class PhoneticTextXform extends BaseXform {
constructor() {
super();
this.map = {
r: new RichTextXform(),
t: new TextXform(),
};
}
get tag() {
return 'rPh';
}
render(xmlStream, model) {
xmlStream.openNode(this.tag, {
sb: model.sb || 0,
eb: model.eb || 0,
});
if (model && model.hasOwnProperty('richText') && model.richText) {
const {r} = this.map;
model.richText.forEach(text => {
r.render(xmlStream, text);
});
} else if (model) {
this.map.t.render(xmlStream, model.text);
}
xmlStream.closeNode();
}
parseOpen(node) {
const {name} = node;
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
if (name === this.tag) {
this.model = {
sb: parseInt(node.attributes.sb, 10),
eb: parseInt(node.attributes.eb, 10),
};
return true;
}
this.parser = this.map[name];
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
return false;
}
parseText(text) {
if (this.parser) {
this.parser.parseText(text);
}
}
parseClose(name) {
if (this.parser) {
if (!this.parser.parseClose(name)) {
switch (name) {
case 'r': {
let rt = this.model.richText;
if (!rt) {
rt = this.model.richText = [];
}
rt.push(this.parser.model);
break;
}
case 't':
this.model.text = this.parser.model;
break;
default:
break;
}
this.parser = undefined;
}
return true;
}
switch (name) {
case this.tag:
return false;
default:
return true;
}
}
}
module.exports = PhoneticTextXform;

View File

@@ -0,0 +1,101 @@
const TextXform = require('./text-xform');
const FontXform = require('../style/font-xform');
const BaseXform = require('../base-xform');
// <r>
// <rPr>
// <sz val="11"/>
// <color theme="1" tint="5"/>
// <rFont val="Calibri"/>
// <family val="2"/>
// <scheme val="minor"/>
// </rPr>
// <t xml:space="preserve"> is </t>
// </r>
class RichTextXform extends BaseXform {
constructor(model) {
super();
this.model = model;
}
get tag() {
return 'r';
}
get textXform() {
return this._textXform || (this._textXform = new TextXform());
}
get fontXform() {
return this._fontXform || (this._fontXform = new FontXform(RichTextXform.FONT_OPTIONS));
}
render(xmlStream, model) {
model = model || this.model;
xmlStream.openNode('r');
if (model.font) {
this.fontXform.render(xmlStream, model.font);
}
this.textXform.render(xmlStream, model.text);
xmlStream.closeNode();
}
parseOpen(node) {
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
switch (node.name) {
case 'r':
this.model = {};
return true;
case 't':
this.parser = this.textXform;
this.parser.parseOpen(node);
return true;
case 'rPr':
this.parser = this.fontXform;
this.parser.parseOpen(node);
return true;
default:
return false;
}
}
parseText(text) {
if (this.parser) {
this.parser.parseText(text);
}
}
parseClose(name) {
switch (name) {
case 'r':
return false;
case 't':
this.model.text = this.parser.model;
this.parser = undefined;
return true;
case 'rPr':
this.model.font = this.parser.model;
this.parser = undefined;
return true;
default:
if (this.parser) {
this.parser.parseClose(name);
}
return true;
}
}
}
RichTextXform.FONT_OPTIONS = {
tagName: 'rPr',
fontNameTag: 'rFont',
};
module.exports = RichTextXform;

View File

@@ -0,0 +1,102 @@
const TextXform = require('./text-xform');
const RichTextXform = require('./rich-text-xform');
const PhoneticTextXform = require('./phonetic-text-xform');
const BaseXform = require('../base-xform');
// <si>
// <r></r><r></r>...
// </si>
// <si>
// <t></t>
// </si>
class SharedStringXform extends BaseXform {
constructor(model) {
super();
this.model = model;
this.map = {
r: new RichTextXform(),
t: new TextXform(),
rPh: new PhoneticTextXform(),
};
}
get tag() {
return 'si';
}
render(xmlStream, model) {
xmlStream.openNode(this.tag);
if (model && model.hasOwnProperty('richText') && model.richText) {
if (model.richText.length) {
model.richText.forEach(text => {
this.map.r.render(xmlStream, text);
});
} else {
this.map.t.render(xmlStream, '');
}
} else if (model !== undefined && model !== null) {
this.map.t.render(xmlStream, model);
}
xmlStream.closeNode();
}
parseOpen(node) {
const {name} = node;
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
if (name === this.tag) {
this.model = {};
return true;
}
this.parser = this.map[name];
if (this.parser) {
this.parser.parseOpen(node);
return true;
}
return false;
}
parseText(text) {
if (this.parser) {
this.parser.parseText(text);
}
}
parseClose(name) {
if (this.parser) {
if (!this.parser.parseClose(name)) {
switch (name) {
case 'r': {
let rt = this.model.richText;
if (!rt) {
rt = this.model.richText = [];
}
rt.push(this.parser.model);
break;
}
case 't':
this.model = this.parser.model;
break;
default:
break;
}
this.parser = undefined;
}
return true;
}
switch (name) {
case this.tag:
return false;
default:
return true;
}
}
}
module.exports = SharedStringXform;

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