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.
@@ -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
@@ -76,8 +76,8 @@ function resolveTargetPath(ownerPath, target) {
76
76
  * @returns {Relationship[]}
77
77
  */
78
78
  function parseRelationships(relsDoc) {
79
- const root = relsDoc?.Relationships ?? relsDoc;
80
- const rels = asArray(root?.Relationship);
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 (!filePath.endsWith('.rels')) continue;
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
- out.push(tail);
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('./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,12 +1,12 @@
1
1
  /**
2
2
  * 表格提取(design.html §4.5)
3
3
  */
4
- const { asArray, attr, child } = 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');
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 asArray(tbl['a:tr'])) {
53
+ for (const tr of children(tbl, 'a:tr')) {
54
54
  const cells = [];
55
- for (const tc of asArray(tr['a:tc'])) {
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 = asArray(child(grid, 'a:gridCol'));
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
+ };