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.
@@ -2,7 +2,7 @@
2
2
  * 母版 / 版式占位符继承(design.html §4.1)
3
3
  * 优先级:slide → slideLayout → slideMaster
4
4
  */
5
- const { asArray, attr, child } = require('./xml-utils');
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 asArray(spTree['p:sp'])) {
112
+ for (const sp of children(spTree, 'p:sp')) {
116
113
  yield sp;
117
114
  }
118
- for (const grp of asArray(spTree['p:grpSp'])) {
119
- const inner = child(grp, 'p:spTree');
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
- 'a:off': off ?? { $: { x: '0', y: '0' } },
167
- 'a:ext': ext ?? { $: { cx: '0', cy: '0' } },
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 {object|null}
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 { $: values };
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
- * 合并 txBody 默认样式:用 layout/master 的首段/首 run 补全 slide 层缺失的 rPr
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 slideParas = asArray(slideBody['a:p']);
230
- const fallbackPara = asArray(fallbackBody['a:p'])[0];
231
- const fallbackRPr = fallbackPara && child(asArray(fallbackPara['a:r'])[0], 'a:rPr');
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
- if (!fallbackRPr) return slideBody;
328
+ const fallbackRPr = fallbackBody
329
+ ? getDefaultRPrFromTxBody(fallbackBody)
330
+ : null;
331
+ if (!fallbackRPr) return merged;
234
332
 
235
- const mergedParas = slideParas.map((p) => {
236
- const runs = asArray(p['a:r']).map((r) => {
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
- if (rPr) return r;
239
- return { ...r, 'a:rPr': JSON.parse(JSON.stringify(fallbackRPr)) };
240
- });
241
- return { ...p, 'a:r': runs.length === 1 ? runs[0] : runs };
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
- return { ...slideBody, 'a:p': mergedParas.length === 1 ? mergedParas[0] : mergedParas };
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
  };
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * presentation.xml:幻灯片列表与版面尺寸
3
3
  */
4
- const { asArray, attr, child } = require('./xml-utils');
5
- const { emuToInch } = require('./utils/emu');
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 = child(doc, 'p:presentation');
16
+ const pres = documentRoot(doc, 'p:presentation');
17
17
  if (!pres) return [];
18
18
 
19
- const sldIds = asArray(child(child(pres, 'p:sldIdLst'), 'p:sldId'));
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 = child(parsed[PRESENTATION_PATH], 'p:presentation');
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 { asArray, attr } = require('./xml-utils');
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(target);
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, target));
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?.Relationships ?? relsDoc;
67
- const rels = asArray(root?.Relationship);
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 (!filePath.endsWith('.rels')) continue;
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();
@@ -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('./utils/bounds');
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 == null) return;
78
- if (typeof node === 'string') return;
79
- if (typeof node !== 'object') return;
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
- for (const [key, val] of Object.entries(node)) {
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 { asArray, attr, child, textContent } = require('./xml-utils');
4
+ const { attr, child, children } = require('./xml-utils');
5
5
  const { getGraphicXfrm } = require('./graphic');
6
- const { boundsFromXfrm } = require('./utils/bounds');
7
- const { resolveFillColor } = require('./utils/color');
8
- const { emuToInch } = require('./utils/emu');
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 asArray(tbl['a:tr'])) {
53
+ for (const tr of children(tbl, 'a:tr')) {
53
54
  const cells = [];
54
- for (const tc of asArray(tr['a:tc'])) {
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 = { color: 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 extractCellBorder(tcPr, scheme) {
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 = asArray(child(grid, 'a:gridCol'));
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);