Skip to content

Commit 63745a0

Browse files
Merge pull request #33 from protobi/exceljs-3014
Fix exceljs#3014: Add support for HAN CELL Excel files
2 parents d850f91 + cfe7425 commit 63745a0

4 files changed

Lines changed: 289 additions & 213 deletions

File tree

lib/utils/parse-sax.js

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
const {SaxesParser} = require('saxes');
2-
const {PassThrough} = require('readable-stream');
3-
const {bufferToString} = require('./browser-buffer-decode');
1+
const { SaxesParser } = require("saxes");
2+
const { PassThrough } = require("readable-stream");
3+
const { bufferToString } = require("./browser-buffer-decode");
4+
5+
// Strip 'x:' prefix used by HAN CELL for spreadsheet tags (but preserve other prefixes like dc:, cp:, etc.)
6+
function normalizeTagName(name) {
7+
return name.startsWith("x:") ? name.substring(2) : name;
8+
}
49

510
module.exports = async function* (iterable) {
611
// TODO: Remove once node v8 is deprecated
@@ -10,13 +15,21 @@ module.exports = async function* (iterable) {
1015
}
1116
const saxesParser = new SaxesParser();
1217
let error;
13-
saxesParser.on('error', err => {
18+
saxesParser.on("error", (err) => {
1419
error = err;
1520
});
1621
let events = [];
17-
saxesParser.on('opentag', value => events.push({eventType: 'opentag', value}));
18-
saxesParser.on('text', value => events.push({eventType: 'text', value}));
19-
saxesParser.on('closetag', value => events.push({eventType: 'closetag', value}));
22+
saxesParser.on("opentag", (value) => {
23+
// Normalize 'x:' prefix for compatibility with HAN CELL
24+
const normalizedValue = { ...value, name: normalizeTagName(value.name) };
25+
events.push({ eventType: "opentag", value: normalizedValue });
26+
});
27+
saxesParser.on("text", (value) => events.push({ eventType: "text", value }));
28+
saxesParser.on("closetag", (value) => {
29+
// Normalize 'x:' prefix for compatibility with HAN CELL
30+
const normalizedValue = { ...value, name: normalizeTagName(value.name) };
31+
events.push({ eventType: "closetag", value: normalizedValue });
32+
});
2033
for await (const chunk of iterable) {
2134
saxesParser.write(bufferToString(chunk));
2235
// saxesParser.write and saxesParser.on() are synchronous,

lib/xlsx/xform/core/core-xform.js

Lines changed: 75 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,38 @@
1-
const XmlStream = require('../../../utils/xml-stream');
2-
const BaseXform = require('../base-xform');
3-
const DateXform = require('../simple/date-xform');
4-
const StringXform = require('../simple/string-xform');
5-
const IntegerXform = require('../simple/integer-xform');
1+
const XmlStream = require("../../../utils/xml-stream");
2+
const BaseXform = require("../base-xform");
3+
const DateXform = require("../simple/date-xform");
4+
const StringXform = require("../simple/string-xform");
5+
const IntegerXform = require("../simple/integer-xform");
66

77
class CoreXform extends BaseXform {
88
constructor() {
99
super();
1010

1111
this.map = {
12-
'dc:creator': new StringXform({tag: 'dc:creator'}),
13-
'dc:title': new StringXform({tag: 'dc:title'}),
14-
'dc:subject': new StringXform({tag: 'dc:subject'}),
15-
'dc:description': new StringXform({tag: 'dc:description'}),
16-
'dc:identifier': new StringXform({tag: 'dc:identifier'}),
17-
'dc:language': new StringXform({tag: 'dc:language'}),
18-
'cp:keywords': new StringXform({tag: 'cp:keywords'}),
19-
'cp:category': new StringXform({tag: 'cp:category'}),
20-
'cp:lastModifiedBy': new StringXform({tag: 'cp:lastModifiedBy'}),
21-
'cp:lastPrinted': new DateXform({tag: 'cp:lastPrinted', format: CoreXform.DateFormat}),
22-
'cp:revision': new IntegerXform({tag: 'cp:revision'}),
23-
'cp:version': new StringXform({tag: 'cp:version'}),
24-
'cp:contentStatus': new StringXform({tag: 'cp:contentStatus'}),
25-
'cp:contentType': new StringXform({tag: 'cp:contentType'}),
26-
'dcterms:created': new DateXform({
27-
tag: 'dcterms:created',
12+
"dc:creator": new StringXform({ tag: "dc:creator" }),
13+
"dc:title": new StringXform({ tag: "dc:title" }),
14+
"dc:subject": new StringXform({ tag: "dc:subject" }),
15+
"dc:description": new StringXform({ tag: "dc:description" }),
16+
"dc:identifier": new StringXform({ tag: "dc:identifier" }),
17+
"dc:language": new StringXform({ tag: "dc:language" }),
18+
"cp:keywords": new StringXform({ tag: "cp:keywords" }),
19+
"cp:category": new StringXform({ tag: "cp:category" }),
20+
"cp:lastModifiedBy": new StringXform({ tag: "cp:lastModifiedBy" }),
21+
"cp:lastPrinted": new DateXform({
22+
tag: "cp:lastPrinted",
23+
format: CoreXform.DateFormat,
24+
}),
25+
"cp:revision": new IntegerXform({ tag: "cp:revision" }),
26+
"cp:version": new StringXform({ tag: "cp:version" }),
27+
"cp:contentStatus": new StringXform({ tag: "cp:contentStatus" }),
28+
"cp:contentType": new StringXform({ tag: "cp:contentType" }),
29+
"dcterms:created": new DateXform({
30+
tag: "dcterms:created",
2831
attrs: CoreXform.DateAttrs,
2932
format: CoreXform.DateFormat,
3033
}),
31-
'dcterms:modified': new DateXform({
32-
tag: 'dcterms:modified',
34+
"dcterms:modified": new DateXform({
35+
tag: "dcterms:modified",
3336
attrs: CoreXform.DateAttrs,
3437
format: CoreXform.DateFormat,
3538
}),
@@ -39,24 +42,24 @@ class CoreXform extends BaseXform {
3942
render(xmlStream, model) {
4043
xmlStream.openXml(XmlStream.StdDocAttributes);
4144

42-
xmlStream.openNode('cp:coreProperties', CoreXform.CORE_PROPERTY_ATTRIBUTES);
45+
xmlStream.openNode("cp:coreProperties", CoreXform.CORE_PROPERTY_ATTRIBUTES);
4346

44-
this.map['dc:creator'].render(xmlStream, model.creator);
45-
this.map['dc:title'].render(xmlStream, model.title);
46-
this.map['dc:subject'].render(xmlStream, model.subject);
47-
this.map['dc:description'].render(xmlStream, model.description);
48-
this.map['dc:identifier'].render(xmlStream, model.identifier);
49-
this.map['dc:language'].render(xmlStream, model.language);
50-
this.map['cp:keywords'].render(xmlStream, model.keywords);
51-
this.map['cp:category'].render(xmlStream, model.category);
52-
this.map['cp:lastModifiedBy'].render(xmlStream, model.lastModifiedBy);
53-
this.map['cp:lastPrinted'].render(xmlStream, model.lastPrinted);
54-
this.map['cp:revision'].render(xmlStream, model.revision);
55-
this.map['cp:version'].render(xmlStream, model.version);
56-
this.map['cp:contentStatus'].render(xmlStream, model.contentStatus);
57-
this.map['cp:contentType'].render(xmlStream, model.contentType);
58-
this.map['dcterms:created'].render(xmlStream, model.created);
59-
this.map['dcterms:modified'].render(xmlStream, model.modified);
47+
this.map["dc:creator"].render(xmlStream, model.creator);
48+
this.map["dc:title"].render(xmlStream, model.title);
49+
this.map["dc:subject"].render(xmlStream, model.subject);
50+
this.map["dc:description"].render(xmlStream, model.description);
51+
this.map["dc:identifier"].render(xmlStream, model.identifier);
52+
this.map["dc:language"].render(xmlStream, model.language);
53+
this.map["cp:keywords"].render(xmlStream, model.keywords);
54+
this.map["cp:category"].render(xmlStream, model.category);
55+
this.map["cp:lastModifiedBy"].render(xmlStream, model.lastModifiedBy);
56+
this.map["cp:lastPrinted"].render(xmlStream, model.lastPrinted);
57+
this.map["cp:revision"].render(xmlStream, model.revision);
58+
this.map["cp:version"].render(xmlStream, model.version);
59+
this.map["cp:contentStatus"].render(xmlStream, model.contentStatus);
60+
this.map["cp:contentType"].render(xmlStream, model.contentType);
61+
this.map["dcterms:created"].render(xmlStream, model.created);
62+
this.map["dcterms:modified"].render(xmlStream, model.modified);
6063

6164
xmlStream.closeNode();
6265
}
@@ -67,16 +70,17 @@ class CoreXform extends BaseXform {
6770
return true;
6871
}
6972
switch (node.name) {
70-
case 'cp:coreProperties':
71-
case 'coreProperties':
73+
case "cp:coreProperties":
74+
case "coreProperties":
7275
return true;
7376
default:
7477
this.parser = this.map[node.name];
7578
if (this.parser) {
7679
this.parser.parseOpen(node);
7780
return true;
7881
}
79-
throw new Error(`Unexpected xml node in parseOpen: ${JSON.stringify(node)}`);
82+
// Ignore unknown tags for compatibility with non-Excel spreadsheet applications
83+
return false;
8084
}
8185
}
8286

@@ -94,43 +98,45 @@ class CoreXform extends BaseXform {
9498
return true;
9599
}
96100
switch (name) {
97-
case 'cp:coreProperties':
98-
case 'coreProperties':
101+
case "cp:coreProperties":
102+
case "coreProperties":
99103
this.model = {
100-
creator: this.map['dc:creator'].model,
101-
title: this.map['dc:title'].model,
102-
subject: this.map['dc:subject'].model,
103-
description: this.map['dc:description'].model,
104-
identifier: this.map['dc:identifier'].model,
105-
language: this.map['dc:language'].model,
106-
keywords: this.map['cp:keywords'].model,
107-
category: this.map['cp:category'].model,
108-
lastModifiedBy: this.map['cp:lastModifiedBy'].model,
109-
lastPrinted: this.map['cp:lastPrinted'].model,
110-
revision: this.map['cp:revision'].model,
111-
contentStatus: this.map['cp:contentStatus'].model,
112-
contentType: this.map['cp:contentType'].model,
113-
created: this.map['dcterms:created'].model,
114-
modified: this.map['dcterms:modified'].model,
104+
creator: this.map["dc:creator"].model,
105+
title: this.map["dc:title"].model,
106+
subject: this.map["dc:subject"].model,
107+
description: this.map["dc:description"].model,
108+
identifier: this.map["dc:identifier"].model,
109+
language: this.map["dc:language"].model,
110+
keywords: this.map["cp:keywords"].model,
111+
category: this.map["cp:category"].model,
112+
lastModifiedBy: this.map["cp:lastModifiedBy"].model,
113+
lastPrinted: this.map["cp:lastPrinted"].model,
114+
revision: this.map["cp:revision"].model,
115+
contentStatus: this.map["cp:contentStatus"].model,
116+
contentType: this.map["cp:contentType"].model,
117+
created: this.map["dcterms:created"].model,
118+
modified: this.map["dcterms:modified"].model,
115119
};
116120
return false;
117121
default:
118-
throw new Error(`Unexpected xml node in parseClose: ${name}`);
122+
// Ignore unknown tags for compatibility with non-Excel spreadsheet applications
123+
return true;
119124
}
120125
}
121126
}
122127

123-
CoreXform.DateFormat = function(dt) {
124-
return dt.toISOString().replace(/[.]\d{3}/, '');
128+
CoreXform.DateFormat = function (dt) {
129+
return dt.toISOString().replace(/[.]\d{3}/, "");
125130
};
126-
CoreXform.DateAttrs = {'xsi:type': 'dcterms:W3CDTF'};
131+
CoreXform.DateAttrs = { "xsi:type": "dcterms:W3CDTF" };
127132

128133
CoreXform.CORE_PROPERTY_ATTRIBUTES = {
129-
'xmlns:cp': 'http://schemas.openxmlformats.org/package/2006/metadata/core-properties',
130-
'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
131-
'xmlns:dcterms': 'http://purl.org/dc/terms/',
132-
'xmlns:dcmitype': 'http://purl.org/dc/dcmitype/',
133-
'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
134+
"xmlns:cp":
135+
"http://schemas.openxmlformats.org/package/2006/metadata/core-properties",
136+
"xmlns:dc": "http://purl.org/dc/elements/1.1/",
137+
"xmlns:dcterms": "http://purl.org/dc/terms/",
138+
"xmlns:dcmitype": "http://purl.org/dc/dcmitype/",
139+
"xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
134140
};
135141

136142
module.exports = CoreXform;

lib/xlsx/xform/strings/shared-strings-xform.js

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
const XmlStream = require('../../../utils/xml-stream');
2-
const BaseXform = require('../base-xform');
3-
const SharedStringXform = require('./shared-string-xform');
1+
const XmlStream = require("../../../utils/xml-stream");
2+
const BaseXform = require("../base-xform");
3+
const SharedStringXform = require("./shared-string-xform");
44

55
class SharedStringsXform extends BaseXform {
66
constructor(model) {
@@ -15,7 +15,10 @@ class SharedStringsXform extends BaseXform {
1515
}
1616

1717
get sharedStringXform() {
18-
return this._sharedStringXform || (this._sharedStringXform = new SharedStringXform());
18+
return (
19+
this._sharedStringXform ||
20+
(this._sharedStringXform = new SharedStringXform())
21+
);
1922
}
2023

2124
get values() {
@@ -70,14 +73,14 @@ class SharedStringsXform extends BaseXform {
7073
model = model || this._values;
7174
xmlStream.openXml(XmlStream.StdDocAttributes);
7275

73-
xmlStream.openNode('sst', {
74-
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
76+
xmlStream.openNode("sst", {
77+
xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
7578
count: model.count,
7679
uniqueCount: model.values.length,
7780
});
7881

7982
const sx = this.sharedStringXform;
80-
model.values.forEach(sharedString => {
83+
model.values.forEach((sharedString) => {
8184
sx.render(xmlStream, sharedString);
8285
});
8386
xmlStream.closeNode();
@@ -89,14 +92,15 @@ class SharedStringsXform extends BaseXform {
8992
return true;
9093
}
9194
switch (node.name) {
92-
case 'sst':
95+
case "sst":
9396
return true;
94-
case 'si':
97+
case "si":
9598
this.parser = this.sharedStringXform;
9699
this.parser.parseOpen(node);
97100
return true;
98101
default:
99-
throw new Error(`Unexpected xml node in parseOpen: ${JSON.stringify(node)}`);
102+
// Ignore unknown tags for compatibility with non-Excel spreadsheet applications
103+
return false;
100104
}
101105
}
102106

@@ -116,10 +120,11 @@ class SharedStringsXform extends BaseXform {
116120
return true;
117121
}
118122
switch (name) {
119-
case 'sst':
123+
case "sst":
120124
return false;
121125
default:
122-
throw new Error(`Unexpected xml node in parseClose: ${name}`);
126+
// Ignore unknown tags for compatibility with non-Excel spreadsheet applications
127+
return true;
123128
}
124129
}
125130
}

0 commit comments

Comments
 (0)