| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527 |
- /* eslint-disable max-classes-per-file */
- const Enums = require('../../../doc/enums');
- const XmlStream = require('../../../utils/xml-stream');
- const BaseXform = require('../base-xform');
- const StaticXform = require('../static-xform');
- const ListXform = require('../list-xform');
- const FontXform = require('./font-xform');
- const FillXform = require('./fill-xform');
- const BorderXform = require('./border-xform');
- const NumFmtXform = require('./numfmt-xform');
- const StyleXform = require('./style-xform');
- const DxfXform = require('./dxf-xform');
- // custom numfmt ids start here
- const NUMFMT_BASE = 164;
- // =============================================================================
- // StylesXform is used to generate and parse the styles.xml file
- // it manages the collections of fonts, number formats, alignments, etc
- class StylesXform extends BaseXform {
- constructor(initialise) {
- super();
- this.map = {
- numFmts: new ListXform({tag: 'numFmts', count: true, childXform: new NumFmtXform()}),
- fonts: new ListXform({
- tag: 'fonts',
- count: true,
- childXform: new FontXform(),
- $: {'x14ac:knownFonts': 1},
- }),
- fills: new ListXform({tag: 'fills', count: true, childXform: new FillXform()}),
- borders: new ListXform({tag: 'borders', count: true, childXform: new BorderXform()}),
- cellStyleXfs: new ListXform({tag: 'cellStyleXfs', count: true, childXform: new StyleXform()}),
- cellXfs: new ListXform({
- tag: 'cellXfs',
- count: true,
- childXform: new StyleXform({xfId: true}),
- }),
- dxfs: new ListXform({tag: 'dxfs', always: true, count: true, childXform: new DxfXform()}),
- // for style manager
- numFmt: new NumFmtXform(),
- font: new FontXform(),
- fill: new FillXform(),
- border: new BorderXform(),
- style: new StyleXform({xfId: true}),
- cellStyles: StylesXform.STATIC_XFORMS.cellStyles,
- tableStyles: StylesXform.STATIC_XFORMS.tableStyles,
- extLst: StylesXform.STATIC_XFORMS.extLst,
- };
- if (initialise) {
- // StylesXform also acts as style manager and is used to build up styles-model during worksheet processing
- this.init();
- }
- }
- initIndex() {
- this.index = {
- style: {},
- numFmt: {},
- numFmtNextId: 164, // start custom format ids here
- font: {},
- border: {},
- fill: {},
- };
- }
- init() {
- // Prepare for Style Manager role
- this.model = {
- styles: [],
- numFmts: [],
- fonts: [],
- borders: [],
- fills: [],
- dxfs: [],
- };
- this.initIndex();
- // default (zero) border
- this._addBorder({});
- // add default (all zero) style
- this._addStyle({numFmtId: 0, fontId: 0, fillId: 0, borderId: 0, xfId: 0});
- // add default fills
- this._addFill({type: 'pattern', pattern: 'none'});
- this._addFill({type: 'pattern', pattern: 'gray125'});
- this.weakMap = new WeakMap();
- }
- render(xmlStream, model) {
- model = model || this.model;
- //
- // <fonts count="2" x14ac:knownFonts="1">
- xmlStream.openXml(XmlStream.StdDocAttributes);
- xmlStream.openNode('styleSheet', StylesXform.STYLESHEET_ATTRIBUTES);
- if (this.index) {
- // model has been built by style manager role (contains xml)
- if (model.numFmts && model.numFmts.length) {
- xmlStream.openNode('numFmts', {count: model.numFmts.length});
- model.numFmts.forEach(numFmtXml => {
- xmlStream.writeXml(numFmtXml);
- });
- xmlStream.closeNode();
- }
- if (!model.fonts.length) {
- // default (zero) font
- this._addFont({size: 11, color: {theme: 1}, name: 'Calibri', family: 2, scheme: 'minor'});
- }
- xmlStream.openNode('fonts', {count: model.fonts.length, 'x14ac:knownFonts': 1});
- model.fonts.forEach(fontXml => {
- xmlStream.writeXml(fontXml);
- });
- xmlStream.closeNode();
- xmlStream.openNode('fills', {count: model.fills.length});
- model.fills.forEach(fillXml => {
- xmlStream.writeXml(fillXml);
- });
- xmlStream.closeNode();
- xmlStream.openNode('borders', {count: model.borders.length});
- model.borders.forEach(borderXml => {
- xmlStream.writeXml(borderXml);
- });
- xmlStream.closeNode();
- this.map.cellStyleXfs.render(xmlStream, [{numFmtId: 0, fontId: 0, fillId: 0, borderId: 0, xfId: 0}]);
- xmlStream.openNode('cellXfs', {count: model.styles.length});
- model.styles.forEach(styleXml => {
- xmlStream.writeXml(styleXml);
- });
- xmlStream.closeNode();
- } else {
- // model is plain JSON and needs to be xformed
- this.map.numFmts.render(xmlStream, model.numFmts);
- this.map.fonts.render(xmlStream, model.fonts);
- this.map.fills.render(xmlStream, model.fills);
- this.map.borders.render(xmlStream, model.borders);
- this.map.cellStyleXfs.render(xmlStream, [{numFmtId: 0, fontId: 0, fillId: 0, borderId: 0, xfId: 0}]);
- this.map.cellXfs.render(xmlStream, model.styles);
- }
- StylesXform.STATIC_XFORMS.cellStyles.render(xmlStream);
- this.map.dxfs.render(xmlStream, model.dxfs);
- StylesXform.STATIC_XFORMS.tableStyles.render(xmlStream);
- StylesXform.STATIC_XFORMS.extLst.render(xmlStream);
- xmlStream.closeNode();
- }
- parseOpen(node) {
- if (this.parser) {
- this.parser.parseOpen(node);
- return true;
- }
- switch (node.name) {
- case 'styleSheet':
- this.initIndex();
- 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 'styleSheet': {
- this.model = {};
- const add = (propName, xform) => {
- if (xform.model && xform.model.length) {
- this.model[propName] = xform.model;
- }
- };
- add('numFmts', this.map.numFmts);
- add('fonts', this.map.fonts);
- add('fills', this.map.fills);
- add('borders', this.map.borders);
- add('styles', this.map.cellXfs);
- add('dxfs', this.map.dxfs);
- // index numFmts
- this.index = {
- model: [],
- numFmt: [],
- };
- if (this.model.numFmts) {
- const numFmtIndex = this.index.numFmt;
- this.model.numFmts.forEach(numFmt => {
- numFmtIndex[numFmt.id] = numFmt.formatCode;
- });
- }
- return false;
- }
- default:
- // not quite sure how we get here!
- return true;
- }
- }
- // add a cell's style model to the collection
- // each style property is processed and cross-referenced, etc.
- // the styleId is returned. Note: cellType is used when numFmt not defined
- addStyleModel(model, cellType) {
- if (!model) {
- return 0;
- }
- // if we have no default font, add it here now
- if (!this.model.fonts.length) {
- // default (zero) font
- this._addFont({size: 11, color: {theme: 1}, name: 'Calibri', family: 2, scheme: 'minor'});
- }
- // if we have seen this style object before, assume it has the same styleId
- if (this.weakMap && this.weakMap.has(model)) {
- return this.weakMap.get(model);
- }
- const style = {};
- cellType = cellType || Enums.ValueType.Number;
- if (model.numFmt) {
- style.numFmtId = this._addNumFmtStr(model.numFmt);
- } else {
- switch (cellType) {
- case Enums.ValueType.Number:
- style.numFmtId = this._addNumFmtStr('General');
- break;
- case Enums.ValueType.Date:
- style.numFmtId = this._addNumFmtStr('mm-dd-yy');
- break;
- default:
- break;
- }
- }
- if (model.font) {
- style.fontId = this._addFont(model.font);
- }
- if (model.border) {
- style.borderId = this._addBorder(model.border);
- }
- if (model.fill) {
- style.fillId = this._addFill(model.fill);
- }
- if (model.alignment) {
- style.alignment = model.alignment;
- }
- if (model.protection) {
- style.protection = model.protection;
- }
- const styleId = this._addStyle(style);
- if (this.weakMap) {
- this.weakMap.set(model, styleId);
- }
- return styleId;
- }
- // given a styleId (i.e. s="n"), get the cell's style model
- // objects are shared where possible.
- getStyleModel(id) {
- // if the style doesn't exist return null
- const style = this.model.styles[id];
- if (!style) return null;
- // have we built this model before?
- let model = this.index.model[id];
- if (model) return model;
- // build a new model
- model = this.index.model[id] = {};
- // -------------------------------------------------------
- // number format
- if (style.numFmtId) {
- const numFmt = this.index.numFmt[style.numFmtId] || NumFmtXform.getDefaultFmtCode(style.numFmtId);
- if (numFmt) {
- model.numFmt = numFmt;
- }
- }
- function addStyle(name, group, styleId) {
- if (styleId || styleId === 0) {
- const part = group[styleId];
- if (part) {
- model[name] = part;
- }
- }
- }
- addStyle('font', this.model.fonts, style.fontId);
- addStyle('border', this.model.borders, style.borderId);
- addStyle('fill', this.model.fills, style.fillId);
- // -------------------------------------------------------
- // alignment
- if (style.alignment) {
- model.alignment = style.alignment;
- }
- // -------------------------------------------------------
- // protection
- if (style.protection) {
- model.protection = style.protection;
- }
- return model;
- }
- addDxfStyle(style) {
- if (style.numFmt) {
- // register numFmtId to use it during dxf-xform rendering
- style.numFmtId = this._addNumFmtStr(style.numFmt);
- }
- this.model.dxfs.push(style);
- return this.model.dxfs.length - 1;
- }
- getDxfStyle(id) {
- return this.model.dxfs[id];
- }
- // =========================================================================
- // Private Interface
- _addStyle(style) {
- const xml = this.map.style.toXml(style);
- let index = this.index.style[xml];
- if (index === undefined) {
- index = this.index.style[xml] = this.model.styles.length;
- this.model.styles.push(xml);
- }
- return index;
- }
- // =========================================================================
- // Number Formats
- _addNumFmtStr(formatCode) {
- // check if default format
- let index = NumFmtXform.getDefaultFmtId(formatCode);
- if (index !== undefined) return index;
- // check if already in
- index = this.index.numFmt[formatCode];
- if (index !== undefined) return index;
- index = this.index.numFmt[formatCode] = NUMFMT_BASE + this.model.numFmts.length;
- const xml = this.map.numFmt.toXml({id: index, formatCode});
- this.model.numFmts.push(xml);
- return index;
- }
- // =========================================================================
- // Fonts
- _addFont(font) {
- const xml = this.map.font.toXml(font);
- let index = this.index.font[xml];
- if (index === undefined) {
- index = this.index.font[xml] = this.model.fonts.length;
- this.model.fonts.push(xml);
- }
- return index;
- }
- // =========================================================================
- // Borders
- _addBorder(border) {
- const xml = this.map.border.toXml(border);
- let index = this.index.border[xml];
- if (index === undefined) {
- index = this.index.border[xml] = this.model.borders.length;
- this.model.borders.push(xml);
- }
- return index;
- }
- // =========================================================================
- // Fills
- _addFill(fill) {
- const xml = this.map.fill.toXml(fill);
- let index = this.index.fill[xml];
- if (index === undefined) {
- index = this.index.fill[xml] = this.model.fills.length;
- this.model.fills.push(xml);
- }
- return index;
- }
- // =========================================================================
- }
- StylesXform.STYLESHEET_ATTRIBUTES = {
- xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
- 'xmlns:mc': 'http://schemas.openxmlformats.org/markup-compatibility/2006',
- 'mc:Ignorable': 'x14ac x16r2',
- 'xmlns:x14ac': 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac',
- 'xmlns:x16r2': 'http://schemas.microsoft.com/office/spreadsheetml/2015/02/main',
- };
- StylesXform.STATIC_XFORMS = {
- cellStyles: new StaticXform({
- tag: 'cellStyles',
- $: {count: 1},
- c: [{tag: 'cellStyle', $: {name: 'Normal', xfId: 0, builtinId: 0}}],
- }),
- dxfs: new StaticXform({tag: 'dxfs', $: {count: 0}}),
- tableStyles: new StaticXform({
- tag: 'tableStyles',
- $: {count: 0, defaultTableStyle: 'TableStyleMedium2', defaultPivotStyle: 'PivotStyleLight16'},
- }),
- extLst: new StaticXform({
- tag: 'extLst',
- c: [
- {
- tag: 'ext',
- $: {
- uri: '{EB79DEF2-80B8-43e5-95BD-54CBDDF9020C}',
- 'xmlns:x14': 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/main',
- },
- c: [{tag: 'x14:slicerStyles', $: {defaultSlicerStyle: 'SlicerStyleLight1'}}],
- },
- {
- tag: 'ext',
- $: {
- uri: '{9260A510-F301-46a8-8635-F512D64BE5F5}',
- 'xmlns:x15': 'http://schemas.microsoft.com/office/spreadsheetml/2010/11/main',
- },
- c: [{tag: 'x15:timelineStyles', $: {defaultTimelineStyle: 'TimeSlicerStyleLight1'}}],
- },
- ],
- }),
- };
- // the stylemanager mock acts like StyleManager except that it always returns 0 or {}
- class StylesXformMock extends StylesXform {
- constructor() {
- super();
- this.model = {
- styles: [{numFmtId: 0, fontId: 0, fillId: 0, borderId: 0, xfId: 0}],
- numFmts: [],
- fonts: [{size: 11, color: {theme: 1}, name: 'Calibri', family: 2, scheme: 'minor'}],
- borders: [{}],
- fills: [
- {type: 'pattern', pattern: 'none'},
- {type: 'pattern', pattern: 'gray125'},
- ],
- };
- }
- // =========================================================================
- // Style Manager Interface
- // override normal behaviour - consume and dispose
- parseStream(stream) {
- stream.autodrain();
- return Promise.resolve();
- }
- // add a cell's style model to the collection
- // each style property is processed and cross-referenced, etc.
- // the styleId is returned. Note: cellType is used when numFmt not defined
- addStyleModel(model, cellType) {
- switch (cellType) {
- case Enums.ValueType.Date:
- return this.dateStyleId;
- default:
- return 0;
- }
- }
- get dateStyleId() {
- if (!this._dateStyleId) {
- const dateStyle = {
- numFmtId: NumFmtXform.getDefaultFmtId('mm-dd-yy'),
- };
- this._dateStyleId = this.model.styles.length;
- this.model.styles.push(dateStyle);
- }
- return this._dateStyleId;
- }
- // given a styleId (i.e. s="n"), get the cell's style model
- // objects are shared where possible.
- getStyleModel(/* id */) {
- return {};
- }
- }
- StylesXform.Mock = StylesXformMock;
- module.exports = StylesXform;
|