pptx2js 0.4.1 → 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 +54 -18
- package/lib/codegen.js +5 -2
- package/lib/color.js +275 -0
- package/lib/convert.js +12 -8
- package/lib/extractor.js +188 -195
- package/lib/mapper.js +3 -0
- package/lib/packager.js +65 -8
- package/lib/placeholder.js +161 -29
- package/lib/presentation.js +5 -5
- package/lib/rels.js +4 -4
- package/lib/run-utils.js +3 -1
- package/lib/smartart.js +7 -15
- package/lib/table.js +7 -8
- package/lib/text-utils.js +235 -0
- package/lib/xml-parser.js +191 -15
- package/lib/xml-utils.js +82 -36
- package/package.json +2 -3
- package/lib/utils/color.js +0 -128
- package/lib/utils/emu.js +0 -20
package/lib/placeholder.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* 母版 / 版式占位符继承(design.html §4.1)
|
|
3
3
|
* 优先级:slide → slideLayout → slideMaster
|
|
4
4
|
*/
|
|
5
|
-
const {
|
|
5
|
+
const { attr, child, children, documentRoot } = require('./xml-utils');
|
|
6
6
|
|
|
7
7
|
const REL_SLIDE_LAYOUT =
|
|
8
8
|
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout';
|
|
@@ -69,10 +69,7 @@ function resolveRelByType(relIndex, sourcePath, typeFragment) {
|
|
|
69
69
|
function getSpTree(parsed, partPath) {
|
|
70
70
|
const doc = parsed[partPath];
|
|
71
71
|
if (!doc) return null;
|
|
72
|
-
const root =
|
|
73
|
-
child(doc, 'p:sld') ??
|
|
74
|
-
child(doc, 'p:sldLayout') ??
|
|
75
|
-
child(doc, 'p:sldMaster');
|
|
72
|
+
const root = documentRoot(doc, 'p:sld', 'p:sldLayout', 'p:sldMaster');
|
|
76
73
|
if (!root) return null;
|
|
77
74
|
return child(child(root, 'p:cSld'), 'p:spTree');
|
|
78
75
|
}
|
|
@@ -112,12 +109,11 @@ function findSpByPlaceholder(spTree, key) {
|
|
|
112
109
|
* @returns {object[]}
|
|
113
110
|
*/
|
|
114
111
|
function* walkAllSp(spTree) {
|
|
115
|
-
for (const sp of
|
|
112
|
+
for (const sp of children(spTree, 'p:sp')) {
|
|
116
113
|
yield sp;
|
|
117
114
|
}
|
|
118
|
-
for (const grp of
|
|
119
|
-
|
|
120
|
-
if (inner) yield* walkAllSp(inner);
|
|
115
|
+
for (const grp of children(spTree, 'p:grpSp')) {
|
|
116
|
+
yield* walkAllSp(grp);
|
|
121
117
|
}
|
|
122
118
|
}
|
|
123
119
|
|
|
@@ -152,10 +148,12 @@ function resolvePlaceholderSps(slideSp, inheritance) {
|
|
|
152
148
|
function mergeXfrm(slideXfrm, layoutXfrm, masterXfrm) {
|
|
153
149
|
const layers = [slideXfrm, layoutXfrm, masterXfrm];
|
|
154
150
|
const off = mergeXfrmPart(
|
|
151
|
+
'a:off',
|
|
155
152
|
layers.map((x) => child(x, 'a:off')),
|
|
156
153
|
['x', 'y']
|
|
157
154
|
);
|
|
158
155
|
const ext = mergeXfrmPart(
|
|
156
|
+
'a:ext',
|
|
159
157
|
layers.map((x) => child(x, 'a:ext')),
|
|
160
158
|
['cx', 'cy'],
|
|
161
159
|
{ skipZero: true }
|
|
@@ -163,18 +161,24 @@ function mergeXfrm(slideXfrm, layoutXfrm, masterXfrm) {
|
|
|
163
161
|
|
|
164
162
|
if (!off && !ext) return null;
|
|
165
163
|
return {
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
tag: 'a:xfrm',
|
|
165
|
+
attrs: {},
|
|
166
|
+
children: [
|
|
167
|
+
off ?? makeXfrmPart('a:off', { x: '0', y: '0' }),
|
|
168
|
+
ext ?? makeXfrmPart('a:ext', { cx: '0', cy: '0' }),
|
|
169
|
+
],
|
|
170
|
+
text: '',
|
|
168
171
|
};
|
|
169
172
|
}
|
|
170
173
|
|
|
171
174
|
/**
|
|
175
|
+
* @param {string} tag
|
|
172
176
|
* @param {(object|null|undefined)[]} parts
|
|
173
177
|
* @param {string[]} attrNames
|
|
174
178
|
* @param {{ skipZero?: boolean }} [opts]
|
|
175
|
-
* @returns {
|
|
179
|
+
* @returns {import('./xml-parser').OONode|null}
|
|
176
180
|
*/
|
|
177
|
-
function mergeXfrmPart(parts, attrNames, opts = {}) {
|
|
181
|
+
function mergeXfrmPart(tag, parts, attrNames, opts = {}) {
|
|
178
182
|
const values = {};
|
|
179
183
|
let hasAny = false;
|
|
180
184
|
|
|
@@ -193,7 +197,29 @@ function mergeXfrmPart(parts, attrNames, opts = {}) {
|
|
|
193
197
|
}
|
|
194
198
|
|
|
195
199
|
if (!hasAny) return null;
|
|
196
|
-
return
|
|
200
|
+
return makeXfrmPart(tag, values);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @param {string} tag
|
|
205
|
+
* @param {Record<string, string>} attrs
|
|
206
|
+
* @returns {import('./xml-parser').OONode}
|
|
207
|
+
*/
|
|
208
|
+
function makeXfrmPart(tag, attrs) {
|
|
209
|
+
return { tag, attrs, children: [], text: '' };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @param {import('./xml-parser').OONode} node
|
|
214
|
+
* @returns {import('./xml-parser').OONode}
|
|
215
|
+
*/
|
|
216
|
+
function cloneNode(node) {
|
|
217
|
+
return {
|
|
218
|
+
tag: node.tag,
|
|
219
|
+
attrs: { ...node.attrs },
|
|
220
|
+
children: node.children.map(cloneNode),
|
|
221
|
+
text: node.text,
|
|
222
|
+
};
|
|
197
223
|
}
|
|
198
224
|
|
|
199
225
|
/**
|
|
@@ -213,35 +239,137 @@ function getEffectiveXfrm(slideSp, inheritance) {
|
|
|
213
239
|
}
|
|
214
240
|
|
|
215
241
|
/**
|
|
216
|
-
*
|
|
242
|
+
* @param {import('./xml-parser').OONode} slide p:sld
|
|
243
|
+
* @returns {boolean}
|
|
244
|
+
*/
|
|
245
|
+
function shouldShowMasterShapes(slide) {
|
|
246
|
+
const v = attr(slide, 'showMasterSp');
|
|
247
|
+
if (v === '0' || v === 'false') return false;
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 从 txBody 的 lstStyle / endParaRPr 取默认 run 样式
|
|
253
|
+
* @param {object|null|undefined} txBody
|
|
254
|
+
* @returns {import('./xml-parser').OONode|null}
|
|
255
|
+
*/
|
|
256
|
+
function getDefaultRPrFromTxBody(txBody) {
|
|
257
|
+
if (!txBody) return null;
|
|
258
|
+
|
|
259
|
+
const lstStyle = child(txBody, 'a:lstStyle');
|
|
260
|
+
if (lstStyle) {
|
|
261
|
+
for (let lvl = 1; lvl <= 9; lvl++) {
|
|
262
|
+
const lvlPPr = child(lstStyle, `a:lvl${lvl}pPr`);
|
|
263
|
+
const defRPr = lvlPPr && child(lvlPPr, 'a:defRPr');
|
|
264
|
+
if (defRPr) return defRPr;
|
|
265
|
+
}
|
|
266
|
+
const defPPr = child(lstStyle, 'a:defPPr');
|
|
267
|
+
const defRPr = defPPr && child(defPPr, 'a:defRPr');
|
|
268
|
+
if (defRPr) return defRPr;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const firstP = children(txBody, 'a:p')[0];
|
|
272
|
+
if (firstP) {
|
|
273
|
+
const endRPr = child(firstP, 'a:endParaRPr');
|
|
274
|
+
if (endRPr) return endRPr;
|
|
275
|
+
const firstR = children(firstP, 'a:r')[0];
|
|
276
|
+
if (firstR) {
|
|
277
|
+
const rPr = child(firstR, 'a:rPr');
|
|
278
|
+
if (rPr) return rPr;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* @param {import('./xml-parser').OONode|null|undefined} slideRPr
|
|
286
|
+
* @param {import('./xml-parser').OONode} fallbackRPr
|
|
287
|
+
* @returns {import('./xml-parser').OONode}
|
|
288
|
+
*/
|
|
289
|
+
function mergeRPrAttrs(slideRPr, fallbackRPr) {
|
|
290
|
+
if (!fallbackRPr) return slideRPr;
|
|
291
|
+
if (!slideRPr) return cloneNode(fallbackRPr);
|
|
292
|
+
|
|
293
|
+
const merged = cloneNode(slideRPr);
|
|
294
|
+
for (const [k, v] of Object.entries(fallbackRPr.attrs)) {
|
|
295
|
+
if (merged.attrs[k] == null || merged.attrs[k] === '') {
|
|
296
|
+
merged.attrs[k] = v;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
for (const fc of fallbackRPr.children) {
|
|
300
|
+
if (!child(merged, fc.tag)) {
|
|
301
|
+
merged.children.push(cloneNode(fc));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return merged;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* 合并 txBody:补全 lstStyle 与 layout/master 的 defRPr
|
|
217
309
|
* @param {object} slideTxBody
|
|
218
310
|
* @param {object|null} layoutSp
|
|
219
311
|
* @param {object|null} masterSp
|
|
220
312
|
* @returns {object}
|
|
221
313
|
*/
|
|
222
314
|
function mergeTxBody(slideTxBody, layoutSp, masterSp) {
|
|
223
|
-
const slideBody = slideTxBody;
|
|
224
315
|
const fallbackBody =
|
|
225
316
|
(layoutSp && child(layoutSp, 'p:txBody')) ||
|
|
226
317
|
(masterSp && child(masterSp, 'p:txBody'));
|
|
227
|
-
if (!fallbackBody) return slideBody;
|
|
228
318
|
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
319
|
+
const merged = cloneNode(slideTxBody);
|
|
320
|
+
|
|
321
|
+
if (fallbackBody) {
|
|
322
|
+
const fallbackLst = child(fallbackBody, 'a:lstStyle');
|
|
323
|
+
if (fallbackLst && !child(merged, 'a:lstStyle')) {
|
|
324
|
+
merged.children.unshift(cloneNode(fallbackLst));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
232
327
|
|
|
233
|
-
|
|
328
|
+
const fallbackRPr = fallbackBody
|
|
329
|
+
? getDefaultRPrFromTxBody(fallbackBody)
|
|
330
|
+
: null;
|
|
331
|
+
if (!fallbackRPr) return merged;
|
|
234
332
|
|
|
235
|
-
const
|
|
236
|
-
const
|
|
333
|
+
for (const p of children(merged, 'a:p')) {
|
|
334
|
+
for (const r of children(p, 'a:r')) {
|
|
237
335
|
const rPr = child(r, 'a:rPr');
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
336
|
+
const mergedRPr = mergeRPrAttrs(rPr, fallbackRPr);
|
|
337
|
+
if (rPr) {
|
|
338
|
+
const idx = r.children.findIndex((c) => c.tag === 'a:rPr');
|
|
339
|
+
r.children[idx] = mergedRPr;
|
|
340
|
+
} else {
|
|
341
|
+
r.children.unshift(mergedRPr);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const endRPr = child(p, 'a:endParaRPr');
|
|
345
|
+
if (endRPr) {
|
|
346
|
+
const idx = p.children.findIndex((c) => c.tag === 'a:endParaRPr');
|
|
347
|
+
p.children[idx] = mergeRPrAttrs(endRPr, fallbackRPr);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return merged;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* 按级别读取 lstStyle 中的 defRPr(lvl1pPr → 索引 0)
|
|
355
|
+
* @param {object|null|undefined} txBody
|
|
356
|
+
* @returns {Record<number, import('./xml-parser').OONode>}
|
|
357
|
+
*/
|
|
358
|
+
function getLstStyleDefRPrByLevel(txBody) {
|
|
359
|
+
/** @type {Record<number, import('./xml-parser').OONode>} */
|
|
360
|
+
const byLevel = {};
|
|
361
|
+
const lstStyle = txBody && child(txBody, 'a:lstStyle');
|
|
362
|
+
if (!lstStyle) return byLevel;
|
|
243
363
|
|
|
244
|
-
|
|
364
|
+
for (let lvl = 1; lvl <= 9; lvl++) {
|
|
365
|
+
const lvlPPr = child(lstStyle, `a:lvl${lvl}pPr`);
|
|
366
|
+
const defRPr = lvlPPr && child(lvlPPr, 'a:defRPr');
|
|
367
|
+
if (defRPr) byLevel[lvl - 1] = defRPr;
|
|
368
|
+
}
|
|
369
|
+
const defPPr = child(lstStyle, 'a:defPPr');
|
|
370
|
+
const defRPr = defPPr && child(defPPr, 'a:defRPr');
|
|
371
|
+
if (defRPr && byLevel[0] == null) byLevel[0] = defRPr;
|
|
372
|
+
return byLevel;
|
|
245
373
|
}
|
|
246
374
|
|
|
247
375
|
module.exports = {
|
|
@@ -254,5 +382,9 @@ module.exports = {
|
|
|
254
382
|
mergeXfrm,
|
|
255
383
|
getEffectiveXfrm,
|
|
256
384
|
mergeTxBody,
|
|
385
|
+
mergeRPrAttrs,
|
|
386
|
+
getDefaultRPrFromTxBody,
|
|
387
|
+
getLstStyleDefRPrByLevel,
|
|
388
|
+
shouldShowMasterShapes,
|
|
257
389
|
getSpTree,
|
|
258
390
|
};
|
package/lib/presentation.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* presentation.xml:幻灯片列表与版面尺寸
|
|
3
3
|
*/
|
|
4
|
-
const {
|
|
5
|
-
const { emuToInch } = require('./
|
|
4
|
+
const { attr, child, children, documentRoot } = require('./xml-utils');
|
|
5
|
+
const { emuToInch } = require('./bounds');
|
|
6
6
|
|
|
7
7
|
const PRESENTATION_PATH = 'ppt/presentation.xml';
|
|
8
8
|
|
|
@@ -13,10 +13,10 @@ const PRESENTATION_PATH = 'ppt/presentation.xml';
|
|
|
13
13
|
*/
|
|
14
14
|
function getSlidePaths(parsed, relIndex) {
|
|
15
15
|
const doc = parsed[PRESENTATION_PATH];
|
|
16
|
-
const pres =
|
|
16
|
+
const pres = documentRoot(doc, 'p:presentation');
|
|
17
17
|
if (!pres) return [];
|
|
18
18
|
|
|
19
|
-
const sldIds =
|
|
19
|
+
const sldIds = children(child(pres, 'p:sldIdLst'), 'p:sldId');
|
|
20
20
|
const paths = [];
|
|
21
21
|
for (const sldId of sldIds) {
|
|
22
22
|
const relId = attr(sldId, 'r:id');
|
|
@@ -32,7 +32,7 @@ function getSlidePaths(parsed, relIndex) {
|
|
|
32
32
|
* @returns {{ width: number, height: number }}
|
|
33
33
|
*/
|
|
34
34
|
function getSlideSizeInches(parsed) {
|
|
35
|
-
const pres =
|
|
35
|
+
const pres = documentRoot(parsed[PRESENTATION_PATH], 'p:presentation');
|
|
36
36
|
const sldSz = child(pres, 'p:sldSz');
|
|
37
37
|
const cx = parseInt(attr(sldSz, 'cx') ?? '9144000', 10);
|
|
38
38
|
const cy = parseInt(attr(sldSz, 'cy') ?? '6858000', 10);
|
package/lib/rels.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* OOXML 关系(.rels)解析与路径解析
|
|
3
3
|
*/
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const {
|
|
5
|
+
const { attr, children, documentRoot } = require('./xml-utils');
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @typedef {object} Relationship
|
|
@@ -76,8 +76,8 @@ function resolveTargetPath(ownerPath, target) {
|
|
|
76
76
|
* @returns {Relationship[]}
|
|
77
77
|
*/
|
|
78
78
|
function parseRelationships(relsDoc) {
|
|
79
|
-
const root = relsDoc
|
|
80
|
-
const rels =
|
|
79
|
+
const root = documentRoot(relsDoc, 'Relationships');
|
|
80
|
+
const rels = children(root, 'Relationship');
|
|
81
81
|
return rels
|
|
82
82
|
.map((rel) => ({
|
|
83
83
|
id: attr(rel, 'Id'),
|
|
@@ -97,7 +97,7 @@ function buildRelationIndex(parsed) {
|
|
|
97
97
|
const bySource = new Map();
|
|
98
98
|
|
|
99
99
|
for (const [filePath, doc] of Object.entries(parsed)) {
|
|
100
|
-
if (
|
|
100
|
+
if (!/\.rels$/i.test(filePath)) continue;
|
|
101
101
|
const owner = relsOwnerPath(filePath);
|
|
102
102
|
const relationships = parseRelationships(doc);
|
|
103
103
|
const map = new Map();
|
package/lib/run-utils.js
CHANGED
|
@@ -97,7 +97,9 @@ function compressTextRuns(runs) {
|
|
|
97
97
|
if (optionsEqual(prev.options, tail.options) && !prev.text.endsWith('\n')) {
|
|
98
98
|
prev.text += tail.text.includes('\r') ? '\r\n' : '\n';
|
|
99
99
|
} else {
|
|
100
|
-
|
|
100
|
+
const nlOpts = { ...tail.options };
|
|
101
|
+
delete nlOpts.bullet;
|
|
102
|
+
out.push({ text: tail.text, options: nlOpts });
|
|
101
103
|
break;
|
|
102
104
|
}
|
|
103
105
|
}
|
package/lib/smartart.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
const { attr, child, textContent } = require('./xml-utils');
|
|
5
5
|
const { getGraphicXfrm } = require('./graphic');
|
|
6
|
-
const { boundsFromXfrm } = require('./
|
|
6
|
+
const { boundsFromXfrm } = require('./bounds');
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* @param {object} graphicFrame
|
|
@@ -74,22 +74,14 @@ function extractSmartArtTexts(graphicFrame, ctx) {
|
|
|
74
74
|
* @param {string[]} out
|
|
75
75
|
*/
|
|
76
76
|
function collectTextNodes(node, out) {
|
|
77
|
-
if (node
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (node['a:t'] != null) {
|
|
82
|
-
const t = textContent(node['a:t']);
|
|
77
|
+
if (!node || typeof node !== 'object') return;
|
|
78
|
+
if (node.tag === 'a:t') {
|
|
79
|
+
const t = textContent(node);
|
|
83
80
|
if (t) out.push(t);
|
|
81
|
+
return;
|
|
84
82
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (key === '$') continue;
|
|
88
|
-
if (Array.isArray(val)) {
|
|
89
|
-
for (const item of val) collectTextNodes(item, out);
|
|
90
|
-
} else if (val && typeof val === 'object') {
|
|
91
|
-
collectTextNodes(val, out);
|
|
92
|
-
}
|
|
83
|
+
for (const c of node.children || []) {
|
|
84
|
+
collectTextNodes(c, out);
|
|
93
85
|
}
|
|
94
86
|
}
|
|
95
87
|
|
package/lib/table.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 表格提取(design.html §4.5)
|
|
3
3
|
*/
|
|
4
|
-
const {
|
|
4
|
+
const { attr, child, children } = require('./xml-utils');
|
|
5
5
|
const { getGraphicXfrm } = require('./graphic');
|
|
6
|
-
const { boundsFromXfrm } = require('./
|
|
7
|
-
const { resolveFillColor } = require('./
|
|
8
|
-
const { emuToInch } = require('./utils/emu');
|
|
6
|
+
const { boundsFromXfrm, emuToInch } = require('./bounds');
|
|
7
|
+
const { resolveFillColor } = require('./color');
|
|
9
8
|
const { compressCellBorder, flattenTableCellText } = require('./run-utils');
|
|
9
|
+
const { extractTextRuns } = require('./text-utils');
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* @param {object} graphicFrame
|
|
@@ -50,16 +50,15 @@ function extractTable(graphicFrame, ctx) {
|
|
|
50
50
|
*/
|
|
51
51
|
function extractTableRows(tbl, scheme) {
|
|
52
52
|
const rows = [];
|
|
53
|
-
for (const tr of
|
|
53
|
+
for (const tr of children(tbl, 'a:tr')) {
|
|
54
54
|
const cells = [];
|
|
55
|
-
for (const tc of
|
|
55
|
+
for (const tc of children(tr, 'a:tc')) {
|
|
56
56
|
const tcPr = child(tc, 'a:tcPr');
|
|
57
57
|
const txBody = child(tc, 'a:txBody');
|
|
58
58
|
let text = '';
|
|
59
59
|
/** @type {object} */
|
|
60
60
|
let runOptions = {};
|
|
61
61
|
if (txBody) {
|
|
62
|
-
const { extractTextRuns } = require('./extractor');
|
|
63
62
|
const flat = flattenTableCellText(extractTextRuns(txBody, scheme, null));
|
|
64
63
|
text = flat.text;
|
|
65
64
|
runOptions = flat.runOptions;
|
|
@@ -133,7 +132,7 @@ function extractCellBorderSides(tcPr, scheme) {
|
|
|
133
132
|
*/
|
|
134
133
|
function extractColWidths(tbl) {
|
|
135
134
|
const grid = child(tbl, 'a:tblGrid');
|
|
136
|
-
const cols =
|
|
135
|
+
const cols = children(grid, 'a:gridCol');
|
|
137
136
|
if (!cols.length) return undefined;
|
|
138
137
|
return cols.map((col) => {
|
|
139
138
|
const w = parseInt(attr(col, 'w') ?? '0', 10);
|
|
@@ -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
|
+
};
|