officeparser 6.1.0 → 7.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.
Files changed (70) hide show
  1. package/README.md +284 -86
  2. package/dist/OfficeConverter.d.ts +46 -0
  3. package/dist/OfficeConverter.js +72 -0
  4. package/dist/OfficeGenerator.d.ts +19 -0
  5. package/dist/OfficeGenerator.js +48 -0
  6. package/dist/OfficeParser.d.ts +6 -0
  7. package/dist/OfficeParser.js +55 -28
  8. package/dist/cli.d.ts +3 -1
  9. package/dist/cli.js +107 -22
  10. package/dist/defaults.d.ts +41 -0
  11. package/dist/defaults.js +172 -0
  12. package/dist/generators/BaseGenerator.d.ts +58 -0
  13. package/dist/generators/BaseGenerator.js +107 -0
  14. package/dist/generators/ChunkingGenerator.d.ts +81 -0
  15. package/dist/generators/ChunkingGenerator.js +683 -0
  16. package/dist/generators/CsvGenerator.d.ts +30 -0
  17. package/dist/generators/CsvGenerator.js +233 -0
  18. package/dist/generators/HtmlGenerator.d.ts +37 -0
  19. package/dist/generators/HtmlGenerator.js +1013 -0
  20. package/dist/generators/MarkdownGenerator.d.ts +59 -0
  21. package/dist/generators/MarkdownGenerator.js +481 -0
  22. package/dist/generators/PdfGenerator.d.ts +22 -0
  23. package/dist/generators/PdfGenerator.js +118 -0
  24. package/dist/generators/RtfGenerator.d.ts +15 -0
  25. package/dist/generators/RtfGenerator.js +208 -0
  26. package/dist/generators/TextGenerator.d.ts +13 -0
  27. package/dist/generators/TextGenerator.js +108 -0
  28. package/dist/index.d.ts +11 -3
  29. package/dist/index.js +17 -2
  30. package/dist/index.mjs +2 -2
  31. package/dist/officeparser.browser.d.ts +878 -5
  32. package/dist/officeparser.browser.iife.js +703 -49
  33. package/dist/officeparser.browser.mjs +703 -49
  34. package/dist/parsers/CsvParser.d.ts +9 -0
  35. package/dist/parsers/CsvParser.js +110 -0
  36. package/dist/parsers/ExcelParser.d.ts +2 -2
  37. package/dist/parsers/ExcelParser.js +145 -114
  38. package/dist/parsers/HtmlParser.d.ts +2 -0
  39. package/dist/parsers/HtmlParser.js +539 -0
  40. package/dist/parsers/MarkdownParser.d.ts +2 -0
  41. package/dist/parsers/MarkdownParser.js +360 -0
  42. package/dist/parsers/OpenOfficeParser.d.ts +2 -2
  43. package/dist/parsers/OpenOfficeParser.js +237 -128
  44. package/dist/parsers/PdfParser.d.ts +2 -2
  45. package/dist/parsers/PdfParser.js +52 -49
  46. package/dist/parsers/PowerPointParser.d.ts +2 -2
  47. package/dist/parsers/PowerPointParser.js +132 -123
  48. package/dist/parsers/RtfParser.d.ts +22 -2
  49. package/dist/parsers/RtfParser.js +1398 -1282
  50. package/dist/parsers/WordParser.d.ts +3 -2
  51. package/dist/parsers/WordParser.js +333 -115
  52. package/dist/sbom.cdx.json +103 -103
  53. package/dist/types.d.ts +833 -5
  54. package/dist/types.js +71 -0
  55. package/dist/utils/astUtils.d.ts +16 -0
  56. package/dist/utils/astUtils.js +32 -0
  57. package/dist/utils/configUtils.d.ts +26 -0
  58. package/dist/utils/configUtils.js +140 -0
  59. package/dist/utils/envUtils.js +56 -2
  60. package/dist/utils/errorUtils.d.ts +17 -29
  61. package/dist/utils/errorUtils.js +109 -52
  62. package/dist/utils/moduleLoader.js +15 -9
  63. package/dist/utils/ocrUtils.js +2 -1
  64. package/dist/utils/sheetUtils.d.ts +7 -0
  65. package/dist/utils/sheetUtils.js +35 -0
  66. package/dist/utils/styleMapper.d.ts +36 -0
  67. package/dist/utils/styleMapper.js +224 -0
  68. package/dist/utils/xmlUtils.d.ts +0 -8
  69. package/dist/utils/xmlUtils.js +2 -1
  70. package/package.json +28 -9
