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
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
|
|
@@ -51,11 +51,24 @@ function isExternalTarget(rel) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
function resolveTargetPath(ownerPath, target) {
|
|
54
|
+
const normalized = String(target).replace(/\\/g, '/');
|
|
55
|
+
|
|
56
|
+
// PptxGenJS 等写入的包根绝对路径:/ppt/charts/chart1.xml
|
|
57
|
+
if (normalized.startsWith('/')) {
|
|
58
|
+
return path.posix.normalize(normalized.slice(1));
|
|
59
|
+
}
|
|
60
|
+
|
|
54
61
|
if (!ownerPath) {
|
|
55
|
-
return path.posix.normalize(
|
|
62
|
+
return path.posix.normalize(normalized);
|
|
56
63
|
}
|
|
64
|
+
|
|
65
|
+
// 已是包内绝对路径(无 ..):ppt/charts/chart1.xml
|
|
66
|
+
if (normalized.startsWith('ppt/') && !normalized.includes('..')) {
|
|
67
|
+
return path.posix.normalize(normalized);
|
|
68
|
+
}
|
|
69
|
+
|
|
57
70
|
const ownerDir = path.posix.dirname(ownerPath);
|
|
58
|
-
return path.posix.normalize(path.posix.join(ownerDir,
|
|
71
|
+
return path.posix.normalize(path.posix.join(ownerDir, normalized));
|
|
59
72
|
}
|
|
60
73
|
|
|
61
74
|
/**
|
|
@@ -63,8 +76,8 @@ function resolveTargetPath(ownerPath, target) {
|
|
|
63
76
|
* @returns {Relationship[]}
|
|
64
77
|
*/
|
|
65
78
|
function parseRelationships(relsDoc) {
|
|
66
|
-
const root = relsDoc
|
|
67
|
-
const rels =
|
|
79
|
+
const root = documentRoot(relsDoc, 'Relationships');
|
|
80
|
+
const rels = children(root, 'Relationship');
|
|
68
81
|
return rels
|
|
69
82
|
.map((rel) => ({
|
|
70
83
|
id: attr(rel, 'Id'),
|
|
@@ -84,7 +97,7 @@ function buildRelationIndex(parsed) {
|
|
|
84
97
|
const bySource = new Map();
|
|
85
98
|
|
|
86
99
|
for (const [filePath, doc] of Object.entries(parsed)) {
|
|
87
|
-
if (
|
|
100
|
+
if (!/\.rels$/i.test(filePath)) continue;
|
|
88
101
|
const owner = relsOwnerPath(filePath);
|
|
89
102
|
const relationships = parseRelationships(doc);
|
|
90
103
|
const map = new Map();
|
package/lib/run-utils.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文本 run 合并与 PptxGenJS 选项压缩(IR → 代码 简化层)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const DEFAULT_TEXT_COLOR = '000000';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {object} a
|
|
9
|
+
* @param {object} b
|
|
10
|
+
*/
|
|
11
|
+
function optionsEqual(a, b) {
|
|
12
|
+
return optionsKey(a) === optionsKey(b);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {object} opts
|
|
17
|
+
*/
|
|
18
|
+
function optionsKey(opts) {
|
|
19
|
+
const o = compressRunOptions({ ...opts });
|
|
20
|
+
return JSON.stringify(o, Object.keys(o).sort());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} text
|
|
25
|
+
*/
|
|
26
|
+
function isNewlineOnly(text) {
|
|
27
|
+
return typeof text === 'string' && /^[\r\n\s]*$/.test(text) && /[\r\n]/.test(text);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 剔除 PptxGenJS 默认值,简化 underline
|
|
32
|
+
* @param {object} opts
|
|
33
|
+
*/
|
|
34
|
+
function compressRunOptions(opts) {
|
|
35
|
+
if (!opts || typeof opts !== 'object') return {};
|
|
36
|
+
const copy = { ...opts };
|
|
37
|
+
delete copy._degraded;
|
|
38
|
+
|
|
39
|
+
if (copy.color === DEFAULT_TEXT_COLOR || copy.color === '000000') {
|
|
40
|
+
delete copy.color;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (copy.underline && typeof copy.underline === 'object') {
|
|
44
|
+
if ((copy.underline.style ?? 'sng') === 'sng') {
|
|
45
|
+
copy.underline = true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return copy;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 合并相邻同样式 run,将独立换行 run 并入前一段文本
|
|
54
|
+
* @param {Array<{ text: string, options?: object }>} runs
|
|
55
|
+
*/
|
|
56
|
+
function compressTextRuns(runs) {
|
|
57
|
+
if (!runs?.length) return [];
|
|
58
|
+
|
|
59
|
+
/** @type {Array<{ text: string, options: object }>} */
|
|
60
|
+
const out = [];
|
|
61
|
+
|
|
62
|
+
for (const run of runs) {
|
|
63
|
+
const text = run.text ?? '';
|
|
64
|
+
const options = compressRunOptions(run.options ?? {});
|
|
65
|
+
|
|
66
|
+
if (!text && Object.keys(options).length === 0) continue;
|
|
67
|
+
|
|
68
|
+
if (!out.length) {
|
|
69
|
+
out.push({ text, options });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const last = out[out.length - 1];
|
|
74
|
+
|
|
75
|
+
if (isNewlineOnly(text)) {
|
|
76
|
+
const nlOpts = { ...options };
|
|
77
|
+
delete nlOpts.bullet;
|
|
78
|
+
if (optionsEqual(last.options, nlOpts) || optionsEqual(last.options, options)) {
|
|
79
|
+
if (!last.text.endsWith('\n')) {
|
|
80
|
+
last.text += text.includes('\r') ? '\r\n' : '\n';
|
|
81
|
+
}
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (optionsEqual(last.options, options)) {
|
|
87
|
+
last.text += text;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
out.push({ text, options });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
while (out.length > 1 && isNewlineOnly(out[out.length - 1].text)) {
|
|
95
|
+
const tail = out.pop();
|
|
96
|
+
const prev = out[out.length - 1];
|
|
97
|
+
if (optionsEqual(prev.options, tail.options) && !prev.text.endsWith('\n')) {
|
|
98
|
+
prev.text += tail.text.includes('\r') ? '\r\n' : '\n';
|
|
99
|
+
} else {
|
|
100
|
+
const nlOpts = { ...tail.options };
|
|
101
|
+
delete nlOpts.bullet;
|
|
102
|
+
out.push({ text: tail.text, options: nlOpts });
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return out.map((run) => {
|
|
108
|
+
if (!isNewlineOnly(run.text)) return run;
|
|
109
|
+
const options = { ...run.options };
|
|
110
|
+
delete options.bullet;
|
|
111
|
+
return { text: run.text, options };
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 四边相同时合并为 PptxGenJS 简写 border
|
|
117
|
+
* @param {object|null} border
|
|
118
|
+
*/
|
|
119
|
+
function compressCellBorder(border) {
|
|
120
|
+
if (!border) return null;
|
|
121
|
+
|
|
122
|
+
const sides = ['left', 'right', 'top', 'bottom'];
|
|
123
|
+
const present = sides.filter((s) => border[s]);
|
|
124
|
+
if (present.length === 0) return null;
|
|
125
|
+
|
|
126
|
+
if (present.length < 4) return border;
|
|
127
|
+
|
|
128
|
+
const first = border[present[0]];
|
|
129
|
+
const allSame = present.every((s) => {
|
|
130
|
+
const side = border[s];
|
|
131
|
+
return (
|
|
132
|
+
(side.color ?? null) === (first.color ?? null) &&
|
|
133
|
+
(side.pt ?? 1) === (first.pt ?? 1)
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (allSame) {
|
|
138
|
+
return {
|
|
139
|
+
type: 'solid',
|
|
140
|
+
color: first.color,
|
|
141
|
+
pt: first.pt ?? 1,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return border;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 表格单元格:压缩 runs → 纯文本或内联多 run
|
|
150
|
+
* @param {Array<{ text: string, options?: object }>} runs
|
|
151
|
+
*/
|
|
152
|
+
function flattenTableCellText(runs) {
|
|
153
|
+
const merged = compressTextRuns(runs);
|
|
154
|
+
if (!merged.length) return { text: '', runOptions: {} };
|
|
155
|
+
|
|
156
|
+
const allSame =
|
|
157
|
+
merged.length === 1 ||
|
|
158
|
+
merged.every((r) => optionsEqual(r.options, merged[0].options));
|
|
159
|
+
|
|
160
|
+
if (allSame) {
|
|
161
|
+
const text = merged.map((r) => r.text).join('').replace(/\r\n/g, '\n');
|
|
162
|
+
return { text, runOptions: compressRunOptions(merged[0].options) };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
text: merged.map((r) => ({
|
|
167
|
+
text: String(r.text).replace(/\r\n/g, '\n'),
|
|
168
|
+
options: compressRunOptions(r.options),
|
|
169
|
+
})),
|
|
170
|
+
runOptions: {},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = {
|
|
175
|
+
compressRunOptions,
|
|
176
|
+
compressTextRuns,
|
|
177
|
+
compressCellBorder,
|
|
178
|
+
flattenTableCellText,
|
|
179
|
+
optionsEqual,
|
|
180
|
+
isNewlineOnly,
|
|
181
|
+
};
|
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,11 +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 {
|
|
6
|
+
const { boundsFromXfrm, emuToInch } = require('./bounds');
|
|
7
|
+
const { resolveFillColor } = require('./color');
|
|
8
|
+
const { compressCellBorder, flattenTableCellText } = require('./run-utils');
|
|
9
|
+
const { extractTextRuns } = require('./text-utils');
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* @param {object} graphicFrame
|
|
@@ -49,23 +50,42 @@ function extractTable(graphicFrame, ctx) {
|
|
|
49
50
|
*/
|
|
50
51
|
function extractTableRows(tbl, scheme) {
|
|
51
52
|
const rows = [];
|
|
52
|
-
for (const tr of
|
|
53
|
+
for (const tr of children(tbl, 'a:tr')) {
|
|
53
54
|
const cells = [];
|
|
54
|
-
for (const tc of
|
|
55
|
-
const text = extractCellText(tc);
|
|
55
|
+
for (const tc of children(tr, 'a:tc')) {
|
|
56
56
|
const tcPr = child(tc, 'a:tcPr');
|
|
57
|
+
const txBody = child(tc, 'a:txBody');
|
|
58
|
+
let text = '';
|
|
59
|
+
/** @type {object} */
|
|
60
|
+
let runOptions = {};
|
|
61
|
+
if (txBody) {
|
|
62
|
+
const flat = flattenTableCellText(extractTextRuns(txBody, scheme, null));
|
|
63
|
+
text = flat.text;
|
|
64
|
+
runOptions = flat.runOptions;
|
|
65
|
+
}
|
|
66
|
+
|
|
57
67
|
const fill = tcPr ? resolveFillColor(tcPr, scheme).color : null;
|
|
68
|
+
const borderRaw = extractCellBorderSides(tcPr, scheme);
|
|
69
|
+
const borderColors = borderRaw
|
|
70
|
+
? Object.values(borderRaw)
|
|
71
|
+
.map((b) => b.color)
|
|
72
|
+
.filter(Boolean)
|
|
73
|
+
: [];
|
|
74
|
+
const border = compressCellBorder(borderRaw);
|
|
75
|
+
const fillIsJustBorderColor =
|
|
76
|
+
fill &&
|
|
77
|
+
borderColors.length > 0 &&
|
|
78
|
+
borderColors.every((c) => c === fill);
|
|
79
|
+
|
|
58
80
|
const merge = {};
|
|
59
81
|
const rowSpan = attr(tcPr, 'rowSpan');
|
|
60
82
|
const gridSpan = attr(tcPr, 'gridSpan');
|
|
61
83
|
if (rowSpan && rowSpan !== '1') merge.rowspan = parseInt(rowSpan, 10);
|
|
62
84
|
if (gridSpan && gridSpan !== '1') merge.colspan = parseInt(gridSpan, 10);
|
|
63
85
|
|
|
64
|
-
const cell = { text, options: {} };
|
|
65
|
-
if (fill) cell.options.fill =
|
|
86
|
+
const cell = { text, options: { ...runOptions } };
|
|
87
|
+
if (fill && !fillIsJustBorderColor) cell.options.fill = fill;
|
|
66
88
|
if (Object.keys(merge).length) Object.assign(cell.options, merge);
|
|
67
|
-
|
|
68
|
-
const border = extractCellBorder(tcPr, scheme);
|
|
69
89
|
if (border) cell.options.border = border;
|
|
70
90
|
|
|
71
91
|
cells.push(cell);
|
|
@@ -79,7 +99,7 @@ function extractTableRows(tbl, scheme) {
|
|
|
79
99
|
* @param {object|null|undefined} tcPr
|
|
80
100
|
* @param {Record<string, string>} scheme
|
|
81
101
|
*/
|
|
82
|
-
function
|
|
102
|
+
function extractCellBorderSides(tcPr, scheme) {
|
|
83
103
|
if (!tcPr) return null;
|
|
84
104
|
const border = {};
|
|
85
105
|
const sides = {
|
|
@@ -106,28 +126,13 @@ function extractCellBorder(tcPr, scheme) {
|
|
|
106
126
|
return hasAny ? border : null;
|
|
107
127
|
}
|
|
108
128
|
|
|
109
|
-
/**
|
|
110
|
-
* @param {object} tc
|
|
111
|
-
*/
|
|
112
|
-
function extractCellText(tc) {
|
|
113
|
-
const txBody = child(tc, 'a:txBody');
|
|
114
|
-
if (!txBody) return '';
|
|
115
|
-
const parts = [];
|
|
116
|
-
for (const p of asArray(txBody['a:p'])) {
|
|
117
|
-
for (const r of asArray(p['a:r'])) {
|
|
118
|
-
parts.push(textContent(r['a:t']));
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return parts.join('');
|
|
122
|
-
}
|
|
123
|
-
|
|
124
129
|
/**
|
|
125
130
|
* @param {object} tbl
|
|
126
131
|
* @returns {number[]|undefined}
|
|
127
132
|
*/
|
|
128
133
|
function extractColWidths(tbl) {
|
|
129
134
|
const grid = child(tbl, 'a:tblGrid');
|
|
130
|
-
const cols =
|
|
135
|
+
const cols = children(grid, 'a:gridCol');
|
|
131
136
|
if (!cols.length) return undefined;
|
|
132
137
|
return cols.map((col) => {
|
|
133
138
|
const w = parseInt(attr(col, 'w') ?? '0', 10);
|