officeparser 7.4.0 → 7.5.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 +48 -4
- package/dist/generators/BaseGenerator.d.ts +15 -0
- package/dist/generators/BaseGenerator.js +31 -0
- package/dist/generators/HtmlGenerator.d.ts +9 -0
- package/dist/generators/HtmlGenerator.js +34 -4
- package/dist/generators/MarkdownGenerator.d.ts +13 -0
- package/dist/generators/MarkdownGenerator.js +115 -41
- package/dist/generators/RtfGenerator.d.ts +13 -0
- package/dist/generators/RtfGenerator.js +23 -2
- package/dist/officeparser.browser.iife.js +148 -148
- package/dist/officeparser.browser.mjs +186 -186
- package/dist/officeparser.browser.slim.iife.js +153 -153
- package/dist/officeparser.browser.slim.mjs +153 -153
- package/dist/parsers/HtmlParser.js +39 -0
- package/dist/parsers/OpenOfficeParser.js +122 -164
- package/dist/parsers/PowerPointParser.js +27 -6
- package/dist/parsers/WordParser.js +21 -0
- package/dist/sbom.cdx.json +92 -92
- package/dist/utils/mathUtils.d.ts +42 -0
- package/dist/utils/mathUtils.js +385 -0
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -128,7 +128,7 @@ npx officeparser my_document --fileType=docx --to=json
|
|
|
128
128
|
| `--includeRawContent` | boolean | `false` | Include raw XML/RTF in nodes |
|
|
129
129
|
| `--serializeRawContent` | boolean | `true` | Include stringified XML in metadata |
|
|
130
130
|
| `--preserveXmlWhitespace` | boolean | `false` | Keep raw formatting space |
|
|
131
|
-
| `--includeBreakNodes` | boolean | `false` | Include break nodes (DOCX
|
|
131
|
+
| `--includeBreakNodes` | boolean | `false` | Include break nodes (DOCX and ODF) |
|
|
132
132
|
| `--verbose` | boolean | `false` | Show full error stack traces and warning logs |
|
|
133
133
|
| `--includeFormatting` | boolean | `true` | Include formatting style map matching |
|
|
134
134
|
| `--renderMetadata` | boolean | `false` | Render metadata as visible content in the generated output |
|
|
@@ -608,7 +608,14 @@ formatting: {
|
|
|
608
608
|
}
|
|
609
609
|
```
|
|
610
610
|
|
|
611
|
-
|
|
611
|
+
> [!NOTE]
|
|
612
|
+
> On a **content node**, an absent flag and `false` mean the same thing — the flag is simply not
|
|
613
|
+
> applied. On **`ast.metadata.styleMap`**, they differ: an absent flag means the style says nothing
|
|
614
|
+
> about that property (so it inherits), while `false` means the style explicitly turns it off
|
|
615
|
+
> (ODF's `fo:font-weight="normal"`, DOCX's `<w:b w:val="0"/>`). Code resolving inheritance itself
|
|
616
|
+
> must test `=== undefined`, not truthiness, or it will treat "explicitly off" as "unspecified".
|
|
617
|
+
|
|
618
|
+
### 6. Break Nodes (DOCX and ODF)
|
|
612
619
|
|
|
613
620
|
When `includeBreakNodes: true`, break elements appear as nodes:
|
|
614
621
|
|
|
@@ -623,6 +630,43 @@ Break Node (type: 'break')
|
|
|
623
630
|
> [!NOTE]
|
|
624
631
|
> Break nodes have no `text` property, but `ast.toText()` and `ast.to('text')` automatically convert them to the configured newline delimiter.
|
|
625
632
|
|
|
633
|
+
> [!NOTE]
|
|
634
|
+
> DOCX writes breaks inline (`w:br`/`w:cr`), so they land as children of the paragraph. ODF instead
|
|
635
|
+
> carries page and column breaks on the paragraph *style* (`fo:break-before`/`fo:break-after`), so those
|
|
636
|
+
> are emitted as siblings around the paragraph rather than inside it. `<text:soft-page-break/>` maps onto
|
|
637
|
+
> `lastRenderedPage`, the same type as DOCX's `w:lastRenderedPageBreak`.
|
|
638
|
+
|
|
639
|
+
### 6b. Equations
|
|
640
|
+
|
|
641
|
+
Equations are extracted from every format that can carry them and normalized to **LaTeX**, so a
|
|
642
|
+
formula means the same thing whichever format it arrived in:
|
|
643
|
+
|
|
644
|
+
| Source format | Markup in the file |
|
|
645
|
+
|---|---|
|
|
646
|
+
| DOCX, PPTX | OOXML `<m:oMath>` / `<m:oMathPara>` |
|
|
647
|
+
| ODT, ODP, ODS | MathML inside the embedded formula object |
|
|
648
|
+
| HTML, EPUB | native MathML `<math>` |
|
|
649
|
+
| Markdown | `$inline$` / `$$block$$` |
|
|
650
|
+
|
|
651
|
+
They all land as the same node:
|
|
652
|
+
|
|
653
|
+
```text
|
|
654
|
+
Code Node (type: 'code')
|
|
655
|
+
├── text: '\\frac{1}{2}' // LaTeX, whatever the source markup was
|
|
656
|
+
└── metadata: { math: 'inline' | 'block' }
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
Fractions, sub/superscripts, radicals, delimiters, n-ary operators (sums, integrals), named
|
|
660
|
+
functions, accents, bars, matrices and math alphabets (`ℝ`, `𝒜`, …) are all preserved. When a
|
|
661
|
+
document supplies its own TeX source in an `<annotation encoding="application/x-tex">`, that is
|
|
662
|
+
used verbatim in preference to anything reconstructed from the presentation markup.
|
|
663
|
+
|
|
664
|
+
> [!NOTE]
|
|
665
|
+
> Equation text is *structure*, not prose: a fraction whose numerator and denominator are simply
|
|
666
|
+
> concatenated reads as a different number rather than as obviously-missing content. Consumers that
|
|
667
|
+
> index document text should treat `code` nodes carrying `math` as opaque LaTeX rather than
|
|
668
|
+
> splitting them as words.
|
|
669
|
+
|
|
626
670
|
### 7. Document Metadata
|
|
627
671
|
|
|
628
672
|
```ts
|
|
@@ -688,7 +732,7 @@ idempotent and `.md → AST → HTML → AST → .md` survives unchanged.
|
|
|
688
732
|
| Attribute lists | `{width=50% .centered}` | `ImageMetadata.width` / `.align`, `TableMetadata.align` |
|
|
689
733
|
| Citations | `[@smith2024]` | `TextMetadata.citationKey` |
|
|
690
734
|
| Wikilinks | `[[Page]]` / `[[Page\|Alias]]` | `TextMetadata.wikilink`, `.link`, `.linkType` |
|
|
691
|
-
| Inline/block math | `$E=mc^2$` / `` $$...$$ `` | `
|
|
735
|
+
| Inline/block math | `$E=mc^2$` / `` $$...$$ `` | `type: 'code'`, `CodeMetadata.math` (`'inline' \| 'block'`) |
|
|
692
736
|
| Frontmatter arrays | `tags: [a, b]` or `tags: ["a","b"]` | Real array in `metadata.customProperties`/`nativeProperties` |
|
|
693
737
|
| MDX components (import-only) | `<Component prop="x">...</Component>` | Stripped; inner Markdown is kept. Never generated back. |
|
|
694
738
|
|
|
@@ -888,7 +932,7 @@ Pass as the second argument to `parseOffice(file, config)`.
|
|
|
888
932
|
| `includeRawContent` | `boolean` | `false` | Attach raw XML/RTF source to each node |
|
|
889
933
|
| `serializeRawContent` | `boolean` | `true` | Re-serialize XML to clean strings (only if `includeRawContent: true`) |
|
|
890
934
|
| `preserveXmlWhitespace` | `boolean` | `false` | Preserve original XML whitespace during serialization |
|
|
891
|
-
| `includeBreakNodes` | `boolean` | `false` | Include `w:br
|
|
935
|
+
| `includeBreakNodes` | `boolean` | `false` | Include typed break nodes: DOCX `w:br`/`w:cr`, ODF `fo:break-before`/`fo:break-after` and `text:soft-page-break` |
|
|
892
936
|
| `ignoreInternalLinks` | `boolean` | `false` | Strip bookmarks and internal cross-references from AST |
|
|
893
937
|
| `fileType` | `SupportedFileType \| null` | `null` | **Required for text-based binary data** (`'md'`, `'html'`, `'csv'`) as these lack magic bytes. |
|
|
894
938
|
| `csvDelimiter` | `string` | `','` | Input delimiter when parsing CSV files |
|
|
@@ -81,6 +81,21 @@ export declare abstract class BaseGenerator<D extends UniversalGeneratorFormat =
|
|
|
81
81
|
* hasn't already been claimed; otherwise a sequential counter guarantees uniqueness.
|
|
82
82
|
*/
|
|
83
83
|
protected getFootnoteKey(note: OfficeContentNode): string;
|
|
84
|
+
/**
|
|
85
|
+
* True when every content-bearing text descendant satisfies `test` - i.e. the property is
|
|
86
|
+
* uniform across the whole node and therefore says nothing the node type does not already say.
|
|
87
|
+
*
|
|
88
|
+
* Used to decide whether a heading's or header row's inherited formatting can be dropped. The
|
|
89
|
+
* distinction matters: an ODF heading whose paragraph style is bold and 14pt yields a heading
|
|
90
|
+
* where *every* run is bold and 14pt, and re-emitting that gives `# **Heading**` in Markdown
|
|
91
|
+
* and, worse in RTF/HTML, an inner font-size that overrides the heading's own and visibly
|
|
92
|
+
* shrinks it. But `# Normal **Bold** Normal` is an author contrasting one word against the
|
|
93
|
+
* rest, and dropping that would discard real meaning. Only the uniform case is safe.
|
|
94
|
+
*
|
|
95
|
+
* Returns false when there is no text to judge, so an empty or image-only node never triggers
|
|
96
|
+
* suppression.
|
|
97
|
+
*/
|
|
98
|
+
protected hasUniformFormatting(node: OfficeContentNode, test: (formatting: OfficeContentNode['formatting']) => boolean): boolean;
|
|
84
99
|
/**
|
|
85
100
|
* Recursively extracts plain text from a node and its children.
|
|
86
101
|
*/
|
|
@@ -187,6 +187,37 @@ class BaseGenerator {
|
|
|
187
187
|
this.noteFootnoteKeys.set(note, key);
|
|
188
188
|
return key;
|
|
189
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* True when every content-bearing text descendant satisfies `test` - i.e. the property is
|
|
192
|
+
* uniform across the whole node and therefore says nothing the node type does not already say.
|
|
193
|
+
*
|
|
194
|
+
* Used to decide whether a heading's or header row's inherited formatting can be dropped. The
|
|
195
|
+
* distinction matters: an ODF heading whose paragraph style is bold and 14pt yields a heading
|
|
196
|
+
* where *every* run is bold and 14pt, and re-emitting that gives `# **Heading**` in Markdown
|
|
197
|
+
* and, worse in RTF/HTML, an inner font-size that overrides the heading's own and visibly
|
|
198
|
+
* shrinks it. But `# Normal **Bold** Normal` is an author contrasting one word against the
|
|
199
|
+
* rest, and dropping that would discard real meaning. Only the uniform case is safe.
|
|
200
|
+
*
|
|
201
|
+
* Returns false when there is no text to judge, so an empty or image-only node never triggers
|
|
202
|
+
* suppression.
|
|
203
|
+
*/
|
|
204
|
+
hasUniformFormatting(node, test) {
|
|
205
|
+
let sawText = false;
|
|
206
|
+
const walk = (n) => {
|
|
207
|
+
if (n.type === 'text') {
|
|
208
|
+
// Whitespace-only runs carry no visible formatting either way, so they neither
|
|
209
|
+
// count as evidence nor veto - otherwise a stray unformatted space between two
|
|
210
|
+
// bold runs would defeat the check on almost every real heading.
|
|
211
|
+
if (!(n.text || '').trim())
|
|
212
|
+
return true;
|
|
213
|
+
sawText = true;
|
|
214
|
+
return test(n.formatting);
|
|
215
|
+
}
|
|
216
|
+
return (n.children ?? []).every(walk);
|
|
217
|
+
};
|
|
218
|
+
const uniform = (node.children ?? []).every(walk);
|
|
219
|
+
return sawText && uniform;
|
|
220
|
+
}
|
|
190
221
|
/**
|
|
191
222
|
* Recursively extracts plain text from a node and its children.
|
|
192
223
|
*/
|
|
@@ -6,6 +6,14 @@ import { BaseGenerator } from './BaseGenerator.js';
|
|
|
6
6
|
export declare class HtmlGenerator extends BaseGenerator<'html'> {
|
|
7
7
|
private chartCounter;
|
|
8
8
|
private isSpreadsheetMode;
|
|
9
|
+
/**
|
|
10
|
+
* Set while rendering a heading's children, so `formatText` can drop the run-level bold and
|
|
11
|
+
* font-size the `<hN>` already establishes. See the note there, and the identical flag in
|
|
12
|
+
* `RtfGenerator`, where the same inherited size actively shrinks the heading.
|
|
13
|
+
*/
|
|
14
|
+
private inHeading;
|
|
15
|
+
/** As `inHeading`, but for the inherited font size - see `hasUniformFormatting`. */
|
|
16
|
+
private headingUniformSize;
|
|
9
17
|
constructor(ast: OfficeParserAST, config?: GeneratorConfig<'html'>);
|
|
10
18
|
/**
|
|
11
19
|
* Generates HTML string from the provided AST.
|
|
@@ -24,6 +32,7 @@ export declare class HtmlGenerator extends BaseGenerator<'html'> {
|
|
|
24
32
|
*/
|
|
25
33
|
private tableNestingLevel;
|
|
26
34
|
protected processNodeRecursive(node: OfficeContentNode, processor: (node: OfficeContentNode, childrenOutput: string) => string | Promise<string>, override?: string | boolean | void): Promise<string>;
|
|
35
|
+
private processNodeRecursiveInner;
|
|
27
36
|
/**
|
|
28
37
|
* Internal processor for individual nodes.
|
|
29
38
|
*/
|
|
@@ -88,6 +88,14 @@ function resolveStandalone(standalone) {
|
|
|
88
88
|
class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
89
89
|
chartCounter = 0;
|
|
90
90
|
isSpreadsheetMode = false;
|
|
91
|
+
/**
|
|
92
|
+
* Set while rendering a heading's children, so `formatText` can drop the run-level bold and
|
|
93
|
+
* font-size the `<hN>` already establishes. See the note there, and the identical flag in
|
|
94
|
+
* `RtfGenerator`, where the same inherited size actively shrinks the heading.
|
|
95
|
+
*/
|
|
96
|
+
inHeading = false;
|
|
97
|
+
/** As `inHeading`, but for the inherited font size - see `hasUniformFormatting`. */
|
|
98
|
+
headingUniformSize = false;
|
|
91
99
|
constructor(ast, config) {
|
|
92
100
|
super('html', ast, config);
|
|
93
101
|
}
|
|
@@ -502,6 +510,21 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
502
510
|
// method entirely, so without repeating the check here the signal would be silently
|
|
503
511
|
// inert for this generator - which is exactly how it was missed.
|
|
504
512
|
(0, errorUtils_js_1.checkAbortSignal)(this.config.abortSignal);
|
|
513
|
+
const wasInHeading = this.inHeading;
|
|
514
|
+
const wasHeadingSize = this.headingUniformSize;
|
|
515
|
+
if (node.type === 'heading') {
|
|
516
|
+
this.inHeading = this.hasUniformFormatting(node, f => f?.bold === true);
|
|
517
|
+
this.headingUniformSize = this.hasUniformFormatting(node, f => !!f?.size);
|
|
518
|
+
}
|
|
519
|
+
try {
|
|
520
|
+
return await this.processNodeRecursiveInner(node, processor, override);
|
|
521
|
+
}
|
|
522
|
+
finally {
|
|
523
|
+
this.inHeading = wasInHeading;
|
|
524
|
+
this.headingUniformSize = wasHeadingSize;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
async processNodeRecursiveInner(node, processor, override) {
|
|
505
528
|
// Use pre-evaluated override if provided, otherwise call handleOnNode
|
|
506
529
|
const actualOverride = override !== undefined ? override : await this.handleOnNode(node);
|
|
507
530
|
// Returning false skips the node and its children
|
|
@@ -1076,7 +1099,12 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
1076
1099
|
let result = this.escape(text);
|
|
1077
1100
|
const f = node.formatting;
|
|
1078
1101
|
if (this.config.includeFormatting && f) {
|
|
1079
|
-
|
|
1102
|
+
// Inside an `<hN>`, the heading's own styling is authoritative. A run that also carries
|
|
1103
|
+
// bold and a font size - the normal case for ODF, where a heading's paragraph style is
|
|
1104
|
+
// inherited by its runs - would wrap the text in `<b>` the heading already implies and,
|
|
1105
|
+
// worse, in a `<span style="font-size: 14pt">` that *shrinks* the heading to the size
|
|
1106
|
+
// its paragraph style happened to name. See the same suppression in RtfGenerator.
|
|
1107
|
+
if (f.bold && !this.inHeading)
|
|
1080
1108
|
result = `<b>${result}</b>`;
|
|
1081
1109
|
if (f.italic)
|
|
1082
1110
|
result = `<i>${result}</i>`;
|
|
@@ -1088,7 +1116,9 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
1088
1116
|
result = `<sub>${result}</sub>`;
|
|
1089
1117
|
if (f.superscript)
|
|
1090
1118
|
result = `<sup>${result}</sup>`;
|
|
1091
|
-
const styles = this.
|
|
1119
|
+
const styles = this.headingUniformSize
|
|
1120
|
+
? this.getInlineStyles(node, { skipFontSize: true })
|
|
1121
|
+
: this.getInlineStyles(node);
|
|
1092
1122
|
if (styles) {
|
|
1093
1123
|
result = `<span style="${styles}">${result}</span>`;
|
|
1094
1124
|
}
|
|
@@ -1120,7 +1150,7 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
1120
1150
|
}
|
|
1121
1151
|
return result;
|
|
1122
1152
|
}
|
|
1123
|
-
getInlineStyles(node) {
|
|
1153
|
+
getInlineStyles(node, options = {}) {
|
|
1124
1154
|
const styles = [];
|
|
1125
1155
|
// Colors/sizes/fonts/alignments are free strings from an untrusted document;
|
|
1126
1156
|
// run each through sanitizeCssValue so it can't break out of the style="" attribute
|
|
@@ -1154,7 +1184,7 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
1154
1184
|
pushSafe('color', f.color);
|
|
1155
1185
|
if (f.backgroundColor)
|
|
1156
1186
|
pushSafe('background-color', f.backgroundColor);
|
|
1157
|
-
if (f.size)
|
|
1187
|
+
if (f.size && !options.skipFontSize)
|
|
1158
1188
|
pushSafe('font-size', f.size);
|
|
1159
1189
|
if (f.font) {
|
|
1160
1190
|
const safeFont = (0, sanitize_js_1.sanitizeCssValue)(f.font);
|
|
@@ -31,6 +31,18 @@ import { BaseGenerator } from './BaseGenerator.js';
|
|
|
31
31
|
*/
|
|
32
32
|
export declare class MarkdownGenerator extends BaseGenerator<'md'> {
|
|
33
33
|
private isInsideTable;
|
|
34
|
+
/**
|
|
35
|
+
* Set while rendering the children of a heading, or the cells of a table's header row.
|
|
36
|
+
*
|
|
37
|
+
* Markdown already conveys "this is a heading" with `#` and "this is a header row" with the
|
|
38
|
+
* separator line, so a run inside one that also carries bold - the normal case for ODF, whose
|
|
39
|
+
* heading and header-row paragraph styles are bold and are now inherited by their runs - would
|
|
40
|
+
* render as `# **Heading**` and `| **ITEM** |`. That is redundant rather than wrong, but it
|
|
41
|
+
* also round-trips back into bold text nodes nested inside a heading, so the noise compounds
|
|
42
|
+
* on every parse/generate cycle. Emphasis the node type already implies is dropped; every
|
|
43
|
+
* other formatting flag still comes through.
|
|
44
|
+
*/
|
|
45
|
+
private inImplicitBold;
|
|
34
46
|
private hoistedContent;
|
|
35
47
|
private collectedAbbreviations;
|
|
36
48
|
private resolvedDialect;
|
|
@@ -72,6 +84,7 @@ export declare class MarkdownGenerator extends BaseGenerator<'md'> {
|
|
|
72
84
|
private optimizeNodes;
|
|
73
85
|
private areFormattingEqual;
|
|
74
86
|
private renderMarkdownTable;
|
|
87
|
+
private collectNotesFrom;
|
|
75
88
|
private renderMarkdownTableInternal;
|
|
76
89
|
private hasNestedTable;
|
|
77
90
|
private hasColspanOrRowspan;
|
|
@@ -113,6 +113,18 @@ function resolveFallbackToHtml(fallbackToHtml) {
|
|
|
113
113
|
*/
|
|
114
114
|
class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
115
115
|
isInsideTable = false;
|
|
116
|
+
/**
|
|
117
|
+
* Set while rendering the children of a heading, or the cells of a table's header row.
|
|
118
|
+
*
|
|
119
|
+
* Markdown already conveys "this is a heading" with `#` and "this is a header row" with the
|
|
120
|
+
* separator line, so a run inside one that also carries bold - the normal case for ODF, whose
|
|
121
|
+
* heading and header-row paragraph styles are bold and are now inherited by their runs - would
|
|
122
|
+
* render as `# **Heading**` and `| **ITEM** |`. That is redundant rather than wrong, but it
|
|
123
|
+
* also round-trips back into bold text nodes nested inside a heading, so the noise compounds
|
|
124
|
+
* on every parse/generate cycle. Emphasis the node type already implies is dropped; every
|
|
125
|
+
* other formatting flag still comes through.
|
|
126
|
+
*/
|
|
127
|
+
inImplicitBold = false;
|
|
116
128
|
hoistedContent = [];
|
|
117
129
|
collectedAbbreviations = new Map();
|
|
118
130
|
resolvedDialect;
|
|
@@ -245,7 +257,7 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
245
257
|
let text = (0, sanitize_js_1.markdownEscapeText)(node.text || '');
|
|
246
258
|
if (this.config.includeFormatting && node.formatting) {
|
|
247
259
|
const emphasisAsterisk = this.resolvedDialect.emphasisMarker === 'asterisk';
|
|
248
|
-
if (node.formatting.bold)
|
|
260
|
+
if (node.formatting.bold && !this.inImplicitBold)
|
|
249
261
|
text = emphasisAsterisk ? `**${text}**` : `__${text}__`;
|
|
250
262
|
if (node.formatting.italic)
|
|
251
263
|
text = emphasisAsterisk ? `*${text}*` : `_${text}_`;
|
|
@@ -544,7 +556,12 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
544
556
|
if (!this.resolvedDialect.definitionLists)
|
|
545
557
|
return `${childrenOutput}\n\n`;
|
|
546
558
|
return `: ${childrenOutput}\n`;
|
|
547
|
-
|
|
559
|
+
case 'chart':
|
|
560
|
+
case 'drawing':
|
|
561
|
+
case 'comment':
|
|
562
|
+
case 'header':
|
|
563
|
+
case 'footer':
|
|
564
|
+
case 'slideMaster':
|
|
548
565
|
return childrenOutput;
|
|
549
566
|
}
|
|
550
567
|
};
|
|
@@ -612,14 +629,19 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
612
629
|
if (typeof override === 'string') {
|
|
613
630
|
return override;
|
|
614
631
|
}
|
|
632
|
+
const walkedByProcessor = node.type === 'table' || node.type === 'sheet';
|
|
633
|
+
const wasInImplicitBold = this.inImplicitBold;
|
|
634
|
+
if (node.type === 'heading' && this.hasUniformFormatting(node, f => f?.bold === true))
|
|
635
|
+
this.inImplicitBold = true;
|
|
615
636
|
let childrenOutput = '';
|
|
616
|
-
if (node.children && node.children.length > 0) {
|
|
637
|
+
if (!walkedByProcessor && node.children && node.children.length > 0) {
|
|
617
638
|
// Optimization: Merge adjacent text nodes with identical formatting
|
|
618
639
|
const optimizedChildren = this.optimizeNodes(node.children);
|
|
619
640
|
for (const child of optimizedChildren) {
|
|
620
641
|
childrenOutput += await this.processNodeRecursive(child, processor);
|
|
621
642
|
}
|
|
622
643
|
}
|
|
644
|
+
this.inImplicitBold = wasInImplicitBold;
|
|
623
645
|
// When the dialect has no footnote syntax, a footnote/endnote is inlined right at its
|
|
624
646
|
// reference point instead (see below) - so it must not also be collected into the
|
|
625
647
|
// end-of-document "### Notes" section, or its content would be duplicated.
|
|
@@ -627,11 +649,7 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
627
649
|
const meta = note.metadata;
|
|
628
650
|
return (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') && !this.resolvedDialect.footnotes;
|
|
629
651
|
};
|
|
630
|
-
|
|
631
|
-
if (node.type !== 'slide') {
|
|
632
|
-
this.collectedNotes.push(...node.notes.filter(note => !isInlinedFootnote(note)));
|
|
633
|
-
}
|
|
634
|
-
}
|
|
652
|
+
this.collectNotesFrom(node);
|
|
635
653
|
let result = await processor(node, childrenOutput);
|
|
636
654
|
if (node.type === 'slide' && node.notes && node.notes.length > 0) {
|
|
637
655
|
for (const note of node.notes) {
|
|
@@ -732,46 +750,73 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
732
750
|
this.isInsideTable = false;
|
|
733
751
|
return result;
|
|
734
752
|
}
|
|
753
|
+
collectNotesFrom(node) {
|
|
754
|
+
if (!node.notes || node.notes.length === 0)
|
|
755
|
+
return;
|
|
756
|
+
if (node.type === 'slide')
|
|
757
|
+
return;
|
|
758
|
+
const isInlinedFootnote = (note) => {
|
|
759
|
+
const meta = note.metadata;
|
|
760
|
+
return (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') && !this.resolvedDialect.footnotes;
|
|
761
|
+
};
|
|
762
|
+
this.collectedNotes.push(...node.notes.filter(note => !isInlinedFootnote(note)));
|
|
763
|
+
}
|
|
735
764
|
async renderMarkdownTableInternal(node, processor) {
|
|
736
765
|
let tableOutput = '';
|
|
737
766
|
let maxCols = 0;
|
|
738
767
|
// First pass: Process rows and determine max columns (accounting for colspans)
|
|
739
768
|
const processedRows = [];
|
|
740
769
|
for (const rowNode of (node.children ?? [])) {
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
if (
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
770
|
+
// The first row becomes the header row - the `| --- |` separator emitted below marks
|
|
771
|
+
// it as such - so bold inside it is already implied.
|
|
772
|
+
const wasInImplicitBold = this.inImplicitBold;
|
|
773
|
+
if (processedRows.length === 0 && this.hasUniformFormatting(rowNode, f => f?.bold === true))
|
|
774
|
+
this.inImplicitBold = true;
|
|
775
|
+
try {
|
|
776
|
+
const override = await this.handleOnNode(rowNode);
|
|
777
|
+
if (override === false)
|
|
778
|
+
continue;
|
|
779
|
+
if (typeof override === 'string') {
|
|
780
|
+
processedRows.push([override]);
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
// After the override checks, not before: a row the caller skipped via `onNode` must not
|
|
784
|
+
// still contribute its footnote to the end-of-document Notes section, where it would
|
|
785
|
+
// appear with no `[^id]` marker anywhere in the document pointing at it.
|
|
786
|
+
// `renderTableAsHtml` gets this right by returning early, so collecting here keeps the
|
|
787
|
+
// pipe and HTML paths agreeing on what a skipped row means.
|
|
788
|
+
this.collectNotesFrom(rowNode);
|
|
789
|
+
const rowCells = [];
|
|
790
|
+
let lastCol = -1;
|
|
791
|
+
if (rowNode.children) {
|
|
792
|
+
const cellNodes = rowNode.children.filter(c => c.type === 'cell');
|
|
793
|
+
for (const cellNode of cellNodes) {
|
|
794
|
+
const currentCol = cellNode.metadata?.col ?? (lastCol + 1);
|
|
795
|
+
// Fill gaps with empty cells
|
|
796
|
+
while (lastCol < currentCol - 1) {
|
|
797
|
+
rowCells.push(' ');
|
|
798
|
+
lastCol++;
|
|
799
|
+
}
|
|
800
|
+
// Process cell content
|
|
801
|
+
let cellContent = await this.processNodeRecursive(cellNode, processor);
|
|
802
|
+
// Use <br> fallback only if allowed, otherwise space
|
|
803
|
+
const br = this.resolvedFallbackToHtml.cellLineBreaks ? '<br>' : ' ';
|
|
804
|
+
cellContent = cellContent.trim().replace(/\n+/g, br).replace(/\|/g, '\\|');
|
|
805
|
+
rowCells.push(cellContent);
|
|
806
|
+
// Handle colspan by adding empty cells
|
|
807
|
+
const colSpan = cellNode.metadata?.colSpan || 1;
|
|
808
|
+
for (let i = 1; i < colSpan; i++) {
|
|
809
|
+
rowCells.push(' ');
|
|
810
|
+
}
|
|
811
|
+
lastCol = currentCol + colSpan - 1;
|
|
769
812
|
}
|
|
770
|
-
lastCol = currentCol + colSpan - 1;
|
|
771
813
|
}
|
|
814
|
+
processedRows.push(rowCells);
|
|
815
|
+
maxCols = Math.max(maxCols, rowCells.length);
|
|
816
|
+
}
|
|
817
|
+
finally {
|
|
818
|
+
this.inImplicitBold = wasInImplicitBold;
|
|
772
819
|
}
|
|
773
|
-
processedRows.push(rowCells);
|
|
774
|
-
maxCols = Math.max(maxCols, rowCells.length);
|
|
775
820
|
}
|
|
776
821
|
// Second pass: Build table string with separator
|
|
777
822
|
for (let i = 0; i < processedRows.length; i++) {
|
|
@@ -842,6 +887,7 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
842
887
|
return `<table${alignAttr}>\n${rows}</table>\n`;
|
|
843
888
|
}
|
|
844
889
|
else if (node.type === 'row') {
|
|
890
|
+
this.collectNotesFrom(node);
|
|
845
891
|
let cells = '';
|
|
846
892
|
if (node.children) {
|
|
847
893
|
for (const cell of node.children) {
|
|
@@ -851,6 +897,7 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
851
897
|
return ` <tr>\n${cells} </tr>\n`;
|
|
852
898
|
}
|
|
853
899
|
else if (node.type === 'cell') {
|
|
900
|
+
this.collectNotesFrom(node);
|
|
854
901
|
const meta = node.metadata;
|
|
855
902
|
const rs = meta?.rowSpan > 1 ? ` rowspan="${meta.rowSpan}"` : '';
|
|
856
903
|
const cs = meta?.colSpan > 1 ? ` colspan="${meta.colSpan}"` : '';
|
|
@@ -861,10 +908,16 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
861
908
|
content += await this.processNodeRecursive(child, async (n, co) => {
|
|
862
909
|
switch (n.type) {
|
|
863
910
|
case 'text': {
|
|
911
|
+
if (n.metadata) {
|
|
912
|
+
const m = n.metadata;
|
|
913
|
+
if (m.abbreviationTitle) {
|
|
914
|
+
this.collectedAbbreviations.set(n.text || '', m.abbreviationTitle);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
864
917
|
// Inside HTML table cells, entity-encode angle brackets so cell
|
|
865
918
|
// text can't inject a raw tag (e.g. </td><script>).
|
|
866
919
|
let text = (0, sanitize_js_1.markdownEscapeText)(n.text || '');
|
|
867
|
-
if (n.formatting?.bold)
|
|
920
|
+
if (n.formatting?.bold && !this.inImplicitBold)
|
|
868
921
|
text = `<b>${text}</b>`;
|
|
869
922
|
if (n.formatting?.italic)
|
|
870
923
|
text = `<i>${text}</i>`;
|
|
@@ -882,7 +935,28 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
882
935
|
return `<h${level}>${co}</h${level}>`;
|
|
883
936
|
}
|
|
884
937
|
case 'table': return await this.renderTableAsHtml(n);
|
|
885
|
-
|
|
938
|
+
case 'list':
|
|
939
|
+
case 'image':
|
|
940
|
+
case 'chart':
|
|
941
|
+
case 'drawing':
|
|
942
|
+
case 'slide':
|
|
943
|
+
case 'note':
|
|
944
|
+
case 'sheet':
|
|
945
|
+
case 'row':
|
|
946
|
+
case 'cell':
|
|
947
|
+
case 'page':
|
|
948
|
+
case 'break':
|
|
949
|
+
case 'code':
|
|
950
|
+
case 'comment':
|
|
951
|
+
case 'header':
|
|
952
|
+
case 'footer':
|
|
953
|
+
case 'slideMaster':
|
|
954
|
+
case 'embed':
|
|
955
|
+
case 'admonition':
|
|
956
|
+
case 'definitionList':
|
|
957
|
+
case 'definitionTerm':
|
|
958
|
+
case 'definitionDescription':
|
|
959
|
+
return co;
|
|
886
960
|
}
|
|
887
961
|
});
|
|
888
962
|
}
|
|
@@ -6,6 +6,19 @@ import { BaseGenerator } from './BaseGenerator.js';
|
|
|
6
6
|
export declare class RtfGenerator extends BaseGenerator<'rtf'> {
|
|
7
7
|
private colorTable;
|
|
8
8
|
private inTable;
|
|
9
|
+
/**
|
|
10
|
+
* Set while rendering a heading's children.
|
|
11
|
+
*
|
|
12
|
+
* A heading emits its own `{\\b\\fs44 ...}` wrapper, so a run inside it that also carries bold
|
|
13
|
+
* and a size - which is now the normal case for ODF, where the heading's paragraph style is
|
|
14
|
+
* inherited by its runs - would emit a nested `\\fs28` that *overrides* the outer `\\fs44`.
|
|
15
|
+
* The heading then renders at the body-text size it was styled with rather than at heading
|
|
16
|
+
* size. Suppressing the inherited weight and size inside a heading keeps the heading's own
|
|
17
|
+
* wrapper authoritative; every other property (colour, font) still comes through.
|
|
18
|
+
*/
|
|
19
|
+
private inHeading;
|
|
20
|
+
/** As `inHeading`, but for the inherited font size - see `hasUniformFormatting`. */
|
|
21
|
+
private headingUniformSize;
|
|
9
22
|
constructor(ast: OfficeParserAST, config?: GeneratorConfig<'rtf'>);
|
|
10
23
|
generate(): Promise<ConversionResult<'rtf'>>;
|
|
11
24
|
protected processNodeRecursive(node: OfficeContentNode, processor: (node: OfficeContentNode, childrenOutput: string) => Promise<string>): Promise<string>;
|
|
@@ -10,6 +10,19 @@ const errorUtils_js_1 = require("../utils/errorUtils.js");
|
|
|
10
10
|
class RtfGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
11
11
|
colorTable = [];
|
|
12
12
|
inTable = false;
|
|
13
|
+
/**
|
|
14
|
+
* Set while rendering a heading's children.
|
|
15
|
+
*
|
|
16
|
+
* A heading emits its own `{\\b\\fs44 ...}` wrapper, so a run inside it that also carries bold
|
|
17
|
+
* and a size - which is now the normal case for ODF, where the heading's paragraph style is
|
|
18
|
+
* inherited by its runs - would emit a nested `\\fs28` that *overrides* the outer `\\fs44`.
|
|
19
|
+
* The heading then renders at the body-text size it was styled with rather than at heading
|
|
20
|
+
* size. Suppressing the inherited weight and size inside a heading keeps the heading's own
|
|
21
|
+
* wrapper authoritative; every other property (colour, font) still comes through.
|
|
22
|
+
*/
|
|
23
|
+
inHeading = false;
|
|
24
|
+
/** As `inHeading`, but for the inherited font size - see `hasUniformFormatting`. */
|
|
25
|
+
headingUniformSize = false;
|
|
13
26
|
constructor(ast, config) {
|
|
14
27
|
super('rtf', ast, config);
|
|
15
28
|
}
|
|
@@ -68,8 +81,16 @@ class RtfGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
68
81
|
const wasInTable = this.inTable;
|
|
69
82
|
if (node.type === 'table')
|
|
70
83
|
this.inTable = true;
|
|
84
|
+
const wasInHeading = this.inHeading;
|
|
85
|
+
const wasHeadingSize = this.headingUniformSize;
|
|
86
|
+
if (node.type === 'heading') {
|
|
87
|
+
this.inHeading = this.hasUniformFormatting(node, f => f?.bold === true);
|
|
88
|
+
this.headingUniformSize = this.hasUniformFormatting(node, f => !!f?.size);
|
|
89
|
+
}
|
|
71
90
|
const result = await super.processNodeRecursive(node, processor);
|
|
72
91
|
this.inTable = wasInTable;
|
|
92
|
+
this.inHeading = wasInHeading;
|
|
93
|
+
this.headingUniformSize = wasHeadingSize;
|
|
73
94
|
return result;
|
|
74
95
|
}
|
|
75
96
|
async renderBody(ast) {
|
|
@@ -98,7 +119,7 @@ class RtfGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
98
119
|
if (this.config.includeFormatting && f) {
|
|
99
120
|
let prefix = '';
|
|
100
121
|
let suffix = '';
|
|
101
|
-
if (f.bold) {
|
|
122
|
+
if (f.bold && !this.inHeading) {
|
|
102
123
|
prefix += '\\b ';
|
|
103
124
|
suffix = '\\b0 ' + suffix;
|
|
104
125
|
}
|
|
@@ -127,7 +148,7 @@ class RtfGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
127
148
|
const idx = this.getColorIndex(f.backgroundColor);
|
|
128
149
|
prefix += `\\highlight${idx + 1} `;
|
|
129
150
|
}
|
|
130
|
-
if (f.size) {
|
|
151
|
+
if (f.size && !this.headingUniformSize) {
|
|
131
152
|
let pt = 12; // default
|
|
132
153
|
const val = parseFloat(f.size);
|
|
133
154
|
if (!isNaN(val)) {
|