@@ -0,0 +1,59 @@
1
+ import { ConversionResult, GeneratorConfig, OfficeContentNode, OfficeParserAST } from '../types.js';
2
+ import { BaseGenerator } from './BaseGenerator.js';
3
+ /**
4
+ * Generates Markdown from an AST.
5
+ *
6
+ * DESIGN PRINCIPLES:
7
+ * 1. **Strict Native Preference**: Always utilize native Markdown syntax for features that
8
+ * are natively supported (headings, lists, bold/italic, etc.). HTML tags should NEVER
9
+ * be used for these features.
10
+ *
11
+ * 2. **Fidelity vs. Purity (The `fallbackToHtml` Principle)**:
12
+ * - When `fallbackToHtml` is TRUE: The generator prioritizes high-fidelity document
13
+ * conversion. It will use HTML tags for features that Markdown cannot natively
14
+ * represent (e.g., `<u>` for underline, `<div>` for alignment, `<table>` for
15
+ * nested structures or merged cells).
16
+ * - When `fallbackToHtml` is FALSE: The generator prioritizes "pure" Markdown.
17
+ * Unsupported features are either:
18
+ * - **Skipped**: Non-essential formatting like underline, subscript, superscript,
19
+ * or text alignment is omitted.
20
+ * - **Simplified/Hoisted**: Complex structures like nested tables are hoisted out
21
+ * of their parent cells and rendered as separate sequential tables to maintain
22
+ * valid Markdown syntax.
23
+ *
24
+ * 3. **Consistency**: All similar structural or formatting ideological problems must be
25
+ * resolved using these same rules to ensure predictable output.
26
+ */
27
+ export declare class MarkdownGenerator extends BaseGenerator<'md'> {
28
+ private isInsideTable;
29
+ private hoistedContent;
30
+ constructor(ast: OfficeParserAST, config?: GeneratorConfig<'md'>);
31
+ /**
32
+ * Renders anchor tags if HTML fallback is allowed.
33
+ */
34
+ private renderAnchors;
35
+ /**
36
+ * Generates Markdown string from the provided AST.
37
+ *
38
+ * @returns A Markdown string
39
+ */
40
+ generate(): Promise<ConversionResult>;
41
+ /**
42
+ * Recursively processes nodes and builds output.
43
+ * Overridden to provide AST optimization (merging adjacent text nodes).
44
+ */
45
+ protected processNodeRecursive(node: OfficeContentNode, processor: (node: OfficeContentNode, childrenOutput: string) => string | Promise<string>): Promise<string>;
46
+ /**
47
+ * Merges adjacent text nodes with identical formatting and metadata.
48
+ */
49
+ private optimizeNodes;
50
+ private areFormattingEqual;
51
+ private renderMarkdownTable;
52
+ private renderMarkdownTableInternal;
53
+ private hasNestedTable;
54
+ private hasColspanOrRowspan;
55
+ /**
56
+ * Renders a complex table as HTML since Markdown doesn't support nested tables or rowspans.
57
+ */
58
+ private renderTableAsHtml;
59
+ }
@@ -0,0 +1,481 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MarkdownGenerator = void 0;
4
+ const BaseGenerator_js_1 = require("./BaseGenerator.js");
5
+ /**
6
+ * Generates Markdown from an AST.
7
+ *
8
+ * DESIGN PRINCIPLES:
9
+ * 1. **Strict Native Preference**: Always utilize native Markdown syntax for features that
10
+ * are natively supported (headings, lists, bold/italic, etc.). HTML tags should NEVER
11
+ * be used for these features.
12
+ *
13
+ * 2. **Fidelity vs. Purity (The `fallbackToHtml` Principle)**:
14
+ * - When `fallbackToHtml` is TRUE: The generator prioritizes high-fidelity document
15
+ * conversion. It will use HTML tags for features that Markdown cannot natively
16
+ * represent (e.g., `<u>` for underline, `<div>` for alignment, `<table>` for
17
+ * nested structures or merged cells).
18
+ * - When `fallbackToHtml` is FALSE: The generator prioritizes "pure" Markdown.
19
+ * Unsupported features are either:
20
+ * - **Skipped**: Non-essential formatting like underline, subscript, superscript,
21
+ * or text alignment is omitted.
22
+ * - **Simplified/Hoisted**: Complex structures like nested tables are hoisted out
23
+ * of their parent cells and rendered as separate sequential tables to maintain
24
+ * valid Markdown syntax.
25
+ *
26
+ * 3. **Consistency**: All similar structural or formatting ideological problems must be
27
+ * resolved using these same rules to ensure predictable output.
28
+ */
29
+ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
30
+ isInsideTable = false;
31
+ hoistedContent = [];
32
+ constructor(ast, config) {
33
+ super('md', ast, config);
34
+ }
35
+ /**
36
+ * Renders anchor tags if HTML fallback is allowed.
37
+ */
38
+ renderAnchors(metadata) {
39
+ if (!this.config.mdConfig.fallbackToHtml || this.config.ignoreInternalLinks)
40
+ return '';
41
+ const ids = metadata?.anchorIds || [];
42
+ return ids.map((aid) => `<a id="${this.slugify(aid)}"></a>`).join('');
43
+ }
44
+ /**
45
+ * Generates Markdown string from the provided AST.
46
+ *
47
+ * @returns A Markdown string
48
+ */
49
+ async generate() {
50
+ let output = '';
51
+ // Add Metadata (YAML Front Matter)
52
+ if (this.ast.metadata) {
53
+ output += '---\n';
54
+ if (this.ast.metadata.title)
55
+ output += `title: "${this.ast.metadata.title}"\n`;
56
+ if (this.ast.metadata.author)
57
+ output += `author: "${this.ast.metadata.author}"\n`;
58
+ if (this.ast.metadata.created)
59
+ output += `created: ${new Date(this.ast.metadata.created).toISOString()}\n`;
60
+ if (this.ast.metadata.modified)
61
+ output += `modified: ${new Date(this.ast.metadata.modified).toISOString()}\n`;
62
+ if (this.ast.metadata.description)
63
+ output += `description: "${this.ast.metadata.description}"\n`;
64
+ if (this.ast.metadata.customProperties) {
65
+ for (const [key, val] of Object.entries(this.ast.metadata.customProperties)) {
66
+ output += `${key}: ${JSON.stringify(val)}\n`;
67
+ }
68
+ }
69
+ output += '---\n\n';
70
+ }
71
+ const processor = async (node, childrenOutput) => {
72
+ // Handle Style Mapping for Markdown using the semantic mapping helper
73
+ const mapping = this.getSemanticMapping(node);
74
+ if (mapping) {
75
+ // Map common HTML tags to Markdown equivalents
76
+ if (mapping.tag === 'blockquote')
77
+ return `> ${childrenOutput}\n\n`;
78
+ if (mapping.tag === 'code')
79
+ return `\`${childrenOutput}\` `;
80
+ if (mapping.tag === 'pre')
81
+ return `\`\`\`\n${childrenOutput}\n\`\`\`\n\n`;
82
+ const hMatch = mapping.tag.match(/^h([1-6])$/);
83
+ if (hMatch) {
84
+ const level = parseInt(hMatch[1]);
85
+ return `${'#'.repeat(level)} ${childrenOutput}\n\n`;
86
+ }
87
+ }
88
+ switch (node.type) {
89
+ case 'text': {
90
+ let text = node.text || '';
91
+ if (this.config.includeFormatting && node.formatting) {
92
+ if (node.formatting.bold)
93
+ text = `**${text}**`;
94
+ if (node.formatting.italic)
95
+ text = `*${text}*`;
96
+ if (node.formatting.strikethrough)
97
+ text = `~~${text}~~`;
98
+ // Use HTML tags for formatting not natively supported by standard Markdown
99
+ if (this.config.mdConfig.fallbackToHtml) {
100
+ if (node.formatting.underline)
101
+ text = `<u>${text}</u>`;
102
+ if (node.formatting.subscript)
103
+ text = `<sub>${text}</sub>`;
104
+ if (node.formatting.superscript)
105
+ text = `<sup>${text}</sup>`;
106
+ }
107
+ }
108
+ const meta = node.metadata;
109
+ if (meta?.link) {
110
+ const isInternal = meta.linkType !== 'external';
111
+ if (!this.config.ignoreInternalLinks || !isInternal) {
112
+ let link = meta.link;
113
+ // Slugify internal link targets to match heading IDs if generating IDs
114
+ if (isInternal && link.startsWith('#') && (this.config.generateIds || this.config.mdConfig.fallbackToHtml)) {
115
+ const target = link.substring(1);
116
+ link = '#' + this.slugify(target);
117
+ }
118
+ text = `[${text}](${link})`;
119
+ }
120
+ }
121
+ return text;
122
+ }
123
+ case 'heading': {
124
+ const meta = node.metadata;
125
+ const level = Math.min(Math.max(meta?.level || 1, 1), 6);
126
+ const prefix = '#'.repeat(level) + ' ';
127
+ let id = '';
128
+ let remainingAnchors = [];
129
+ if (!this.config.ignoreInternalLinks && meta?.anchorIds && meta.anchorIds.length > 0) {
130
+ const ids = [...meta.anchorIds];
131
+ const lastId = ids.pop();
132
+ // Slugify the explicit ID to ensure it's a valid Markdown identifier
133
+ id = ` {#${this.slugify(lastId)}}`;
134
+ remainingAnchors = ids;
135
+ }
136
+ else if (this.config.generateIds) {
137
+ id = ` {#${this.slugify(this.getNodeText(node))}}`;
138
+ }
139
+ const anchors = remainingAnchors.map(aid => `<a name="${aid}"></a>`).join('');
140
+ let content = `${prefix}${childrenOutput}${id}`;
141
+ // Alignment fallback via HTML div/p
142
+ if (this.config.mdConfig.fallbackToHtml && meta?.alignment && meta.alignment !== 'left') {
143
+ // Use extra newlines to ensure Markdown inside the div is parsed
144
+ content = `<div style="text-align: ${meta.alignment}">\n\n${content}\n\n</div>`;
145
+ }
146
+ return `${anchors}${anchors ? '\n' : ''}${content}\n\n`;
147
+ }
148
+ case 'paragraph': {
149
+ const meta = node.metadata;
150
+ const anchors = this.renderAnchors(meta);
151
+ let content = childrenOutput;
152
+ // Alignment fallback via HTML div/p
153
+ if (this.config.mdConfig.fallbackToHtml && meta?.alignment && meta.alignment !== 'left') {
154
+ content = `<div style="text-align: ${meta.alignment}">${content}</div>`;
155
+ }
156
+ return childrenOutput ? `${anchors}${content}\n\n` : '';
157
+ }
158
+ case 'list': {
159
+ const meta = node.metadata;
160
+ const indentSpaces = ' '.repeat(4);
161
+ const indent = indentSpaces.repeat(meta?.indentation || 0);
162
+ const marker = meta?.listType === 'ordered' ? `${(meta.itemIndex ?? 0) + 1}. ` : '- ';
163
+ const anchors = this.renderAnchors(meta);
164
+ return `${indent}${marker}${anchors}${childrenOutput}\n`;
165
+ }
166
+ case 'image': {
167
+ if (!this.config.includeImages)
168
+ return '';
169
+ const meta = node.metadata;
170
+ const alt = meta?.altText || 'image';
171
+ let src = meta?.url || meta?.attachmentName || '';
172
+ // Resolve attachment to data URI if no external URL is provided
173
+ if (!meta?.url && meta?.attachmentName && this.ast) {
174
+ const attachment = this.ast.attachments.find(a => a.name === meta.attachmentName);
175
+ if (attachment) {
176
+ src = `data:${attachment.mimeType || 'image/png'};base64,${attachment.data}`;
177
+ }
178
+ }
179
+ const anchors = this.renderAnchors(meta);
180
+ return `${anchors}${anchors ? '\n' : ''}![${alt}](${src})`;
181
+ }
182
+ case 'table': {
183
+ const anchors = this.renderAnchors(node.metadata);
184
+ const tableOutput = await this.renderMarkdownTable(node, processor);
185
+ return `${anchors}${anchors ? '\n' : ''}${tableOutput}`;
186
+ }
187
+ case 'row':
188
+ case 'cell': {
189
+ // These are handled manually in the 'table' case above
190
+ return childrenOutput;
191
+ }
192
+ case 'break': {
193
+ return '\n';
194
+ }
195
+ case 'code': {
196
+ const meta = node.metadata;
197
+ const lang = meta?.language || '';
198
+ // Block code if it contains newlines, else inline
199
+ if (node.text && node.text.includes('\n')) {
200
+ return `\n\`\`\`${lang}\n${node.text}\n\`\`\`\n\n`;
201
+ }
202
+ else {
203
+ return `\`${node.text || ''}\` `;
204
+ }
205
+ }
206
+ case 'sheet': {
207
+ const anchors = this.renderAnchors(node.metadata);
208
+ const tableOutput = await this.renderMarkdownTable(node, processor);
209
+ return `\n---\n\n${anchors}${anchors ? '\n' : ''}${tableOutput}\n\n`;
210
+ }
211
+ case 'slide': {
212
+ const anchors = this.renderAnchors(node.metadata);
213
+ return `\n---\n\n${anchors}${anchors ? '\n' : ''}${childrenOutput}\n\n`;
214
+ }
215
+ case 'page': {
216
+ const anchors = this.renderAnchors(node.metadata);
217
+ return `\n---\n\n${anchors}${anchors ? '\n' : ''}${childrenOutput}\n\n`;
218
+ }
219
+ default:
220
+ return childrenOutput;
221
+ }
222
+ };
223
+ const optimizedContent = this.optimizeNodes(this.ast.content);
224
+ for (let i = 0; i < optimizedContent.length; i++) {
225
+ const node = optimizedContent[i];
226
+ const nextNode = optimizedContent[i + 1];
227
+ let result = await this.processNodeRecursive(node, processor);
228
+ // Ensure lists and other block elements are separated from non-similar content by a blank line
229
+ if (nextNode) {
230
+ const isBothLists = node.type === 'list' && nextNode.type === 'list';
231
+ if (!isBothLists) {
232
+ if (!result.endsWith('\n\n')) {
233
+ if (result.endsWith('\n'))
234
+ result += '\n';
235
+ else
236
+ result += '\n\n';
237
+ }
238
+ }
239
+ }
240
+ output += result;
241
+ }
242
+ return {
243
+ value: (output + '\n\n' + this.hoistedContent.join('\n\n')).trim(),
244
+ messages: this.messages
245
+ };
246
+ }
247
+ /**
248
+ * Recursively processes nodes and builds output.
249
+ * Overridden to provide AST optimization (merging adjacent text nodes).
250
+ */
251
+ async processNodeRecursive(node, processor) {
252
+ // Allow user to completely override rendering or skip via onNode
253
+ const override = await this.handleOnNode(node);
254
+ if (override === false) {
255
+ return '';
256
+ }
257
+ if (typeof override === 'string') {
258
+ return override;
259
+ }
260
+ let childrenOutput = '';
261
+ if (node.children && node.children.length > 0) {
262
+ // Optimization: Merge adjacent text nodes with identical formatting
263
+ const optimizedChildren = this.optimizeNodes(node.children);
264
+ for (const child of optimizedChildren) {
265
+ childrenOutput += await this.processNodeRecursive(child, processor);
266
+ }
267
+ }
268
+ return await processor(node, childrenOutput);
269
+ }
270
+ /**
271
+ * Merges adjacent text nodes with identical formatting and metadata.
272
+ */
273
+ optimizeNodes(nodes) {
274
+ if (nodes.length <= 1)
275
+ return nodes;
276
+ const result = [];
277
+ let current = null;
278
+ for (const node of nodes) {
279
+ if (node.type === 'text' && current && current.type === 'text' &&
280
+ this.areFormattingEqual(node.formatting, current.formatting) &&
281
+ JSON.stringify(node.metadata) === JSON.stringify(current.metadata)) {
282
+ current.text = (current.text || '') + (node.text || '');
283
+ if (current.rawContent && node.rawContent)
284
+ current.rawContent += node.rawContent;
285
+ }
286
+ else {
287
+ current = { ...node }; // Clone
288
+ result.push(current);
289
+ }
290
+ }
291
+ return result;
292
+ }
293
+ areFormattingEqual(f1, f2) {
294
+ if (f1 === f2)
295
+ return true;
296
+ if (!f1 || !f2)
297
+ return false;
298
+ const keys1 = Object.keys(f1);
299
+ const keys2 = Object.keys(f2);
300
+ if (keys1.length !== keys2.length)
301
+ return false;
302
+ return keys1.every(key => f1[key] === f2[key]);
303
+ }
304
+ async renderMarkdownTable(node, processor) {
305
+ if (!node.children || node.children.length === 0)
306
+ return '';
307
+ // If table is complex, nested, or uses merges, fallback to HTML for high fidelity if allowed
308
+ const isComplex = this.hasNestedTable(node) || this.hasColspanOrRowspan(node);
309
+ if (this.config.mdConfig.fallbackToHtml && isComplex) {
310
+ return '\n' + await this.renderTableAsHtml(node) + '\n';
311
+ }
312
+ // Handle nested tables in pure Markdown by hoisting them out
313
+ if (this.isInsideTable && !this.config.mdConfig.fallbackToHtml) {
314
+ const wasInside = this.isInsideTable;
315
+ this.isInsideTable = false; // Reset to allow rendering the hoisted table correctly
316
+ const hoistedId = this.hoistedContent.length + 1;
317
+ const tableOutput = await this.renderMarkdownTableInternal(node, processor);
318
+ this.hoistedContent.push(`**Table ${hoistedId} (Hoisted from cell content):**\n${tableOutput}`);
319
+ this.isInsideTable = wasInside;
320
+ return `*(See Table ${hoistedId} below)*`;
321
+ }
322
+ this.isInsideTable = true;
323
+ const result = await this.renderMarkdownTableInternal(node, processor);
324
+ this.isInsideTable = false;
325
+ return result;
326
+ }
327
+ async renderMarkdownTableInternal(node, processor) {
328
+ let tableOutput = '';
329
+ let maxCols = 0;
330
+ // First pass: Process rows and determine max columns (accounting for colspans)
331
+ const processedRows = [];
332
+ for (const rowNode of (node.children ?? [])) {
333
+ const override = await this.handleOnNode(rowNode);
334
+ if (override === false)
335
+ continue;
336
+ if (typeof override === 'string') {
337
+ processedRows.push([override]);
338
+ continue;
339
+ }
340
+ const rowCells = [];
341
+ let lastCol = -1;
342
+ if (rowNode.children) {
343
+ const cellNodes = rowNode.children.filter(c => c.type === 'cell');
344
+ for (const cellNode of cellNodes) {
345
+ const currentCol = cellNode.metadata?.col ?? (lastCol + 1);
346
+ // Fill gaps with empty cells
347
+ while (lastCol < currentCol - 1) {
348
+ rowCells.push(' ');
349
+ lastCol++;
350
+ }
351
+ // Process cell content
352
+ let cellContent = await this.processNodeRecursive(cellNode, processor);
353
+ // Use <br> fallback only if allowed, otherwise space
354
+ const br = this.config.mdConfig.fallbackToHtml ? '<br>' : ' ';
355
+ cellContent = cellContent.trim().replace(/\n+/g, br).replace(/\|/g, '\\|');
356
+ rowCells.push(cellContent);
357
+ // Handle colspan by adding empty cells
358
+ const colSpan = cellNode.metadata?.colSpan || 1;
359
+ for (let i = 1; i < colSpan; i++) {
360
+ rowCells.push(' ');
361
+ }
362
+ lastCol = currentCol + colSpan - 1;
363
+ }
364
+ }
365
+ processedRows.push(rowCells);
366
+ maxCols = Math.max(maxCols, rowCells.length);
367
+ }
368
+ // Second pass: Build table string with separator
369
+ for (let i = 0; i < processedRows.length; i++) {
370
+ const row = processedRows[i];
371
+ // Pad row with empty cells if it has fewer than maxCols
372
+ while (row.length < maxCols)
373
+ row.push(' ');
374
+ tableOutput += `| ${row.join(' | ')} |\n`;
375
+ if (i === 0) {
376
+ // Header separator
377
+ tableOutput += `| ${Array(maxCols).fill(' --- ').join(' | ')} |\n`;
378
+ }
379
+ }
380
+ return `\n${tableOutput}\n`;
381
+ }
382
+ hasNestedTable(node) {
383
+ if (!node.children)
384
+ return false;
385
+ for (const child of node.children) {
386
+ if (child.type === 'table')
387
+ return true;
388
+ if (this.hasNestedTable(child))
389
+ return true;
390
+ }
391
+ return false;
392
+ }
393
+ hasColspanOrRowspan(node) {
394
+ if (!node.children)
395
+ return false;
396
+ for (const row of node.children) {
397
+ if (row.type === 'row' && row.children) {
398
+ for (const cell of row.children) {
399
+ if (cell.type === 'cell') {
400
+ const meta = cell.metadata;
401
+ if ((meta?.colSpan && meta.colSpan > 1) || (meta?.rowSpan && meta.rowSpan > 1)) {
402
+ return true;
403
+ }
404
+ }
405
+ }
406
+ }
407
+ }
408
+ return false;
409
+ }
410
+ /**
411
+ * Renders a complex table as HTML since Markdown doesn't support nested tables or rowspans.
412
+ */
413
+ async renderTableAsHtml(node, override) {
414
+ if (override === false)
415
+ return '';
416
+ if (typeof override === 'string') {
417
+ if (node.type === 'row')
418
+ return ` <tr><td colspan="100%">${override}</td></tr>\n`;
419
+ if (node.type === 'cell')
420
+ return `<td>${override}</td>`;
421
+ return override;
422
+ }
423
+ if (node.type === 'table') {
424
+ let rows = '';
425
+ if (node.children) {
426
+ for (const row of node.children) {
427
+ rows += await this.renderTableAsHtml(row, await this.handleOnNode(row));
428
+ }
429
+ }
430
+ return `<table>\n${rows}</table>\n`;
431
+ }
432
+ else if (node.type === 'row') {
433
+ let cells = '';
434
+ if (node.children) {
435
+ for (const cell of node.children) {
436
+ cells += await this.renderTableAsHtml(cell, await this.handleOnNode(cell));
437
+ }
438
+ }
439
+ return ` <tr>\n${cells} </tr>\n`;
440
+ }
441
+ else if (node.type === 'cell') {
442
+ const meta = node.metadata;
443
+ const rs = meta?.rowSpan > 1 ? ` rowspan="${meta.rowSpan}"` : '';
444
+ const cs = meta?.colSpan > 1 ? ` colspan="${meta.colSpan}"` : '';
445
+ let content = '';
446
+ if (node.children) {
447
+ // Use a simplified HTML processor for cell content
448
+ for (const child of this.optimizeNodes(node.children)) {
449
+ content += await this.processNodeRecursive(child, async (n, co) => {
450
+ switch (n.type) {
451
+ case 'text': {
452
+ let text = n.text || '';
453
+ if (n.formatting?.bold)
454
+ text = `<b>${text}</b>`;
455
+ if (n.formatting?.italic)
456
+ text = `<i>${text}</i>`;
457
+ if (n.formatting?.underline)
458
+ text = `<u>${text}</u>`;
459
+ if (n.formatting?.subscript)
460
+ text = `<sub>${text}</sub>`;
461
+ if (n.formatting?.superscript)
462
+ text = `<sup>${text}</sup>`;
463
+ return text;
464
+ }
465
+ case 'paragraph': return `<p>${co}</p>`;
466
+ case 'heading': {
467
+ const level = n.metadata?.level || 1;
468
+ return `<h${level}>${co}</h${level}>`;
469
+ }
470
+ case 'table': return await this.renderTableAsHtml(n);
471
+ default: return co;
472
+ }
473
+ });
474
+ }
475
+ }
476
+ return ` <td${rs}${cs}>${content}</td>\n`;
477
+ }
478
+ return '';
479
+ }
480
+ }
481
+ exports.MarkdownGenerator = MarkdownGenerator;
@@ -0,0 +1,22 @@
1
+ import { ConversionResult, GeneratorConfig, OfficeParserAST } from '../types.js';
2
+ import { BaseGenerator } from './BaseGenerator.js';
3
+ /**
4
+ * Generates high-fidelity PDF documents using a headless browser engine.
5
+ *
6
+ * Uses an environment-aware strategy:
7
+ * - Node.js: Uses Puppeteer (peer dependency) for server-side rendering.
8
+ * - Browser: Leverages native browser print capabilities.
9
+ */
10
+ export declare class PdfGenerator extends BaseGenerator<'pdf'> {
11
+ constructor(ast: OfficeParserAST, config?: GeneratorConfig<'pdf'>);
12
+ generate(): Promise<ConversionResult>;
13
+ /**
14
+ * Node.js implementation using Puppeteer.
15
+ * Uses dynamic import to avoid bundling puppeteer into the library core.
16
+ */
17
+ private generateInNode;
18
+ /**
19
+ * Browser implementation using hidden iframe and native print.
20
+ */
21
+ private generateInBrowser;
22
+ }
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PdfGenerator = void 0;
4
+ const types_js_1 = require("../types.js");
5
+ const envUtils_js_1 = require("../utils/envUtils.js");
6
+ const BaseGenerator_js_1 = require("./BaseGenerator.js");
7
+ const HtmlGenerator_js_1 = require("./HtmlGenerator.js");
8
+ /**
9
+ * Generates high-fidelity PDF documents using a headless browser engine.
10
+ *
11
+ * Uses an environment-aware strategy:
12
+ * - Node.js: Uses Puppeteer (peer dependency) for server-side rendering.
13
+ * - Browser: Leverages native browser print capabilities.
14
+ */
15
+ class PdfGenerator extends BaseGenerator_js_1.BaseGenerator {
16
+ constructor(ast, config) {
17
+ super('pdf', ast, config);
18
+ }
19
+ async generate() {
20
+ // Step 1: Generate high-fidelity HTML as the source for PDF rendering
21
+ // We reuse the current configuration but ensure standalone mode is on for HTML
22
+ const htmlGenerator = new HtmlGenerator_js_1.HtmlGenerator(this.ast, {
23
+ ...this.config,
24
+ htmlConfig: { ...this.config.htmlConfig, standalone: true },
25
+ mdConfig: undefined,
26
+ chunksConfig: undefined,
27
+ csvConfig: undefined,
28
+ textConfig: undefined,
29
+ pdfConfig: undefined,
30
+ rtfConfig: undefined,
31
+ });
32
+ const htmlResult = await htmlGenerator.generate();
33
+ const html = typeof htmlResult.value === 'string' ? htmlResult.value : '';
34
+ // Step 2: Render to PDF based on environment
35
+ if (envUtils_js_1.isBrowser) {
36
+ return this.generateInBrowser(html);
37
+ }
38
+ else {
39
+ return this.generateInNode(html);
40
+ }
41
+ }
42
+ /**
43
+ * Node.js implementation using Puppeteer.
44
+ * Uses dynamic import to avoid bundling puppeteer into the library core.
45
+ */
46
+ async generateInNode(html) {
47
+ try {
48
+ // Dynamic import for peer dependency
49
+ // @ts-ignore
50
+ const puppeteerModule = await import('puppeteer');
51
+ const puppeteer = puppeteerModule.default || puppeteerModule;
52
+ const launchOptions = { ...this.config.pdfConfig.launchOptions };
53
+ // Handle Apple Silicon / Rosetta performance warning and binary detection
54
+ const isMac = process.platform === 'darwin';
55
+ const isX64 = process.arch === 'x64';
56
+ let isRosetta = false;
57
+ if (isMac && isX64) {
58
+ try {
59
+ const { execSync } = await import('child_process');
60
+ isRosetta = execSync('sysctl -n hw.optional.arm64', { stdio: 'pipe' }).toString().trim() === '1';
61
+ }
62
+ catch (e) {
63
+ // Ignore errors in detection
64
+ }
65
+ }
66
+ if (isRosetta) {
67
+ this.warn(types_js_1.OfficeWarningType.PERFORMANCE_TIP, "You are running on Apple Silicon using an x64 Node.js installation. PDF generation will be significantly faster (avoiding Rosetta translation) if you switch to a native arm64 Node.js version.");
68
+ }
69
+ // Note: We are no longer suppressing the Puppeteer 'Degraded performance' warning here
70
+ // to ensure transparency about the environment state. Programmatically fixing this
71
+ // would require force-downloading a ~300MB arm64 browser binary or switching to
72
+ // a system-installed Chrome, both of which are too intrusive for a library.
73
+ const browser = await puppeteer.launch(launchOptions);
74
+ const page = await browser.newPage();
75
+ // Set content and wait for network/assets to load
76
+ await page.setContent(html, { waitUntil: 'networkidle0' });
77
+ const pdfConfig = this.config.pdfConfig;
78
+ const pdfBuffer = await page.pdf({
79
+ format: pdfConfig.format,
80
+ width: pdfConfig.width,
81
+ height: pdfConfig.height,
82
+ landscape: pdfConfig.landscape,
83
+ printBackground: pdfConfig.printBackground,
84
+ scale: pdfConfig.scale,
85
+ margin: pdfConfig.margin,
86
+ displayHeaderFooter: pdfConfig.displayHeaderFooter,
87
+ headerTemplate: pdfConfig.headerTemplate,
88
+ footerTemplate: pdfConfig.footerTemplate,
89
+ });
90
+ await browser.close();
91
+ return {
92
+ value: new Uint8Array(pdfBuffer),
93
+ messages: this.messages
94
+ };
95
+ }
96
+ catch (err) {
97
+ this.warn(types_js_1.OfficeWarningType.DEPENDENCY_LOAD_FAILED, `puppeteer. Please install it with 'npm install puppeteer'. Error: ${err.message}`);
98
+ return {
99
+ value: '',
100
+ messages: this.messages
101
+ };
102
+ }
103
+ }
104
+ /**
105
+ * Browser implementation using hidden iframe and native print.
106
+ */
107
+ async generateInBrowser(html) {
108
+ this.warn(types_js_1.OfficeWarningType.BROWSER_GENERATION_LIMITATION, "Browser-based PDF generation triggered. For automated 'Save as PDF' without user interaction, we recommend using 'html2pdf.js' as a custom generator hook.");
109
+ // In a browser environment, we return the HTML and suggest using window.print()
110
+ // Or we could trigger a print dialog immediately if desired,
111
+ // but returning the string allows the user to decide where to inject it.
112
+ return {
113
+ value: html,
114
+ messages: this.messages
115
+ };
116
+ }
117
+ }
118
+ exports.PdfGenerator = PdfGenerator;