pptx2js 0.4.0 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,235 @@
1
+ /**
2
+ * 文本 run 提取(从 extractor 拆出,避免 extractor ↔ table 循环依赖)
3
+ */
4
+ const { attr, child, childNodes, children, textContent } = require('./xml-utils');
5
+ const {
6
+ resolveFillColor,
7
+ resolveColorFromContainer,
8
+ } = require('./color');
9
+ const { compressTextRuns } = require('./run-utils');
10
+ const {
11
+ getLstStyleDefRPrByLevel,
12
+ mergeRPrAttrs,
13
+ } = require('./placeholder');
14
+
15
+ const PARA_ALIGN_MAP = {
16
+ l: 'left',
17
+ ctr: 'center',
18
+ r: 'right',
19
+ just: 'justify',
20
+ dist: 'justify',
21
+ };
22
+
23
+ /**
24
+ * @param {object} txBody a:txBody
25
+ * @param {Record<string, string>} scheme
26
+ * @param {{ relIndex?: import('./rels').RelationIndex, slidePath?: string }|null} linkCtx
27
+ */
28
+ function extractTextRuns(txBody, scheme, linkCtx) {
29
+ const runs = [];
30
+ const bodyParaOpts = extractBodyPrParaOptions(txBody);
31
+ const defRPrByLevel = getLstStyleDefRPrByLevel(txBody);
32
+ const defaultFontSize =
33
+ bodyParaOpts._defaultFontSize ??
34
+ runFontSizeFromDefRPr(defRPrByLevel[0]);
35
+ const paragraphs = children(txBody, 'a:p');
36
+
37
+ for (let pi = 0; pi < paragraphs.length; pi++) {
38
+ const p = paragraphs[pi];
39
+ let currentPPr = null;
40
+ let firstPPr = null;
41
+
42
+ for (const node of childNodes(p)) {
43
+ if (node.tag === 'a:pPr') {
44
+ currentPPr = node;
45
+ if (!firstPPr) firstPPr = node;
46
+ continue;
47
+ }
48
+
49
+ const pPr = currentPPr ?? firstPPr;
50
+ const paraOpts = {
51
+ ...bodyParaOpts,
52
+ ...extractParaOptions(pPr, defaultFontSize),
53
+ };
54
+ delete paraOpts._defaultFontSize;
55
+ const bullet0 = extractBullet(firstPPr);
56
+ const bullet = extractBullet(pPr) ?? bullet0;
57
+
58
+ const lvl = paraOpts.indentLevel ?? 0;
59
+ const defRPr =
60
+ defRPrByLevel[lvl] ?? defRPrByLevel[0] ?? null;
61
+
62
+ if (node.tag === 'a:r') {
63
+ const text = textContent(child(node, 'a:t'));
64
+ if (!text) continue;
65
+ const rPr = mergeRPrAttrs(child(node, 'a:rPr'), defRPr);
66
+ const options = extractRunOptions(rPr, scheme, linkCtx);
67
+ Object.assign(options, paraOpts);
68
+ if (bullet) Object.assign(options, bullet);
69
+ runs.push({ text, options, degraded: options._degraded });
70
+ } else if (node.tag === 'a:fld') {
71
+ const text = textContent(child(node, 'a:t'));
72
+ if (!text) continue;
73
+ const rPr = mergeRPrAttrs(child(node, 'a:rPr'), defRPr);
74
+ const options = extractRunOptions(rPr, scheme, linkCtx);
75
+ Object.assign(options, paraOpts);
76
+ runs.push({ text, options, degraded: options._degraded });
77
+ } else if (node.tag === 'a:br') {
78
+ runs.push({ text: '\n', options: { ...paraOpts }, degraded: false });
79
+ }
80
+ }
81
+
82
+ if (pi < paragraphs.length - 1) {
83
+ const last = runs[runs.length - 1];
84
+ if (!last || !last.text.endsWith('\n')) {
85
+ const paraOpts = {
86
+ ...bodyParaOpts,
87
+ ...extractParaOptions(firstPPr, defaultFontSize),
88
+ };
89
+ delete paraOpts._defaultFontSize;
90
+ if (extractBullet(firstPPr)) delete paraOpts.bullet;
91
+ runs.push({ text: '\n', options: { ...paraOpts }, degraded: false });
92
+ }
93
+ }
94
+ }
95
+ return compressTextRuns(runs);
96
+ }
97
+
98
+ /**
99
+ * @param {import('./xml-parser').OONode|null|undefined} defRPr
100
+ * @returns {number|undefined}
101
+ */
102
+ function runFontSizeFromDefRPr(defRPr) {
103
+ if (!defRPr) return undefined;
104
+ const sz = attr(defRPr, 'sz');
105
+ return sz ? parseInt(sz, 10) / 100 : undefined;
106
+ }
107
+
108
+ /**
109
+ * @param {object|null|undefined} txBody
110
+ */
111
+ function extractBodyPrParaOptions(txBody) {
112
+ if (!txBody) return {};
113
+ const bodyPr = child(txBody, 'a:bodyPr');
114
+ const lstStyle = child(txBody, 'a:lstStyle');
115
+ const defPPr = lstStyle && child(lstStyle, 'a:defPPr');
116
+ const defRPr = defPPr && child(defPPr, 'a:defRPr');
117
+ const fontSize = runFontSizeFromDefRPr(defRPr);
118
+ const opts = defPPr ? extractParaOptions(defPPr, fontSize) : {};
119
+ if (fontSize != null) opts._defaultFontSize = fontSize;
120
+ if (bodyPr && attr(bodyPr, 'anchor')) {
121
+ // bodyPr anchor 不映射到 run
122
+ }
123
+ return opts;
124
+ }
125
+
126
+ /**
127
+ * @param {object|null|undefined} spcParent a:spcBef | a:spcAft | a:lnSpc
128
+ * @param {number|undefined} fontSizePt
129
+ * @returns {number|undefined} 点数
130
+ */
131
+ function readSpacingPt(spcParent, fontSizePt) {
132
+ if (!spcParent) return undefined;
133
+ const spcPts = child(spcParent, 'a:spcPts');
134
+ if (spcPts) {
135
+ const val = attr(spcPts, 'val');
136
+ return val ? parseInt(val, 10) / 100 : undefined;
137
+ }
138
+ const spcPct = child(spcParent, 'a:spcPct');
139
+ if (spcPct && fontSizePt) {
140
+ const val = attr(spcPct, 'val');
141
+ if (!val) return undefined;
142
+ return (fontSizePt * parseInt(val, 10)) / 100000;
143
+ }
144
+ return undefined;
145
+ }
146
+
147
+ /**
148
+ * @param {object|null|undefined} pPr
149
+ * @param {number|undefined} [defaultFontSizePt]
150
+ */
151
+ function extractParaOptions(pPr, defaultFontSizePt) {
152
+ if (!pPr) return {};
153
+ const node = pPr;
154
+ const opts = {};
155
+
156
+ const algn = attr(node, 'algn');
157
+ if (algn && PARA_ALIGN_MAP[algn]) opts.align = PARA_ALIGN_MAP[algn];
158
+
159
+ const lvl = attr(node, 'lvl');
160
+ if (lvl) opts.indentLevel = parseInt(lvl, 10);
161
+
162
+ const defRPr = child(node, 'a:defRPr');
163
+ const fontSize = runFontSizeFromDefRPr(defRPr) ?? defaultFontSizePt;
164
+
165
+ const spcBef = readSpacingPt(child(node, 'a:spcBef'), fontSize);
166
+ if (spcBef != null) opts.paraSpaceBefore = spcBef;
167
+
168
+ const spcAft = readSpacingPt(child(node, 'a:spcAft'), fontSize);
169
+ if (spcAft != null) opts.paraSpaceAfter = spcAft;
170
+
171
+ const lnSpc = readSpacingPt(child(node, 'a:lnSpc'), fontSize);
172
+ if (lnSpc != null) opts.lineSpacing = lnSpc;
173
+
174
+ return opts;
175
+ }
176
+
177
+ /**
178
+ * @param {object|null|undefined} pPr
179
+ */
180
+ function extractBullet(pPr) {
181
+ if (!pPr) return null;
182
+ if (child(pPr, 'a:buChar')) return { bullet: true };
183
+ if (child(pPr, 'a:buAutoNum')) return { bullet: { type: 'number' } };
184
+ return null;
185
+ }
186
+
187
+ /**
188
+ * @param {object|null|undefined} rPr
189
+ * @param {Record<string, string>} scheme
190
+ * @param {{ relIndex?: import('./rels').RelationIndex, slidePath?: string }|null} linkCtx
191
+ */
192
+ function extractRunOptions(rPr, scheme, linkCtx) {
193
+ const options = {};
194
+ let degraded = false;
195
+ if (!rPr) return options;
196
+
197
+ const sz = attr(rPr, 'sz');
198
+ if (sz) options.fontSize = parseInt(sz, 10) / 100;
199
+ if (attr(rPr, 'b') === '1') options.bold = true;
200
+ if (attr(rPr, 'i') === '1') options.italic = true;
201
+
202
+ const u = attr(rPr, 'u');
203
+ if (u && u !== 'none') options.underline = { style: 'sng' };
204
+
205
+ const face = attr(child(rPr, 'a:latin'), 'typeface');
206
+ if (face) options.fontFace = face;
207
+
208
+ const solid = child(rPr, 'a:solidFill');
209
+ const grad = child(rPr, 'a:gradFill');
210
+ if (grad) {
211
+ degraded = true;
212
+ const g = resolveFillColor(grad, scheme);
213
+ if (g.color) options.color = g.color;
214
+ } else if (solid) {
215
+ const c = resolveColorFromContainer(solid, scheme);
216
+ if (c) options.color = c;
217
+ }
218
+
219
+ const hlink = child(rPr, 'a:hlinkClick');
220
+ if (hlink && linkCtx) {
221
+ const relId = attr(hlink, 'r:id');
222
+ const url = relId && linkCtx.relIndex.resolve(linkCtx.slidePath, relId);
223
+ if (url) options.hyperlink = { url };
224
+ }
225
+
226
+ if (degraded) options._degraded = true;
227
+ return options;
228
+ }
229
+
230
+ module.exports = {
231
+ extractTextRuns,
232
+ extractParaOptions,
233
+ extractRunOptions,
234
+ readSpacingPt,
235
+ };
package/lib/xml-parser.js CHANGED
@@ -1,25 +1,201 @@
1
1
  /**
2
- * 项目唯一导出的 XML 解析器实例。
3
- * 全项目必须使用此入口,禁止在模块内自行实例化解析器。
4
- * @see design.html §4.6
2
+ * OOXML 专用 XML 解析器(无外部依赖)
3
+ *
4
+ * 节点结构:
5
+ * { tag: 'a:r', attrs: { 'r:id': 'rId1', w: '25400' }, children: [...], text: '' }
6
+ *
7
+ * 特性:
8
+ * - children 保留原始 XML 子节点顺序
9
+ * - children() 始终返回数组(消除 xml2js 单/多不一致)
10
+ * - 空节点(如 <a:buNone/>)返回节点对象而非 '' / null(修复 xml2js 的 falsy 陷阱)
11
+ * - 命名空间前缀保留,与 OOXML 原文一致
5
12
  */
