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.
- package/README.md +284 -86
- package/dist/OfficeConverter.d.ts +46 -0
- package/dist/OfficeConverter.js +72 -0
- package/dist/OfficeGenerator.d.ts +19 -0
- package/dist/OfficeGenerator.js +48 -0
- package/dist/OfficeParser.d.ts +6 -0
- package/dist/OfficeParser.js +55 -28
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +107 -22
- package/dist/defaults.d.ts +41 -0
- package/dist/defaults.js +172 -0
- package/dist/generators/BaseGenerator.d.ts +58 -0
- package/dist/generators/BaseGenerator.js +107 -0
- package/dist/generators/ChunkingGenerator.d.ts +81 -0
- package/dist/generators/ChunkingGenerator.js +683 -0
- package/dist/generators/CsvGenerator.d.ts +30 -0
- package/dist/generators/CsvGenerator.js +233 -0
- package/dist/generators/HtmlGenerator.d.ts +37 -0
- package/dist/generators/HtmlGenerator.js +1013 -0
- package/dist/generators/MarkdownGenerator.d.ts +59 -0
- package/dist/generators/MarkdownGenerator.js +481 -0
- package/dist/generators/PdfGenerator.d.ts +22 -0
- package/dist/generators/PdfGenerator.js +118 -0
- package/dist/generators/RtfGenerator.d.ts +15 -0
- package/dist/generators/RtfGenerator.js +208 -0
- package/dist/generators/TextGenerator.d.ts +13 -0
- package/dist/generators/TextGenerator.js +108 -0
- package/dist/index.d.ts +11 -3
- package/dist/index.js +17 -2
- package/dist/index.mjs +2 -2
- package/dist/officeparser.browser.d.ts +878 -5
- package/dist/officeparser.browser.iife.js +703 -49
- package/dist/officeparser.browser.mjs +703 -49
- package/dist/parsers/CsvParser.d.ts +9 -0
- package/dist/parsers/CsvParser.js +110 -0
- package/dist/parsers/ExcelParser.d.ts +2 -2
- package/dist/parsers/ExcelParser.js +145 -114
- package/dist/parsers/HtmlParser.d.ts +2 -0
- package/dist/parsers/HtmlParser.js +539 -0
- package/dist/parsers/MarkdownParser.d.ts +2 -0
- package/dist/parsers/MarkdownParser.js +360 -0
- package/dist/parsers/OpenOfficeParser.d.ts +2 -2
- package/dist/parsers/OpenOfficeParser.js +237 -128
- package/dist/parsers/PdfParser.d.ts +2 -2
- package/dist/parsers/PdfParser.js +52 -49
- package/dist/parsers/PowerPointParser.d.ts +2 -2
- package/dist/parsers/PowerPointParser.js +132 -123
- package/dist/parsers/RtfParser.d.ts +22 -2
- package/dist/parsers/RtfParser.js +1398 -1282
- package/dist/parsers/WordParser.d.ts +3 -2
- package/dist/parsers/WordParser.js +333 -115
- package/dist/sbom.cdx.json +103 -103
- package/dist/types.d.ts +833 -5
- package/dist/types.js +71 -0
- package/dist/utils/astUtils.d.ts +16 -0
- package/dist/utils/astUtils.js +32 -0
- package/dist/utils/configUtils.d.ts +26 -0
- package/dist/utils/configUtils.js +140 -0
- package/dist/utils/envUtils.js +56 -2
- package/dist/utils/errorUtils.d.ts +17 -29
- package/dist/utils/errorUtils.js +109 -52
- package/dist/utils/moduleLoader.js +15 -9
- package/dist/utils/ocrUtils.js +2 -1
- package/dist/utils/sheetUtils.d.ts +7 -0
- package/dist/utils/sheetUtils.js +35 -0
- package/dist/utils/styleMapper.d.ts +36 -0
- package/dist/utils/styleMapper.js +224 -0
- package/dist/utils/xmlUtils.d.ts +0 -8
- package/dist/utils/xmlUtils.js +2 -1
- 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' : ''}`;
|
|
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;
|