pptx-react-viewer 1.0.0
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.md +982 -0
- package/dist/PowerPointViewer-K2URyPlJ.d.mts +522 -0
- package/dist/PowerPointViewer-K2URyPlJ.d.ts +522 -0
- package/dist/index.d.mts +71 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +121771 -0
- package/dist/index.mjs +121737 -0
- package/dist/pptx-viewer.css +2 -0
- package/dist/viewer/index.d.mts +267 -0
- package/dist/viewer/index.d.ts +267 -0
- package/dist/viewer/index.js +121947 -0
- package/dist/viewer/index.mjs +121908 -0
- package/node_modules/emf-converter/README.md +629 -0
- package/node_modules/emf-converter/dist/index.d.mts +86 -0
- package/node_modules/emf-converter/dist/index.d.ts +86 -0
- package/node_modules/emf-converter/dist/index.js +4199 -0
- package/node_modules/emf-converter/dist/index.mjs +4195 -0
- package/node_modules/emf-converter/package.json +42 -0
- package/node_modules/mtx-decompressor/README.md +271 -0
- package/node_modules/mtx-decompressor/dist/index.d.mts +83 -0
- package/node_modules/mtx-decompressor/dist/index.d.ts +83 -0
- package/node_modules/mtx-decompressor/dist/index.js +1510 -0
- package/node_modules/mtx-decompressor/dist/index.mjs +1506 -0
- package/node_modules/mtx-decompressor/package.json +37 -0
- package/node_modules/pptx-viewer-core/README.md +1294 -0
- package/node_modules/pptx-viewer-core/dist/SvgExporter-BZJguJbp.d.ts +557 -0
- package/node_modules/pptx-viewer-core/dist/SvgExporter-DqcmwxFu.d.mts +557 -0
- package/node_modules/pptx-viewer-core/dist/cli/index.d.mts +150 -0
- package/node_modules/pptx-viewer-core/dist/cli/index.d.ts +150 -0
- package/node_modules/pptx-viewer-core/dist/cli/index.js +39790 -0
- package/node_modules/pptx-viewer-core/dist/cli/index.mjs +39757 -0
- package/node_modules/pptx-viewer-core/dist/converter/index.d.mts +48 -0
- package/node_modules/pptx-viewer-core/dist/converter/index.d.ts +48 -0
- package/node_modules/pptx-viewer-core/dist/converter/index.js +3676 -0
- package/node_modules/pptx-viewer-core/dist/converter/index.mjs +3664 -0
- package/node_modules/pptx-viewer-core/dist/index.d.mts +10796 -0
- package/node_modules/pptx-viewer-core/dist/index.d.ts +10796 -0
- package/node_modules/pptx-viewer-core/dist/index.js +49658 -0
- package/node_modules/pptx-viewer-core/dist/index.mjs +49270 -0
- package/node_modules/pptx-viewer-core/dist/presentation-Bo7cMMCe.d.mts +4558 -0
- package/node_modules/pptx-viewer-core/dist/presentation-Bo7cMMCe.d.ts +4558 -0
- package/node_modules/pptx-viewer-core/dist/text-operations-Bo-WG-Z8.d.mts +134 -0
- package/node_modules/pptx-viewer-core/dist/text-operations-D0f1jred.d.ts +134 -0
- package/node_modules/pptx-viewer-core/package.json +61 -0
- package/package.json +89 -0
|
@@ -0,0 +1,3676 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/converter/media-context.ts
|
|
4
|
+
var MIME_TO_EXT = {
|
|
5
|
+
"image/png": "png",
|
|
6
|
+
"image/jpeg": "jpg",
|
|
7
|
+
"image/jpg": "jpg",
|
|
8
|
+
"image/gif": "gif",
|
|
9
|
+
"image/svg+xml": "svg",
|
|
10
|
+
"image/webp": "webp",
|
|
11
|
+
"image/bmp": "bmp",
|
|
12
|
+
"image/tiff": "tiff"
|
|
13
|
+
};
|
|
14
|
+
function mimeSubtypeToExt(mime) {
|
|
15
|
+
const parts = mime.split("/");
|
|
16
|
+
if (parts.length === 2) {
|
|
17
|
+
return parts[1].replace(/[^a-z0-9]/g, "");
|
|
18
|
+
}
|
|
19
|
+
return "bin";
|
|
20
|
+
}
|
|
21
|
+
function dataUrlToMediaBytes(dataUrl) {
|
|
22
|
+
const match = dataUrl.match(/^data:([^;]+);base64,(.+)$/s);
|
|
23
|
+
if (!match) {
|
|
24
|
+
throw new Error("Invalid data URL format");
|
|
25
|
+
}
|
|
26
|
+
const mime = match[1].toLowerCase();
|
|
27
|
+
const ext = MIME_TO_EXT[mime] ?? mimeSubtypeToExt(mime);
|
|
28
|
+
const base64 = match[2];
|
|
29
|
+
const binary = atob(base64);
|
|
30
|
+
const bytes = new Uint8Array(binary.length);
|
|
31
|
+
for (let i = 0; i < binary.length; i++) {
|
|
32
|
+
bytes[i] = binary.charCodeAt(i);
|
|
33
|
+
}
|
|
34
|
+
return { bytes, ext };
|
|
35
|
+
}
|
|
36
|
+
function generateMediaFilename(index, ext) {
|
|
37
|
+
const padded = String(index).padStart(3, "0");
|
|
38
|
+
const cleanExt = ext.startsWith(".") ? ext.slice(1) : ext;
|
|
39
|
+
return `image-${padded}.${cleanExt}`;
|
|
40
|
+
}
|
|
41
|
+
var MediaContext = class {
|
|
42
|
+
/**
|
|
43
|
+
* @param outputDir - Root output directory for the conversion.
|
|
44
|
+
* @param folderName - Sub-folder name for media files (e.g. `"media"`).
|
|
45
|
+
* @param fs - Optional file system adapter; omit for in-memory-only conversion.
|
|
46
|
+
*/
|
|
47
|
+
constructor(outputDir, folderName, fs) {
|
|
48
|
+
this.folderName = folderName;
|
|
49
|
+
this.fs = fs;
|
|
50
|
+
this.resolvedMediaDir = `${outputDir}/${this.folderName}`;
|
|
51
|
+
}
|
|
52
|
+
/** Running counter used to assign unique sequential filenames. */
|
|
53
|
+
imageIndex = 0;
|
|
54
|
+
/** Whether the media output directory has been created yet. */
|
|
55
|
+
initialized = false;
|
|
56
|
+
/** Fully resolved path to the media output directory. */
|
|
57
|
+
resolvedMediaDir;
|
|
58
|
+
/** Returns the total number of images saved so far in this session. */
|
|
59
|
+
get totalImages() {
|
|
60
|
+
return this.imageIndex;
|
|
61
|
+
}
|
|
62
|
+
/** Returns the absolute path to the media output directory. */
|
|
63
|
+
get mediaDir() {
|
|
64
|
+
return this.resolvedMediaDir;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Decodes a Base64 `data:` URL, saves the image to disk (if a FS adapter
|
|
68
|
+
* is available), and returns a relative path suitable for embedding in markdown.
|
|
69
|
+
*
|
|
70
|
+
* @param dataUrl - The Base64-encoded data URL of the image.
|
|
71
|
+
* @param prefix - Optional filename prefix (e.g. `"slide3"`) for disambiguation.
|
|
72
|
+
* @returns A relative path like `"./media/slide3-image-001.png"`.
|
|
73
|
+
* @throws Error if the data URL is malformed.
|
|
74
|
+
*/
|
|
75
|
+
async saveImage(dataUrl, prefix) {
|
|
76
|
+
const decoded = dataUrlToMediaBytes(dataUrl);
|
|
77
|
+
return this.saveImageBytes(decoded.bytes, decoded.ext, prefix);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Saves raw image bytes to disk and returns a relative path for markdown embedding.
|
|
81
|
+
*
|
|
82
|
+
* @param bytes - Raw binary content of the image.
|
|
83
|
+
* @param ext - File extension (e.g. `"png"`, `"jpg"`).
|
|
84
|
+
* @param prefix - Optional filename prefix for disambiguation.
|
|
85
|
+
* @returns A relative path like `"./media/image-001.png"`.
|
|
86
|
+
*/
|
|
87
|
+
async saveImageBytes(bytes, ext, prefix) {
|
|
88
|
+
await this.ensureInitialized();
|
|
89
|
+
this.imageIndex += 1;
|
|
90
|
+
const baseName = generateMediaFilename(this.imageIndex, ext);
|
|
91
|
+
const filename = prefix ? `${prefix}-${baseName}` : baseName;
|
|
92
|
+
if (this.fs) {
|
|
93
|
+
const filePath = `${this.resolvedMediaDir}/${filename}`;
|
|
94
|
+
await this.fs.writeBinaryFile(filePath, bytes);
|
|
95
|
+
}
|
|
96
|
+
return `./${this.folderName}/${filename}`;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Lazily creates the media output directory on the first call.
|
|
100
|
+
* Subsequent calls are no-ops.
|
|
101
|
+
*/
|
|
102
|
+
async ensureInitialized() {
|
|
103
|
+
if (this.initialized) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (this.fs) {
|
|
107
|
+
await this.fs.createFolder(this.resolvedMediaDir);
|
|
108
|
+
}
|
|
109
|
+
this.initialized = true;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// src/converter/base.ts
|
|
114
|
+
function normalizePath(pathValue) {
|
|
115
|
+
return pathValue.trim().replace(/\\/g, "/");
|
|
116
|
+
}
|
|
117
|
+
function getDirectory(filePath) {
|
|
118
|
+
const normalized = normalizePath(filePath);
|
|
119
|
+
const index = normalized.lastIndexOf("/");
|
|
120
|
+
if (index < 0) {
|
|
121
|
+
return ".";
|
|
122
|
+
}
|
|
123
|
+
if (index === 0) {
|
|
124
|
+
return "/";
|
|
125
|
+
}
|
|
126
|
+
return normalized.slice(0, index);
|
|
127
|
+
}
|
|
128
|
+
function deriveOutputPath(sourcePath, explicitPath) {
|
|
129
|
+
if (explicitPath) {
|
|
130
|
+
return explicitPath;
|
|
131
|
+
}
|
|
132
|
+
if (!sourcePath) {
|
|
133
|
+
return void 0;
|
|
134
|
+
}
|
|
135
|
+
const normalized = normalizePath(sourcePath);
|
|
136
|
+
const dotIndex = normalized.lastIndexOf(".");
|
|
137
|
+
if (dotIndex < 0) {
|
|
138
|
+
return `${normalized}.md`;
|
|
139
|
+
}
|
|
140
|
+
return `${normalized.slice(0, dotIndex)}.md`;
|
|
141
|
+
}
|
|
142
|
+
var DocumentConverter = class {
|
|
143
|
+
/**
|
|
144
|
+
* @param outputDir - Root directory for all output files (markdown + media).
|
|
145
|
+
* @param options - Conversion options (output path, media folder name, metadata flag).
|
|
146
|
+
* @param fs - Optional file system adapter for writing files to disk.
|
|
147
|
+
*/
|
|
148
|
+
constructor(outputDir, options, fs) {
|
|
149
|
+
this.outputDir = outputDir;
|
|
150
|
+
this.options = options;
|
|
151
|
+
this.fs = fs;
|
|
152
|
+
this.mediaContext = new MediaContext(outputDir, options.mediaFolderName, fs);
|
|
153
|
+
}
|
|
154
|
+
/** Shared context for extracting and saving media (images) during conversion. */
|
|
155
|
+
mediaContext;
|
|
156
|
+
/**
|
|
157
|
+
* Builds a YAML front-matter block from a key-value metadata dictionary.
|
|
158
|
+
* String values are quoted; numeric values are emitted bare.
|
|
159
|
+
*
|
|
160
|
+
* @param metadata - Key-value pairs to include in the front matter.
|
|
161
|
+
* @returns A complete front-matter string (including `---` delimiters and trailing blank lines).
|
|
162
|
+
*/
|
|
163
|
+
buildFrontMatter(metadata) {
|
|
164
|
+
const lines = ["---"];
|
|
165
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
166
|
+
if (typeof value === "string") {
|
|
167
|
+
lines.push(`${key}: "${value}"`);
|
|
168
|
+
} else {
|
|
169
|
+
lines.push(`${key}: ${value}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
lines.push("---", "", "");
|
|
173
|
+
return lines.join("\n");
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Writes the generated markdown content to an output file using the
|
|
177
|
+
* configured {@link FileSystemAdapter}.
|
|
178
|
+
*
|
|
179
|
+
* @param content - The markdown string to write.
|
|
180
|
+
* @param outputPath - Destination file path.
|
|
181
|
+
* @throws Error if no `FileSystemAdapter` was provided at construction time.
|
|
182
|
+
*/
|
|
183
|
+
async writeOutput(content, outputPath) {
|
|
184
|
+
if (!this.fs) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
"FileSystemAdapter is required for writing output files. Provide one via the constructor or use convert() without outputPath."
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
await this.fs.writeFile(outputPath, content);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// src/converter/elements/ChartElementProcessor.ts
|
|
194
|
+
var ChartElementProcessor = class {
|
|
195
|
+
supportedTypes = ["chart"];
|
|
196
|
+
async process(element, _ctx) {
|
|
197
|
+
if (element.type !== "chart") {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const chartElement = element;
|
|
201
|
+
const chartData = chartElement.chartData;
|
|
202
|
+
if (!chartData) {
|
|
203
|
+
return "*[Chart: no data]*";
|
|
204
|
+
}
|
|
205
|
+
const output = [];
|
|
206
|
+
const title = chartData.title?.trim() || "Untitled Chart";
|
|
207
|
+
const chartType = this.humanizeType(chartData.chartType);
|
|
208
|
+
output.push(`**${title}**`);
|
|
209
|
+
output.push(`*Type: ${chartType}*`);
|
|
210
|
+
const axisInfo = this.renderAxes(chartData.axes);
|
|
211
|
+
if (axisInfo) {
|
|
212
|
+
output.push(axisInfo);
|
|
213
|
+
}
|
|
214
|
+
const isPie = chartData.chartType === "pie" || chartData.chartType === "pie3D" || chartData.chartType === "doughnut";
|
|
215
|
+
if (chartData.categories.length > 0 && chartData.series.length > 0) {
|
|
216
|
+
output.push(this.renderDataTable(chartData.categories, chartData.series, isPie));
|
|
217
|
+
} else if (chartData.series.length > 0) {
|
|
218
|
+
for (const series of chartData.series) {
|
|
219
|
+
const values = series.values.map((value) => String(value)).join(", ");
|
|
220
|
+
output.push(`- **${series.name}**: ${values}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const dataLabels = this.renderDataLabels(chartData.series);
|
|
224
|
+
if (dataLabels) {
|
|
225
|
+
output.push(dataLabels);
|
|
226
|
+
}
|
|
227
|
+
const errBars = this.renderErrorBars(chartData.series);
|
|
228
|
+
if (errBars) {
|
|
229
|
+
output.push(errBars);
|
|
230
|
+
}
|
|
231
|
+
if (chartData.grouping) {
|
|
232
|
+
output.push(`*Grouping: ${chartData.grouping}*`);
|
|
233
|
+
}
|
|
234
|
+
if (chartData.style?.legendPosition) {
|
|
235
|
+
output.push(`*Legend: ${chartData.style.legendPosition}*`);
|
|
236
|
+
}
|
|
237
|
+
if (chartData.dataTable) {
|
|
238
|
+
const flags = [];
|
|
239
|
+
if (chartData.dataTable.showHorzBorder) {
|
|
240
|
+
flags.push("horizontal borders");
|
|
241
|
+
}
|
|
242
|
+
if (chartData.dataTable.showVertBorder) {
|
|
243
|
+
flags.push("vertical borders");
|
|
244
|
+
}
|
|
245
|
+
if (chartData.dataTable.showOutline) {
|
|
246
|
+
flags.push("outline");
|
|
247
|
+
}
|
|
248
|
+
if (chartData.dataTable.showKeys) {
|
|
249
|
+
flags.push("keys");
|
|
250
|
+
}
|
|
251
|
+
if (flags.length > 0) {
|
|
252
|
+
output.push(`*Data table: ${flags.join(", ")}*`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const trendlines = chartData.series.flatMap(
|
|
256
|
+
(series) => (series.trendlines ?? []).map((trendline) => `${series.name} (${trendline.trendlineType})`)
|
|
257
|
+
);
|
|
258
|
+
if (trendlines.length > 0) {
|
|
259
|
+
output.push(`*Trendlines: ${trendlines.join(", ")}*`);
|
|
260
|
+
}
|
|
261
|
+
if (chartData.externalData?.targetPath) {
|
|
262
|
+
output.push(`*External data: ${chartData.externalData.targetPath}*`);
|
|
263
|
+
}
|
|
264
|
+
return output.join("\n\n");
|
|
265
|
+
}
|
|
266
|
+
renderAxes(axes) {
|
|
267
|
+
if (!axes || axes.length === 0) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
const parts = [];
|
|
271
|
+
for (const axis of axes) {
|
|
272
|
+
const label = this.humanizeAxisType(axis.axisType);
|
|
273
|
+
const details = [];
|
|
274
|
+
if (axis.titleText) {
|
|
275
|
+
details.push(`"${axis.titleText}"`);
|
|
276
|
+
}
|
|
277
|
+
if (axis.numFmt?.formatCode) {
|
|
278
|
+
details.push(`format: ${axis.numFmt.formatCode}`);
|
|
279
|
+
}
|
|
280
|
+
if (details.length > 0) {
|
|
281
|
+
parts.push(`${label}: ${details.join(", ")}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (parts.length === 0) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
return `*Axes: ${parts.join(" | ")}*`;
|
|
288
|
+
}
|
|
289
|
+
humanizeAxisType(axisType) {
|
|
290
|
+
switch (axisType) {
|
|
291
|
+
case "catAx":
|
|
292
|
+
return "Category";
|
|
293
|
+
case "valAx":
|
|
294
|
+
return "Value";
|
|
295
|
+
case "dateAx":
|
|
296
|
+
return "Date";
|
|
297
|
+
case "serAx":
|
|
298
|
+
return "Series";
|
|
299
|
+
default:
|
|
300
|
+
return axisType;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
renderDataLabels(series) {
|
|
304
|
+
const labels = [];
|
|
305
|
+
for (const s of series) {
|
|
306
|
+
if (!s.dataLabels || s.dataLabels.length === 0) {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
for (const dl of s.dataLabels) {
|
|
310
|
+
const desc = this.describeDataLabel(dl, s.name);
|
|
311
|
+
if (desc) {
|
|
312
|
+
labels.push(desc);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (labels.length === 0) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
return `*Data labels: ${labels.join("; ")}*`;
|
|
320
|
+
}
|
|
321
|
+
describeDataLabel(dl, seriesName) {
|
|
322
|
+
if (dl.text) {
|
|
323
|
+
return `${seriesName}[${dl.idx}]: "${dl.text}"`;
|
|
324
|
+
}
|
|
325
|
+
const flags = [];
|
|
326
|
+
if (dl.showVal) {
|
|
327
|
+
flags.push("value");
|
|
328
|
+
}
|
|
329
|
+
if (dl.showCatName) {
|
|
330
|
+
flags.push("category");
|
|
331
|
+
}
|
|
332
|
+
if (dl.showSerName) {
|
|
333
|
+
flags.push("series");
|
|
334
|
+
}
|
|
335
|
+
if (dl.showPercent) {
|
|
336
|
+
flags.push("percent");
|
|
337
|
+
}
|
|
338
|
+
if (flags.length === 0) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
return `${seriesName}[${dl.idx}]: ${flags.join("+")}`;
|
|
342
|
+
}
|
|
343
|
+
renderErrorBars(series) {
|
|
344
|
+
const bars = [];
|
|
345
|
+
for (const s of series) {
|
|
346
|
+
if (!s.errBars || s.errBars.length === 0) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
for (const eb of s.errBars) {
|
|
350
|
+
bars.push(this.describeErrorBar(eb, s.name));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (bars.length === 0) {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
return `*Error bars: ${bars.join("; ")}*`;
|
|
357
|
+
}
|
|
358
|
+
describeErrorBar(eb, seriesName) {
|
|
359
|
+
const valDesc = eb.val !== void 0 ? ` ${eb.val}` : "";
|
|
360
|
+
return `${seriesName} ${eb.direction}-axis ${eb.valType}${valDesc} (${eb.barType})`;
|
|
361
|
+
}
|
|
362
|
+
renderDataTable(categories, series, includePctColumn) {
|
|
363
|
+
const totalForPct = includePctColumn ? this.computeSeriesTotal(series) : 0;
|
|
364
|
+
const showPct = includePctColumn && totalForPct > 0;
|
|
365
|
+
const headers = ["Category", ...series.map((entry) => entry.name)];
|
|
366
|
+
if (showPct) {
|
|
367
|
+
headers.push("%");
|
|
368
|
+
}
|
|
369
|
+
const widths = headers.map((header) => Math.max(3, header.length));
|
|
370
|
+
for (let rowIndex = 0; rowIndex < categories.length; rowIndex += 1) {
|
|
371
|
+
widths[0] = Math.max(widths[0], categories[rowIndex]?.length ?? 0);
|
|
372
|
+
for (let seriesIndex = 0; seriesIndex < series.length; seriesIndex += 1) {
|
|
373
|
+
const value = String(series[seriesIndex].values[rowIndex] ?? "");
|
|
374
|
+
widths[seriesIndex + 1] = Math.max(widths[seriesIndex + 1], value.length);
|
|
375
|
+
}
|
|
376
|
+
if (showPct) {
|
|
377
|
+
const pct = this.computeRowPct(series, rowIndex, totalForPct);
|
|
378
|
+
widths[widths.length - 1] = Math.max(widths[widths.length - 1], pct.length);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const formatRow = (cells) => {
|
|
382
|
+
const padded = cells.map((cell, index) => cell.padEnd(widths[index]));
|
|
383
|
+
return `| ${padded.join(" | ")} |`;
|
|
384
|
+
};
|
|
385
|
+
const separator = `| ${widths.map((width) => "-".repeat(width)).join(" | ")} |`;
|
|
386
|
+
const rows = categories.map((category, rowIndex) => {
|
|
387
|
+
const row = [category, ...series.map((entry) => String(entry.values[rowIndex] ?? ""))];
|
|
388
|
+
if (showPct) {
|
|
389
|
+
row.push(this.computeRowPct(series, rowIndex, totalForPct));
|
|
390
|
+
}
|
|
391
|
+
return formatRow(row);
|
|
392
|
+
});
|
|
393
|
+
return [formatRow(headers), separator, ...rows].join("\n");
|
|
394
|
+
}
|
|
395
|
+
computeSeriesTotal(series) {
|
|
396
|
+
let total = 0;
|
|
397
|
+
for (const s of series) {
|
|
398
|
+
for (const v of s.values) {
|
|
399
|
+
total += Math.abs(v ?? 0);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return total;
|
|
403
|
+
}
|
|
404
|
+
computeRowPct(series, rowIndex, total) {
|
|
405
|
+
let rowSum = 0;
|
|
406
|
+
for (const s of series) {
|
|
407
|
+
rowSum += Math.abs(s.values[rowIndex] ?? 0);
|
|
408
|
+
}
|
|
409
|
+
if (total === 0) {
|
|
410
|
+
return "0.0%";
|
|
411
|
+
}
|
|
412
|
+
return `${(rowSum / total * 100).toFixed(1)}%`;
|
|
413
|
+
}
|
|
414
|
+
humanizeType(value) {
|
|
415
|
+
return value.replace(/([a-z])([A-Z0-9])/g, "$1 $2").replace(/^./, (char) => char.toUpperCase());
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
// src/converter/elements/ElementProcessor.ts
|
|
420
|
+
var ElementProcessorRegistry = class {
|
|
421
|
+
processors = /* @__PURE__ */ new Map();
|
|
422
|
+
register(processor) {
|
|
423
|
+
for (const type of processor.supportedTypes) {
|
|
424
|
+
this.processors.set(type, processor);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
getProcessor(type) {
|
|
428
|
+
return this.processors.get(type) ?? null;
|
|
429
|
+
}
|
|
430
|
+
async processElement(element, ctx) {
|
|
431
|
+
const processor = this.getProcessor(element.type);
|
|
432
|
+
if (!processor) {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
let result = await processor.process(element, ctx);
|
|
436
|
+
if (result === null) {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
const base = element;
|
|
440
|
+
if (base.hidden) {
|
|
441
|
+
result = `*[Hidden]* ${result}`;
|
|
442
|
+
}
|
|
443
|
+
result += this.buildActionAnnotation(base.actionClick, "Click");
|
|
444
|
+
result += this.buildActionAnnotation(base.actionHover, "Hover");
|
|
445
|
+
return result;
|
|
446
|
+
}
|
|
447
|
+
buildActionAnnotation(action, _trigger) {
|
|
448
|
+
if (!action) {
|
|
449
|
+
return "";
|
|
450
|
+
}
|
|
451
|
+
if (action.url) {
|
|
452
|
+
const linkText = action.tooltip ?? action.url;
|
|
453
|
+
return `
|
|
454
|
+
|
|
455
|
+
[${linkText}](${action.url})`;
|
|
456
|
+
}
|
|
457
|
+
if (action.targetSlideIndex !== void 0) {
|
|
458
|
+
const label = `Jump to slide ${action.targetSlideIndex + 1}`;
|
|
459
|
+
return `
|
|
460
|
+
|
|
461
|
+
*${label}*`;
|
|
462
|
+
}
|
|
463
|
+
if (action.action) {
|
|
464
|
+
return `
|
|
465
|
+
|
|
466
|
+
*${action.action}*`;
|
|
467
|
+
}
|
|
468
|
+
return "";
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
// src/converter/elements/FallbackElementProcessor.ts
|
|
473
|
+
var FallbackElementProcessor = class {
|
|
474
|
+
supportedTypes = ["zoom", "contentPart", "unknown"];
|
|
475
|
+
async process(element, ctx) {
|
|
476
|
+
if (element.type === "zoom") {
|
|
477
|
+
return this.renderZoom(element, ctx);
|
|
478
|
+
}
|
|
479
|
+
if (element.type === "contentPart") {
|
|
480
|
+
return this.renderContentPart(element);
|
|
481
|
+
}
|
|
482
|
+
if (element.type === "unknown") {
|
|
483
|
+
return "*[Unsupported Element]*";
|
|
484
|
+
}
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
async renderZoom(zoomElement, ctx) {
|
|
488
|
+
const slideNumber = zoomElement.targetSlideIndex + 1;
|
|
489
|
+
const parts = [];
|
|
490
|
+
if (zoomElement.zoomType === "section") {
|
|
491
|
+
if (zoomElement.targetSectionId) {
|
|
492
|
+
parts.push(`*[Zoom to Section ${zoomElement.targetSectionId} (Slide ${slideNumber})]*`);
|
|
493
|
+
} else {
|
|
494
|
+
parts.push(`*[Zoom to Section (Slide ${slideNumber})]*`);
|
|
495
|
+
}
|
|
496
|
+
} else {
|
|
497
|
+
parts.push(`*[Zoom to Slide ${slideNumber}]*`);
|
|
498
|
+
}
|
|
499
|
+
const imagePath = await this.extractZoomImage(zoomElement, ctx);
|
|
500
|
+
if (imagePath) {
|
|
501
|
+
const alt = zoomElement.altText?.trim() || `Zoom preview slide ${slideNumber}`;
|
|
502
|
+
parts.push(``);
|
|
503
|
+
}
|
|
504
|
+
return parts.join("\n\n");
|
|
505
|
+
}
|
|
506
|
+
async extractZoomImage(zoomElement, ctx) {
|
|
507
|
+
const dataUrl = zoomElement.imageData ?? zoomElement.svgData;
|
|
508
|
+
if (!dataUrl || !dataUrl.startsWith("data:")) {
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
try {
|
|
512
|
+
return await ctx.mediaContext.saveImage(dataUrl, `slide${ctx.slideNumber}-zoom`);
|
|
513
|
+
} catch {
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
renderContentPart(contentPart) {
|
|
518
|
+
if (contentPart.inkStrokes && contentPart.inkStrokes.length > 0) {
|
|
519
|
+
return `*[Ink Content: ${contentPart.inkStrokes.length} stroke${contentPart.inkStrokes.length === 1 ? "" : "s"}]*`;
|
|
520
|
+
}
|
|
521
|
+
return "*[Content Part]*";
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
// src/converter/elements/GroupElementProcessor.ts
|
|
526
|
+
var GroupElementProcessor = class {
|
|
527
|
+
supportedTypes = ["group"];
|
|
528
|
+
async process(element, ctx) {
|
|
529
|
+
if (element.type !== "group") {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
const groupElement = element;
|
|
533
|
+
if (groupElement.children.length === 0) {
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
const children = await ctx.processElements(groupElement.children);
|
|
537
|
+
if (children.length === 0) {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
return children.join("\n\n");
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
// src/core/types/type-guards.ts
|
|
545
|
+
function isImageLikeElement(element) {
|
|
546
|
+
return element.type === "image" || element.type === "picture";
|
|
547
|
+
}
|
|
548
|
+
function hasTextProperties(element) {
|
|
549
|
+
return element.type === "text" || element.type === "shape" || element.type === "connector";
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/converter/elements/ImageElementProcessor.ts
|
|
553
|
+
var ImageElementProcessor = class {
|
|
554
|
+
supportedTypes = ["image", "picture"];
|
|
555
|
+
async process(element, ctx) {
|
|
556
|
+
if (!isImageLikeElement(element)) {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
const imageElement = element;
|
|
560
|
+
const altText = this.sanitiseAltText(imageElement.altText);
|
|
561
|
+
const imagePath = await this.extractImage(imageElement, ctx);
|
|
562
|
+
if (!imagePath) {
|
|
563
|
+
const hasData = Boolean(imageElement.imageData);
|
|
564
|
+
const dataPreview = imageElement.imageData ? imageElement.imageData.substring(0, 80) : "undefined";
|
|
565
|
+
const msg = `Image extraction failed on slide ${ctx.slideNumber}: id="${imageElement.id}", imagePath="${imageElement.imagePath ?? "unknown"}", hasImageData=${hasData}, imageDataPreview="${dataPreview}", hasSvgData=${Boolean(imageElement.svgData)}`;
|
|
566
|
+
console.error(`[image-processor] ${msg}`);
|
|
567
|
+
return `> **[Image extraction failed]** ${imageElement.id} (slide ${ctx.slideNumber})`;
|
|
568
|
+
}
|
|
569
|
+
if (ctx.semanticMode) {
|
|
570
|
+
return ``;
|
|
571
|
+
}
|
|
572
|
+
if (ctx.layoutScale) {
|
|
573
|
+
return `<img src="${imagePath}" alt="${altText}" style="max-width:100%;height:auto">`;
|
|
574
|
+
}
|
|
575
|
+
const dims = this.computeDisplaySize(element.width, element.height);
|
|
576
|
+
return `<img src="${imagePath}" alt="${altText}" width="${dims.w}" height="${dims.h}">`;
|
|
577
|
+
}
|
|
578
|
+
/** Scale element dimensions to a sensible display size, capping width. */
|
|
579
|
+
computeDisplaySize(origW, origH, maxW = 600) {
|
|
580
|
+
if (origW <= 0 || origH <= 0) {
|
|
581
|
+
return { w: 100, h: 100 };
|
|
582
|
+
}
|
|
583
|
+
if (origW <= maxW) {
|
|
584
|
+
return { w: Math.round(origW), h: Math.round(origH) };
|
|
585
|
+
}
|
|
586
|
+
const scale = maxW / origW;
|
|
587
|
+
return { w: maxW, h: Math.round(origH * scale) };
|
|
588
|
+
}
|
|
589
|
+
async extractImage(imageElement, ctx) {
|
|
590
|
+
if (imageElement.imageData && imageElement.imageData.startsWith("data:")) {
|
|
591
|
+
return await ctx.mediaContext.saveImage(imageElement.imageData, `slide${ctx.slideNumber}`);
|
|
592
|
+
}
|
|
593
|
+
if (imageElement.svgData && imageElement.svgData.startsWith("data:")) {
|
|
594
|
+
return await ctx.mediaContext.saveImage(imageElement.svgData, `slide${ctx.slideNumber}`);
|
|
595
|
+
}
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
/** Clean and truncate image alt text for readable markdown output. */
|
|
599
|
+
sanitiseAltText(raw) {
|
|
600
|
+
if (!raw) {
|
|
601
|
+
return "";
|
|
602
|
+
}
|
|
603
|
+
const MAX_ALT_LENGTH = 100;
|
|
604
|
+
const cleaned = raw.replace(/&#x[0-9A-Fa-f]+;/g, " ").replace(/&#\d+;/g, " ").replace(/&[a-zA-Z]+;/g, " ").replace(/[\r\n]+/g, " ").replace(/\s+/g, " ").trim();
|
|
605
|
+
if (cleaned.length <= MAX_ALT_LENGTH) {
|
|
606
|
+
return cleaned;
|
|
607
|
+
}
|
|
608
|
+
return `${cleaned.slice(0, MAX_ALT_LENGTH).trimEnd()}\u2026`;
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
// src/converter/elements/InkElementProcessor.ts
|
|
613
|
+
var InkElementProcessor = class {
|
|
614
|
+
supportedTypes = ["ink"];
|
|
615
|
+
async process(element, _ctx) {
|
|
616
|
+
if (element.type !== "ink") {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
const inkElement = element;
|
|
620
|
+
const details = [];
|
|
621
|
+
const strokeCount = inkElement.inkPaths.length;
|
|
622
|
+
details.push(`${strokeCount} stroke${strokeCount === 1 ? "" : "s"}`);
|
|
623
|
+
if (inkElement.inkColors && inkElement.inkColors.length > 0) {
|
|
624
|
+
const unique = [...new Set(inkElement.inkColors)];
|
|
625
|
+
if (unique.length <= 4) {
|
|
626
|
+
details.push(`colors ${unique.join(", ")}`);
|
|
627
|
+
} else {
|
|
628
|
+
details.push(`${unique.length} colors`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (inkElement.inkTool) {
|
|
632
|
+
details.push(`tool ${inkElement.inkTool}`);
|
|
633
|
+
}
|
|
634
|
+
if (inkElement.inkOpacities && inkElement.inkOpacities.length > 0) {
|
|
635
|
+
const avgOpacity = inkElement.inkOpacities.reduce((sum, value) => sum + value, 0) / inkElement.inkOpacities.length;
|
|
636
|
+
details.push(`opacity ${Math.round(avgOpacity * 100)}%`);
|
|
637
|
+
}
|
|
638
|
+
return `*[Ink Drawing: ${details.join(" | ")}]*`;
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
// src/converter/elements/MediaElementProcessor.ts
|
|
643
|
+
var MediaElementProcessor = class {
|
|
644
|
+
supportedTypes = ["media"];
|
|
645
|
+
async process(element, ctx) {
|
|
646
|
+
if (element.type !== "media") {
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
const mediaElement = element;
|
|
650
|
+
const label = this.resolveLabel(mediaElement);
|
|
651
|
+
const output = [`*[${label}]*`];
|
|
652
|
+
const details = this.buildDetails(mediaElement);
|
|
653
|
+
if (details.length > 0) {
|
|
654
|
+
output.push(`*${details.join(" | ")}*`);
|
|
655
|
+
}
|
|
656
|
+
if (mediaElement.posterFrameData && mediaElement.posterFrameData.startsWith("data:")) {
|
|
657
|
+
try {
|
|
658
|
+
const posterPath = await ctx.mediaContext.saveImage(
|
|
659
|
+
mediaElement.posterFrameData,
|
|
660
|
+
`slide${ctx.slideNumber}-poster`
|
|
661
|
+
);
|
|
662
|
+
output.push(``);
|
|
663
|
+
} catch {
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (mediaElement.captionTracks && mediaElement.captionTracks.length > 0) {
|
|
667
|
+
const captions = mediaElement.captionTracks.map((track) => `${track.label} (${track.language})`).join(", ");
|
|
668
|
+
output.push(`*Captions: ${captions}*`);
|
|
669
|
+
}
|
|
670
|
+
if (mediaElement.mediaMissing) {
|
|
671
|
+
output.push("*Media source is missing*");
|
|
672
|
+
}
|
|
673
|
+
return output.join("\n\n");
|
|
674
|
+
}
|
|
675
|
+
resolveLabel(mediaElement) {
|
|
676
|
+
const fileName = mediaElement.mediaPath?.split("/").pop();
|
|
677
|
+
if (mediaElement.mediaType === "video") {
|
|
678
|
+
return `Video: ${fileName ?? "embedded media"}`;
|
|
679
|
+
}
|
|
680
|
+
if (mediaElement.mediaType === "audio") {
|
|
681
|
+
return `Audio: ${fileName ?? "embedded media"}`;
|
|
682
|
+
}
|
|
683
|
+
return `Media: ${fileName ?? "embedded media"}`;
|
|
684
|
+
}
|
|
685
|
+
buildDetails(mediaElement) {
|
|
686
|
+
const details = [];
|
|
687
|
+
if (mediaElement.mediaPath) {
|
|
688
|
+
details.push(`Path: ${mediaElement.mediaPath}`);
|
|
689
|
+
}
|
|
690
|
+
if (typeof mediaElement.metadata?.duration === "number") {
|
|
691
|
+
details.push(`Duration: ${this.formatDuration(mediaElement.metadata.duration)}`);
|
|
692
|
+
}
|
|
693
|
+
if (typeof mediaElement.metadata?.videoWidth === "number" && typeof mediaElement.metadata?.videoHeight === "number") {
|
|
694
|
+
details.push(
|
|
695
|
+
`Resolution: ${mediaElement.metadata.videoWidth}x${mediaElement.metadata.videoHeight}`
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
if (mediaElement.loop) {
|
|
699
|
+
details.push("Looping");
|
|
700
|
+
}
|
|
701
|
+
if (mediaElement.autoPlay) {
|
|
702
|
+
details.push("Auto-play");
|
|
703
|
+
}
|
|
704
|
+
if (mediaElement.playAcrossSlides) {
|
|
705
|
+
details.push("Plays across slides");
|
|
706
|
+
}
|
|
707
|
+
if (mediaElement.mediaMimeType) {
|
|
708
|
+
details.push(`MIME: ${mediaElement.mediaMimeType}`);
|
|
709
|
+
}
|
|
710
|
+
return details;
|
|
711
|
+
}
|
|
712
|
+
formatDuration(seconds) {
|
|
713
|
+
const minutes = Math.floor(seconds / 60);
|
|
714
|
+
const remainder = Math.round(seconds % 60);
|
|
715
|
+
return `${minutes}:${String(remainder).padStart(2, "0")}`;
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
// src/converter/elements/OleElementProcessor.ts
|
|
720
|
+
var OleElementProcessor = class {
|
|
721
|
+
supportedTypes = ["ole"];
|
|
722
|
+
async process(element, ctx) {
|
|
723
|
+
if (element.type !== "ole") {
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
const oleElement = element;
|
|
727
|
+
const objectType = oleElement.oleObjectType ?? "unknown";
|
|
728
|
+
const fileName = oleElement.fileName ?? oleElement.oleName ?? "embedded-object";
|
|
729
|
+
const output = [`*[Embedded ${objectType}: ${fileName}]*`];
|
|
730
|
+
if (oleElement.oleFileExtension) {
|
|
731
|
+
output.push(`*Extension: .${oleElement.oleFileExtension}*`);
|
|
732
|
+
}
|
|
733
|
+
if (oleElement.oleProgId) {
|
|
734
|
+
output.push(`*Program ID: ${oleElement.oleProgId}*`);
|
|
735
|
+
}
|
|
736
|
+
if (oleElement.isLinked) {
|
|
737
|
+
output.push("*Linked object*");
|
|
738
|
+
}
|
|
739
|
+
const previewSource = oleElement.previewImageData ?? oleElement.previewImage;
|
|
740
|
+
if (previewSource && previewSource.startsWith("data:")) {
|
|
741
|
+
try {
|
|
742
|
+
const previewPath = await ctx.mediaContext.saveImage(
|
|
743
|
+
previewSource,
|
|
744
|
+
`slide${ctx.slideNumber}-ole`
|
|
745
|
+
);
|
|
746
|
+
output.push(``);
|
|
747
|
+
} catch {
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return output.join("\n\n");
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
// src/converter/elements/SmartArtElementProcessor.ts
|
|
755
|
+
var ORDERED_LAYOUTS = /* @__PURE__ */ new Set(["process", "cycle", "timeline"]);
|
|
756
|
+
var NESTED_LAYOUTS = /* @__PURE__ */ new Set(["hierarchy", "pyramid", "funnel"]);
|
|
757
|
+
var BULLET_LAYOUTS = /* @__PURE__ */ new Set(["list", "matrix", "gear", "target"]);
|
|
758
|
+
var SmartArtElementProcessor = class {
|
|
759
|
+
supportedTypes = ["smartArt"];
|
|
760
|
+
async process(element, _ctx) {
|
|
761
|
+
if (element.type !== "smartArt") {
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
const smartArtData = element.smartArtData;
|
|
765
|
+
if (!smartArtData || smartArtData.nodes.length === 0) {
|
|
766
|
+
return "*[SmartArt: no nodes]*";
|
|
767
|
+
}
|
|
768
|
+
const roots = this.resolveRoots(smartArtData.nodes);
|
|
769
|
+
const layoutType = smartArtData.resolvedLayoutType ?? "unknown";
|
|
770
|
+
const parts = [`*[SmartArt: ${layoutType}]*`];
|
|
771
|
+
if (NESTED_LAYOUTS.has(layoutType)) {
|
|
772
|
+
parts.push(this.renderNestedList(roots, 0));
|
|
773
|
+
} else if (ORDERED_LAYOUTS.has(layoutType)) {
|
|
774
|
+
parts.push(this.renderOrderedSequence(roots));
|
|
775
|
+
} else if (BULLET_LAYOUTS.has(layoutType)) {
|
|
776
|
+
parts.push(this.renderBulletList(roots));
|
|
777
|
+
} else if (layoutType === "relationship") {
|
|
778
|
+
parts.push(this.renderRelationshipText(roots));
|
|
779
|
+
} else {
|
|
780
|
+
parts.push(this.renderBulletList(roots));
|
|
781
|
+
}
|
|
782
|
+
return parts.join("\n\n");
|
|
783
|
+
}
|
|
784
|
+
resolveRoots(nodes) {
|
|
785
|
+
if (nodes.some((node) => Array.isArray(node.children) && node.children.length > 0)) {
|
|
786
|
+
return nodes;
|
|
787
|
+
}
|
|
788
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
789
|
+
for (const node of nodes) {
|
|
790
|
+
nodeMap.set(node.id, { ...node, children: [] });
|
|
791
|
+
}
|
|
792
|
+
const roots = [];
|
|
793
|
+
for (const node of nodeMap.values()) {
|
|
794
|
+
if (node.parentId && nodeMap.has(node.parentId)) {
|
|
795
|
+
nodeMap.get(node.parentId)?.children?.push(node);
|
|
796
|
+
} else {
|
|
797
|
+
roots.push(node);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
return roots.length > 0 ? roots : [...nodeMap.values()];
|
|
801
|
+
}
|
|
802
|
+
renderNestedList(nodes, level) {
|
|
803
|
+
const lines = [];
|
|
804
|
+
const indent = " ".repeat(level);
|
|
805
|
+
for (const node of nodes) {
|
|
806
|
+
const text = node.text.trim();
|
|
807
|
+
if (text) {
|
|
808
|
+
lines.push(`${indent}- ${text}`);
|
|
809
|
+
}
|
|
810
|
+
if (node.children && node.children.length > 0) {
|
|
811
|
+
lines.push(this.renderNestedList(node.children, level + 1));
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return lines.join("\n");
|
|
815
|
+
}
|
|
816
|
+
renderOrderedSequence(nodes) {
|
|
817
|
+
const flattened = this.flattenNodes(nodes).map((node) => node.text.trim()).filter((text) => text.length > 0);
|
|
818
|
+
const lines = [];
|
|
819
|
+
for (let index = 0; index < flattened.length; index += 1) {
|
|
820
|
+
lines.push(`${index + 1}. ${flattened[index]}`);
|
|
821
|
+
}
|
|
822
|
+
return lines.join("\n");
|
|
823
|
+
}
|
|
824
|
+
renderBulletList(nodes) {
|
|
825
|
+
return this.flattenNodes(nodes).map((node) => node.text.trim()).filter((text) => text.length > 0).map((text) => `- ${text}`).join("\n");
|
|
826
|
+
}
|
|
827
|
+
renderRelationshipText(nodes) {
|
|
828
|
+
const entries = this.flattenNodes(nodes).map((node) => node.text.trim()).filter((text) => text.length > 0);
|
|
829
|
+
if (entries.length === 0) {
|
|
830
|
+
return "*[SmartArt relationship]*";
|
|
831
|
+
}
|
|
832
|
+
if (entries.length === 1) {
|
|
833
|
+
return entries[0];
|
|
834
|
+
}
|
|
835
|
+
return entries.join(" -> ");
|
|
836
|
+
}
|
|
837
|
+
flattenNodes(nodes) {
|
|
838
|
+
const flattened = [];
|
|
839
|
+
for (const node of nodes) {
|
|
840
|
+
flattened.push(node);
|
|
841
|
+
if (node.children && node.children.length > 0) {
|
|
842
|
+
flattened.push(...this.flattenNodes(node.children));
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return flattened;
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
// src/core/utils/font-substitution.ts
|
|
850
|
+
var FONT_SUBSTITUTION_MAP = {
|
|
851
|
+
// Microsoft Office default fonts
|
|
852
|
+
Calibri: ["Carlito", "Liberation Sans", "Arial", "sans-serif"],
|
|
853
|
+
"Calibri Light": ["Carlito", "Liberation Sans", "Arial", "sans-serif"],
|
|
854
|
+
Cambria: ["Caladea", "Liberation Serif", "Times New Roman", "serif"],
|
|
855
|
+
"Cambria Math": ["STIX Two Math", "Latin Modern Math", "Times New Roman", "serif"],
|
|
856
|
+
Consolas: ["Liberation Mono", "Courier New", "monospace"],
|
|
857
|
+
"Segoe UI": ["Liberation Sans", "Helvetica Neue", "Arial", "sans-serif"],
|
|
858
|
+
"Segoe UI Light": ["Liberation Sans", "Helvetica Neue", "Arial", "sans-serif"],
|
|
859
|
+
"Segoe UI Semibold": ["Liberation Sans", "Helvetica Neue", "Arial", "sans-serif"],
|
|
860
|
+
// Classic Windows fonts → cross-platform equivalents
|
|
861
|
+
"Times New Roman": ["Liberation Serif", "Times", "serif"],
|
|
862
|
+
Arial: ["Liberation Sans", "Helvetica", "sans-serif"],
|
|
863
|
+
"Arial Black": ["Liberation Sans", "Helvetica", "sans-serif"],
|
|
864
|
+
"Arial Narrow": ["Liberation Sans Narrow", "Helvetica Neue", "sans-serif"],
|
|
865
|
+
"Courier New": ["Liberation Mono", "Courier", "monospace"],
|
|
866
|
+
Verdana: ["DejaVu Sans", "Bitstream Vera Sans", "sans-serif"],
|
|
867
|
+
Georgia: ["Liberation Serif", "Times New Roman", "serif"],
|
|
868
|
+
Tahoma: ["DejaVu Sans", "Liberation Sans", "sans-serif"],
|
|
869
|
+
Trebuchet: ["Liberation Sans", "sans-serif"],
|
|
870
|
+
"Trebuchet MS": ["Liberation Sans", "sans-serif"],
|
|
871
|
+
"Comic Sans MS": ["Comic Neue", "cursive"],
|
|
872
|
+
Impact: ["Charcoal", "sans-serif"],
|
|
873
|
+
"Lucida Console": ["Liberation Mono", "monospace"],
|
|
874
|
+
"Lucida Sans Unicode": ["Lucida Grande", "Liberation Sans", "sans-serif"],
|
|
875
|
+
Palatino: ["Palatino Linotype", "Book Antiqua", "serif"],
|
|
876
|
+
"Palatino Linotype": ["Palatino", "Book Antiqua", "serif"],
|
|
877
|
+
"Book Antiqua": ["Palatino Linotype", "Palatino", "serif"],
|
|
878
|
+
// CJK fonts
|
|
879
|
+
"MS PGothic": ["Noto Sans CJK JP", "Hiragino Sans", "Yu Gothic", "sans-serif"],
|
|
880
|
+
"MS PMincho": ["Noto Serif CJK JP", "Hiragino Mincho ProN", "Yu Mincho", "serif"],
|
|
881
|
+
"MS Gothic": ["Noto Sans CJK JP", "Hiragino Sans", "Yu Gothic", "sans-serif"],
|
|
882
|
+
"MS Mincho": ["Noto Serif CJK JP", "Hiragino Mincho ProN", "Yu Mincho", "serif"],
|
|
883
|
+
SimSun: ["Noto Serif CJK SC", "STSong", "serif"],
|
|
884
|
+
SimHei: ["Noto Sans CJK SC", "STHeiti", "sans-serif"],
|
|
885
|
+
"Microsoft YaHei": ["Noto Sans CJK SC", "PingFang SC", "sans-serif"],
|
|
886
|
+
NSimSun: ["Noto Serif CJK SC", "STSong", "serif"],
|
|
887
|
+
FangSong: ["Noto Serif CJK SC", "STFangsong", "serif"],
|
|
888
|
+
KaiTi: ["Noto Serif CJK SC", "STKaiti", "serif"],
|
|
889
|
+
Batang: ["Noto Serif CJK KR", "AppleMyungjo", "serif"],
|
|
890
|
+
Dotum: ["Noto Sans CJK KR", "AppleGothic", "sans-serif"],
|
|
891
|
+
Gulim: ["Noto Sans CJK KR", "AppleGothic", "sans-serif"],
|
|
892
|
+
Malgun: ["Noto Sans CJK KR", "Apple SD Gothic Neo", "sans-serif"],
|
|
893
|
+
"Malgun Gothic": ["Noto Sans CJK KR", "Apple SD Gothic Neo", "sans-serif"],
|
|
894
|
+
// Complex script fonts
|
|
895
|
+
"Arabic Typesetting": ["Noto Naskh Arabic", "serif"],
|
|
896
|
+
"Simplified Arabic": ["Noto Sans Arabic", "sans-serif"],
|
|
897
|
+
"Traditional Arabic": ["Noto Naskh Arabic", "serif"],
|
|
898
|
+
Mangal: ["Noto Sans Devanagari", "sans-serif"],
|
|
899
|
+
Vrinda: ["Noto Sans Bengali", "sans-serif"],
|
|
900
|
+
Raavi: ["Noto Sans Gurmukhi", "sans-serif"],
|
|
901
|
+
Shruti: ["Noto Sans Gujarati", "sans-serif"],
|
|
902
|
+
Tunga: ["Noto Sans Kannada", "sans-serif"],
|
|
903
|
+
Kartika: ["Noto Sans Malayalam", "sans-serif"],
|
|
904
|
+
Iskoola: ["Noto Sans Sinhala", "sans-serif"],
|
|
905
|
+
"Iskoola Pota": ["Noto Sans Sinhala", "sans-serif"],
|
|
906
|
+
Leelawadee: ["Noto Sans Thai", "sans-serif"],
|
|
907
|
+
"Leelawadee UI": ["Noto Sans Thai", "sans-serif"],
|
|
908
|
+
"Cordia New": ["Noto Sans Thai", "sans-serif"],
|
|
909
|
+
DokChampa: ["Noto Sans Lao", "sans-serif"],
|
|
910
|
+
Nyala: ["Noto Sans Ethiopic", "sans-serif"],
|
|
911
|
+
MoolBoran: ["Noto Sans Khmer", "sans-serif"],
|
|
912
|
+
// Decorative / Display
|
|
913
|
+
"Century Gothic": ["URW Gothic", "Futura", "sans-serif"],
|
|
914
|
+
"Franklin Gothic": ["Liberation Sans", "Helvetica Neue", "sans-serif"],
|
|
915
|
+
"Franklin Gothic Medium": ["Liberation Sans", "Helvetica Neue", "sans-serif"],
|
|
916
|
+
Garamond: ["EB Garamond", "Cormorant Garamond", "serif"],
|
|
917
|
+
"Tw Cen MT": ["Century Gothic", "Futura", "sans-serif"],
|
|
918
|
+
Rockwell: ["Roboto Slab", "Rockwell", "serif"],
|
|
919
|
+
Candara: ["Liberation Sans", "Optima", "sans-serif"],
|
|
920
|
+
Constantia: ["Liberation Serif", "Palatino", "serif"],
|
|
921
|
+
Corbel: ["Liberation Sans", "Lucida Grande", "sans-serif"]
|
|
922
|
+
};
|
|
923
|
+
function getSubstituteFontFamily(fontName, panose) {
|
|
924
|
+
const trimmed = fontName.trim();
|
|
925
|
+
if (!trimmed) {
|
|
926
|
+
return "sans-serif";
|
|
927
|
+
}
|
|
928
|
+
const directSubs = FONT_SUBSTITUTION_MAP[trimmed];
|
|
929
|
+
if (directSubs) {
|
|
930
|
+
return buildFontFamilyString(trimmed, directSubs);
|
|
931
|
+
}
|
|
932
|
+
return buildFontFamilyString(trimmed, ["sans-serif"]);
|
|
933
|
+
}
|
|
934
|
+
var CSS_GENERIC_FAMILIES = /* @__PURE__ */ new Set([
|
|
935
|
+
"serif",
|
|
936
|
+
"sans-serif",
|
|
937
|
+
"monospace",
|
|
938
|
+
"cursive",
|
|
939
|
+
"fantasy",
|
|
940
|
+
"system-ui",
|
|
941
|
+
"ui-serif",
|
|
942
|
+
"ui-sans-serif",
|
|
943
|
+
"ui-monospace",
|
|
944
|
+
"ui-rounded",
|
|
945
|
+
"math",
|
|
946
|
+
"emoji",
|
|
947
|
+
"fangsong"
|
|
948
|
+
]);
|
|
949
|
+
function buildFontFamilyString(primary, fallbacks) {
|
|
950
|
+
const parts = [quoteFontName(primary)];
|
|
951
|
+
for (const fb of fallbacks) {
|
|
952
|
+
const quoted = quoteFontName(fb);
|
|
953
|
+
if (!parts.includes(quoted)) {
|
|
954
|
+
parts.push(quoted);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return parts.join(", ");
|
|
958
|
+
}
|
|
959
|
+
function quoteFontName(name) {
|
|
960
|
+
const trimmed = name.trim();
|
|
961
|
+
if (CSS_GENERIC_FAMILIES.has(trimmed.toLowerCase())) {
|
|
962
|
+
return trimmed;
|
|
963
|
+
}
|
|
964
|
+
return `"${trimmed}"`;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// src/converter/elements/TableElementProcessor.ts
|
|
968
|
+
var TableElementProcessor = class {
|
|
969
|
+
supportedTypes = ["table"];
|
|
970
|
+
async process(element, ctx) {
|
|
971
|
+
if (element.type !== "table") {
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
974
|
+
const tableElement = element;
|
|
975
|
+
const tableData = tableElement.tableData;
|
|
976
|
+
if (!tableData || tableData.rows.length === 0) {
|
|
977
|
+
return null;
|
|
978
|
+
}
|
|
979
|
+
if (ctx.semanticMode && this.canRenderAsMarkdownTable(tableData)) {
|
|
980
|
+
return this.renderMarkdownTable(tableData);
|
|
981
|
+
}
|
|
982
|
+
const rowSpanOccupancy = /* @__PURE__ */ new Map();
|
|
983
|
+
const htmlRows = [];
|
|
984
|
+
for (let ri = 0; ri < tableData.rows.length; ri++) {
|
|
985
|
+
const row = tableData.rows[ri];
|
|
986
|
+
const isHeader = ri === 0 && tableData.firstRowHeader !== false;
|
|
987
|
+
const tag = isHeader ? "th" : "td";
|
|
988
|
+
const cells = [];
|
|
989
|
+
let ci = 0;
|
|
990
|
+
for (const cell of row.cells) {
|
|
991
|
+
while (this.isOccupied(rowSpanOccupancy, ri, ci)) {
|
|
992
|
+
ci++;
|
|
993
|
+
}
|
|
994
|
+
const gridSpan = Math.max(1, cell.gridSpan ?? 1);
|
|
995
|
+
if (cell.vMerge || cell.hMerge) {
|
|
996
|
+
ci += gridSpan;
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
const rowSpan = Math.max(1, cell.rowSpan ?? 1);
|
|
1000
|
+
if (rowSpan > 1) {
|
|
1001
|
+
for (let ro = 1; ro < rowSpan; ro++) {
|
|
1002
|
+
for (let s = 0; s < gridSpan; s++) {
|
|
1003
|
+
this.markOccupied(rowSpanOccupancy, ri + ro, ci + s);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
const attrs2 = this.buildCellAttrs(cell, gridSpan, rowSpan, isHeader);
|
|
1008
|
+
const content = this.renderCellContent(cell);
|
|
1009
|
+
cells.push(`<${tag}${attrs2}>${content}</${tag}>`);
|
|
1010
|
+
ci += gridSpan;
|
|
1011
|
+
}
|
|
1012
|
+
if (cells.length > 0) {
|
|
1013
|
+
htmlRows.push(`<tr>${cells.join("")}</tr>`);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if (htmlRows.length === 0) {
|
|
1017
|
+
return null;
|
|
1018
|
+
}
|
|
1019
|
+
return `<table>
|
|
1020
|
+
${htmlRows.join("\n")}
|
|
1021
|
+
</table>`;
|
|
1022
|
+
}
|
|
1023
|
+
/** Builds inline-style + colspan/rowspan attributes for a cell. */
|
|
1024
|
+
buildCellAttrs(cell, gridSpan, rowSpan, isHeader) {
|
|
1025
|
+
const parts = [];
|
|
1026
|
+
if (gridSpan > 1) {
|
|
1027
|
+
parts.push(` colspan="${gridSpan}"`);
|
|
1028
|
+
}
|
|
1029
|
+
if (rowSpan > 1) {
|
|
1030
|
+
parts.push(` rowspan="${rowSpan}"`);
|
|
1031
|
+
}
|
|
1032
|
+
const css = this.buildCellCss(cell.style, isHeader);
|
|
1033
|
+
if (css) {
|
|
1034
|
+
parts.push(` style="${css}"`);
|
|
1035
|
+
}
|
|
1036
|
+
return parts.join("");
|
|
1037
|
+
}
|
|
1038
|
+
/** Converts PptxTableCellStyle to an inline CSS string. */
|
|
1039
|
+
buildCellCss(style, isHeader) {
|
|
1040
|
+
const rules = [];
|
|
1041
|
+
if (style?.backgroundColor) {
|
|
1042
|
+
rules.push(`background:${style.backgroundColor}`);
|
|
1043
|
+
}
|
|
1044
|
+
if (style?.align) {
|
|
1045
|
+
rules.push(`text-align:${style.align}`);
|
|
1046
|
+
}
|
|
1047
|
+
if (style?.vAlign) {
|
|
1048
|
+
rules.push(`vertical-align:${style.vAlign}`);
|
|
1049
|
+
}
|
|
1050
|
+
if (style?.fontSize) {
|
|
1051
|
+
rules.push(`font-size:${Math.round(style.fontSize)}px`);
|
|
1052
|
+
}
|
|
1053
|
+
if (style?.color) {
|
|
1054
|
+
rules.push(`color:${style.color}`);
|
|
1055
|
+
}
|
|
1056
|
+
if (style?.bold || isHeader) {
|
|
1057
|
+
rules.push("font-weight:bold");
|
|
1058
|
+
}
|
|
1059
|
+
if (style?.italic) {
|
|
1060
|
+
rules.push("font-style:italic");
|
|
1061
|
+
}
|
|
1062
|
+
const border = this.buildBorderCss(style);
|
|
1063
|
+
if (border) {
|
|
1064
|
+
rules.push(border);
|
|
1065
|
+
}
|
|
1066
|
+
const padding = this.buildPaddingCss(style);
|
|
1067
|
+
if (padding) {
|
|
1068
|
+
rules.push(padding);
|
|
1069
|
+
}
|
|
1070
|
+
return rules.join(";");
|
|
1071
|
+
}
|
|
1072
|
+
/** Builds per-edge border CSS from cell style. */
|
|
1073
|
+
buildBorderCss(style) {
|
|
1074
|
+
if (!style) {
|
|
1075
|
+
return "";
|
|
1076
|
+
}
|
|
1077
|
+
const edges = [];
|
|
1078
|
+
if (style.borderTopWidth && style.borderTopColor) {
|
|
1079
|
+
edges.push(`border-top:${style.borderTopWidth}px solid ${style.borderTopColor}`);
|
|
1080
|
+
}
|
|
1081
|
+
if (style.borderBottomWidth && style.borderBottomColor) {
|
|
1082
|
+
edges.push(`border-bottom:${style.borderBottomWidth}px solid ${style.borderBottomColor}`);
|
|
1083
|
+
}
|
|
1084
|
+
if (style.borderLeftWidth && style.borderLeftColor) {
|
|
1085
|
+
edges.push(`border-left:${style.borderLeftWidth}px solid ${style.borderLeftColor}`);
|
|
1086
|
+
}
|
|
1087
|
+
if (style.borderRightWidth && style.borderRightColor) {
|
|
1088
|
+
edges.push(`border-right:${style.borderRightWidth}px solid ${style.borderRightColor}`);
|
|
1089
|
+
}
|
|
1090
|
+
if (edges.length === 0 && style.borderColor) {
|
|
1091
|
+
return `border:1px solid ${style.borderColor}`;
|
|
1092
|
+
}
|
|
1093
|
+
return edges.join(";");
|
|
1094
|
+
}
|
|
1095
|
+
/** Builds padding CSS from cell margin values. */
|
|
1096
|
+
buildPaddingCss(style) {
|
|
1097
|
+
if (!style) {
|
|
1098
|
+
return "";
|
|
1099
|
+
}
|
|
1100
|
+
const t = style.marginTop ?? 0;
|
|
1101
|
+
const r = style.marginRight ?? 0;
|
|
1102
|
+
const b = style.marginBottom ?? 0;
|
|
1103
|
+
const l = style.marginLeft ?? 0;
|
|
1104
|
+
if (t === 0 && r === 0 && b === 0 && l === 0) {
|
|
1105
|
+
return "";
|
|
1106
|
+
}
|
|
1107
|
+
return `padding:${t}px ${r}px ${b}px ${l}px`;
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* Renders cell content as HTML spans preserving per-run styling
|
|
1111
|
+
* (font-family, font-size, color, bold, italic).
|
|
1112
|
+
*/
|
|
1113
|
+
renderCellContent(cell) {
|
|
1114
|
+
const segments = this.getCellSegments(cell);
|
|
1115
|
+
if (segments.length === 0) {
|
|
1116
|
+
return this.escapeHtml(cell.text ?? "");
|
|
1117
|
+
}
|
|
1118
|
+
const parts = [];
|
|
1119
|
+
for (const seg of segments) {
|
|
1120
|
+
if (seg.isParagraphBreak) {
|
|
1121
|
+
parts.push("<br>");
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
const text = this.escapeHtml(seg.text);
|
|
1125
|
+
if (!text) {
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
const css = this.buildRunCss(seg, cell.style);
|
|
1129
|
+
if (css) {
|
|
1130
|
+
parts.push(`<span style="${css}">${text}</span>`);
|
|
1131
|
+
} else {
|
|
1132
|
+
parts.push(text);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
return parts.join("");
|
|
1136
|
+
}
|
|
1137
|
+
/** Builds inline CSS for a single text run, only for properties that
|
|
1138
|
+
* differ from the cell-level defaults. */
|
|
1139
|
+
buildRunCss(seg, cellStyle) {
|
|
1140
|
+
const s = seg.style;
|
|
1141
|
+
const rules = [];
|
|
1142
|
+
if (s.fontFamily) {
|
|
1143
|
+
rules.push(`font-family:${getSubstituteFontFamily(s.fontFamily)}`);
|
|
1144
|
+
}
|
|
1145
|
+
if (s.fontSize && s.fontSize !== cellStyle?.fontSize) {
|
|
1146
|
+
rules.push(`font-size:${Math.round(s.fontSize)}px`);
|
|
1147
|
+
}
|
|
1148
|
+
if (s.color && s.color !== cellStyle?.color) {
|
|
1149
|
+
rules.push(`color:${s.color}`);
|
|
1150
|
+
}
|
|
1151
|
+
if (s.bold && !cellStyle?.bold) {
|
|
1152
|
+
rules.push("font-weight:bold");
|
|
1153
|
+
}
|
|
1154
|
+
if (s.italic && !cellStyle?.italic) {
|
|
1155
|
+
rules.push("font-style:italic");
|
|
1156
|
+
}
|
|
1157
|
+
if (s.underline) {
|
|
1158
|
+
rules.push("text-decoration:underline");
|
|
1159
|
+
}
|
|
1160
|
+
if (s.strikethrough) {
|
|
1161
|
+
rules.push("text-decoration:line-through");
|
|
1162
|
+
}
|
|
1163
|
+
return rules.join(";");
|
|
1164
|
+
}
|
|
1165
|
+
/** Extracts typed TextSegment[] from a cell if present. */
|
|
1166
|
+
getCellSegments(cell) {
|
|
1167
|
+
const raw = cell.textSegments;
|
|
1168
|
+
if (!Array.isArray(raw)) {
|
|
1169
|
+
return [];
|
|
1170
|
+
}
|
|
1171
|
+
return raw.filter(
|
|
1172
|
+
(s) => Boolean(s) && typeof s === "object" && typeof s.text === "string"
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
escapeHtml(text) {
|
|
1176
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1177
|
+
}
|
|
1178
|
+
markOccupied(occupancy, row, column) {
|
|
1179
|
+
const set = occupancy.get(row) ?? /* @__PURE__ */ new Set();
|
|
1180
|
+
set.add(column);
|
|
1181
|
+
occupancy.set(row, set);
|
|
1182
|
+
}
|
|
1183
|
+
isOccupied(occupancy, row, column) {
|
|
1184
|
+
return occupancy.get(row)?.has(column) ?? false;
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Returns true if the table can be rendered as a simple markdown table
|
|
1188
|
+
* (no merged cells, no row spans, no col spans > 1).
|
|
1189
|
+
*/
|
|
1190
|
+
canRenderAsMarkdownTable(tableData) {
|
|
1191
|
+
for (const row of tableData.rows) {
|
|
1192
|
+
for (const cell of row.cells) {
|
|
1193
|
+
if (cell.vMerge || cell.hMerge) {
|
|
1194
|
+
return false;
|
|
1195
|
+
}
|
|
1196
|
+
if ((cell.gridSpan ?? 1) > 1) {
|
|
1197
|
+
return false;
|
|
1198
|
+
}
|
|
1199
|
+
if ((cell.rowSpan ?? 1) > 1) {
|
|
1200
|
+
return false;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
return true;
|
|
1205
|
+
}
|
|
1206
|
+
/** Render a simple table as markdown (no merges/spans). */
|
|
1207
|
+
renderMarkdownTable(tableData) {
|
|
1208
|
+
const rows = tableData.rows;
|
|
1209
|
+
if (rows.length === 0) {
|
|
1210
|
+
return "";
|
|
1211
|
+
}
|
|
1212
|
+
const columnCount = Math.max(...rows.map((r) => r.cells.length));
|
|
1213
|
+
const mdRows = [];
|
|
1214
|
+
const hasHeader = tableData.firstRowHeader !== false;
|
|
1215
|
+
for (let ri = 0; ri < rows.length; ri++) {
|
|
1216
|
+
const cells = rows[ri].cells.map((cell) => {
|
|
1217
|
+
const text = this.getCellFormattedText(cell);
|
|
1218
|
+
return this.escapeMarkdownTableCell(text);
|
|
1219
|
+
});
|
|
1220
|
+
while (cells.length < columnCount) {
|
|
1221
|
+
cells.push("");
|
|
1222
|
+
}
|
|
1223
|
+
mdRows.push(`| ${cells.join(" | ")} |`);
|
|
1224
|
+
if (ri === 0 && hasHeader) {
|
|
1225
|
+
const divider = Array.from({ length: columnCount }, () => "---");
|
|
1226
|
+
mdRows.push(`| ${divider.join(" | ")} |`);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
if (!hasHeader) {
|
|
1230
|
+
const emptyHeader = Array.from({ length: columnCount }, () => "");
|
|
1231
|
+
const divider = Array.from({ length: columnCount }, () => "---");
|
|
1232
|
+
mdRows.unshift(`| ${emptyHeader.join(" | ")} |`, `| ${divider.join(" | ")} |`);
|
|
1233
|
+
}
|
|
1234
|
+
return mdRows.join("\n");
|
|
1235
|
+
}
|
|
1236
|
+
/** Gets cell text with basic markdown formatting (bold/italic). */
|
|
1237
|
+
getCellFormattedText(cell) {
|
|
1238
|
+
const segments = this.getCellSegments(cell);
|
|
1239
|
+
if (segments.length === 0) {
|
|
1240
|
+
return cell.text ?? "";
|
|
1241
|
+
}
|
|
1242
|
+
const parts = [];
|
|
1243
|
+
for (const seg of segments) {
|
|
1244
|
+
if (seg.isParagraphBreak) {
|
|
1245
|
+
parts.push(" ");
|
|
1246
|
+
continue;
|
|
1247
|
+
}
|
|
1248
|
+
let text = seg.text;
|
|
1249
|
+
if (!text) {
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
if (seg.style.bold && seg.style.italic) {
|
|
1253
|
+
text = `***${text}***`;
|
|
1254
|
+
} else if (seg.style.bold) {
|
|
1255
|
+
text = `**${text}**`;
|
|
1256
|
+
} else if (seg.style.italic) {
|
|
1257
|
+
text = `*${text}*`;
|
|
1258
|
+
}
|
|
1259
|
+
if (seg.style.strikethrough) {
|
|
1260
|
+
text = `~~${text}~~`;
|
|
1261
|
+
}
|
|
1262
|
+
if (seg.style.hyperlink) {
|
|
1263
|
+
text = `[${text}](${seg.style.hyperlink})`;
|
|
1264
|
+
}
|
|
1265
|
+
parts.push(text);
|
|
1266
|
+
}
|
|
1267
|
+
return parts.join("").trim();
|
|
1268
|
+
}
|
|
1269
|
+
escapeMarkdownTableCell(text) {
|
|
1270
|
+
return text.replace(/\|/g, "\\|").replace(/\n+/g, " ").trim();
|
|
1271
|
+
}
|
|
1272
|
+
};
|
|
1273
|
+
|
|
1274
|
+
// src/converter/ShapeTextRenderer.ts
|
|
1275
|
+
var DEFAULT_FONT_SIZE = 18;
|
|
1276
|
+
var FALLBACK_PAD_FACTOR = 0.06;
|
|
1277
|
+
function drawTextSegments(ctx, segments, bodyStyle, w, h, scaleFactor = 1) {
|
|
1278
|
+
const padL = bodyStyle?.bodyInsetLeft ? bodyStyle.bodyInsetLeft * scaleFactor : Math.round(w * FALLBACK_PAD_FACTOR);
|
|
1279
|
+
const padR = bodyStyle?.bodyInsetRight ? bodyStyle.bodyInsetRight * scaleFactor : Math.round(w * FALLBACK_PAD_FACTOR);
|
|
1280
|
+
const padT = bodyStyle?.bodyInsetTop ? bodyStyle.bodyInsetTop * scaleFactor : Math.round(h * FALLBACK_PAD_FACTOR);
|
|
1281
|
+
const padB = bodyStyle?.bodyInsetBottom ? bodyStyle.bodyInsetBottom * scaleFactor : Math.round(h * FALLBACK_PAD_FACTOR);
|
|
1282
|
+
const maxWidth = w - padL - padR;
|
|
1283
|
+
if (maxWidth <= 0) {
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
const paragraphs = splitIntoParagraphs(segments);
|
|
1287
|
+
const lines = layoutParagraphs(ctx, paragraphs, maxWidth, scaleFactor);
|
|
1288
|
+
if (lines.length === 0) {
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
const totalHeight = lines.reduce((sum, l) => sum + l.height * 1.3, 0);
|
|
1292
|
+
const usableH = h - padT - padB;
|
|
1293
|
+
const vAlign = bodyStyle?.vAlign ?? "top";
|
|
1294
|
+
let y;
|
|
1295
|
+
if (vAlign === "middle") {
|
|
1296
|
+
y = padT + (usableH - totalHeight) / 2;
|
|
1297
|
+
} else if (vAlign === "bottom") {
|
|
1298
|
+
y = h - padB - totalHeight;
|
|
1299
|
+
} else {
|
|
1300
|
+
y = padT;
|
|
1301
|
+
}
|
|
1302
|
+
for (const line of lines) {
|
|
1303
|
+
y += line.height;
|
|
1304
|
+
const hAlign = line.align ?? bodyStyle?.align ?? "left";
|
|
1305
|
+
let x;
|
|
1306
|
+
if (hAlign === "center") {
|
|
1307
|
+
x = padL + (maxWidth - line.width) / 2;
|
|
1308
|
+
} else if (hAlign === "right") {
|
|
1309
|
+
x = padL + maxWidth - line.width;
|
|
1310
|
+
} else {
|
|
1311
|
+
x = padL;
|
|
1312
|
+
}
|
|
1313
|
+
for (const run of line.runs) {
|
|
1314
|
+
ctx.font = buildFont(run.style, scaleFactor);
|
|
1315
|
+
ctx.fillStyle = run.style.color ?? "#000000";
|
|
1316
|
+
ctx.fillText(run.text, x, y);
|
|
1317
|
+
x += run.measuredWidth;
|
|
1318
|
+
}
|
|
1319
|
+
y += line.height * 0.3;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
function splitIntoParagraphs(segments) {
|
|
1323
|
+
const paragraphs = [[]];
|
|
1324
|
+
for (const seg of segments) {
|
|
1325
|
+
if (seg.isParagraphBreak) {
|
|
1326
|
+
paragraphs.push([]);
|
|
1327
|
+
} else if (seg.text) {
|
|
1328
|
+
paragraphs[paragraphs.length - 1].push(seg);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
return paragraphs.filter((p) => p.length > 0);
|
|
1332
|
+
}
|
|
1333
|
+
function layoutParagraphs(ctx, paragraphs, maxWidth, scaleFactor) {
|
|
1334
|
+
const lines = [];
|
|
1335
|
+
for (const para of paragraphs) {
|
|
1336
|
+
const paraAlign = para[0]?.style?.align;
|
|
1337
|
+
let currentLine = [];
|
|
1338
|
+
let lineWidth = 0;
|
|
1339
|
+
let lineHeight = 0;
|
|
1340
|
+
for (const seg of para) {
|
|
1341
|
+
ctx.font = buildFont(seg.style, scaleFactor);
|
|
1342
|
+
const fontSize = (seg.style.fontSize ?? DEFAULT_FONT_SIZE) * scaleFactor;
|
|
1343
|
+
const segHeight = fontSize * 1.2;
|
|
1344
|
+
const tokens = seg.text.split(/(?<=\s)(?=\S)|(?<=\S)(?=\s)/);
|
|
1345
|
+
for (const raw of tokens) {
|
|
1346
|
+
if (!raw) {
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
1349
|
+
if (/^\s+$/.test(raw)) {
|
|
1350
|
+
if (currentLine.length > 0) {
|
|
1351
|
+
const spaceW = ctx.measureText(raw).width;
|
|
1352
|
+
currentLine.push({
|
|
1353
|
+
text: raw,
|
|
1354
|
+
style: seg.style,
|
|
1355
|
+
measuredWidth: spaceW
|
|
1356
|
+
});
|
|
1357
|
+
lineWidth += spaceW;
|
|
1358
|
+
}
|
|
1359
|
+
continue;
|
|
1360
|
+
}
|
|
1361
|
+
const word = raw;
|
|
1362
|
+
const wordW = ctx.measureText(word).width;
|
|
1363
|
+
if (lineWidth + wordW > maxWidth && currentLine.length > 0) {
|
|
1364
|
+
while (currentLine.length > 0 && /^\s+$/.test(currentLine[currentLine.length - 1].text)) {
|
|
1365
|
+
const removed = currentLine.pop();
|
|
1366
|
+
lineWidth -= removed.measuredWidth;
|
|
1367
|
+
}
|
|
1368
|
+
lines.push({
|
|
1369
|
+
runs: currentLine,
|
|
1370
|
+
width: lineWidth,
|
|
1371
|
+
height: lineHeight,
|
|
1372
|
+
align: paraAlign
|
|
1373
|
+
});
|
|
1374
|
+
currentLine = [];
|
|
1375
|
+
lineWidth = 0;
|
|
1376
|
+
lineHeight = 0;
|
|
1377
|
+
}
|
|
1378
|
+
currentLine.push({
|
|
1379
|
+
text: word,
|
|
1380
|
+
style: seg.style,
|
|
1381
|
+
measuredWidth: wordW
|
|
1382
|
+
});
|
|
1383
|
+
lineWidth += wordW;
|
|
1384
|
+
lineHeight = Math.max(lineHeight, segHeight);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
if (currentLine.length > 0) {
|
|
1388
|
+
lines.push({
|
|
1389
|
+
runs: currentLine,
|
|
1390
|
+
width: lineWidth,
|
|
1391
|
+
height: lineHeight,
|
|
1392
|
+
align: paraAlign
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
return lines;
|
|
1397
|
+
}
|
|
1398
|
+
function buildFont(style, scaleFactor) {
|
|
1399
|
+
const size = (style.fontSize ?? DEFAULT_FONT_SIZE) * scaleFactor;
|
|
1400
|
+
const weight = style.bold ? "bold" : "normal";
|
|
1401
|
+
const slant = style.italic ? "italic" : "normal";
|
|
1402
|
+
const family = style.fontFamily ? getSubstituteFontFamily(style.fontFamily) : "sans-serif";
|
|
1403
|
+
return `${slant} ${weight} ${size}px ${family}`;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// src/converter/ShapeImageRenderer.ts
|
|
1407
|
+
var MAX_DIM = 400;
|
|
1408
|
+
var MAX_DIM_TEXT = 800;
|
|
1409
|
+
var MIN_DIM = 4;
|
|
1410
|
+
function renderShapeToDataUrl(input) {
|
|
1411
|
+
const style = input.shapeStyle;
|
|
1412
|
+
if (!style) {
|
|
1413
|
+
return null;
|
|
1414
|
+
}
|
|
1415
|
+
if (!hasVisibleFill(style) && !hasVisibleStroke(style)) {
|
|
1416
|
+
return null;
|
|
1417
|
+
}
|
|
1418
|
+
let w = Math.round(input.width);
|
|
1419
|
+
let h = Math.round(input.height);
|
|
1420
|
+
if (w < MIN_DIM || h < MIN_DIM) {
|
|
1421
|
+
return null;
|
|
1422
|
+
}
|
|
1423
|
+
const hasText = (input.textSegments?.length ?? 0) > 0;
|
|
1424
|
+
const maxDim = hasText ? MAX_DIM_TEXT : MAX_DIM;
|
|
1425
|
+
let scaleFactor = 1;
|
|
1426
|
+
if (w > maxDim || h > maxDim) {
|
|
1427
|
+
scaleFactor = maxDim / Math.max(w, h);
|
|
1428
|
+
w = Math.round(w * scaleFactor);
|
|
1429
|
+
h = Math.round(h * scaleFactor);
|
|
1430
|
+
}
|
|
1431
|
+
const canvas = createCanvasElement(w, h);
|
|
1432
|
+
if (!canvas) {
|
|
1433
|
+
return null;
|
|
1434
|
+
}
|
|
1435
|
+
const ctx = canvas.getContext("2d");
|
|
1436
|
+
if (!ctx) {
|
|
1437
|
+
return null;
|
|
1438
|
+
}
|
|
1439
|
+
applyFill(ctx, style, w, h);
|
|
1440
|
+
if (input.pathData) {
|
|
1441
|
+
const pw = input.pathWidth ?? w;
|
|
1442
|
+
const ph = input.pathHeight ?? h;
|
|
1443
|
+
const sx = w / (pw || 1);
|
|
1444
|
+
const sy = h / (ph || 1);
|
|
1445
|
+
const path = new Path2D(input.pathData);
|
|
1446
|
+
ctx.save();
|
|
1447
|
+
ctx.scale(sx, sy);
|
|
1448
|
+
ctx.fill(path);
|
|
1449
|
+
if (hasVisibleStroke(style)) {
|
|
1450
|
+
applyStroke(ctx, style);
|
|
1451
|
+
ctx.stroke(path);
|
|
1452
|
+
}
|
|
1453
|
+
ctx.restore();
|
|
1454
|
+
} else {
|
|
1455
|
+
drawPresetShape(ctx, input.shapeType, w, h);
|
|
1456
|
+
ctx.fill();
|
|
1457
|
+
if (hasVisibleStroke(style)) {
|
|
1458
|
+
applyStroke(ctx, style);
|
|
1459
|
+
drawPresetShape(ctx, input.shapeType, w, h);
|
|
1460
|
+
ctx.stroke();
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
ctx.globalAlpha = 1;
|
|
1464
|
+
if (input.textSegments?.length) {
|
|
1465
|
+
drawTextSegments(ctx, input.textSegments, input.textStyle, w, h, scaleFactor);
|
|
1466
|
+
}
|
|
1467
|
+
return canvas.toDataURL("image/png");
|
|
1468
|
+
}
|
|
1469
|
+
function createCanvasElement(w, h) {
|
|
1470
|
+
try {
|
|
1471
|
+
if (typeof document !== "undefined" && document.createElement) {
|
|
1472
|
+
const c = document.createElement("canvas");
|
|
1473
|
+
c.width = w;
|
|
1474
|
+
c.height = h;
|
|
1475
|
+
return c;
|
|
1476
|
+
}
|
|
1477
|
+
} catch {
|
|
1478
|
+
}
|
|
1479
|
+
return null;
|
|
1480
|
+
}
|
|
1481
|
+
function drawPresetShape(ctx, shapeType, w, h) {
|
|
1482
|
+
ctx.beginPath();
|
|
1483
|
+
const type = shapeType ?? "rect";
|
|
1484
|
+
switch (type) {
|
|
1485
|
+
case "ellipse":
|
|
1486
|
+
ctx.ellipse(w / 2, h / 2, w / 2, h / 2, 0, 0, Math.PI * 2);
|
|
1487
|
+
break;
|
|
1488
|
+
case "roundRect": {
|
|
1489
|
+
const r = Math.min(w, h) * 0.1;
|
|
1490
|
+
roundRect(ctx, 0, 0, w, h, r);
|
|
1491
|
+
break;
|
|
1492
|
+
}
|
|
1493
|
+
case "triangle":
|
|
1494
|
+
case "flowChartProcess":
|
|
1495
|
+
case "rect":
|
|
1496
|
+
default:
|
|
1497
|
+
ctx.rect(0, 0, w, h);
|
|
1498
|
+
break;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
function roundRect(ctx, x, y, w, h, r) {
|
|
1502
|
+
ctx.moveTo(x + r, y);
|
|
1503
|
+
ctx.lineTo(x + w - r, y);
|
|
1504
|
+
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
|
|
1505
|
+
ctx.lineTo(x + w, y + h - r);
|
|
1506
|
+
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
|
|
1507
|
+
ctx.lineTo(x + r, y + h);
|
|
1508
|
+
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
|
|
1509
|
+
ctx.lineTo(x, y + r);
|
|
1510
|
+
ctx.quadraticCurveTo(x, y, x + r, y);
|
|
1511
|
+
ctx.closePath();
|
|
1512
|
+
}
|
|
1513
|
+
function applyFill(ctx, style, w, h) {
|
|
1514
|
+
if (style.fillMode === "gradient" && style.fillGradientStops?.length) {
|
|
1515
|
+
const angle = (style.fillGradientAngle ?? 0) * Math.PI / 180;
|
|
1516
|
+
const cx = w / 2;
|
|
1517
|
+
const cy = h / 2;
|
|
1518
|
+
const len = Math.max(w, h) / 2;
|
|
1519
|
+
const dx = Math.cos(angle) * len;
|
|
1520
|
+
const dy = Math.sin(angle) * len;
|
|
1521
|
+
let grad;
|
|
1522
|
+
if (style.fillGradientType === "radial") {
|
|
1523
|
+
grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, len);
|
|
1524
|
+
} else {
|
|
1525
|
+
grad = ctx.createLinearGradient(cx - dx, cy - dy, cx + dx, cy + dy);
|
|
1526
|
+
}
|
|
1527
|
+
for (const stop of style.fillGradientStops) {
|
|
1528
|
+
const pos = Math.max(0, Math.min(1, stop.position));
|
|
1529
|
+
grad.addColorStop(pos, stop.color);
|
|
1530
|
+
}
|
|
1531
|
+
ctx.fillStyle = grad;
|
|
1532
|
+
} else if (style.fillColor && style.fillColor !== "transparent") {
|
|
1533
|
+
ctx.fillStyle = style.fillColor;
|
|
1534
|
+
} else {
|
|
1535
|
+
ctx.fillStyle = "rgba(0,0,0,0)";
|
|
1536
|
+
}
|
|
1537
|
+
if (style.fillOpacity !== void 0 && style.fillOpacity < 1) {
|
|
1538
|
+
ctx.globalAlpha = style.fillOpacity;
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
function applyStroke(ctx, style) {
|
|
1542
|
+
ctx.strokeStyle = style.strokeColor ?? "#000000";
|
|
1543
|
+
ctx.lineWidth = style.strokeWidth ?? 1;
|
|
1544
|
+
if (style.strokeOpacity !== void 0 && style.strokeOpacity < 1) {
|
|
1545
|
+
ctx.globalAlpha = style.strokeOpacity;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
function hasVisibleFill(style) {
|
|
1549
|
+
if (style.fillMode === "none") {
|
|
1550
|
+
return false;
|
|
1551
|
+
}
|
|
1552
|
+
if (style.fillMode === "gradient" && style.fillGradientStops?.length) {
|
|
1553
|
+
return true;
|
|
1554
|
+
}
|
|
1555
|
+
if (style.fillMode === "solid" || style.fillMode === "pattern") {
|
|
1556
|
+
return Boolean(style.fillColor) && style.fillColor !== "transparent";
|
|
1557
|
+
}
|
|
1558
|
+
if (style.fillMode === "image") {
|
|
1559
|
+
return Boolean(style.fillImageUrl);
|
|
1560
|
+
}
|
|
1561
|
+
if (style.fillColor && style.fillColor !== "transparent") {
|
|
1562
|
+
return true;
|
|
1563
|
+
}
|
|
1564
|
+
return false;
|
|
1565
|
+
}
|
|
1566
|
+
function hasVisibleStroke(style) {
|
|
1567
|
+
return Boolean(style.strokeColor) && style.strokeColor !== "transparent" && (style.strokeWidth ?? 0) > 0;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
// src/converter/elements/TextElementProcessor.ts
|
|
1571
|
+
var TextElementProcessor = class {
|
|
1572
|
+
constructor(textRenderer) {
|
|
1573
|
+
this.textRenderer = textRenderer;
|
|
1574
|
+
}
|
|
1575
|
+
supportedTypes = ["text", "shape", "connector"];
|
|
1576
|
+
async process(element, ctx) {
|
|
1577
|
+
if (!hasTextProperties(element)) {
|
|
1578
|
+
return null;
|
|
1579
|
+
}
|
|
1580
|
+
const textElement = element;
|
|
1581
|
+
const parts = [];
|
|
1582
|
+
const fillImage = await this.extractShapeFillImage(element, ctx);
|
|
1583
|
+
if (fillImage) {
|
|
1584
|
+
parts.push(fillImage);
|
|
1585
|
+
}
|
|
1586
|
+
const hasVisualShape = this.shapeHasVisibleStyling(element);
|
|
1587
|
+
if (textElement.textSegments && textElement.textSegments.length > 0) {
|
|
1588
|
+
let compositeRendered = false;
|
|
1589
|
+
if (hasVisualShape) {
|
|
1590
|
+
const compositeImage = await this.renderShapeAsImage(
|
|
1591
|
+
element,
|
|
1592
|
+
ctx,
|
|
1593
|
+
textElement.textSegments,
|
|
1594
|
+
textElement.textStyle
|
|
1595
|
+
);
|
|
1596
|
+
if (compositeImage) {
|
|
1597
|
+
parts.push(compositeImage);
|
|
1598
|
+
compositeRendered = true;
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
const bulletImages = await this.extractPictureBullets(textElement.textSegments, ctx);
|
|
1602
|
+
if (bulletImages.length > 0) {
|
|
1603
|
+
parts.push(...bulletImages);
|
|
1604
|
+
}
|
|
1605
|
+
if (!compositeRendered) {
|
|
1606
|
+
const renderOpts = {
|
|
1607
|
+
paragraphIndents: textElement.paragraphIndents,
|
|
1608
|
+
slideNumber: ctx.slideNumber,
|
|
1609
|
+
htmlFormatting: ctx.layoutScale !== void 0 && !ctx.semanticMode
|
|
1610
|
+
};
|
|
1611
|
+
const content = this.textRenderer.render(textElement.textSegments, renderOpts);
|
|
1612
|
+
if (content.trim()) {
|
|
1613
|
+
let textBlock = content;
|
|
1614
|
+
if (textElement.linkedTxbxId !== void 0 && (textElement.linkedTxbxSeq ?? 0) > 0) {
|
|
1615
|
+
const contLabel = `continued from linked text box ${textElement.linkedTxbxId}`;
|
|
1616
|
+
textBlock += `
|
|
1617
|
+
|
|
1618
|
+
*[${contLabel}]*`;
|
|
1619
|
+
}
|
|
1620
|
+
parts.push(textBlock);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
} else {
|
|
1624
|
+
const fallbackText = textElement.text?.trim();
|
|
1625
|
+
if (fallbackText) {
|
|
1626
|
+
const align = textElement.textStyle?.align;
|
|
1627
|
+
if (align && align !== "left") {
|
|
1628
|
+
parts.push(`<p align="${align}">${fallbackText}</p>`);
|
|
1629
|
+
} else {
|
|
1630
|
+
parts.push(fallbackText);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
const warp = textElement.textStyle?.textWarpPreset;
|
|
1635
|
+
if (warp && warp !== "textNoShape") {
|
|
1636
|
+
parts.push(`*Text warp: ${warp}*`);
|
|
1637
|
+
}
|
|
1638
|
+
if (parts.length === 0 && textElement.promptText) {
|
|
1639
|
+
parts.push(`*[Placeholder: ${textElement.promptText}]*`);
|
|
1640
|
+
}
|
|
1641
|
+
if (parts.length === 0) {
|
|
1642
|
+
const shapeImage = await this.renderShapeAsImage(element, ctx, void 0, void 0);
|
|
1643
|
+
if (shapeImage) {
|
|
1644
|
+
parts.push(shapeImage);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
if (parts.length === 0) {
|
|
1648
|
+
return null;
|
|
1649
|
+
}
|
|
1650
|
+
return parts.join("\n\n");
|
|
1651
|
+
}
|
|
1652
|
+
async extractPictureBullets(segments, ctx) {
|
|
1653
|
+
const extracted = [];
|
|
1654
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1655
|
+
for (const segment of segments) {
|
|
1656
|
+
const dataUrl = segment.bulletInfo?.imageDataUrl;
|
|
1657
|
+
if (!dataUrl || !dataUrl.startsWith("data:") || seen.has(dataUrl)) {
|
|
1658
|
+
continue;
|
|
1659
|
+
}
|
|
1660
|
+
seen.add(dataUrl);
|
|
1661
|
+
try {
|
|
1662
|
+
const path = await ctx.mediaContext.saveImage(dataUrl, `slide${ctx.slideNumber}-bullet`);
|
|
1663
|
+
extracted.push(`<img src="${path}" alt="Bullet image">`);
|
|
1664
|
+
} catch {
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
return extracted;
|
|
1668
|
+
}
|
|
1669
|
+
async extractShapeFillImage(element, ctx) {
|
|
1670
|
+
const style = element.shapeStyle;
|
|
1671
|
+
if (!style) {
|
|
1672
|
+
return null;
|
|
1673
|
+
}
|
|
1674
|
+
if (style.fillMode !== "image" || !style.fillImageUrl) {
|
|
1675
|
+
return null;
|
|
1676
|
+
}
|
|
1677
|
+
if (!style.fillImageUrl.startsWith("data:")) {
|
|
1678
|
+
return null;
|
|
1679
|
+
}
|
|
1680
|
+
try {
|
|
1681
|
+
const path = await ctx.mediaContext.saveImage(
|
|
1682
|
+
style.fillImageUrl,
|
|
1683
|
+
`slide${ctx.slideNumber}-shapefill`
|
|
1684
|
+
);
|
|
1685
|
+
if (ctx.semanticMode) {
|
|
1686
|
+
return ``;
|
|
1687
|
+
}
|
|
1688
|
+
return `<img src="${path}" alt="Shape fill">`;
|
|
1689
|
+
} catch {
|
|
1690
|
+
return null;
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* Renders a shape with visible fill/stroke (optionally with text)
|
|
1695
|
+
* to a PNG image so it appears in the markdown output.
|
|
1696
|
+
*/
|
|
1697
|
+
async renderShapeAsImage(element, ctx, textSegments, textStyle) {
|
|
1698
|
+
const shape = element;
|
|
1699
|
+
if (!shape.shapeStyle || !shape.width || !shape.height) {
|
|
1700
|
+
return null;
|
|
1701
|
+
}
|
|
1702
|
+
try {
|
|
1703
|
+
const dataUrl = renderShapeToDataUrl({
|
|
1704
|
+
width: shape.width,
|
|
1705
|
+
height: shape.height,
|
|
1706
|
+
shapeType: shape.shapeType,
|
|
1707
|
+
pathData: shape.pathData,
|
|
1708
|
+
pathWidth: shape.pathWidth,
|
|
1709
|
+
pathHeight: shape.pathHeight,
|
|
1710
|
+
shapeStyle: shape.shapeStyle,
|
|
1711
|
+
textSegments,
|
|
1712
|
+
textStyle
|
|
1713
|
+
});
|
|
1714
|
+
if (!dataUrl) {
|
|
1715
|
+
return null;
|
|
1716
|
+
}
|
|
1717
|
+
const imgPath = await ctx.mediaContext.saveImage(dataUrl, `slide${ctx.slideNumber}-shape`);
|
|
1718
|
+
if (ctx.semanticMode) {
|
|
1719
|
+
return ``;
|
|
1720
|
+
}
|
|
1721
|
+
if (ctx.layoutScale) {
|
|
1722
|
+
return `<img src="${imgPath}" alt="Shape" style="max-width:100%;height:auto">`;
|
|
1723
|
+
}
|
|
1724
|
+
const dims = this.computeDisplaySize(shape.width, shape.height);
|
|
1725
|
+
return `<img src="${imgPath}" alt="Shape" width="${dims.w}" height="${dims.h}">`;
|
|
1726
|
+
} catch {
|
|
1727
|
+
return null;
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
/**
|
|
1731
|
+
* Checks whether the element has a visible fill or stroke that
|
|
1732
|
+
* would benefit from being rendered as an image.
|
|
1733
|
+
*/
|
|
1734
|
+
shapeHasVisibleStyling(element) {
|
|
1735
|
+
const style = element.shapeStyle;
|
|
1736
|
+
if (!style) {
|
|
1737
|
+
return false;
|
|
1738
|
+
}
|
|
1739
|
+
if (style.fillMode === "none" && !style.strokeColor) {
|
|
1740
|
+
return false;
|
|
1741
|
+
}
|
|
1742
|
+
if (style.fillColor && style.fillColor !== "transparent") {
|
|
1743
|
+
return true;
|
|
1744
|
+
}
|
|
1745
|
+
if (style.fillMode === "gradient" && (style.fillGradientStops?.length ?? 0) > 0) {
|
|
1746
|
+
return true;
|
|
1747
|
+
}
|
|
1748
|
+
if (style.strokeColor && style.strokeColor !== "transparent" && (style.strokeWidth ?? 0) > 0) {
|
|
1749
|
+
return true;
|
|
1750
|
+
}
|
|
1751
|
+
return false;
|
|
1752
|
+
}
|
|
1753
|
+
/** Scale element dimensions to a sensible display size, capping width. */
|
|
1754
|
+
computeDisplaySize(origW, origH, maxW = 600) {
|
|
1755
|
+
if (origW <= 0 || origH <= 0) {
|
|
1756
|
+
return { w: 100, h: 100 };
|
|
1757
|
+
}
|
|
1758
|
+
if (origW <= maxW) {
|
|
1759
|
+
return { w: Math.round(origW), h: Math.round(origH) };
|
|
1760
|
+
}
|
|
1761
|
+
const scale = maxW / origW;
|
|
1762
|
+
return { w: maxW, h: Math.round(origH * scale) };
|
|
1763
|
+
}
|
|
1764
|
+
};
|
|
1765
|
+
|
|
1766
|
+
// src/converter/SlideMetadataRenderer.ts
|
|
1767
|
+
var SlideMetadataRenderer = class {
|
|
1768
|
+
constructor(textRenderer) {
|
|
1769
|
+
this.textRenderer = textRenderer;
|
|
1770
|
+
}
|
|
1771
|
+
/**
|
|
1772
|
+
* Renders the slide transition effect as a short metadata line.
|
|
1773
|
+
*/
|
|
1774
|
+
renderTransition(slide) {
|
|
1775
|
+
const tr = slide.transition;
|
|
1776
|
+
if (!tr || !tr.type || tr.type === "none") {
|
|
1777
|
+
return "";
|
|
1778
|
+
}
|
|
1779
|
+
const parts = [];
|
|
1780
|
+
parts.push(`**Transition:** ${tr.type}`);
|
|
1781
|
+
if (tr.direction) {
|
|
1782
|
+
parts.push(`direction: ${tr.direction}`);
|
|
1783
|
+
}
|
|
1784
|
+
if (typeof tr.durationMs === "number") {
|
|
1785
|
+
parts.push(`${tr.durationMs}ms`);
|
|
1786
|
+
}
|
|
1787
|
+
if (tr.advanceOnClick === false) {
|
|
1788
|
+
parts.push("no click advance");
|
|
1789
|
+
}
|
|
1790
|
+
if (typeof tr.advanceAfterMs === "number") {
|
|
1791
|
+
parts.push(`auto-advance: ${tr.advanceAfterMs}ms`);
|
|
1792
|
+
}
|
|
1793
|
+
if (tr.soundFileName) {
|
|
1794
|
+
parts.push(`sound: ${tr.soundFileName}`);
|
|
1795
|
+
}
|
|
1796
|
+
return `*${parts.join(" | ")}*`;
|
|
1797
|
+
}
|
|
1798
|
+
/**
|
|
1799
|
+
* Renders the slide's animation effects grouped by click sequence.
|
|
1800
|
+
*/
|
|
1801
|
+
renderAnimations(slide) {
|
|
1802
|
+
const native = slide.nativeAnimations;
|
|
1803
|
+
const legacy = slide.animations;
|
|
1804
|
+
const items = native?.length ? native : this.mapLegacyAnimations(legacy);
|
|
1805
|
+
if (items.length === 0) {
|
|
1806
|
+
return "";
|
|
1807
|
+
}
|
|
1808
|
+
const clickGroups = this.groupByClickSequence(items);
|
|
1809
|
+
const lines = ["### Animations"];
|
|
1810
|
+
for (let gi = 0; gi < clickGroups.length; gi += 1) {
|
|
1811
|
+
const group = clickGroups[gi];
|
|
1812
|
+
lines.push(`- **Click ${gi + 1}:**`);
|
|
1813
|
+
for (const anim of group) {
|
|
1814
|
+
lines.push(` - ${this.summariseAnimation(anim)}`);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
return lines.length > 1 ? lines.join("\n") : "";
|
|
1818
|
+
}
|
|
1819
|
+
/**
|
|
1820
|
+
* Renders any compatibility warnings for the slide.
|
|
1821
|
+
*/
|
|
1822
|
+
renderWarnings(slide) {
|
|
1823
|
+
const raw = slide.warnings;
|
|
1824
|
+
if (!raw || raw.length === 0) {
|
|
1825
|
+
return "";
|
|
1826
|
+
}
|
|
1827
|
+
const lines = ["### Warnings"];
|
|
1828
|
+
for (const w of raw) {
|
|
1829
|
+
const icon = w.severity === "warning" ? "\u26A0\uFE0F" : "\u2139\uFE0F";
|
|
1830
|
+
lines.push(`- ${icon} ${w.message}`);
|
|
1831
|
+
}
|
|
1832
|
+
return lines.join("\n");
|
|
1833
|
+
}
|
|
1834
|
+
/**
|
|
1835
|
+
* Renders any review comments attached to the slide.
|
|
1836
|
+
*/
|
|
1837
|
+
renderComments(slide) {
|
|
1838
|
+
if (!slide.comments || slide.comments.length === 0) {
|
|
1839
|
+
return "";
|
|
1840
|
+
}
|
|
1841
|
+
const lines = ["### Comments"];
|
|
1842
|
+
for (const comment of slide.comments) {
|
|
1843
|
+
const author = comment.author?.trim() || "Unknown";
|
|
1844
|
+
const createdAt = comment.createdAt ? ` (${comment.createdAt})` : "";
|
|
1845
|
+
const resolved = comment.resolved ? " [resolved]" : "";
|
|
1846
|
+
lines.push(`- **${author}**${createdAt}: ${comment.text}${resolved}`);
|
|
1847
|
+
}
|
|
1848
|
+
return lines.join("\n");
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* Renders the slide's speaker notes as a Markdown blockquote.
|
|
1852
|
+
*/
|
|
1853
|
+
renderNotes(slide) {
|
|
1854
|
+
const notesFromSegments = slide.notesSegments ? this.textRenderer.render(slide.notesSegments) : "";
|
|
1855
|
+
const notesText = (notesFromSegments || slide.notes || "").trim();
|
|
1856
|
+
if (!notesText) {
|
|
1857
|
+
return "";
|
|
1858
|
+
}
|
|
1859
|
+
const quoted = notesText.split(/\r?\n/).map((line) => `> ${line}`).join("\n");
|
|
1860
|
+
return `> **Speaker Notes**
|
|
1861
|
+
${quoted}`;
|
|
1862
|
+
}
|
|
1863
|
+
/**
|
|
1864
|
+
* Groups animations by click sequence. Each `onClick` trigger starts
|
|
1865
|
+
* a new group; `withPrevious` and `afterPrevious` are appended to the
|
|
1866
|
+
* current group.
|
|
1867
|
+
*/
|
|
1868
|
+
groupByClickSequence(items) {
|
|
1869
|
+
const groups = [];
|
|
1870
|
+
for (const item of items) {
|
|
1871
|
+
const trigger = item.trigger ?? "onClick";
|
|
1872
|
+
if (trigger === "onClick" || groups.length === 0) {
|
|
1873
|
+
groups.push([item]);
|
|
1874
|
+
} else {
|
|
1875
|
+
groups[groups.length - 1].push(item);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
return groups;
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Converts legacy animation data into the NativeAnimationLike shape.
|
|
1882
|
+
*/
|
|
1883
|
+
mapLegacyAnimations(legacy) {
|
|
1884
|
+
if (!legacy?.length) {
|
|
1885
|
+
return [];
|
|
1886
|
+
}
|
|
1887
|
+
return legacy.map((a) => {
|
|
1888
|
+
let presetClass = "entr";
|
|
1889
|
+
if (a.exit) {
|
|
1890
|
+
presetClass = "exit";
|
|
1891
|
+
} else if (a.emphasis) {
|
|
1892
|
+
presetClass = "emph";
|
|
1893
|
+
} else if (a.motionPath) {
|
|
1894
|
+
presetClass = "path";
|
|
1895
|
+
}
|
|
1896
|
+
return {
|
|
1897
|
+
trigger: a.trigger,
|
|
1898
|
+
presetClass,
|
|
1899
|
+
durationMs: a.durationMs,
|
|
1900
|
+
motionPath: a.motionPath
|
|
1901
|
+
};
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
/**
|
|
1905
|
+
* Produces a human-readable summary of a single animation effect.
|
|
1906
|
+
*/
|
|
1907
|
+
summariseAnimation(anim) {
|
|
1908
|
+
const classLabels = {
|
|
1909
|
+
entr: "Entrance",
|
|
1910
|
+
exit: "Exit",
|
|
1911
|
+
emph: "Emphasis",
|
|
1912
|
+
path: "Motion Path"
|
|
1913
|
+
};
|
|
1914
|
+
const classLabel = classLabels[anim.presetClass ?? "entr"] ?? "Effect";
|
|
1915
|
+
const name = anim.presetName ?? (anim.presetId ? `preset ${anim.presetId}` : "effect");
|
|
1916
|
+
const details = [];
|
|
1917
|
+
if (anim.targetId) {
|
|
1918
|
+
details.push(`target: ${anim.targetId}`);
|
|
1919
|
+
}
|
|
1920
|
+
if (typeof anim.durationMs === "number") {
|
|
1921
|
+
details.push(`${anim.durationMs}ms`);
|
|
1922
|
+
}
|
|
1923
|
+
if (typeof anim.delayMs === "number" && anim.delayMs > 0) {
|
|
1924
|
+
details.push(`delay: ${anim.delayMs}ms`);
|
|
1925
|
+
}
|
|
1926
|
+
const trigger = anim.trigger ?? "onClick";
|
|
1927
|
+
if (trigger !== "onClick") {
|
|
1928
|
+
details.push(
|
|
1929
|
+
trigger.replace(/([A-Z])/g, " $1").trim().toLowerCase()
|
|
1930
|
+
);
|
|
1931
|
+
}
|
|
1932
|
+
if (typeof anim.repeatCount === "number" && anim.repeatCount > 1) {
|
|
1933
|
+
details.push(`repeat: ${anim.repeatCount}x`);
|
|
1934
|
+
}
|
|
1935
|
+
if (anim.autoReverse) {
|
|
1936
|
+
details.push("auto-reverse");
|
|
1937
|
+
}
|
|
1938
|
+
if (anim.buildType) {
|
|
1939
|
+
details.push(`build: ${anim.buildType}`);
|
|
1940
|
+
}
|
|
1941
|
+
const suffix = details.length > 0 ? ` (${details.join(", ")})` : "";
|
|
1942
|
+
return `${classLabel}: ${name}${suffix}`;
|
|
1943
|
+
}
|
|
1944
|
+
};
|
|
1945
|
+
|
|
1946
|
+
// src/converter/SlideProcessor.ts
|
|
1947
|
+
var SlideProcessor = class {
|
|
1948
|
+
/**
|
|
1949
|
+
* @param registry - Registry of element-type processors.
|
|
1950
|
+
* @param mediaContext - Shared media extraction context.
|
|
1951
|
+
* @param textRenderer - Renderer for rich-text segments.
|
|
1952
|
+
*/
|
|
1953
|
+
constructor(registry, mediaContext, textRenderer) {
|
|
1954
|
+
this.registry = registry;
|
|
1955
|
+
this.mediaContext = mediaContext;
|
|
1956
|
+
this.textRenderer = textRenderer;
|
|
1957
|
+
this.metadataRenderer = new SlideMetadataRenderer(textRenderer);
|
|
1958
|
+
}
|
|
1959
|
+
/** Delegate for rendering slide-level metadata sections. */
|
|
1960
|
+
metadataRenderer;
|
|
1961
|
+
/** Processes a slide into a complete Markdown section. */
|
|
1962
|
+
async processSlide(slide, options) {
|
|
1963
|
+
const isSemantic = options.semanticMode === true;
|
|
1964
|
+
const title = this.detectTitle(slide);
|
|
1965
|
+
const heading = this.buildHeading(slide, title);
|
|
1966
|
+
const context = {
|
|
1967
|
+
mediaContext: this.mediaContext,
|
|
1968
|
+
slideNumber: slide.slideNumber,
|
|
1969
|
+
slideWidth: options.slideWidth,
|
|
1970
|
+
slideHeight: options.slideHeight,
|
|
1971
|
+
semanticMode: isSemantic,
|
|
1972
|
+
processElements: async (elements) => {
|
|
1973
|
+
const sorted = this.sortElementsByReadingOrder(elements);
|
|
1974
|
+
const output = [];
|
|
1975
|
+
for (const element of sorted) {
|
|
1976
|
+
const rendered = await this.registry.processElement(element, context);
|
|
1977
|
+
if (rendered && rendered.trim().length > 0) {
|
|
1978
|
+
output.push(rendered);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
return output;
|
|
1982
|
+
}
|
|
1983
|
+
};
|
|
1984
|
+
const parts = [heading];
|
|
1985
|
+
const transition = this.metadataRenderer.renderTransition(slide);
|
|
1986
|
+
if (transition) {
|
|
1987
|
+
parts.push(transition);
|
|
1988
|
+
}
|
|
1989
|
+
if (isSemantic) {
|
|
1990
|
+
const elementContent = await this.processElementsSemantic(slide.elements, context);
|
|
1991
|
+
parts.push(...elementContent);
|
|
1992
|
+
} else {
|
|
1993
|
+
const backgroundHtml = await this.renderBackgroundHtml(slide, context);
|
|
1994
|
+
const elementContent = await this.processElementsWithLayout(
|
|
1995
|
+
slide.elements,
|
|
1996
|
+
context,
|
|
1997
|
+
backgroundHtml
|
|
1998
|
+
);
|
|
1999
|
+
parts.push(...elementContent);
|
|
2000
|
+
}
|
|
2001
|
+
const animations = this.metadataRenderer.renderAnimations(slide);
|
|
2002
|
+
if (animations) {
|
|
2003
|
+
parts.push(animations);
|
|
2004
|
+
}
|
|
2005
|
+
const warnings = this.metadataRenderer.renderWarnings(slide);
|
|
2006
|
+
if (warnings) {
|
|
2007
|
+
parts.push(warnings);
|
|
2008
|
+
}
|
|
2009
|
+
const comments = this.metadataRenderer.renderComments(slide);
|
|
2010
|
+
if (comments) {
|
|
2011
|
+
parts.push(comments);
|
|
2012
|
+
}
|
|
2013
|
+
if (options.includeSpeakerNotes) {
|
|
2014
|
+
const notes = this.metadataRenderer.renderNotes(slide);
|
|
2015
|
+
if (notes) {
|
|
2016
|
+
parts.push(notes);
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
return parts.join("\n\n");
|
|
2020
|
+
}
|
|
2021
|
+
/**
|
|
2022
|
+
* Builds the Markdown heading line for a slide, including its number,
|
|
2023
|
+
* detected title text, and flags (hidden, layout name).
|
|
2024
|
+
*/
|
|
2025
|
+
buildHeading(slide, title) {
|
|
2026
|
+
const flags = [];
|
|
2027
|
+
if (slide.hidden) {
|
|
2028
|
+
flags.push("hidden");
|
|
2029
|
+
}
|
|
2030
|
+
if (slide.layoutName) {
|
|
2031
|
+
flags.push(`layout: ${slide.layoutName}`);
|
|
2032
|
+
}
|
|
2033
|
+
const suffix = flags.length > 0 ? ` *(${flags.join(", ")})*` : "";
|
|
2034
|
+
if (title) {
|
|
2035
|
+
return `## Slide ${slide.slideNumber}: ${title}${suffix}`;
|
|
2036
|
+
}
|
|
2037
|
+
return `## Slide ${slide.slideNumber}${suffix}`;
|
|
2038
|
+
}
|
|
2039
|
+
/**
|
|
2040
|
+
* Returns an HTML `<img>` for the slide background that can be
|
|
2041
|
+
* placed as the bottom-most layer inside the positioned container.
|
|
2042
|
+
*/
|
|
2043
|
+
async renderBackgroundHtml(slide, ctx) {
|
|
2044
|
+
if (!slide.backgroundImage) {
|
|
2045
|
+
return void 0;
|
|
2046
|
+
}
|
|
2047
|
+
if (!slide.backgroundImage.startsWith("data:")) {
|
|
2048
|
+
return void 0;
|
|
2049
|
+
}
|
|
2050
|
+
try {
|
|
2051
|
+
const path = await ctx.mediaContext.saveImage(
|
|
2052
|
+
slide.backgroundImage,
|
|
2053
|
+
`slide${slide.slideNumber}-bg`
|
|
2054
|
+
);
|
|
2055
|
+
return `<img src="${path}" alt="Slide background" style="width:100%;height:100%;object-fit:cover">`;
|
|
2056
|
+
} catch {
|
|
2057
|
+
return void 0;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
/** Detects the slide's title from placeholder or first text element. */
|
|
2061
|
+
detectTitle(slide) {
|
|
2062
|
+
for (const element of slide.elements) {
|
|
2063
|
+
const phType = this.getPlaceholderType(element);
|
|
2064
|
+
if (phType === "title" || phType === "ctrTitle" || phType === "subTitle") {
|
|
2065
|
+
if (!hasTextProperties(element)) {
|
|
2066
|
+
continue;
|
|
2067
|
+
}
|
|
2068
|
+
const textFromSegments = element.textSegments ? this.textRenderer.plainText(element.textSegments) : "";
|
|
2069
|
+
const text = (textFromSegments || element.text || "").trim();
|
|
2070
|
+
if (text) {
|
|
2071
|
+
return text.slice(0, 120);
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
const sorted = this.sortElementsByReadingOrder(slide.elements);
|
|
2076
|
+
for (const element of sorted) {
|
|
2077
|
+
if (!hasTextProperties(element)) {
|
|
2078
|
+
continue;
|
|
2079
|
+
}
|
|
2080
|
+
const textFromSegments = element.textSegments ? this.textRenderer.plainText(element.textSegments) : "";
|
|
2081
|
+
const text = (textFromSegments || element.text || "").trim();
|
|
2082
|
+
if (!text) {
|
|
2083
|
+
continue;
|
|
2084
|
+
}
|
|
2085
|
+
return text.slice(0, 120);
|
|
2086
|
+
}
|
|
2087
|
+
return void 0;
|
|
2088
|
+
}
|
|
2089
|
+
/**
|
|
2090
|
+
* Extracts the placeholder type from an element, if present.
|
|
2091
|
+
*/
|
|
2092
|
+
getPlaceholderType(element) {
|
|
2093
|
+
const el = element;
|
|
2094
|
+
return el.placeholderType;
|
|
2095
|
+
}
|
|
2096
|
+
/** Processes elements as clean semantic markdown. */
|
|
2097
|
+
async processElementsSemantic(elements, context) {
|
|
2098
|
+
if (elements.length === 0) {
|
|
2099
|
+
return [];
|
|
2100
|
+
}
|
|
2101
|
+
const sorted = this.sortElementsByReadingOrder(elements);
|
|
2102
|
+
const output = [];
|
|
2103
|
+
for (const elem of sorted) {
|
|
2104
|
+
const rendered = await this.registry.processElement(elem, context);
|
|
2105
|
+
if (rendered?.trim()) {
|
|
2106
|
+
output.push(rendered);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
return output;
|
|
2110
|
+
}
|
|
2111
|
+
/** Processes elements using CSS absolute positioning. */
|
|
2112
|
+
async processElementsWithLayout(elements, context, backgroundHtml) {
|
|
2113
|
+
if (elements.length === 0 && !backgroundHtml) {
|
|
2114
|
+
return [];
|
|
2115
|
+
}
|
|
2116
|
+
const slideW = context.slideWidth || 960;
|
|
2117
|
+
const slideH = context.slideHeight || 540;
|
|
2118
|
+
const maxDisplayW = 960;
|
|
2119
|
+
const scale = slideW > maxDisplayW ? maxDisplayW / slideW : 1;
|
|
2120
|
+
const displayW = Math.round(slideW * scale);
|
|
2121
|
+
const displayH = Math.round(slideH * scale);
|
|
2122
|
+
const sorted = this.sortElementsByReadingOrder(elements);
|
|
2123
|
+
const positionedCells = [];
|
|
2124
|
+
const layoutContext = {
|
|
2125
|
+
...context,
|
|
2126
|
+
layoutScale: scale
|
|
2127
|
+
};
|
|
2128
|
+
if (backgroundHtml) {
|
|
2129
|
+
positionedCells.push(
|
|
2130
|
+
`<div style="position:absolute;left:0;top:0;width:${displayW}px;height:${displayH}px">${backgroundHtml}</div>`
|
|
2131
|
+
);
|
|
2132
|
+
}
|
|
2133
|
+
for (const elem of sorted) {
|
|
2134
|
+
const rendered = await this.registry.processElement(elem, layoutContext);
|
|
2135
|
+
if (!rendered?.trim()) {
|
|
2136
|
+
continue;
|
|
2137
|
+
}
|
|
2138
|
+
const left = Math.round(elem.x * scale);
|
|
2139
|
+
const top = Math.round(elem.y * scale);
|
|
2140
|
+
const w = Math.round(elem.width * scale);
|
|
2141
|
+
const h = Math.round(elem.height * scale);
|
|
2142
|
+
positionedCells.push(
|
|
2143
|
+
`<div style="position:absolute;left:${left}px;top:${top}px;width:${w}px;height:${h}px;overflow:hidden">${rendered}</div>`
|
|
2144
|
+
);
|
|
2145
|
+
}
|
|
2146
|
+
if (positionedCells.length === 0) {
|
|
2147
|
+
return [];
|
|
2148
|
+
}
|
|
2149
|
+
const container = [
|
|
2150
|
+
`<div style="position:relative;width:${displayW}px;height:${displayH}px;border:1px solid #e5e7eb;overflow:hidden;margin:0.5em 0">`,
|
|
2151
|
+
...positionedCells,
|
|
2152
|
+
"</div>"
|
|
2153
|
+
].join("\n");
|
|
2154
|
+
return [container];
|
|
2155
|
+
}
|
|
2156
|
+
/** Sorts elements into natural reading order (top-to-bottom, left-to-right). */
|
|
2157
|
+
sortElementsByReadingOrder(elements) {
|
|
2158
|
+
return [...elements].sort((left, right) => {
|
|
2159
|
+
const yDistance = (left.y ?? 0) - (right.y ?? 0);
|
|
2160
|
+
if (Math.abs(yDistance) > 8) {
|
|
2161
|
+
return yDistance;
|
|
2162
|
+
}
|
|
2163
|
+
return (left.x ?? 0) - (right.x ?? 0);
|
|
2164
|
+
});
|
|
2165
|
+
}
|
|
2166
|
+
};
|
|
2167
|
+
|
|
2168
|
+
// src/converter/ListMarkerHelper.ts
|
|
2169
|
+
function resolveListLevel(bulletInfo, paragraphIndex, paragraphIndents) {
|
|
2170
|
+
const explicitLevel = readNumericProp(bulletInfo, "level");
|
|
2171
|
+
if (typeof explicitLevel === "number") {
|
|
2172
|
+
return Math.max(0, Math.floor(explicitLevel));
|
|
2173
|
+
}
|
|
2174
|
+
const indentInfo = paragraphIndents?.[paragraphIndex];
|
|
2175
|
+
const marginLeft = indentInfo?.marginLeft ?? 0;
|
|
2176
|
+
const indent = indentInfo?.indent ?? 0;
|
|
2177
|
+
const spacingPoints = Math.max(0, marginLeft + Math.max(indent, 0));
|
|
2178
|
+
if (spacingPoints <= 0) {
|
|
2179
|
+
return 0;
|
|
2180
|
+
}
|
|
2181
|
+
return Math.max(0, Math.round(spacingPoints / 24));
|
|
2182
|
+
}
|
|
2183
|
+
function resolveListMarker(bulletInfo, paragraphIndex) {
|
|
2184
|
+
if (bulletInfo.autoNumType) {
|
|
2185
|
+
const startAt = bulletInfo.autoNumStartAt ?? 1;
|
|
2186
|
+
const offset = bulletInfo.paragraphIndex ?? paragraphIndex;
|
|
2187
|
+
const value = Math.max(1, startAt + offset);
|
|
2188
|
+
return formatAutoNumber(value, bulletInfo.autoNumType);
|
|
2189
|
+
}
|
|
2190
|
+
if (bulletInfo.imageRelId || bulletInfo.imageDataUrl) {
|
|
2191
|
+
return "-";
|
|
2192
|
+
}
|
|
2193
|
+
if (bulletInfo.char) {
|
|
2194
|
+
const marker = bulletInfo.char.trim();
|
|
2195
|
+
if (/^[-*+>]$/.test(marker)) {
|
|
2196
|
+
return marker;
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
return "-";
|
|
2200
|
+
}
|
|
2201
|
+
function formatAutoNumber(value, autoNumType) {
|
|
2202
|
+
const normalized = autoNumType.toLowerCase();
|
|
2203
|
+
let token = String(value);
|
|
2204
|
+
if (normalized.includes("roman")) {
|
|
2205
|
+
token = toRoman(value);
|
|
2206
|
+
if (normalized.includes("lc")) {
|
|
2207
|
+
token = token.toLowerCase();
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
if (normalized.includes("alpha")) {
|
|
2211
|
+
token = toAlphabetic(value);
|
|
2212
|
+
if (normalized.includes("uc")) {
|
|
2213
|
+
token = token.toUpperCase();
|
|
2214
|
+
}
|
|
2215
|
+
if (normalized.includes("lc")) {
|
|
2216
|
+
token = token.toLowerCase();
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
if (normalized.includes("parenboth")) {
|
|
2220
|
+
return `(${token})`;
|
|
2221
|
+
}
|
|
2222
|
+
if (normalized.includes("parenr")) {
|
|
2223
|
+
return `${token})`;
|
|
2224
|
+
}
|
|
2225
|
+
if (normalized.includes("minus")) {
|
|
2226
|
+
return `${token}-`;
|
|
2227
|
+
}
|
|
2228
|
+
return `${token}.`;
|
|
2229
|
+
}
|
|
2230
|
+
function toAlphabetic(value) {
|
|
2231
|
+
let remaining = Math.max(1, value);
|
|
2232
|
+
let result = "";
|
|
2233
|
+
while (remaining > 0) {
|
|
2234
|
+
remaining -= 1;
|
|
2235
|
+
result = String.fromCharCode(97 + remaining % 26) + result;
|
|
2236
|
+
remaining = Math.floor(remaining / 26);
|
|
2237
|
+
}
|
|
2238
|
+
return result;
|
|
2239
|
+
}
|
|
2240
|
+
function toRoman(value) {
|
|
2241
|
+
const numerals = [
|
|
2242
|
+
[1e3, "M"],
|
|
2243
|
+
[900, "CM"],
|
|
2244
|
+
[500, "D"],
|
|
2245
|
+
[400, "CD"],
|
|
2246
|
+
[100, "C"],
|
|
2247
|
+
[90, "XC"],
|
|
2248
|
+
[50, "L"],
|
|
2249
|
+
[40, "XL"],
|
|
2250
|
+
[10, "X"],
|
|
2251
|
+
[9, "IX"],
|
|
2252
|
+
[5, "V"],
|
|
2253
|
+
[4, "IV"],
|
|
2254
|
+
[1, "I"]
|
|
2255
|
+
];
|
|
2256
|
+
let remaining = Math.max(1, value);
|
|
2257
|
+
let result = "";
|
|
2258
|
+
for (const [numeric, literal] of numerals) {
|
|
2259
|
+
while (remaining >= numeric) {
|
|
2260
|
+
result += literal;
|
|
2261
|
+
remaining -= numeric;
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
return result;
|
|
2265
|
+
}
|
|
2266
|
+
function readNumericProp(source, key) {
|
|
2267
|
+
if (!source || typeof source !== "object") {
|
|
2268
|
+
return void 0;
|
|
2269
|
+
}
|
|
2270
|
+
const value = source[key];
|
|
2271
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
2272
|
+
return void 0;
|
|
2273
|
+
}
|
|
2274
|
+
return value;
|
|
2275
|
+
}
|
|
2276
|
+
var MONOSPACE_PATTERNS = [
|
|
2277
|
+
"mono",
|
|
2278
|
+
"courier",
|
|
2279
|
+
"consolas",
|
|
2280
|
+
"code",
|
|
2281
|
+
"menlo",
|
|
2282
|
+
"fira",
|
|
2283
|
+
"hack",
|
|
2284
|
+
"inconsolata",
|
|
2285
|
+
"jetbrains",
|
|
2286
|
+
"source code",
|
|
2287
|
+
"cascadia",
|
|
2288
|
+
"sf mono",
|
|
2289
|
+
"roboto mono",
|
|
2290
|
+
"iosevka",
|
|
2291
|
+
"dejavu sans mono",
|
|
2292
|
+
"droid sans mono",
|
|
2293
|
+
"ubuntu mono",
|
|
2294
|
+
"liberation mono",
|
|
2295
|
+
"noto mono",
|
|
2296
|
+
"ibm plex mono",
|
|
2297
|
+
"lucida console",
|
|
2298
|
+
"fixedsys"
|
|
2299
|
+
];
|
|
2300
|
+
function isCodeLikeFont(segment) {
|
|
2301
|
+
if (segment.style.hyperlink) {
|
|
2302
|
+
return false;
|
|
2303
|
+
}
|
|
2304
|
+
const family = segment.style.fontFamily?.toLowerCase() ?? "";
|
|
2305
|
+
return MONOSPACE_PATTERNS.some((pattern) => family.includes(pattern));
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
// src/converter/OmmlLatexConverter.ts
|
|
2309
|
+
var OmmlLatexConverter = class {
|
|
2310
|
+
/** Extracts LaTeX from an OMML XML node tree. */
|
|
2311
|
+
convert(root) {
|
|
2312
|
+
const rendered = this.renderNode(root).replace(/\s+/g, " ").trim();
|
|
2313
|
+
if (rendered) {
|
|
2314
|
+
return rendered;
|
|
2315
|
+
}
|
|
2316
|
+
return this.collectText(root).replace(/\s+/g, " ").trim();
|
|
2317
|
+
}
|
|
2318
|
+
renderNode(node) {
|
|
2319
|
+
if (node === null || node === void 0) {
|
|
2320
|
+
return "";
|
|
2321
|
+
}
|
|
2322
|
+
if (typeof node === "string") {
|
|
2323
|
+
return node;
|
|
2324
|
+
}
|
|
2325
|
+
if (typeof node === "number") {
|
|
2326
|
+
return String(node);
|
|
2327
|
+
}
|
|
2328
|
+
if (Array.isArray(node)) {
|
|
2329
|
+
return node.map((entry) => this.renderNode(entry)).join("");
|
|
2330
|
+
}
|
|
2331
|
+
if (typeof node !== "object") {
|
|
2332
|
+
return "";
|
|
2333
|
+
}
|
|
2334
|
+
const record = node;
|
|
2335
|
+
if (typeof record["#text"] === "string") {
|
|
2336
|
+
return record["#text"];
|
|
2337
|
+
}
|
|
2338
|
+
if (typeof record["m:t"] === "string") {
|
|
2339
|
+
return String(record["m:t"]);
|
|
2340
|
+
}
|
|
2341
|
+
if (record["m:t"]) {
|
|
2342
|
+
return this.renderNode(record["m:t"]);
|
|
2343
|
+
}
|
|
2344
|
+
const handlers = [
|
|
2345
|
+
(r) => this.tryFraction(r),
|
|
2346
|
+
(r) => this.trySuperscript(r),
|
|
2347
|
+
(r) => this.trySubscript(r),
|
|
2348
|
+
(r) => this.trySubSup(r),
|
|
2349
|
+
(r) => this.tryRadical(r),
|
|
2350
|
+
(r) => this.tryNary(r),
|
|
2351
|
+
(r) => this.tryDelimiter(r),
|
|
2352
|
+
(r) => this.tryMatrix(r),
|
|
2353
|
+
(r) => this.tryFunction(r),
|
|
2354
|
+
(r) => this.tryBar(r),
|
|
2355
|
+
(r) => this.tryGroupChar(r),
|
|
2356
|
+
(r) => this.tryLimLow(r),
|
|
2357
|
+
(r) => this.tryLimUpp(r)
|
|
2358
|
+
];
|
|
2359
|
+
for (const handler of handlers) {
|
|
2360
|
+
const result = handler(record);
|
|
2361
|
+
if (result) {
|
|
2362
|
+
return result;
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
const ignored = /* @__PURE__ */ new Set(["@_", "m:rPr", "m:ctrlPr", "m:argPr"]);
|
|
2366
|
+
let output = "";
|
|
2367
|
+
for (const [key, value] of Object.entries(record)) {
|
|
2368
|
+
if (ignored.has(key) || key.startsWith("@_")) {
|
|
2369
|
+
continue;
|
|
2370
|
+
}
|
|
2371
|
+
output += this.renderNode(value);
|
|
2372
|
+
}
|
|
2373
|
+
return output;
|
|
2374
|
+
}
|
|
2375
|
+
tryFraction(node) {
|
|
2376
|
+
const value = node["m:f"];
|
|
2377
|
+
if (!value) {
|
|
2378
|
+
return null;
|
|
2379
|
+
}
|
|
2380
|
+
const f = this.firstRecord(value);
|
|
2381
|
+
if (!f) {
|
|
2382
|
+
return null;
|
|
2383
|
+
}
|
|
2384
|
+
const num = this.renderNode(f["m:num"]).trim();
|
|
2385
|
+
const den = this.renderNode(f["m:den"]).trim();
|
|
2386
|
+
if (!num && !den) {
|
|
2387
|
+
return null;
|
|
2388
|
+
}
|
|
2389
|
+
return `\\frac{${num || " "}}{${den || " "}}`;
|
|
2390
|
+
}
|
|
2391
|
+
trySuperscript(node) {
|
|
2392
|
+
const value = node["m:sSup"];
|
|
2393
|
+
if (!value) {
|
|
2394
|
+
return null;
|
|
2395
|
+
}
|
|
2396
|
+
const sup = this.firstRecord(value);
|
|
2397
|
+
if (!sup) {
|
|
2398
|
+
return null;
|
|
2399
|
+
}
|
|
2400
|
+
const base = this.renderNode(sup["m:e"]).trim();
|
|
2401
|
+
const exp = this.renderNode(sup["m:sup"]).trim();
|
|
2402
|
+
if (!base && !exp) {
|
|
2403
|
+
return null;
|
|
2404
|
+
}
|
|
2405
|
+
return `${base}^{${exp || " "}}`;
|
|
2406
|
+
}
|
|
2407
|
+
trySubscript(node) {
|
|
2408
|
+
const value = node["m:sSub"];
|
|
2409
|
+
if (!value) {
|
|
2410
|
+
return null;
|
|
2411
|
+
}
|
|
2412
|
+
const sub = this.firstRecord(value);
|
|
2413
|
+
if (!sub) {
|
|
2414
|
+
return null;
|
|
2415
|
+
}
|
|
2416
|
+
const base = this.renderNode(sub["m:e"]).trim();
|
|
2417
|
+
const idx = this.renderNode(sub["m:sub"]).trim();
|
|
2418
|
+
if (!base && !idx) {
|
|
2419
|
+
return null;
|
|
2420
|
+
}
|
|
2421
|
+
return `${base}_{${idx || " "}}`;
|
|
2422
|
+
}
|
|
2423
|
+
trySubSup(node) {
|
|
2424
|
+
const value = node["m:sSubSup"];
|
|
2425
|
+
if (!value) {
|
|
2426
|
+
return null;
|
|
2427
|
+
}
|
|
2428
|
+
const ss = this.firstRecord(value);
|
|
2429
|
+
if (!ss) {
|
|
2430
|
+
return null;
|
|
2431
|
+
}
|
|
2432
|
+
const base = this.renderNode(ss["m:e"]).trim();
|
|
2433
|
+
const sub = this.renderNode(ss["m:sub"]).trim();
|
|
2434
|
+
const sup = this.renderNode(ss["m:sup"]).trim();
|
|
2435
|
+
if (!base && !sub && !sup) {
|
|
2436
|
+
return null;
|
|
2437
|
+
}
|
|
2438
|
+
return `${base}_{${sub || " "}}^{${sup || " "}}`;
|
|
2439
|
+
}
|
|
2440
|
+
tryRadical(node) {
|
|
2441
|
+
const value = node["m:rad"];
|
|
2442
|
+
if (!value) {
|
|
2443
|
+
return null;
|
|
2444
|
+
}
|
|
2445
|
+
const rad = this.firstRecord(value);
|
|
2446
|
+
if (!rad) {
|
|
2447
|
+
return null;
|
|
2448
|
+
}
|
|
2449
|
+
const deg = this.renderNode(rad["m:deg"]).trim();
|
|
2450
|
+
const expr = this.renderNode(rad["m:e"]).trim();
|
|
2451
|
+
if (!expr && !deg) {
|
|
2452
|
+
return null;
|
|
2453
|
+
}
|
|
2454
|
+
if (deg) {
|
|
2455
|
+
return `\\sqrt[${deg}]{${expr || " "}}`;
|
|
2456
|
+
}
|
|
2457
|
+
return `\\sqrt{${expr || " "}}`;
|
|
2458
|
+
}
|
|
2459
|
+
tryNary(node) {
|
|
2460
|
+
const value = node["m:nary"];
|
|
2461
|
+
if (!value) {
|
|
2462
|
+
return null;
|
|
2463
|
+
}
|
|
2464
|
+
const nary = this.firstRecord(value);
|
|
2465
|
+
if (!nary) {
|
|
2466
|
+
return null;
|
|
2467
|
+
}
|
|
2468
|
+
const naryPr = this.firstRecord(nary["m:naryPr"]);
|
|
2469
|
+
const chrNode = this.firstRecord(naryPr?.["m:chr"]);
|
|
2470
|
+
const symbol = this.readAttr(chrNode, "val") ?? "\\sum";
|
|
2471
|
+
const lower = this.renderNode(nary["m:sub"]).trim();
|
|
2472
|
+
const upper = this.renderNode(nary["m:sup"]).trim();
|
|
2473
|
+
const expr = this.renderNode(nary["m:e"]).trim();
|
|
2474
|
+
let prefix = symbol;
|
|
2475
|
+
if (lower) {
|
|
2476
|
+
prefix += `_{${lower}}`;
|
|
2477
|
+
}
|
|
2478
|
+
if (upper) {
|
|
2479
|
+
prefix += `^{${upper}}`;
|
|
2480
|
+
}
|
|
2481
|
+
if (!expr) {
|
|
2482
|
+
return prefix;
|
|
2483
|
+
}
|
|
2484
|
+
return `${prefix} ${expr}`;
|
|
2485
|
+
}
|
|
2486
|
+
tryDelimiter(node) {
|
|
2487
|
+
const value = node["m:d"];
|
|
2488
|
+
if (!value) {
|
|
2489
|
+
return null;
|
|
2490
|
+
}
|
|
2491
|
+
const d = this.firstRecord(value);
|
|
2492
|
+
if (!d) {
|
|
2493
|
+
return null;
|
|
2494
|
+
}
|
|
2495
|
+
const dPr = this.firstRecord(d["m:dPr"]);
|
|
2496
|
+
const begin = this.readAttr(this.firstRecord(dPr?.["m:begChr"]), "val") ?? "(";
|
|
2497
|
+
const end = this.readAttr(this.firstRecord(dPr?.["m:endChr"]), "val") ?? ")";
|
|
2498
|
+
const expr = this.renderNode(d["m:e"]).trim();
|
|
2499
|
+
if (!expr) {
|
|
2500
|
+
return null;
|
|
2501
|
+
}
|
|
2502
|
+
return `\\left${begin}${expr}\\right${end}`;
|
|
2503
|
+
}
|
|
2504
|
+
tryMatrix(node) {
|
|
2505
|
+
const value = node["m:m"];
|
|
2506
|
+
if (!value) {
|
|
2507
|
+
return null;
|
|
2508
|
+
}
|
|
2509
|
+
const m = this.firstRecord(value);
|
|
2510
|
+
if (!m) {
|
|
2511
|
+
return null;
|
|
2512
|
+
}
|
|
2513
|
+
const rows = this.toRecordArray(m["m:mr"]).map((row) => {
|
|
2514
|
+
const cells = this.toRecordArray(row["m:e"]).map((entry) => this.renderNode(entry).trim()).filter((entry) => entry.length > 0);
|
|
2515
|
+
return cells.join(" & ");
|
|
2516
|
+
}).filter((row) => row.length > 0);
|
|
2517
|
+
if (rows.length === 0) {
|
|
2518
|
+
return null;
|
|
2519
|
+
}
|
|
2520
|
+
return `\\begin{matrix}${rows.join(" \\\\ ")}\\end{matrix}`;
|
|
2521
|
+
}
|
|
2522
|
+
/** Renders m:func (named functions like sin, cos, lim). */
|
|
2523
|
+
tryFunction(node) {
|
|
2524
|
+
const value = node["m:func"];
|
|
2525
|
+
if (!value) {
|
|
2526
|
+
return null;
|
|
2527
|
+
}
|
|
2528
|
+
const func = this.firstRecord(value);
|
|
2529
|
+
if (!func) {
|
|
2530
|
+
return null;
|
|
2531
|
+
}
|
|
2532
|
+
const name = this.renderNode(func["m:fName"]).trim();
|
|
2533
|
+
const expr = this.renderNode(func["m:e"]).trim();
|
|
2534
|
+
if (!name) {
|
|
2535
|
+
return null;
|
|
2536
|
+
}
|
|
2537
|
+
return `\\${name}{${expr || " "}}`;
|
|
2538
|
+
}
|
|
2539
|
+
/** Renders m:bar (overline / underline). */
|
|
2540
|
+
tryBar(node) {
|
|
2541
|
+
const value = node["m:bar"];
|
|
2542
|
+
if (!value) {
|
|
2543
|
+
return null;
|
|
2544
|
+
}
|
|
2545
|
+
const bar = this.firstRecord(value);
|
|
2546
|
+
if (!bar) {
|
|
2547
|
+
return null;
|
|
2548
|
+
}
|
|
2549
|
+
const barPr = this.firstRecord(bar["m:barPr"]);
|
|
2550
|
+
const pos = this.readAttr(this.firstRecord(barPr?.["m:pos"]), "val");
|
|
2551
|
+
const expr = this.renderNode(bar["m:e"]).trim();
|
|
2552
|
+
if (!expr) {
|
|
2553
|
+
return null;
|
|
2554
|
+
}
|
|
2555
|
+
return pos === "bot" ? `\\underline{${expr}}` : `\\overline{${expr}}`;
|
|
2556
|
+
}
|
|
2557
|
+
/** Renders m:groupChr (brace/bracket above or below expression). */
|
|
2558
|
+
tryGroupChar(node) {
|
|
2559
|
+
const value = node["m:groupChr"];
|
|
2560
|
+
if (!value) {
|
|
2561
|
+
return null;
|
|
2562
|
+
}
|
|
2563
|
+
const gc = this.firstRecord(value);
|
|
2564
|
+
if (!gc) {
|
|
2565
|
+
return null;
|
|
2566
|
+
}
|
|
2567
|
+
const gcPr = this.firstRecord(gc["m:groupChrPr"]);
|
|
2568
|
+
const pos = this.readAttr(this.firstRecord(gcPr?.["m:pos"]), "val");
|
|
2569
|
+
const chr = this.readAttr(this.firstRecord(gcPr?.["m:chr"]), "val") ?? "\u23DF";
|
|
2570
|
+
const expr = this.renderNode(gc["m:e"]).trim();
|
|
2571
|
+
if (!expr) {
|
|
2572
|
+
return null;
|
|
2573
|
+
}
|
|
2574
|
+
if (pos === "top" || chr === "\u23DE") {
|
|
2575
|
+
return `\\overbrace{${expr}}`;
|
|
2576
|
+
}
|
|
2577
|
+
return `\\underbrace{${expr}}`;
|
|
2578
|
+
}
|
|
2579
|
+
/** Renders m:limLow (lower limit, e.g. lim_{x->0}). */
|
|
2580
|
+
tryLimLow(node) {
|
|
2581
|
+
const value = node["m:limLow"];
|
|
2582
|
+
if (!value) {
|
|
2583
|
+
return null;
|
|
2584
|
+
}
|
|
2585
|
+
const ll = this.firstRecord(value);
|
|
2586
|
+
if (!ll) {
|
|
2587
|
+
return null;
|
|
2588
|
+
}
|
|
2589
|
+
const base = this.renderNode(ll["m:e"]).trim();
|
|
2590
|
+
const lim = this.renderNode(ll["m:lim"]).trim();
|
|
2591
|
+
if (!base) {
|
|
2592
|
+
return null;
|
|
2593
|
+
}
|
|
2594
|
+
return lim ? `${base}_{${lim}}` : base;
|
|
2595
|
+
}
|
|
2596
|
+
/** Renders m:limUpp (upper limit). */
|
|
2597
|
+
tryLimUpp(node) {
|
|
2598
|
+
const value = node["m:limUpp"];
|
|
2599
|
+
if (!value) {
|
|
2600
|
+
return null;
|
|
2601
|
+
}
|
|
2602
|
+
const lu = this.firstRecord(value);
|
|
2603
|
+
if (!lu) {
|
|
2604
|
+
return null;
|
|
2605
|
+
}
|
|
2606
|
+
const base = this.renderNode(lu["m:e"]).trim();
|
|
2607
|
+
const lim = this.renderNode(lu["m:lim"]).trim();
|
|
2608
|
+
if (!base) {
|
|
2609
|
+
return null;
|
|
2610
|
+
}
|
|
2611
|
+
return lim ? `${base}^{${lim}}` : base;
|
|
2612
|
+
}
|
|
2613
|
+
collectText(node) {
|
|
2614
|
+
if (node === null || node === void 0) {
|
|
2615
|
+
return "";
|
|
2616
|
+
}
|
|
2617
|
+
if (typeof node === "string" || typeof node === "number") {
|
|
2618
|
+
return String(node);
|
|
2619
|
+
}
|
|
2620
|
+
if (Array.isArray(node)) {
|
|
2621
|
+
return node.map((entry) => this.collectText(entry)).join(" ");
|
|
2622
|
+
}
|
|
2623
|
+
if (typeof node !== "object") {
|
|
2624
|
+
return "";
|
|
2625
|
+
}
|
|
2626
|
+
const record = node;
|
|
2627
|
+
const tokens = [];
|
|
2628
|
+
if (typeof record["m:t"] === "string") {
|
|
2629
|
+
tokens.push(record["m:t"]);
|
|
2630
|
+
}
|
|
2631
|
+
if (typeof record["#text"] === "string") {
|
|
2632
|
+
tokens.push(record["#text"]);
|
|
2633
|
+
}
|
|
2634
|
+
for (const [key, value] of Object.entries(record)) {
|
|
2635
|
+
if (key === "m:t" || key === "#text" || key.startsWith("@_")) {
|
|
2636
|
+
continue;
|
|
2637
|
+
}
|
|
2638
|
+
tokens.push(this.collectText(value));
|
|
2639
|
+
}
|
|
2640
|
+
return tokens.join(" ");
|
|
2641
|
+
}
|
|
2642
|
+
firstRecord(value) {
|
|
2643
|
+
if (Array.isArray(value)) {
|
|
2644
|
+
for (const entry of value) {
|
|
2645
|
+
if (entry && typeof entry === "object" && !Array.isArray(entry)) {
|
|
2646
|
+
return entry;
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
return null;
|
|
2650
|
+
}
|
|
2651
|
+
if (value && typeof value === "object") {
|
|
2652
|
+
return value;
|
|
2653
|
+
}
|
|
2654
|
+
return null;
|
|
2655
|
+
}
|
|
2656
|
+
toRecordArray(value) {
|
|
2657
|
+
if (!value) {
|
|
2658
|
+
return [];
|
|
2659
|
+
}
|
|
2660
|
+
if (Array.isArray(value)) {
|
|
2661
|
+
return value.filter(
|
|
2662
|
+
(entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry)
|
|
2663
|
+
);
|
|
2664
|
+
}
|
|
2665
|
+
if (typeof value === "object") {
|
|
2666
|
+
return [value];
|
|
2667
|
+
}
|
|
2668
|
+
return [];
|
|
2669
|
+
}
|
|
2670
|
+
readAttr(node, key) {
|
|
2671
|
+
if (!node) {
|
|
2672
|
+
return null;
|
|
2673
|
+
}
|
|
2674
|
+
const direct = node[`@_${key}`];
|
|
2675
|
+
if (typeof direct === "string") {
|
|
2676
|
+
return direct;
|
|
2677
|
+
}
|
|
2678
|
+
const fallback = node[key];
|
|
2679
|
+
if (typeof fallback === "string") {
|
|
2680
|
+
return fallback;
|
|
2681
|
+
}
|
|
2682
|
+
return null;
|
|
2683
|
+
}
|
|
2684
|
+
};
|
|
2685
|
+
|
|
2686
|
+
// src/converter/TextSegmentRenderer.ts
|
|
2687
|
+
var TextSegmentRenderer = class {
|
|
2688
|
+
ommlConverter = new OmmlLatexConverter();
|
|
2689
|
+
render(segments, options = {}) {
|
|
2690
|
+
if (segments.length === 0) {
|
|
2691
|
+
return "";
|
|
2692
|
+
}
|
|
2693
|
+
const paragraphs = this.groupParagraphs(segments);
|
|
2694
|
+
const renderedParagraphs = paragraphs.map((group) => this.renderParagraph(group, options)).filter((paragraph) => paragraph.text.length > 0);
|
|
2695
|
+
if (renderedParagraphs.length === 0) {
|
|
2696
|
+
return "";
|
|
2697
|
+
}
|
|
2698
|
+
if (options.inlineMode) {
|
|
2699
|
+
return renderedParagraphs.map((entry) => entry.text).join("<br />");
|
|
2700
|
+
}
|
|
2701
|
+
if (options.htmlFormatting) {
|
|
2702
|
+
return renderedParagraphs.map((entry) => entry.text).join("<br>");
|
|
2703
|
+
}
|
|
2704
|
+
const output = [];
|
|
2705
|
+
for (let index = 0; index < renderedParagraphs.length; index += 1) {
|
|
2706
|
+
if (index > 0) {
|
|
2707
|
+
const previous = renderedParagraphs[index - 1];
|
|
2708
|
+
const current = renderedParagraphs[index];
|
|
2709
|
+
output.push(previous.isListItem && current.isListItem ? "\n" : "\n\n");
|
|
2710
|
+
}
|
|
2711
|
+
output.push(renderedParagraphs[index].text);
|
|
2712
|
+
}
|
|
2713
|
+
return output.join("");
|
|
2714
|
+
}
|
|
2715
|
+
renderInline(segments, options = {}) {
|
|
2716
|
+
return this.render(segments, {
|
|
2717
|
+
...options,
|
|
2718
|
+
inlineMode: true,
|
|
2719
|
+
disableLists: true,
|
|
2720
|
+
disableAlignment: true
|
|
2721
|
+
});
|
|
2722
|
+
}
|
|
2723
|
+
plainText(segments, options = {}) {
|
|
2724
|
+
const paragraphs = this.groupParagraphs(segments);
|
|
2725
|
+
const rendered = paragraphs.map(
|
|
2726
|
+
(group) => group.segments.map((segment) => {
|
|
2727
|
+
const base = this.resolvePlainSegmentText(segment, options);
|
|
2728
|
+
if (segment.rubyText) {
|
|
2729
|
+
return `${base}(${segment.rubyText})`;
|
|
2730
|
+
}
|
|
2731
|
+
return base;
|
|
2732
|
+
}).join("").trim()
|
|
2733
|
+
).filter((text) => text.length > 0);
|
|
2734
|
+
return rendered.join(" ").trim();
|
|
2735
|
+
}
|
|
2736
|
+
groupParagraphs(segments) {
|
|
2737
|
+
const groups = [];
|
|
2738
|
+
let buffer = [];
|
|
2739
|
+
let paragraphIndex = 0;
|
|
2740
|
+
for (const segment of segments) {
|
|
2741
|
+
if (segment.isParagraphBreak) {
|
|
2742
|
+
if (buffer.length > 0) {
|
|
2743
|
+
groups.push({ index: paragraphIndex, segments: buffer });
|
|
2744
|
+
buffer = [];
|
|
2745
|
+
}
|
|
2746
|
+
paragraphIndex += 1;
|
|
2747
|
+
continue;
|
|
2748
|
+
}
|
|
2749
|
+
buffer.push(segment);
|
|
2750
|
+
}
|
|
2751
|
+
if (buffer.length > 0) {
|
|
2752
|
+
groups.push({ index: paragraphIndex, segments: buffer });
|
|
2753
|
+
}
|
|
2754
|
+
return groups;
|
|
2755
|
+
}
|
|
2756
|
+
renderParagraph(group, options) {
|
|
2757
|
+
let rawText = group.segments.map((segment) => this.renderSegment(segment, options)).join("");
|
|
2758
|
+
if (options.htmlFormatting) {
|
|
2759
|
+
rawText = rawText.replace(/<\/strong><strong>/g, "").replace(/<\/em><em>/g, "").replace(/<\/s><s>/g, "");
|
|
2760
|
+
} else {
|
|
2761
|
+
rawText = rawText.replace(/\*\*\*\*/g, "").replace(/~~~~/g, "");
|
|
2762
|
+
}
|
|
2763
|
+
if (options.htmlFormatting) {
|
|
2764
|
+
rawText = rawText.replace(/\n/g, "<br>");
|
|
2765
|
+
}
|
|
2766
|
+
rawText = rawText.trim();
|
|
2767
|
+
if (!rawText) {
|
|
2768
|
+
return { text: "", isListItem: false };
|
|
2769
|
+
}
|
|
2770
|
+
if (!options.disableLists && !options.inlineMode) {
|
|
2771
|
+
const bullet = group.segments.find((segment) => Boolean(segment.bulletInfo))?.bulletInfo;
|
|
2772
|
+
if (bullet && !bullet.none) {
|
|
2773
|
+
const level = resolveListLevel(bullet, group.index, options.paragraphIndents);
|
|
2774
|
+
const marker = resolveListMarker(bullet, group.index);
|
|
2775
|
+
const alreadyNumbered = bullet.autoNumType && /^\d+[.)]\s/.test(rawText);
|
|
2776
|
+
const prefix = alreadyNumbered ? "" : `${marker} `;
|
|
2777
|
+
if (options.htmlFormatting) {
|
|
2778
|
+
const pad = level * 1.5;
|
|
2779
|
+
return {
|
|
2780
|
+
text: `<div style="padding-left:${pad}em">${prefix}${rawText}</div>`,
|
|
2781
|
+
isListItem: true
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
return {
|
|
2785
|
+
text: `${" ".repeat(level)}${prefix}${rawText}`,
|
|
2786
|
+
isListItem: true
|
|
2787
|
+
};
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
const align = group.segments[0]?.style.align;
|
|
2791
|
+
if (!options.disableAlignment && !options.inlineMode && align && align !== "left") {
|
|
2792
|
+
return {
|
|
2793
|
+
text: `<p align="${align}">${rawText}</p>`,
|
|
2794
|
+
isListItem: false
|
|
2795
|
+
};
|
|
2796
|
+
}
|
|
2797
|
+
return { text: rawText, isListItem: false };
|
|
2798
|
+
}
|
|
2799
|
+
renderSegment(segment, options) {
|
|
2800
|
+
const plain = this.resolvePlainSegmentText(segment, options);
|
|
2801
|
+
if (!plain) {
|
|
2802
|
+
return "";
|
|
2803
|
+
}
|
|
2804
|
+
if (segment.equationXml) {
|
|
2805
|
+
const normalized = plain.trim() || "[equation]";
|
|
2806
|
+
return `$${normalized}$`;
|
|
2807
|
+
}
|
|
2808
|
+
const useHtml = options.htmlFormatting ?? false;
|
|
2809
|
+
let text = useHtml ? this.escapeHtml(plain) : this.escapeMarkdown(plain);
|
|
2810
|
+
if (segment.rubyText) {
|
|
2811
|
+
const rt = useHtml ? this.escapeHtml(segment.rubyText) : segment.rubyText;
|
|
2812
|
+
if (useHtml) {
|
|
2813
|
+
text = `<ruby>${text}<rp>(</rp><rt>${rt}</rt><rp>)</rp></ruby>`;
|
|
2814
|
+
} else {
|
|
2815
|
+
text = `${text}(${rt})`;
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
if (segment.style.textCaps === "all") {
|
|
2819
|
+
text = text.toUpperCase();
|
|
2820
|
+
}
|
|
2821
|
+
if (!useHtml && isCodeLikeFont(segment)) {
|
|
2822
|
+
text = this.wrapCode(text);
|
|
2823
|
+
}
|
|
2824
|
+
if (segment.style.strikethrough) {
|
|
2825
|
+
text = useHtml ? `<s>${text}</s>` : `~~${text}~~`;
|
|
2826
|
+
}
|
|
2827
|
+
if (segment.style.bold && segment.style.italic) {
|
|
2828
|
+
text = useHtml ? `<strong><em>${text}</em></strong>` : `***${text}***`;
|
|
2829
|
+
} else {
|
|
2830
|
+
if (segment.style.bold) {
|
|
2831
|
+
text = useHtml ? `<strong>${text}</strong>` : `**${text}**`;
|
|
2832
|
+
}
|
|
2833
|
+
if (segment.style.italic) {
|
|
2834
|
+
text = useHtml ? `<em>${text}</em>` : `*${text}*`;
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
if (segment.style.underline) {
|
|
2838
|
+
text = `<u>${text}</u>`;
|
|
2839
|
+
}
|
|
2840
|
+
if (segment.style.baseline && segment.style.baseline > 0) {
|
|
2841
|
+
text = `<sup>${text}</sup>`;
|
|
2842
|
+
}
|
|
2843
|
+
if (segment.style.baseline && segment.style.baseline < 0) {
|
|
2844
|
+
text = `<sub>${text}</sub>`;
|
|
2845
|
+
}
|
|
2846
|
+
if (segment.style.textCaps === "small") {
|
|
2847
|
+
text = `<span style="font-variant:small-caps">${text}</span>`;
|
|
2848
|
+
}
|
|
2849
|
+
if (segment.style.hyperlink) {
|
|
2850
|
+
const dest = this.renderLinkDestination(segment.style.hyperlink);
|
|
2851
|
+
if (useHtml) {
|
|
2852
|
+
text = `<a href="${dest}">${text}</a>`;
|
|
2853
|
+
} else {
|
|
2854
|
+
const title = segment.style.hyperlinkTooltip ? ` "${this.escapeLinkTitle(segment.style.hyperlinkTooltip)}"` : "";
|
|
2855
|
+
text = `[${text}](${dest}${title})`;
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
if (segment.style.rtl) {
|
|
2859
|
+
text = `\u200F${text}`;
|
|
2860
|
+
}
|
|
2861
|
+
return text;
|
|
2862
|
+
}
|
|
2863
|
+
resolvePlainSegmentText(segment, options) {
|
|
2864
|
+
if (segment.fieldType) {
|
|
2865
|
+
return this.resolveFieldSegment(segment.fieldType, segment.text, options);
|
|
2866
|
+
}
|
|
2867
|
+
if (segment.equationXml) {
|
|
2868
|
+
const fromXml = this.ommlConverter.convert(segment.equationXml).trim();
|
|
2869
|
+
if (fromXml) {
|
|
2870
|
+
return fromXml;
|
|
2871
|
+
}
|
|
2872
|
+
if (segment.text.trim()) {
|
|
2873
|
+
return segment.text.trim();
|
|
2874
|
+
}
|
|
2875
|
+
return "[equation]";
|
|
2876
|
+
}
|
|
2877
|
+
return segment.text;
|
|
2878
|
+
}
|
|
2879
|
+
resolveFieldSegment(fieldType, defaultText, options) {
|
|
2880
|
+
const normalized = fieldType.trim().toLowerCase();
|
|
2881
|
+
if (normalized.includes("slidenum")) {
|
|
2882
|
+
if (typeof options.slideNumber === "number") {
|
|
2883
|
+
return String(options.slideNumber);
|
|
2884
|
+
}
|
|
2885
|
+
return defaultText;
|
|
2886
|
+
}
|
|
2887
|
+
if (normalized.includes("datetime") || normalized.includes("date")) {
|
|
2888
|
+
if (options.dateTimeText) {
|
|
2889
|
+
return options.dateTimeText;
|
|
2890
|
+
}
|
|
2891
|
+
return defaultText;
|
|
2892
|
+
}
|
|
2893
|
+
return defaultText;
|
|
2894
|
+
}
|
|
2895
|
+
wrapCode(text) {
|
|
2896
|
+
const matches = text.match(/`+/g);
|
|
2897
|
+
const maxTicks = matches && matches.length > 0 ? Math.max(...matches.map((entry) => entry.length)) : 0;
|
|
2898
|
+
const fence = "`".repeat(Math.max(1, maxTicks + 1));
|
|
2899
|
+
return `${fence}${text}${fence}`;
|
|
2900
|
+
}
|
|
2901
|
+
renderLinkDestination(href) {
|
|
2902
|
+
if (/\s/.test(href) || href.includes(")")) {
|
|
2903
|
+
return `<${href}>`;
|
|
2904
|
+
}
|
|
2905
|
+
return href;
|
|
2906
|
+
}
|
|
2907
|
+
escapeLinkTitle(value) {
|
|
2908
|
+
return value.replace(/"/g, """);
|
|
2909
|
+
}
|
|
2910
|
+
escapeMarkdown(text) {
|
|
2911
|
+
return text.replace(/([\\`*_{}[\]|])/g, "\\$1");
|
|
2912
|
+
}
|
|
2913
|
+
escapeHtml(text) {
|
|
2914
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2915
|
+
}
|
|
2916
|
+
};
|
|
2917
|
+
|
|
2918
|
+
// src/converter/PptxMarkdownConverter.ts
|
|
2919
|
+
var PptxMarkdownConverter = class extends DocumentConverter {
|
|
2920
|
+
/** Renderer for rich-text segments (bold, italic, hyperlinks, etc.). */
|
|
2921
|
+
textRenderer;
|
|
2922
|
+
/** Registry mapping element types to their dedicated processors. */
|
|
2923
|
+
registry;
|
|
2924
|
+
/** Delegate responsible for converting individual slides. */
|
|
2925
|
+
slideProcessor;
|
|
2926
|
+
/** Number of slides actually converted (after applying the slide range filter). */
|
|
2927
|
+
convertedSlides = 0;
|
|
2928
|
+
/** Total number of slides in the source presentation. */
|
|
2929
|
+
totalSlides = 0;
|
|
2930
|
+
/**
|
|
2931
|
+
* @param outputDir - Root directory for output files.
|
|
2932
|
+
* @param options - PPTX-specific conversion options.
|
|
2933
|
+
* @param fs - Optional file system adapter for writing media to disk.
|
|
2934
|
+
*/
|
|
2935
|
+
constructor(outputDir, options, fs) {
|
|
2936
|
+
super(outputDir, options, fs);
|
|
2937
|
+
this.textRenderer = new TextSegmentRenderer();
|
|
2938
|
+
this.registry = new ElementProcessorRegistry();
|
|
2939
|
+
this.registerProcessors();
|
|
2940
|
+
this.slideProcessor = new SlideProcessor(this.registry, this.mediaContext, this.textRenderer);
|
|
2941
|
+
}
|
|
2942
|
+
/** Returns the number of images extracted and saved during conversion. */
|
|
2943
|
+
get imagesExtracted() {
|
|
2944
|
+
return this.mediaContext.totalImages;
|
|
2945
|
+
}
|
|
2946
|
+
/** Returns the media directory path, or `null` if no images were extracted. */
|
|
2947
|
+
get mediaDir() {
|
|
2948
|
+
return this.mediaContext.totalImages > 0 ? this.mediaContext.mediaDir : null;
|
|
2949
|
+
}
|
|
2950
|
+
/** Returns the number of slides that were actually converted. */
|
|
2951
|
+
get slidesConverted() {
|
|
2952
|
+
return this.convertedSlides;
|
|
2953
|
+
}
|
|
2954
|
+
/** Returns the total number of slides in the source presentation. */
|
|
2955
|
+
get presentationSlides() {
|
|
2956
|
+
return this.totalSlides;
|
|
2957
|
+
}
|
|
2958
|
+
/**
|
|
2959
|
+
* Converts the full PPTX presentation (or a slide subset) into Markdown.
|
|
2960
|
+
*
|
|
2961
|
+
* The conversion pipeline:
|
|
2962
|
+
* 1. Resolves the slide range and selects the target slides.
|
|
2963
|
+
* 2. Processes each slide through {@link SlideProcessor}, producing markdown sections.
|
|
2964
|
+
* 3. Inserts section headings when slides belong to named sections.
|
|
2965
|
+
* 4. Optionally prepends YAML front-matter with document metadata.
|
|
2966
|
+
* 5. Appends header/footer information if present.
|
|
2967
|
+
* 6. Writes the output file if an `outputPath` was configured.
|
|
2968
|
+
*
|
|
2969
|
+
* @param source - The parsed PPTX data structure.
|
|
2970
|
+
* @returns The complete Markdown string.
|
|
2971
|
+
*/
|
|
2972
|
+
async convert(source) {
|
|
2973
|
+
this.totalSlides = source.slides.length;
|
|
2974
|
+
const { start, end } = this.resolveRange(source.slides.length);
|
|
2975
|
+
const selectedSlides = source.slides.slice(start - 1, end);
|
|
2976
|
+
this.convertedSlides = selectedSlides.length;
|
|
2977
|
+
const slideSections = [];
|
|
2978
|
+
let currentSection;
|
|
2979
|
+
for (const slide of selectedSlides) {
|
|
2980
|
+
const sectionLabel = slide.sectionName ?? slide.sectionId;
|
|
2981
|
+
if (sectionLabel && sectionLabel !== currentSection) {
|
|
2982
|
+
currentSection = sectionLabel;
|
|
2983
|
+
slideSections.push(`# ${sectionLabel}`);
|
|
2984
|
+
}
|
|
2985
|
+
slideSections.push(
|
|
2986
|
+
await this.slideProcessor.processSlide(slide, {
|
|
2987
|
+
includeSpeakerNotes: this.getOptions().includeSpeakerNotes,
|
|
2988
|
+
slideWidth: source.width || 960,
|
|
2989
|
+
slideHeight: source.height || 540,
|
|
2990
|
+
semanticMode: this.getOptions().semanticMode
|
|
2991
|
+
})
|
|
2992
|
+
);
|
|
2993
|
+
}
|
|
2994
|
+
let markdown = slideSections.join("\n\n---\n\n");
|
|
2995
|
+
if (this.options.includeMetadata) {
|
|
2996
|
+
const frontMatter = this.buildPptxFrontMatter(source);
|
|
2997
|
+
markdown = `${frontMatter}${markdown}`;
|
|
2998
|
+
}
|
|
2999
|
+
const footer = this.renderHeaderFooter(source.headerFooter);
|
|
3000
|
+
if (footer) {
|
|
3001
|
+
markdown = `${markdown}
|
|
3002
|
+
|
|
3003
|
+
---
|
|
3004
|
+
|
|
3005
|
+
${footer}`;
|
|
3006
|
+
}
|
|
3007
|
+
markdown = markdown.endsWith("\n") ? markdown : `${markdown}
|
|
3008
|
+
`;
|
|
3009
|
+
if (this.options.outputPath) {
|
|
3010
|
+
await this.writeOutput(markdown, this.options.outputPath);
|
|
3011
|
+
}
|
|
3012
|
+
return markdown;
|
|
3013
|
+
}
|
|
3014
|
+
/**
|
|
3015
|
+
* Registers all element-type processors into the internal registry.
|
|
3016
|
+
* Each processor handles one or more {@link PptxElement} types
|
|
3017
|
+
* (text, image, table, chart, etc.).
|
|
3018
|
+
*/
|
|
3019
|
+
registerProcessors() {
|
|
3020
|
+
this.registry.register(new TextElementProcessor(this.textRenderer));
|
|
3021
|
+
this.registry.register(new ImageElementProcessor());
|
|
3022
|
+
this.registry.register(new TableElementProcessor());
|
|
3023
|
+
this.registry.register(new ChartElementProcessor());
|
|
3024
|
+
this.registry.register(new SmartArtElementProcessor());
|
|
3025
|
+
this.registry.register(new GroupElementProcessor());
|
|
3026
|
+
this.registry.register(new MediaElementProcessor());
|
|
3027
|
+
this.registry.register(new OleElementProcessor());
|
|
3028
|
+
this.registry.register(new InkElementProcessor());
|
|
3029
|
+
this.registry.register(new FallbackElementProcessor());
|
|
3030
|
+
}
|
|
3031
|
+
/**
|
|
3032
|
+
* Assembles a YAML front-matter block containing presentation metadata
|
|
3033
|
+
* such as title, author, slide count, theme, dimensions, and more.
|
|
3034
|
+
*
|
|
3035
|
+
* @param source - The parsed PPTX data.
|
|
3036
|
+
* @returns A YAML front-matter string (including `---` delimiters).
|
|
3037
|
+
*/
|
|
3038
|
+
buildPptxFrontMatter(source) {
|
|
3039
|
+
const meta = {
|
|
3040
|
+
source: this.getOptions().sourceName,
|
|
3041
|
+
format: "pptx",
|
|
3042
|
+
slides: source.slides.length,
|
|
3043
|
+
converted: (/* @__PURE__ */ new Date()).toISOString()
|
|
3044
|
+
};
|
|
3045
|
+
this.addCoreProperties(meta, source.coreProperties);
|
|
3046
|
+
this.addAppProperties(meta, source.appProperties);
|
|
3047
|
+
if (source.width && source.height) {
|
|
3048
|
+
meta.dimensions = `${source.width}x${source.height}`;
|
|
3049
|
+
}
|
|
3050
|
+
if (source.sections && source.sections.length > 0) {
|
|
3051
|
+
meta.sections = source.sections.map((s) => s.name).join(", ");
|
|
3052
|
+
}
|
|
3053
|
+
this.addPresentationMeta(meta, source);
|
|
3054
|
+
return this.buildFrontMatter(meta);
|
|
3055
|
+
}
|
|
3056
|
+
/**
|
|
3057
|
+
* Populates metadata entries from OPC core properties (title, author, subject, etc.).
|
|
3058
|
+
*
|
|
3059
|
+
* @param meta - The metadata dictionary to populate.
|
|
3060
|
+
* @param props - Core document properties from the PPTX file.
|
|
3061
|
+
*/
|
|
3062
|
+
addCoreProperties(meta, props) {
|
|
3063
|
+
if (!props) {
|
|
3064
|
+
return;
|
|
3065
|
+
}
|
|
3066
|
+
if (props.title) {
|
|
3067
|
+
meta.title = props.title;
|
|
3068
|
+
}
|
|
3069
|
+
if (props.creator) {
|
|
3070
|
+
meta.author = props.creator;
|
|
3071
|
+
}
|
|
3072
|
+
if (props.subject) {
|
|
3073
|
+
meta.subject = props.subject;
|
|
3074
|
+
}
|
|
3075
|
+
if (props.description) {
|
|
3076
|
+
meta.description = props.description;
|
|
3077
|
+
}
|
|
3078
|
+
if (props.category) {
|
|
3079
|
+
meta.category = props.category;
|
|
3080
|
+
}
|
|
3081
|
+
if (props.lastModifiedBy) {
|
|
3082
|
+
meta.lastModifiedBy = props.lastModifiedBy;
|
|
3083
|
+
}
|
|
3084
|
+
if (props.revision) {
|
|
3085
|
+
meta.revision = props.revision;
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
/**
|
|
3089
|
+
* Populates metadata entries from application-level properties
|
|
3090
|
+
* (application name, editing time, word/paragraph counts).
|
|
3091
|
+
*
|
|
3092
|
+
* @param meta - The metadata dictionary to populate.
|
|
3093
|
+
* @param props - Application properties from the PPTX file.
|
|
3094
|
+
*/
|
|
3095
|
+
addAppProperties(meta, props) {
|
|
3096
|
+
if (!props) {
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
3099
|
+
if (props.application) {
|
|
3100
|
+
meta.application = props.application;
|
|
3101
|
+
}
|
|
3102
|
+
if (typeof props.totalTime === "number") {
|
|
3103
|
+
meta.editingMinutes = props.totalTime;
|
|
3104
|
+
}
|
|
3105
|
+
if (typeof props.words === "number") {
|
|
3106
|
+
meta.words = props.words;
|
|
3107
|
+
}
|
|
3108
|
+
if (typeof props.paragraphs === "number") {
|
|
3109
|
+
meta.paragraphs = props.paragraphs;
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
/**
|
|
3113
|
+
* Adds presentation-level metadata (custom properties, show type, theme,
|
|
3114
|
+
* security warnings, embedded fonts, custom shows) to the front-matter dictionary.
|
|
3115
|
+
*
|
|
3116
|
+
* @param meta - The metadata dictionary to populate.
|
|
3117
|
+
* @param source - The full parsed PPTX data.
|
|
3118
|
+
*/
|
|
3119
|
+
addPresentationMeta(meta, source) {
|
|
3120
|
+
if (source.customProperties?.length) {
|
|
3121
|
+
meta.customProperties = source.customProperties.map((p) => `${p.name}=${p.value}`).join(", ");
|
|
3122
|
+
}
|
|
3123
|
+
const pp = source.presentationProperties;
|
|
3124
|
+
if (pp) {
|
|
3125
|
+
if (pp.showType) {
|
|
3126
|
+
meta.showType = pp.showType;
|
|
3127
|
+
}
|
|
3128
|
+
if (pp.loopContinuously) {
|
|
3129
|
+
meta.loopContinuously = "true";
|
|
3130
|
+
}
|
|
3131
|
+
if (pp.advanceMode) {
|
|
3132
|
+
meta.advanceMode = pp.advanceMode;
|
|
3133
|
+
}
|
|
3134
|
+
if (pp.showWithNarration) {
|
|
3135
|
+
meta.narration = "enabled";
|
|
3136
|
+
}
|
|
3137
|
+
if (pp.showWithAnimation === false) {
|
|
3138
|
+
meta.animation = "disabled";
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
if (source.theme?.name) {
|
|
3142
|
+
meta.theme = source.theme.name;
|
|
3143
|
+
if (source.theme.fontScheme) {
|
|
3144
|
+
const major = source.theme.fontScheme.majorFont?.latin;
|
|
3145
|
+
const minor = source.theme.fontScheme.minorFont?.latin;
|
|
3146
|
+
if (major || minor) {
|
|
3147
|
+
meta.fonts = [major, minor].filter(Boolean).join(", ");
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
if (source.isPasswordProtected) {
|
|
3152
|
+
meta.warning_passwordProtected = "\u26A0 yes";
|
|
3153
|
+
}
|
|
3154
|
+
if (source.hasMacros) {
|
|
3155
|
+
meta.warning_macros = "\u26A0 yes";
|
|
3156
|
+
}
|
|
3157
|
+
if (source.embeddedFonts?.length) {
|
|
3158
|
+
meta.embeddedFonts = source.embeddedFonts.map((f) => f.name).join(", ");
|
|
3159
|
+
}
|
|
3160
|
+
if (source.customShows?.length) {
|
|
3161
|
+
meta.customShows = source.customShows.map((s) => s.name).join(", ");
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
/**
|
|
3165
|
+
* Renders the presentation-level header/footer settings (header text,
|
|
3166
|
+
* footer text, date/time, slide numbers) into a pipe-delimited markdown string.
|
|
3167
|
+
*
|
|
3168
|
+
* @param hf - Header/footer configuration from the presentation.
|
|
3169
|
+
* @returns A formatted string, or an empty string if no header/footer data is present.
|
|
3170
|
+
*/
|
|
3171
|
+
renderHeaderFooter(hf) {
|
|
3172
|
+
if (!hf) {
|
|
3173
|
+
return "";
|
|
3174
|
+
}
|
|
3175
|
+
const parts = [];
|
|
3176
|
+
if (hf.hasHeader && hf.headerText) {
|
|
3177
|
+
parts.push(`**Header:** ${hf.headerText}`);
|
|
3178
|
+
}
|
|
3179
|
+
if (hf.hasFooter && hf.footerText) {
|
|
3180
|
+
parts.push(`**Footer:** ${hf.footerText}`);
|
|
3181
|
+
}
|
|
3182
|
+
if (hf.hasDateTime && hf.dateTimeText) {
|
|
3183
|
+
parts.push(`**Date/Time:** ${hf.dateTimeText}`);
|
|
3184
|
+
}
|
|
3185
|
+
if (hf.hasSlideNumber) {
|
|
3186
|
+
parts.push("**Slide Numbers:** enabled");
|
|
3187
|
+
}
|
|
3188
|
+
if (hf.dateTimeAuto && hf.dateFormat) {
|
|
3189
|
+
parts.push(`**Date Format:** ${hf.dateFormat}`);
|
|
3190
|
+
}
|
|
3191
|
+
return parts.join(" | ");
|
|
3192
|
+
}
|
|
3193
|
+
/**
|
|
3194
|
+
* Resolves and clamps the user-specified slide range to valid bounds.
|
|
3195
|
+
* Defaults to the full deck if no range is configured.
|
|
3196
|
+
*
|
|
3197
|
+
* @param totalSlides - Total number of slides in the presentation.
|
|
3198
|
+
* @returns A 1-based `{ start, end }` range guaranteed to be within bounds.
|
|
3199
|
+
*/
|
|
3200
|
+
resolveRange(totalSlides) {
|
|
3201
|
+
const range = this.getOptions().slideRange;
|
|
3202
|
+
const start = Math.max(1, Math.min(range?.start ?? 1, totalSlides));
|
|
3203
|
+
const end = Math.max(start, Math.min(range?.end ?? totalSlides, totalSlides));
|
|
3204
|
+
return { start, end };
|
|
3205
|
+
}
|
|
3206
|
+
/** Casts the base `options` to the PPTX-specific options type. */
|
|
3207
|
+
getOptions() {
|
|
3208
|
+
return this.options;
|
|
3209
|
+
}
|
|
3210
|
+
};
|
|
3211
|
+
|
|
3212
|
+
// src/converter/SvgExporter.ts
|
|
3213
|
+
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
3214
|
+
var XLINK_NS = "http://www.w3.org/1999/xlink";
|
|
3215
|
+
function escXml(s) {
|
|
3216
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
3217
|
+
}
|
|
3218
|
+
function attrs(map) {
|
|
3219
|
+
let out = "";
|
|
3220
|
+
for (const [k, v] of Object.entries(map)) {
|
|
3221
|
+
if (v !== void 0 && v !== "") {
|
|
3222
|
+
out += ` ${k}="${escXml(String(v))}"`;
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
return out;
|
|
3226
|
+
}
|
|
3227
|
+
function resolveDefaults(opts) {
|
|
3228
|
+
return {
|
|
3229
|
+
defaultFontFamily: opts?.defaultFontFamily ?? "Arial",
|
|
3230
|
+
defaultFontSize: opts?.defaultFontSize ?? 18
|
|
3231
|
+
};
|
|
3232
|
+
}
|
|
3233
|
+
var _markerIdCounter = 0;
|
|
3234
|
+
function arrowMarkerDef(color) {
|
|
3235
|
+
const id = `arrow_${++_markerIdCounter}`;
|
|
3236
|
+
const svg = `<marker id="${id}" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto" markerUnits="strokeWidth"><polygon points="0 0, 10 3.5, 0 7" fill="${escXml(color)}" /></marker>`;
|
|
3237
|
+
return { id, svg };
|
|
3238
|
+
}
|
|
3239
|
+
function renderTransform(el) {
|
|
3240
|
+
const parts = [];
|
|
3241
|
+
parts.push(`translate(${el.x},${el.y})`);
|
|
3242
|
+
if (el.rotation) {
|
|
3243
|
+
parts.push(`rotate(${el.rotation},${el.width / 2},${el.height / 2})`);
|
|
3244
|
+
}
|
|
3245
|
+
const flipX = "flipHorizontal" in el && el.flipHorizontal;
|
|
3246
|
+
const flipY = "flipVertical" in el && el.flipVertical;
|
|
3247
|
+
if (flipX || flipY) {
|
|
3248
|
+
parts.push(`translate(${flipX ? el.width : 0},${flipY ? el.height : 0})`);
|
|
3249
|
+
parts.push(`scale(${flipX ? -1 : 1},${flipY ? -1 : 1})`);
|
|
3250
|
+
}
|
|
3251
|
+
return parts.join(" ");
|
|
3252
|
+
}
|
|
3253
|
+
function renderText(el, defaults) {
|
|
3254
|
+
if (el.type !== "text" && el.type !== "shape" && el.type !== "connector") {
|
|
3255
|
+
return "";
|
|
3256
|
+
}
|
|
3257
|
+
const text = el.text;
|
|
3258
|
+
if (!text) {
|
|
3259
|
+
return "";
|
|
3260
|
+
}
|
|
3261
|
+
const segments = el.textSegments;
|
|
3262
|
+
const style = el.textStyle;
|
|
3263
|
+
const fontFamily = style?.fontFamily ?? defaults.defaultFontFamily;
|
|
3264
|
+
const fontSize = style?.fontSize ?? defaults.defaultFontSize;
|
|
3265
|
+
const color = style?.color ?? "#000000";
|
|
3266
|
+
const align = style?.align ?? "left";
|
|
3267
|
+
let textAnchor = "start";
|
|
3268
|
+
let textX = 4;
|
|
3269
|
+
if (align === "center") {
|
|
3270
|
+
textAnchor = "middle";
|
|
3271
|
+
textX = el.width / 2;
|
|
3272
|
+
} else if (align === "right") {
|
|
3273
|
+
textAnchor = "end";
|
|
3274
|
+
textX = el.width - 4;
|
|
3275
|
+
}
|
|
3276
|
+
if (segments && segments.length > 0) {
|
|
3277
|
+
let svg2 = `<text${attrs({
|
|
3278
|
+
x: textX,
|
|
3279
|
+
"text-anchor": textAnchor,
|
|
3280
|
+
"font-family": fontFamily,
|
|
3281
|
+
"font-size": fontSize,
|
|
3282
|
+
fill: color
|
|
3283
|
+
})}>`;
|
|
3284
|
+
let dy = fontSize * 1.2;
|
|
3285
|
+
let isFirstSegment = true;
|
|
3286
|
+
for (const seg of segments) {
|
|
3287
|
+
if (seg.isParagraphBreak) {
|
|
3288
|
+
dy = fontSize * 1.2;
|
|
3289
|
+
isFirstSegment = true;
|
|
3290
|
+
continue;
|
|
3291
|
+
}
|
|
3292
|
+
if (!seg.text) {
|
|
3293
|
+
continue;
|
|
3294
|
+
}
|
|
3295
|
+
const segStyle = seg.style;
|
|
3296
|
+
const segAttrs = {};
|
|
3297
|
+
if (isFirstSegment) {
|
|
3298
|
+
segAttrs.x = textX;
|
|
3299
|
+
segAttrs.dy = dy;
|
|
3300
|
+
isFirstSegment = false;
|
|
3301
|
+
}
|
|
3302
|
+
if (segStyle.fontFamily && segStyle.fontFamily !== fontFamily) {
|
|
3303
|
+
segAttrs["font-family"] = segStyle.fontFamily;
|
|
3304
|
+
}
|
|
3305
|
+
if (segStyle.fontSize && segStyle.fontSize !== fontSize) {
|
|
3306
|
+
segAttrs["font-size"] = segStyle.fontSize;
|
|
3307
|
+
}
|
|
3308
|
+
if (segStyle.color && segStyle.color !== color) {
|
|
3309
|
+
segAttrs.fill = segStyle.color;
|
|
3310
|
+
}
|
|
3311
|
+
if (segStyle.bold) {
|
|
3312
|
+
segAttrs["font-weight"] = "bold";
|
|
3313
|
+
}
|
|
3314
|
+
if (segStyle.italic) {
|
|
3315
|
+
segAttrs["font-style"] = "italic";
|
|
3316
|
+
}
|
|
3317
|
+
if (segStyle.underline) {
|
|
3318
|
+
segAttrs["text-decoration"] = "underline";
|
|
3319
|
+
}
|
|
3320
|
+
svg2 += `<tspan${attrs(segAttrs)}>${escXml(seg.text)}</tspan>`;
|
|
3321
|
+
}
|
|
3322
|
+
svg2 += `</text>`;
|
|
3323
|
+
return svg2;
|
|
3324
|
+
}
|
|
3325
|
+
const bold = style?.bold;
|
|
3326
|
+
const italic = style?.italic;
|
|
3327
|
+
const lines = text.split("\n");
|
|
3328
|
+
let svg = `<text${attrs({
|
|
3329
|
+
x: textX,
|
|
3330
|
+
"text-anchor": textAnchor,
|
|
3331
|
+
"font-family": fontFamily,
|
|
3332
|
+
"font-size": fontSize,
|
|
3333
|
+
fill: color,
|
|
3334
|
+
"font-weight": bold ? "bold" : void 0,
|
|
3335
|
+
"font-style": italic ? "italic" : void 0
|
|
3336
|
+
})}>`;
|
|
3337
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3338
|
+
svg += `<tspan${attrs({
|
|
3339
|
+
x: textX,
|
|
3340
|
+
dy: i === 0 ? fontSize * 1.2 : fontSize * 1.2
|
|
3341
|
+
})}>${escXml(lines[i])}</tspan>`;
|
|
3342
|
+
}
|
|
3343
|
+
svg += `</text>`;
|
|
3344
|
+
return svg;
|
|
3345
|
+
}
|
|
3346
|
+
function renderShapeBody(el) {
|
|
3347
|
+
if (el.type !== "shape" && el.type !== "text") {
|
|
3348
|
+
return "";
|
|
3349
|
+
}
|
|
3350
|
+
const shapeStyle = "shapeStyle" in el ? el.shapeStyle : void 0;
|
|
3351
|
+
const shapeType = "shapeType" in el ? el.shapeType : void 0;
|
|
3352
|
+
const fillColor = shapeStyle?.fillColor ?? "none";
|
|
3353
|
+
const strokeColor = shapeStyle?.strokeColor ?? "none";
|
|
3354
|
+
const strokeWidth = shapeStyle?.strokeWidth ?? (strokeColor !== "none" ? 1 : 0);
|
|
3355
|
+
const fillMode = shapeStyle?.fillMode;
|
|
3356
|
+
let fill = fillColor;
|
|
3357
|
+
if (fillMode === "none") {
|
|
3358
|
+
fill = "none";
|
|
3359
|
+
} else if (fillMode === "gradient" && shapeStyle?.fillGradient) {
|
|
3360
|
+
fill = shapeStyle.fillGradientStops?.[0]?.color ?? fillColor;
|
|
3361
|
+
}
|
|
3362
|
+
const fillOpacity = shapeStyle?.fillOpacity;
|
|
3363
|
+
const strokeOpacity = shapeStyle?.strokeOpacity;
|
|
3364
|
+
const commonAttrs = {
|
|
3365
|
+
fill,
|
|
3366
|
+
stroke: strokeColor,
|
|
3367
|
+
"stroke-width": strokeWidth || void 0,
|
|
3368
|
+
"fill-opacity": fillOpacity !== void 0 ? fillOpacity : void 0,
|
|
3369
|
+
"stroke-opacity": strokeOpacity !== void 0 ? strokeOpacity : void 0
|
|
3370
|
+
};
|
|
3371
|
+
const pathData = "pathData" in el ? el.pathData : void 0;
|
|
3372
|
+
if (pathData) {
|
|
3373
|
+
return `<path${attrs({ ...commonAttrs, d: pathData })} />`;
|
|
3374
|
+
}
|
|
3375
|
+
switch (shapeType) {
|
|
3376
|
+
case "ellipse":
|
|
3377
|
+
case "oval":
|
|
3378
|
+
return `<ellipse${attrs({
|
|
3379
|
+
...commonAttrs,
|
|
3380
|
+
cx: el.width / 2,
|
|
3381
|
+
cy: el.height / 2,
|
|
3382
|
+
rx: el.width / 2,
|
|
3383
|
+
ry: el.height / 2
|
|
3384
|
+
})} />`;
|
|
3385
|
+
case "triangle":
|
|
3386
|
+
case "isoTriangle":
|
|
3387
|
+
return `<polygon${attrs({
|
|
3388
|
+
...commonAttrs,
|
|
3389
|
+
points: `${el.width / 2},0 ${el.width},${el.height} 0,${el.height}`
|
|
3390
|
+
})} />`;
|
|
3391
|
+
case "diamond":
|
|
3392
|
+
return `<polygon${attrs({
|
|
3393
|
+
...commonAttrs,
|
|
3394
|
+
points: `${el.width / 2},0 ${el.width},${el.height / 2} ${el.width / 2},${el.height} 0,${el.height / 2}`
|
|
3395
|
+
})} />`;
|
|
3396
|
+
default:
|
|
3397
|
+
return `<rect${attrs({
|
|
3398
|
+
...commonAttrs,
|
|
3399
|
+
width: el.width,
|
|
3400
|
+
height: el.height,
|
|
3401
|
+
rx: shapeType === "roundRect" ? Math.min(el.width, el.height) * 0.1 : void 0
|
|
3402
|
+
})} />`;
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
function renderImageElement(el) {
|
|
3406
|
+
if (el.type !== "image" && el.type !== "picture") {
|
|
3407
|
+
return "";
|
|
3408
|
+
}
|
|
3409
|
+
const imageData = el.imageData;
|
|
3410
|
+
if (!imageData) {
|
|
3411
|
+
return `<rect${attrs({ width: el.width, height: el.height, fill: "#E0E0E0", stroke: "#999", "stroke-width": 1 })} /><text${attrs({ x: el.width / 2, y: el.height / 2, "text-anchor": "middle", "font-size": 12, fill: "#666" })}>image</text>`;
|
|
3412
|
+
}
|
|
3413
|
+
return `<image${attrs({
|
|
3414
|
+
width: el.width,
|
|
3415
|
+
height: el.height,
|
|
3416
|
+
preserveAspectRatio: "none"
|
|
3417
|
+
})} href="${escXml(imageData)}" />`;
|
|
3418
|
+
}
|
|
3419
|
+
function renderConnector(el, defs) {
|
|
3420
|
+
if (el.type !== "connector") {
|
|
3421
|
+
return "";
|
|
3422
|
+
}
|
|
3423
|
+
const shapeStyle = el.shapeStyle;
|
|
3424
|
+
const strokeColor = shapeStyle?.strokeColor ?? "#000000";
|
|
3425
|
+
const strokeWidth = shapeStyle?.strokeWidth ?? 1;
|
|
3426
|
+
let markerStart;
|
|
3427
|
+
let markerEnd;
|
|
3428
|
+
if (shapeStyle?.connectorStartArrow && shapeStyle.connectorStartArrow !== "none") {
|
|
3429
|
+
const m = arrowMarkerDef(strokeColor);
|
|
3430
|
+
defs.push(m.svg);
|
|
3431
|
+
markerStart = `url(#${m.id})`;
|
|
3432
|
+
}
|
|
3433
|
+
if (shapeStyle?.connectorEndArrow && shapeStyle.connectorEndArrow !== "none") {
|
|
3434
|
+
const m = arrowMarkerDef(strokeColor);
|
|
3435
|
+
defs.push(m.svg);
|
|
3436
|
+
markerEnd = `url(#${m.id})`;
|
|
3437
|
+
}
|
|
3438
|
+
return `<line${attrs({
|
|
3439
|
+
x1: 0,
|
|
3440
|
+
y1: 0,
|
|
3441
|
+
x2: el.width,
|
|
3442
|
+
y2: el.height,
|
|
3443
|
+
stroke: strokeColor,
|
|
3444
|
+
"stroke-width": strokeWidth,
|
|
3445
|
+
"marker-start": markerStart,
|
|
3446
|
+
"marker-end": markerEnd
|
|
3447
|
+
})} />`;
|
|
3448
|
+
}
|
|
3449
|
+
function renderTable(el, defaults) {
|
|
3450
|
+
if (el.type !== "table") {
|
|
3451
|
+
return "";
|
|
3452
|
+
}
|
|
3453
|
+
const tableData = el.tableData;
|
|
3454
|
+
if (!tableData || !tableData.rows.length) {
|
|
3455
|
+
return `<rect${attrs({ width: el.width, height: el.height, fill: "#F0F0F0", stroke: "#CCC", "stroke-width": 1 })} />`;
|
|
3456
|
+
}
|
|
3457
|
+
const cols = tableData.columnWidths;
|
|
3458
|
+
const numRows = tableData.rows.length;
|
|
3459
|
+
const rowHeight = el.height / numRows;
|
|
3460
|
+
let svg = "";
|
|
3461
|
+
let yOffset = 0;
|
|
3462
|
+
for (const row of tableData.rows) {
|
|
3463
|
+
const h = row.height ?? rowHeight;
|
|
3464
|
+
let xOffset = 0;
|
|
3465
|
+
for (let ci = 0; ci < row.cells.length; ci++) {
|
|
3466
|
+
const cell = row.cells[ci];
|
|
3467
|
+
if (cell.vMerge || cell.hMerge) {
|
|
3468
|
+
xOffset += (cols[ci] ?? 0) * el.width;
|
|
3469
|
+
continue;
|
|
3470
|
+
}
|
|
3471
|
+
const cellWidth = (cols[ci] ?? 1 / row.cells.length) * el.width * (cell.gridSpan ?? 1);
|
|
3472
|
+
const bgColor = cell.style?.backgroundColor ?? "none";
|
|
3473
|
+
const borderColor = cell.style?.borderColor ?? "#CCCCCC";
|
|
3474
|
+
const textColor = cell.style?.color ?? "#000000";
|
|
3475
|
+
const fontSize = cell.style?.fontSize ?? defaults.defaultFontSize;
|
|
3476
|
+
const bold = cell.style?.bold;
|
|
3477
|
+
svg += `<rect${attrs({
|
|
3478
|
+
x: xOffset,
|
|
3479
|
+
y: yOffset,
|
|
3480
|
+
width: cellWidth,
|
|
3481
|
+
height: h,
|
|
3482
|
+
fill: bgColor,
|
|
3483
|
+
stroke: borderColor,
|
|
3484
|
+
"stroke-width": 0.5
|
|
3485
|
+
})} />`;
|
|
3486
|
+
if (cell.text) {
|
|
3487
|
+
svg += `<text${attrs({
|
|
3488
|
+
x: xOffset + 4,
|
|
3489
|
+
y: yOffset + h / 2,
|
|
3490
|
+
"dominant-baseline": "central",
|
|
3491
|
+
"font-family": defaults.defaultFontFamily,
|
|
3492
|
+
"font-size": fontSize,
|
|
3493
|
+
fill: textColor,
|
|
3494
|
+
"font-weight": bold ? "bold" : void 0
|
|
3495
|
+
})}>${escXml(cell.text)}</text>`;
|
|
3496
|
+
}
|
|
3497
|
+
xOffset += cellWidth;
|
|
3498
|
+
}
|
|
3499
|
+
yOffset += h;
|
|
3500
|
+
}
|
|
3501
|
+
return svg;
|
|
3502
|
+
}
|
|
3503
|
+
function renderGroup(el, defaults, defs) {
|
|
3504
|
+
if (el.type !== "group") {
|
|
3505
|
+
return "";
|
|
3506
|
+
}
|
|
3507
|
+
let inner = "";
|
|
3508
|
+
for (const child of el.children) {
|
|
3509
|
+
inner += renderElement(child, defaults, defs);
|
|
3510
|
+
}
|
|
3511
|
+
return inner;
|
|
3512
|
+
}
|
|
3513
|
+
function renderInk(el) {
|
|
3514
|
+
if (el.type !== "ink") {
|
|
3515
|
+
return "";
|
|
3516
|
+
}
|
|
3517
|
+
let svg = "";
|
|
3518
|
+
for (let i = 0; i < el.inkPaths.length; i++) {
|
|
3519
|
+
const path = el.inkPaths[i];
|
|
3520
|
+
const color = el.inkColors?.[i] ?? "#000000";
|
|
3521
|
+
const width = el.inkWidths?.[i] ?? 1;
|
|
3522
|
+
const opacity = el.inkOpacities?.[i];
|
|
3523
|
+
svg += `<path${attrs({
|
|
3524
|
+
d: path,
|
|
3525
|
+
fill: "none",
|
|
3526
|
+
stroke: color,
|
|
3527
|
+
"stroke-width": width,
|
|
3528
|
+
"stroke-opacity": opacity,
|
|
3529
|
+
"stroke-linecap": "round"
|
|
3530
|
+
})} />`;
|
|
3531
|
+
}
|
|
3532
|
+
return svg;
|
|
3533
|
+
}
|
|
3534
|
+
function renderPlaceholder(el) {
|
|
3535
|
+
return `<rect${attrs({
|
|
3536
|
+
width: el.width,
|
|
3537
|
+
height: el.height,
|
|
3538
|
+
fill: "#F5F5F5",
|
|
3539
|
+
stroke: "#CCCCCC",
|
|
3540
|
+
"stroke-width": 1,
|
|
3541
|
+
"stroke-dasharray": "4 2"
|
|
3542
|
+
})} /><text${attrs({
|
|
3543
|
+
x: el.width / 2,
|
|
3544
|
+
y: el.height / 2,
|
|
3545
|
+
"text-anchor": "middle",
|
|
3546
|
+
"dominant-baseline": "central",
|
|
3547
|
+
"font-family": "Arial",
|
|
3548
|
+
"font-size": 11,
|
|
3549
|
+
fill: "#999999"
|
|
3550
|
+
})}>${escXml(el.type)}</text>`;
|
|
3551
|
+
}
|
|
3552
|
+
function renderElement(el, defaults, defs) {
|
|
3553
|
+
if (el.hidden) {
|
|
3554
|
+
return "";
|
|
3555
|
+
}
|
|
3556
|
+
const transform = renderTransform(el);
|
|
3557
|
+
const opacity = el.opacity;
|
|
3558
|
+
let inner = "";
|
|
3559
|
+
switch (el.type) {
|
|
3560
|
+
case "text":
|
|
3561
|
+
inner = renderShapeBody(el) + renderText(el, defaults);
|
|
3562
|
+
break;
|
|
3563
|
+
case "shape":
|
|
3564
|
+
inner = renderShapeBody(el) + renderText(el, defaults);
|
|
3565
|
+
break;
|
|
3566
|
+
case "connector":
|
|
3567
|
+
inner = renderConnector(el, defs);
|
|
3568
|
+
break;
|
|
3569
|
+
case "image":
|
|
3570
|
+
case "picture":
|
|
3571
|
+
inner = renderImageElement(el);
|
|
3572
|
+
break;
|
|
3573
|
+
case "table":
|
|
3574
|
+
inner = renderTable(el, defaults);
|
|
3575
|
+
break;
|
|
3576
|
+
case "group":
|
|
3577
|
+
inner = renderGroup(el, defaults, defs);
|
|
3578
|
+
break;
|
|
3579
|
+
case "ink":
|
|
3580
|
+
inner = renderInk(el);
|
|
3581
|
+
break;
|
|
3582
|
+
case "chart":
|
|
3583
|
+
case "smartArt":
|
|
3584
|
+
case "ole":
|
|
3585
|
+
case "media":
|
|
3586
|
+
case "contentPart":
|
|
3587
|
+
case "zoom":
|
|
3588
|
+
case "model3d":
|
|
3589
|
+
case "unknown":
|
|
3590
|
+
inner = renderPlaceholder(el);
|
|
3591
|
+
break;
|
|
3592
|
+
}
|
|
3593
|
+
if (!inner) {
|
|
3594
|
+
return "";
|
|
3595
|
+
}
|
|
3596
|
+
return `<g${attrs({
|
|
3597
|
+
transform,
|
|
3598
|
+
opacity: opacity !== void 0 && opacity < 1 ? opacity : void 0
|
|
3599
|
+
})}>${inner}</g>`;
|
|
3600
|
+
}
|
|
3601
|
+
function renderBackground(slide, width, height) {
|
|
3602
|
+
if (slide.backgroundImage) {
|
|
3603
|
+
return `<rect${attrs({ width, height, fill: "#FFFFFF" })} /><image${attrs({
|
|
3604
|
+
width,
|
|
3605
|
+
height,
|
|
3606
|
+
preserveAspectRatio: "xMidYMid slice"
|
|
3607
|
+
})} href="${escXml(slide.backgroundImage)}" />`;
|
|
3608
|
+
}
|
|
3609
|
+
const fill = slide.backgroundColor ?? "#FFFFFF";
|
|
3610
|
+
return `<rect${attrs({ width, height, fill })} />`;
|
|
3611
|
+
}
|
|
3612
|
+
var SvgExporter = class _SvgExporter {
|
|
3613
|
+
/**
|
|
3614
|
+
* Export a single slide to an SVG XML string.
|
|
3615
|
+
*
|
|
3616
|
+
* @param slide - The parsed slide.
|
|
3617
|
+
* @param width - SVG viewport width (pixels).
|
|
3618
|
+
* @param height - SVG viewport height (pixels).
|
|
3619
|
+
* @param options - Optional export settings.
|
|
3620
|
+
* @returns A complete SVG document as a string.
|
|
3621
|
+
*/
|
|
3622
|
+
static exportSlide(slide, width, height, options) {
|
|
3623
|
+
_markerIdCounter = 0;
|
|
3624
|
+
const defaults = resolveDefaults(options);
|
|
3625
|
+
const defs = [];
|
|
3626
|
+
let body = renderBackground(slide, width, height);
|
|
3627
|
+
for (const el of slide.elements) {
|
|
3628
|
+
body += renderElement(el, defaults, defs);
|
|
3629
|
+
}
|
|
3630
|
+
let defsBlock = "";
|
|
3631
|
+
if (defs.length) {
|
|
3632
|
+
defsBlock = `<defs>${defs.join("")}</defs>`;
|
|
3633
|
+
}
|
|
3634
|
+
return `<svg${attrs({
|
|
3635
|
+
xmlns: SVG_NS,
|
|
3636
|
+
"xmlns:xlink": XLINK_NS,
|
|
3637
|
+
viewBox: `0 0 ${width} ${height}`,
|
|
3638
|
+
width,
|
|
3639
|
+
height
|
|
3640
|
+
})}>${defsBlock}${body}</svg>`;
|
|
3641
|
+
}
|
|
3642
|
+
/**
|
|
3643
|
+
* Export all (or selected) slides to SVG XML strings.
|
|
3644
|
+
*
|
|
3645
|
+
* @param data - The fully parsed PPTX data.
|
|
3646
|
+
* @param options - Optional export settings.
|
|
3647
|
+
* @returns An array of SVG strings (one per exported slide).
|
|
3648
|
+
*/
|
|
3649
|
+
static exportAll(data, options) {
|
|
3650
|
+
const results = [];
|
|
3651
|
+
const includeHidden = options?.includeHidden ?? false;
|
|
3652
|
+
for (let i = 0; i < data.slides.length; i++) {
|
|
3653
|
+
if (options?.slideIndices && !options.slideIndices.includes(i)) {
|
|
3654
|
+
continue;
|
|
3655
|
+
}
|
|
3656
|
+
const slide = data.slides[i];
|
|
3657
|
+
if (slide.hidden && !includeHidden) {
|
|
3658
|
+
continue;
|
|
3659
|
+
}
|
|
3660
|
+
results.push(_SvgExporter.exportSlide(slide, data.width, data.height, options));
|
|
3661
|
+
}
|
|
3662
|
+
return results;
|
|
3663
|
+
}
|
|
3664
|
+
};
|
|
3665
|
+
|
|
3666
|
+
exports.DocumentConverter = DocumentConverter;
|
|
3667
|
+
exports.MediaContext = MediaContext;
|
|
3668
|
+
exports.PptxMarkdownConverter = PptxMarkdownConverter;
|
|
3669
|
+
exports.SlideMetadataRenderer = SlideMetadataRenderer;
|
|
3670
|
+
exports.SlideProcessor = SlideProcessor;
|
|
3671
|
+
exports.SvgExporter = SvgExporter;
|
|
3672
|
+
exports.dataUrlToMediaBytes = dataUrlToMediaBytes;
|
|
3673
|
+
exports.deriveOutputPath = deriveOutputPath;
|
|
3674
|
+
exports.generateMediaFilename = generateMediaFilename;
|
|
3675
|
+
exports.getDirectory = getDirectory;
|
|
3676
|
+
exports.normalizePath = normalizePath;
|