6
- const xml2js = require('xml2js');
7
13
 
8
- const PARSER_OPTIONS = {
9
- explicitArray: false,
10
- mergeAttrs: false,
11
- explicitCharkey: false,
12
- tagNameProcessors: [],
13
- attrNameProcessors: [],
14
- xmlns: false,
15
- };
14
+ /**
15
+ * @typedef {object} OONode
16
+ * @property {string} tag
17
+ * @property {Record<string,string>} attrs
18
+ * @property {OONode[]} children
19
+ * @property {string} text
20
+ */
16
21
 
17
22
  /**
18
23
  * @param {string} str
19
- * @returns {Promise<object>}
24
+ * @returns {OONode}
20
25
  */
21
26
  function parseXml(str) {
22
- return xml2js.parseStringPromise(str, PARSER_OPTIONS);
27
+ return new Parser(str).parse();
28
+ }
29
+
30
+ class Parser {
31
+ constructor(input) {
32
+ this.s = input;
33
+ this.pos = 0;
34
+ }
35
+
36
+ parse() {
37
+ this.skipProlog();
38
+ return this.parseElement();
39
+ }
40
+
41
+ skipProlog() {
42
+ while (this.pos < this.s.length) {
43
+ this.skipSpaces();
44
+ if (this.peek('<?')) {
45
+ const end = this.s.indexOf('?>', this.pos);
46
+ this.pos = end === -1 ? this.s.length : end + 2;
47
+ } else if (this.peek('<!--')) {
48
+ const end = this.s.indexOf('-->', this.pos);
49
+ this.pos = end === -1 ? this.s.length : end + 3;
50
+ } else {
51
+ break;
52
+ }
53
+ }
54
+ }
55
+
56
+ parseElement() {
57
+ this.skipSpaces();
58
+ this.eat('<');
59
+
60
+ const tag = this.readName();
61
+ const rawAttrs = this.readAttributes();
62
+ this.skipSpaces();
63
+
64
+ // 过滤掉 xmlns 声明,暴露给调用方的 attrs 只有业务属性
65
+ const attrs = {};
66
+ for (const [k, v] of Object.entries(rawAttrs)) {
67
+ if (k !== 'xmlns' && !k.startsWith('xmlns:')) {
68
+ attrs[k] = v;
69
+ }
70
+ }
71
+
72
+ /** @type {OONode} */
73
+ const node = { tag, attrs, children: [], text: '' };
74
+
75
+ if (this.s[this.pos] === '/') {
76
+ this.pos += 2; // />
77
+ return node;
78
+ }
79
+ this.eat('>');
80
+
81
+ // 收集子内容
82
+ let textBuf = '';
83
+
84
+ const flushText = () => {
85
+ if (textBuf) {
86
+ // 只在没有子元素时,或文本非纯空白时记录
87
+ // 这样格式化缩进不会污染有子节点的父元素
88
+ const decoded = decodeEntities(textBuf);
89
+ if (node.children.length === 0) {
90
+ // 叶节点:保留全部文本(包括 \n,因为 <a:t>\n</a:t> 是有意义的)
91
+ node.text += decoded;
92
+ } else {
93
+ // 有子节点的父元素:只保留非纯空白文本
94
+ if (decoded.trim()) {
95
+ node.text += decoded;
96
+ }
97
+ }
98
+ textBuf = '';
99
+ }
100
+ };
101
+
102
+ while (this.pos < this.s.length) {
103
+ if (this.peek('</')) {
104
+ flushText();
105
+ this.pos += 2;
106
+ this.readName();
107
+ this.skipSpaces();
108
+ this.eat('>');
109
+ break;
110
+ } else if (this.peek('<!--')) {
111
+ const end = this.s.indexOf('-->', this.pos);
112
+ this.pos = end === -1 ? this.s.length : end + 3;
113
+ } else if (this.peek('<![CDATA[')) {
114
+ const end = this.s.indexOf(']]>', this.pos);
115
+ if (end === -1) break;
116
+ textBuf += this.s.slice(this.pos + 9, end);
117
+ this.pos = end + 3;
118
+ } else if (this.s[this.pos] === '<') {
119
+ flushText();
120
+ node.children.push(this.parseElement());
121
+ } else {
122
+ // 文本字符
123
+ textBuf += this.s[this.pos++];
124
+ }
125
+ }
126
+
127
+ return node;
128
+ }
129
+
130
+ readName() {
131
+ const start = this.pos;
132
+ while (this.pos < this.s.length && /[\w\-\.:]/.test(this.s[this.pos])) {
133
+ this.pos++;
134
+ }
135
+ return this.s.slice(start, this.pos);
136
+ }
137
+
138
+ readAttributes() {
139
+ const attrs = {};
140
+ while (this.pos < this.s.length) {
141
+ this.skipSpaces();
142
+ const c = this.s[this.pos];
143
+ if (c === '>' || c === '/' || c === '?') break;
144
+
145
+ const name = this.readName();
146
+ if (!name) { this.pos++; continue; }
147
+
148
+ this.skipSpaces();
149
+ if (this.s[this.pos] !== '=') {
150
+ attrs[name] = '';
151
+ continue;
152
+ }
153
+ this.pos++; // =
154
+ this.skipSpaces();
155
+
156
+ const quote = this.s[this.pos];
157
+ if (quote !== '"' && quote !== "'") {
158
+ this.pos++;
159
+ const start = this.pos;
160
+ while (this.pos < this.s.length && !/[\s>]/.test(this.s[this.pos])) this.pos++;
161
+ attrs[name] = decodeEntities(this.s.slice(start, this.pos));
162
+ continue;
163
+ }
164
+ this.pos++;
165
+ const start = this.pos;
166
+ while (this.pos < this.s.length && this.s[this.pos] !== quote) this.pos++;
167
+ attrs[name] = decodeEntities(this.s.slice(start, this.pos));
168
+ this.pos++;
169
+ }
170
+ return attrs;
171
+ }
172
+
173
+ skipSpaces() {
174
+ while (this.pos < this.s.length && /[ \t\r\n]/.test(this.s[this.pos])) this.pos++;
175
+ }
176
+
177
+ peek(s) {
178
+ return this.s.startsWith(s, this.pos);
179
+ }
180
+
181
+ eat(ch) {
182
+ if (this.s[this.pos] !== ch) {
183
+ const ctx = this.s.slice(Math.max(0, this.pos - 30), this.pos + 30);
184
+ throw new Error(`XML parse error: expected '${ch}' at pos ${this.pos}, context: ...${ctx}...`);
185
+ }
186
+ this.pos++;
187
+ }
188
+ }
189
+
190
+ function decodeEntities(s) {
191
+ return s
192
+ .replace(/&amp;/g, '&')
193
+ .replace(/&lt;/g, '<')
194
+ .replace(/&gt;/g, '>')
195
+ .replace(/&quot;/g, '"')
196
+ .replace(/&apos;/g, "'")
197
+ .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(parseInt(n, 10)))
198
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, n) => String.fromCharCode(parseInt(n, 16)));
23
199
  }
