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.
- package/README.html +45 -37
- package/README.md +54 -34
- package/lib/{utils/bounds.js → bounds.js} +27 -4
- package/lib/chart.js +104 -30
- package/lib/codegen.js +66 -7
- package/lib/color.js +275 -0
- package/lib/convert.js +12 -8
- package/lib/extractor.js +200 -153
- package/lib/mapper.js +4 -0
- package/lib/packager.js +65 -8
- package/lib/placeholder.js +161 -29
- package/lib/presentation.js +5 -5
- package/lib/rels.js +19 -6
- package/lib/run-utils.js +181 -0
- package/lib/smartart.js +7 -15
- package/lib/table.js +33 -28
- package/lib/text-utils.js +235 -0
- package/lib/xml-parser.js +191 -15
- package/lib/xml-utils.js +82 -36
- package/package.json +4 -4
- package/lib/utils/color.js +0 -128
- package/lib/utils/emu.js +0 -20
|
@@ -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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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 {
|
|
24
|
+
* @returns {OONode}
|
|
20
25
|
*/
|
|
21
26
|
function parseXml(str) {
|
|
22
|
-
return
|
|
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(/&/g, '&')
|
|
193
|
+
.replace(/</g, '<')
|
|
194
|
+
.replace(/>/g, '>')
|
|
195
|
+
.replace(/"/g, '"')
|
|
196
|
+
.replace(/'/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
|
|
201
|
+
module.exports = { parseXml };
|
package/lib/xml-utils.js
CHANGED
|
@@ -1,68 +1,114 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* OOXML 节点访问工具
|
|
3
|
+
* 配合自定义 xml-parser.js 使用
|
|
4
|
+
*
|
|
5
|
+
* 节点结构:{ tag, attrs, children: OONode[], text }
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
|
-
*
|
|
7
|
-
* @
|
|
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
|
|
10
|
-
if (
|
|
11
|
-
return
|
|
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
|
-
*
|
|
16
|
-
* @
|
|
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
|
|
19
|
-
if (node
|
|
20
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* @
|
|
64
|
+
* 兼容旧版 asArray(对迁移中未改完的调用点提供临时支持)
|
|
65
|
+
* 新代码应直接用 children(node, tag) 代替 asArray(node[tag])
|
|
66
|
+
* @param {*} value
|
|
67
|
+
* @returns {Array}
|
|
37
68
|
*/
|
|
38
|
-
function
|
|
39
|
-
if (
|
|
40
|
-
return
|
|
69
|
+
function asArray(value) {
|
|
70
|
+
if (value == null) return [];
|
|
71
|
+
return Array.isArray(value) ? value : [value];
|
|
41
72
|
}
|
|
42
73
|
|
|
43
74
|
/**
|
|
44
|
-
*
|
|
45
|
-
* @param {
|
|
46
|
-
* @
|
|
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,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
*
|
|
58
|
-
* @
|
|
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
|
|
61
|
-
if (
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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 = {
|
|
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.
|
|
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"
|