24
200
 
25
- module.exports = { parseXml, PARSER_OPTIONS };
201
+ module.exports = { parseXml };
package/lib/xml-utils.js CHANGED
@@ -1,68 +1,114 @@
1
1
  /**
2
- * xml2js 对象树访问工具(配合 lib/xml-parser.js 统一配置)
2
+ * OOXML 节点访问工具
3
+ * 配合自定义 xml-parser.js 使用
4
+ *
5
+ * 节点结构:{ tag, attrs, children: OONode[], text }
3
6
  */
4
7
 
5
8
  /**
6
- * @param {*} value
7
- * @returns {Array}
9
+ * 取第一个匹配 tag 的子节点
10
+ * @param {import('./xml-parser').OONode|null|undefined} node
11
+ * @param {string} tag
12
+ * @returns {import('./xml-parser').OONode|undefined}
8
13
  */
9
- function asArray(value) {
10
- if (value == null) return [];
11
- return Array.isArray(value) ? value : [value];
14
+ function child(node, tag) {
15
+ if (!node || !node.children) return undefined;
16
+ return node.children.find(c => c.tag === tag);
12
17
  }
13
18
 
14
19
  /**
15
- * @param {object|null|undefined} node
16
- * @returns {Record<string, string>}
20
+ * 取所有匹配 tag 的子节点(始终返回数组,消除 xml2js 单/多不一致)
21
+ * @param {import('./xml-parser').OONode|null|undefined} node
22
+ * @param {string} tag
23
+ * @returns {import('./xml-parser').OONode[]}
17
24
  */
18
- function attrs(node) {
19
- if (node == null) return {};
20
- if (node.$) return node.$;
21
- return node;
25
+ function children(node, tag) {
26
+ if (!node || !node.children) return [];
27
+ return node.children.filter(c => c.tag === tag);
22
28
  }
23
29
 
24
30
  /**
25
- * @param {object|null|undefined} node
31
+ * 取节点的有序子节点列表(保留所有 tag,用于顺序敏感场景如 a:p)
32
+ * @param {import('./xml-parser').OONode|null|undefined} node
33
+ * @returns {import('./xml-parser').OONode[]}
34
+ */
35
+ function childNodes(node) {
36
+ if (!node || !node.children) return [];
37
+ return node.children;
38
+ }
39
+
40
+ /**
41
+ * 取属性值
42
+ * @param {import('./xml-parser').OONode|null|undefined} node
26
43
  * @param {string} name
27
44
  * @returns {string|undefined}
28
45
  */
29
46
  function attr(node, name) {
30
- return attrs(node)[name];
47
+ if (!node || !node.attrs) return undefined;
48
+ return node.attrs[name];
49
+ }
50
+
51
+ /**
52
+ * 取文本内容
53
+ * @param {import('./xml-parser').OONode|null|undefined} node
54
+ * @returns {string}
55
+ */
56
+ function textContent(node) {
57
+ if (node == null) return '';
58
+ if (typeof node === 'string') return node;
59
+ if (typeof node === 'number') return String(node);
60
+ return node.text ?? '';
31
61
  }
32
62
 
33
63
  /**
34
- * @param {object|null|undefined} node
35
- * @param {string} key
36
- * @returns {*}
64
+ * 兼容旧版 asArray(对迁移中未改完的调用点提供临时支持)
65
+ * 新代码应直接用 children(node, tag) 代替 asArray(node[tag])
66
+ * @param {*} value
67
+ * @returns {Array}
37
68
  */
38
- function child(node, key) {
39
- if (node == null) return undefined;
40
- return node[key];
69
+ function asArray(value) {
70
+ if (value == null) return [];
71
+ return Array.isArray(value) ? value : [value];
41
72
  }
42
73
 
43
74
  /**
44
- * @param {object|null|undefined} node
45
- * @param {string[]} keys
46
- * @returns {*}
75
+ * 取第一个匹配多个 tag 之一的子节点
76
+ * @param {import('./xml-parser').OONode|null|undefined} node
77
+ * @param {string[]} tags
78
+ * @returns {import('./xml-parser').OONode|undefined}
47
79
  */
48
- function firstChild(node, keys) {
49
- for (const key of keys) {
50
- const v = child(node, key);
51
- if (v != null) return v;
80
+ function firstChild(node, tags) {
81
+ if (!node || !node.children) return undefined;
82
+ for (const tag of tags) {
83
+ const found = node.children.find(c => c.tag === tag);
84
+ if (found != null) return found;
52
85
  }
53
86
  return undefined;
54
87
  }
55
88
 
56
89
  /**
57
- * @param {object|null|undefined} node
58
- * @returns {string}
90
+ * parseXml 返回文档根 OONode;若已是目标 tag 则直接返回,否则取同名子节点
91
+ * @param {import('./xml-parser').OONode|null|undefined} doc
92
+ * @param {...string} tags
93
+ * @returns {import('./xml-parser').OONode|undefined}
59
94
  */
60
- function textContent(node) {
61
- if (node == null) return '';
62
- if (typeof node === 'string') return node;
63
- if (typeof node === 'number') return String(node);
64
- if (node._ != null) return String(node._);
65
- return '';
95
+ function documentRoot(doc, ...tags) {
96
+ if (!doc) return undefined;
97
+ if (tags.includes(doc.tag)) return doc;
98
+ for (const tag of tags) {
99
+ const found = child(doc, tag);
100
+ if (found) return found;
101
+ }
102
+ return undefined;
66
103
  }
67
104
 
68
- module.exports = { asArray, attrs, attr, child, firstChild, textContent };
105
+ module.exports = {
106
+ child,
107
+ children,
108
+ childNodes,
109
+ attr,
110
+ textContent,
111
+ asArray,
112
+ firstChild,
113
+ documentRoot,
114
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pptx2js",
3
- "version": "0.4.0",
3
+ "version": "0.4.3",
4
4
  "description": "将 .pptx 文件转换为可运行的 PptxGenJS 生成脚本",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -8,7 +8,8 @@
8
8
  },
9
9
  "scripts": {
10
10
  "test": "jest",
11
- "test:watch": "jest --watch"
11
+ "test:watch": "jest --watch",
12
+ "test:sample": "npm run generate --prefix testPPT && node bin/pptx2js.js testppt/sample.pptx -o testppt/pptx2js-output"
12
13
  },
13
14
  "keywords": [
14
15
  "pptx",
@@ -27,8 +28,7 @@
27
28
  "dependencies": {
28
29
  "chalk": "^4.1.2",
29
30
  "commander": "^12.1.0",
30
- "jszip": "^3.10.1",
31
- "xml2js": "^0.6.2"
31
+ "jszip": "^3.10.1"
32
32
  },
33
33
  "devDependencies": {
34
34
  "jest": "^29.7.0"