officeparser 7.6.2 → 7.8.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 +9 -4
- package/dist/defaults.js +2 -0
- package/dist/generators/HtmlGenerator.js +72 -17
- package/dist/generators/MarkdownGenerator.d.ts +1 -0
- package/dist/generators/MarkdownGenerator.js +169 -54
- package/dist/officeparser.browser.d.ts +151 -20
- package/dist/officeparser.browser.iife.js +194 -181
- package/dist/officeparser.browser.mjs +194 -181
- package/dist/officeparser.browser.slim.d.ts +151 -20
- package/dist/officeparser.browser.slim.iife.js +194 -181
- package/dist/officeparser.browser.slim.mjs +194 -181
- package/dist/parsers/HtmlParser.js +73 -5
- package/dist/parsers/MarkdownParser.js +186 -11
- package/dist/sbom.cdx.json +161 -93
- package/dist/types.d.ts +151 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -758,7 +758,7 @@ Definition List Node (type: 'definitionList')
|
|
|
758
758
|
```
|
|
759
759
|
|
|
760
760
|
- `admonition` round-trips through both Markdown (`> [!NOTE]` / `:::note ... :::`) and HTML (`<div class="admonition admonition-note" data-type="note">`)
|
|
761
|
-
- `embed`
|
|
761
|
+
- `embed` models YouTube videos and generic iframes. Markdown form is selected by `mdConfig.dialect.embeds`: `'html'` (default; the `<div data-youtube-video>` / `<iframe>` block), `'directive'` (a `::youtube[…]{…}` / `::embed[…]{…}` leaf directive), `'link'`, or `'thumbnail'` (YouTube-only clickable preview). A generic iframe is captured only under `htmlParserConfig.preserveIframes` (the trust input) and can be emitted as an inert click-to-load placeholder via `htmlConfig.gatedEmbeds`. The `'directive'` form is an editor round-trip format, not GitHub-rendered
|
|
762
762
|
- Abbreviations (`*[HTML]: Hypertext Markup Language`) are stored as `TextMetadata.abbreviationTitle` on the abbreviated text node rather than as a separate node type
|
|
763
763
|
|
|
764
764
|
---
|
|
@@ -780,7 +780,10 @@ idempotent and `.md → AST → HTML → AST → .md` survives unchanged.
|
|
|
780
780
|
| Attribute lists | `{width=50% .centered}` | `ImageMetadata.width` / `.align`, `TableMetadata.align` |
|
|
781
781
|
| Citations | `[@smith2024]` | `TextMetadata.citationKey` |
|
|
782
782
|
| Wikilinks | `[[Page]]` / `[[Page\|Alias]]` | `TextMetadata.wikilink`, `.link`, `.linkType` |
|
|
783
|
+
| Highlight | `==text==` | `TextMetadata.backgroundColor` |
|
|
784
|
+
| Link/image titles | `[text](url "Title")` / `` | `TextMetadata.title` / `ImageMetadata.title` |
|
|
783
785
|
| Inline/block math | `$E=mc^2$` / `` $$...$$ `` | `type: 'code'`, `CodeMetadata.math` (`'inline' \| 'block'`) |
|
|
786
|
+
| Embeds | `::youtube[Label]{id=… width=… align=…}` / `::embed[Label]{src=… …}` (leaf directive; see `mdConfig.dialect.embeds`) | `type: 'embed'`, `EmbedMetadata` |
|
|
784
787
|
| Frontmatter arrays | `tags: [a, b]` or `tags: ["a","b"]` | Real array in `metadata.customProperties`/`nativeProperties` |
|
|
785
788
|
| MDX components (import-only) | `<Component prop="x">...</Component>` | Stripped; inner Markdown is kept. Never generated back. |
|
|
786
789
|
|
|
@@ -795,7 +798,8 @@ save→reload cycle:
|
|
|
795
798
|
| HTML attribute | AST field | Notes |
|
|
796
799
|
|---|---|---|
|
|
797
800
|
| `data-width` / `data-align` / inline `style="width:…"` on `<img>` | `ImageMetadata.width` / `.align` | |
|
|
798
|
-
| `data-align` on `<table>` | `TableMetadata.align` | |
|
|
801
|
+
| `data-align` on `<table>` | `TableMetadata.align` | Emitted/parsed as per-column GFM markers (`:---`, `:---:`, `---:`); alignment rides `CellMetadata.align` |
|
|
802
|
+
| `title` on `<a>` / `<img>` | `TextMetadata.title` / `ImageMetadata.title` | Survives both directions (`[text](url "Title")` in Markdown) |
|
|
799
803
|
| `colspan` / `rowspan` on `<td>`/`<th>` | `CellMetadata.colSpan` / `.rowSpan` | Previously dropped on HTML import — merged cells now survive a save→reload cycle |
|
|
800
804
|
| `<div data-youtube-video="ID">` / `<iframe src="...youtube.com...">` | `type: 'embed'` | |
|
|
801
805
|
| `<ul data-type="taskList">` / `<li data-checked>` | `ListMetadata.isTask` / `.checked` | |
|
|
@@ -985,7 +989,7 @@ Pass as the second argument to `parseOffice(file, config)`.
|
|
|
985
989
|
| `fileType` | `SupportedFileType \| null` | `null` | **Required for text-based binary data** (`'md'`, `'html'`, `'csv'`) as these lack magic bytes. |
|
|
986
990
|
| `csvDelimiter` | `string` | `','` | Input delimiter when parsing CSV files |
|
|
987
991
|
| `decompressionLimits` | `DecompressionLimits` | `{ maxUncompressedBytes: 512MB, maxZipEntries: 10000, maxTableCells: 1000000 }` | **New**: Limits applied during ZIP extraction (and ODF cell expansion) to protect against excessive memory and resource usage |
|
|
988
|
-
| `htmlParserConfig` | `HtmlParserConfig` | `{}` | HTML/XHTML/EPUB parsing options. `preserveAttributes` (`boolean`, default `false`): keep generic source attributes no typed field consumed on `node.htmlAttributes`. `preserveIframes` (`boolean \| string[]`, default `false`): preserve non-YouTube `<iframe>` embeds (otherwise dropped) as `embed` nodes — `true` for any, or a hostname allowlist; the src is scheme-checked on generation |
|
|
992
|
+
| `htmlParserConfig` | `HtmlParserConfig` | `{}` | HTML/XHTML/EPUB parsing options. `preserveAttributes` (`boolean`, default `false`): keep generic source attributes no typed field consumed on `node.htmlAttributes`. `preserveIframes` (`boolean \| string[]`, default `false`): preserve non-YouTube `<iframe>` embeds (otherwise dropped) as `embed` nodes — `true` for any, or a hostname allowlist; the src is scheme-checked on generation. `embedFolkForms` (`boolean`, default `false`): opt in to importing ambiguous folk embed forms (Obsidian ``, thumbnail-link) as YouTube embeds |
|
|
989
993
|
| `pdfWorkerSrc` | `string` | CDN (jsDelivr) | Path/URL to `pdf.worker.min.mjs` (required in browser) |
|
|
990
994
|
| `onWarning` | `(issue: OfficeIssue) => void` | — | Callback for non-fatal parsing issues |
|
|
991
995
|
| `abortSignal` | `AbortSignal \| null` | `null` | Optional signal to cancel parsing (rejects with AbortError) |
|
|
@@ -1146,7 +1150,8 @@ Pass as `mdConfig` inside `GeneratorConfig`.
|
|
|
1146
1150
|
|
|
1147
1151
|
| Option | Type | Default | Description |
|
|
1148
1152
|
|--------|------|---------|-------------|
|
|
1149
|
-
| `fallbackToHtml` | `boolean \| FallbackToHtmlConfig` | `true` | Use HTML tags for features Markdown cannot represent (underlines, merged table cells, embeds, etc.). Pass an object for per-feature control. `inlineFormatting` (default `false`, opt-in even when the boolean is `true`) additionally round-trips inline color/highlight/font-size as `<span style="...">` runs. |
|
|
1153
|
+
| `fallbackToHtml` | `boolean \| FallbackToHtmlConfig` | `true` | Use HTML tags for features Markdown cannot represent (underlines, merged table cells, embeds, etc.). Pass an object for per-feature control. `cellLineBreaks`/`itemLineBreaks` (default on) join multi-line table-cell / multi-paragraph list-item content with `<br>` instead of a space. `inlineFormatting` (default `false`, opt-in even when the boolean is `true`) additionally round-trips inline color/highlight/font-size as `<span style="...">` runs. |
|
|
1154
|
+
| `dialect` | `MarkdownDialectPreset \| MarkdownDialectConfig` | `'extended'` | Which native syntax to emit for constructs that differ across targets (GitHub/GitLab/Obsidian/Pandoc/CommonMark). Each capability is typed by the syntax it selects (e.g. `strikethrough: 'tilde'`, `highlight: 'equals'`, `admonitions: 'blockquote'`), with `'none'` to turn it off. See [Markdown Dialect Support](#markdown-dialect-support). The old `boolean` toggles and admonition flavour names (`'github'`/`'gitlab'`/`'pandoc'`) still work but are deprecated. |
|
|
1150
1155
|
|
|
1151
1156
|
### PdfGeneratorConfig
|
|
1152
1157
|
|
package/dist/defaults.js
CHANGED
|
@@ -41,6 +41,7 @@ const DEFAULT_OCR_CONFIG = {
|
|
|
41
41
|
const DEFAULT_HTML_PARSER_CONFIG = {
|
|
42
42
|
preserveAttributes: false,
|
|
43
43
|
preserveIframes: false,
|
|
44
|
+
embedFolkForms: false,
|
|
44
45
|
};
|
|
45
46
|
/**
|
|
46
47
|
* Default configuration for the OfficeParser.
|
|
@@ -89,6 +90,7 @@ const DEFAULT_HTML_GENERATOR_CONFIG = {
|
|
|
89
90
|
bodyEnd: '',
|
|
90
91
|
},
|
|
91
92
|
sourceAttributes: false,
|
|
93
|
+
gatedEmbeds: false,
|
|
92
94
|
};
|
|
93
95
|
/**
|
|
94
96
|
* Default configuration for PDF generation.
|
|
@@ -446,7 +446,11 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
446
446
|
*/
|
|
447
447
|
async processNodeArray(nodes) {
|
|
448
448
|
let html = '';
|
|
449
|
-
// Stack to track active lists
|
|
449
|
+
// Stack to track active lists. `liClose` is the currently-open item's deferred closing
|
|
450
|
+
// suffix (`</li>`, or `</div></li>` for a task item): a list item is rendered WITHOUT its
|
|
451
|
+
// close so a deeper list can land inside it (spec-valid `<li>a<ul>...</ul></li>` rather
|
|
452
|
+
// than the invalid `<li>a</li><ul>...</ul>` sibling shape). The close is emitted when a
|
|
453
|
+
// same-level sibling arrives, when the level is popped, or at the end.
|
|
450
454
|
const listStack = [];
|
|
451
455
|
const openListTag = (type, isTask) => {
|
|
452
456
|
if (isTask)
|
|
@@ -457,7 +461,7 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
457
461
|
const closeListsToLevel = (level) => {
|
|
458
462
|
while (listStack.length > 0 && listStack[listStack.length - 1].indentation > level) {
|
|
459
463
|
const list = listStack.pop();
|
|
460
|
-
html += closeListTag(list.type) + '\n\n';
|
|
464
|
+
html += list.liClose + closeListTag(list.type) + '\n\n';
|
|
461
465
|
}
|
|
462
466
|
};
|
|
463
467
|
for (const node of nodes) {
|
|
@@ -492,27 +496,42 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
492
496
|
closeListsToLevel(indentation);
|
|
493
497
|
// Handle current level
|
|
494
498
|
if (listStack.length > 0 && listStack[listStack.length - 1].indentation === indentation) {
|
|
495
|
-
|
|
496
|
-
|
|
499
|
+
const top = listStack[listStack.length - 1];
|
|
500
|
+
if (top.type !== type || top.isTask !== isTask) {
|
|
501
|
+
// Kind changed at the same level: close the open item and the old list,
|
|
502
|
+
// then open the replacement list.
|
|
497
503
|
const last = listStack.pop();
|
|
498
|
-
html += closeListTag(last.type) + '\n';
|
|
504
|
+
html += last.liClose + closeListTag(last.type) + '\n';
|
|
499
505
|
html += openListTag(type, isTask) + '\n';
|
|
500
|
-
listStack.push({ indentation, type, isTask });
|
|
506
|
+
listStack.push({ indentation, type, isTask, liClose: '' });
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
// Sibling at the same level: close the previous item before this one opens.
|
|
510
|
+
html += top.liClose;
|
|
501
511
|
}
|
|
502
512
|
}
|
|
503
513
|
else {
|
|
504
|
-
//
|
|
514
|
+
// Deeper level (or the first list): open a nested list INSIDE the currently
|
|
515
|
+
// open item, leaving the parent <li>'s close pending on its stack frame.
|
|
505
516
|
html += openListTag(type, isTask) + '\n';
|
|
506
|
-
listStack.push({ indentation, type, isTask });
|
|
517
|
+
listStack.push({ indentation, type, isTask, liClose: '' });
|
|
507
518
|
}
|
|
508
519
|
html += await this.processNodeRecursive(node, this.nodeProcessor.bind(this), override);
|
|
520
|
+
// Defer this item's close so a nested list can land inside it. A string override is
|
|
521
|
+
// a complete replacement item that already carries its own close, so add none.
|
|
522
|
+
listStack[listStack.length - 1].liClose = (typeof override === 'string')
|
|
523
|
+
? ''
|
|
524
|
+
: (isTask ? '</div></li>' : '</li>');
|
|
509
525
|
}
|
|
510
526
|
else {
|
|
511
527
|
// Non-list node closes all active lists
|
|
512
528
|
closeListsToLevel(-1);
|
|
513
529
|
let result = await this.processNodeRecursive(node, this.nodeProcessor.bind(this), override);
|
|
514
|
-
// Add
|
|
515
|
-
|
|
530
|
+
// Add a blank line after BLOCK nodes for readable HTML source. Inline nodes (a
|
|
531
|
+
// paragraph's text/link runs) must concatenate with no separator: adding `\n\n`
|
|
532
|
+
// around an inline <a> put a blank line inside the <p>, which reparsed as a stray
|
|
533
|
+
// space before the following punctuation (`[video](url) .`).
|
|
534
|
+
if (node.type !== 'text' && !result.endsWith('\n\n')) {
|
|
516
535
|
if (result.endsWith('\n'))
|
|
517
536
|
result += '\n';
|
|
518
537
|
else
|
|
@@ -712,7 +731,8 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
712
731
|
imgStyleParts.push('display: block', `margin-left: ${ml}`, `margin-right: ${mr}`);
|
|
713
732
|
}
|
|
714
733
|
const imgStyleAttr = imgStyleParts.length > 0 ? ` style="${imgStyleParts.join('; ')}"` : '';
|
|
715
|
-
const
|
|
734
|
+
const imgTitle = meta?.title ? ` title="${this.escape(meta.title)}"` : '';
|
|
735
|
+
const img = `<img src="${(0, sanitize_js_1.sanitizeImageUrl)(src)}" alt="${this.escape(node.text || meta?.altText || '')}"${imgTitle}${className}${mappedAttrs}${imgDataAttrs}${imgStyleAttr}>`;
|
|
716
736
|
const content = this.config.includeFormatting ? `<div class="image-container">${img}<div class="caption">${this.escape(attachmentName || '')}</div></div>` : img;
|
|
717
737
|
return `${extraAnchors}<div${idAttr}>${content}</div>`;
|
|
718
738
|
}
|
|
@@ -831,7 +851,12 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
831
851
|
}
|
|
832
852
|
const lang = meta?.language ? ` class="language-${this.escape(meta.language)}"` : '';
|
|
833
853
|
const codeHtml = `<code${lang}>${this.escape(node.text || '')}</code>`;
|
|
834
|
-
|
|
854
|
+
// A `code` node is always block-level (inline code is a monospace text run, emitted
|
|
855
|
+
// as <code> by formatText). Wrap in <pre> whenever it carries a language or spans
|
|
856
|
+
// multiple lines; only a bare single-line, language-less code node stays a <span>.
|
|
857
|
+
// Previously a single-line block (e.g. a one-line ```js) emitted <span><code>, which
|
|
858
|
+
// re-imports as inline code and which strict CodeBlock parsers (only <pre><code>) miss.
|
|
859
|
+
if (meta?.language || (node.text && node.text.includes('\n'))) {
|
|
835
860
|
return `${extraAnchors}<pre${idAttr}${className}${mappedAttrs}${styleAttr}>${codeHtml}</pre>`;
|
|
836
861
|
}
|
|
837
862
|
else {
|
|
@@ -839,16 +864,19 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
839
864
|
}
|
|
840
865
|
}
|
|
841
866
|
case 'list': {
|
|
867
|
+
// The closing suffix (`</div></li>` for a task item, `</li>` otherwise) is emitted
|
|
868
|
+
// by processNodeArray's list stack, not here, so a nested list can be placed inside
|
|
869
|
+
// this item before it closes. See `listStack`/`liClose` there.
|
|
842
870
|
const meta = node.metadata;
|
|
843
871
|
if (meta?.isTask) {
|
|
844
872
|
const checkedAttr = ` data-checked="${meta.checked ? 'true' : 'false'}"`;
|
|
845
873
|
const checkedBool = meta.checked ? ' checked' : '';
|
|
846
|
-
return `${extraAnchors}<li${checkedAttr}${idAttr}${className}${mappedAttrs}${styleAttr}><label><input type="checkbox"${checkedBool}><span></span></label><div>${childrenOutput}
|
|
874
|
+
return `${extraAnchors}<li${checkedAttr}${idAttr}${className}${mappedAttrs}${styleAttr}><label><input type="checkbox"${checkedBool}><span></span></label><div>${childrenOutput}`;
|
|
847
875
|
}
|
|
848
876
|
const value = (meta?.listType === 'ordered' && typeof meta.itemIndex === 'number')
|
|
849
877
|
? ` value="${meta.itemIndex + 1}"`
|
|
850
878
|
: '';
|
|
851
|
-
return `${extraAnchors}<li${value}${idAttr}${className}${mappedAttrs}${styleAttr}>${childrenOutput}
|
|
879
|
+
return `${extraAnchors}<li${value}${idAttr}${className}${mappedAttrs}${styleAttr}>${childrenOutput}`;
|
|
852
880
|
}
|
|
853
881
|
case 'table': {
|
|
854
882
|
// Smart Table Header Detection
|
|
@@ -1120,6 +1148,15 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
1120
1148
|
return '';
|
|
1121
1149
|
const w = meta?.width ? ` width="${this.escape(meta.width)}"` : '';
|
|
1122
1150
|
const h = meta?.height ? ` height="${this.escape(meta.height)}"` : '';
|
|
1151
|
+
if (this.config.htmlConfig.gatedEmbeds) {
|
|
1152
|
+
// Inert, never-auto-loading placeholder: the editor renders click-to-load from
|
|
1153
|
+
// it, and HtmlParser reads it back to the same embed node. src already sanitized.
|
|
1154
|
+
const a = meta?.align ? ` data-embed-align="${this.escape(meta.align)}"` : '';
|
|
1155
|
+
const l = meta?.label ? ` data-embed-label="${this.escape(meta.label)}"` : '';
|
|
1156
|
+
const dw = meta?.width ? ` data-embed-width="${this.escape(meta.width)}"` : '';
|
|
1157
|
+
const dh = meta?.height ? ` data-embed-height="${this.escape(meta.height)}"` : '';
|
|
1158
|
+
return `${extraAnchors}<div data-embed-gated data-embed-src="${src}"${dw}${dh}${a}${l}${idAttr}${mappedAttrs}${styleAttr}></div>`;
|
|
1159
|
+
}
|
|
1123
1160
|
return `${extraAnchors}<iframe src="${src}"${w}${h}${idAttr}${mappedAttrs}${styleAttr}></iframe>`;
|
|
1124
1161
|
}
|
|
1125
1162
|
// Match the attribute-driven Youtube wrapper shape so a loaded embed re-hydrates
|
|
@@ -1132,7 +1169,11 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
1132
1169
|
const iframe = id
|
|
1133
1170
|
? `<iframe src="https://www.youtube.com/embed/${this.escape(id)}" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>`
|
|
1134
1171
|
: '';
|
|
1135
|
-
|
|
1172
|
+
// Carry the human label so it survives AST -> editor-HTML -> AST, at parity with the
|
|
1173
|
+
// generic gated path's data-embed-label. Only emitted when a label exists (from a
|
|
1174
|
+
// `::youtube[Label]` directive), so unlabeled youtube embeds are byte-identical.
|
|
1175
|
+
const ytLabel = meta?.label ? ` data-embed-label="${this.escape(meta.label)}"` : '';
|
|
1176
|
+
return `${extraAnchors}<div data-youtube-video="${this.escape(id)}" data-width="${this.escape(width)}" data-align="${this.escape(align)}"${ytLabel} class="youtube-embed"${idAttr}${mappedAttrs} style="width: ${(0, sanitize_js_1.sanitizeCssValue)(width)}; margin-left: ${ml}; margin-right: ${mr};">${iframe}</div>`;
|
|
1136
1177
|
}
|
|
1137
1178
|
case 'admonition': {
|
|
1138
1179
|
// Match the attribute-driven admonition wrapper so a loaded admonition
|
|
@@ -1166,6 +1207,11 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
1166
1207
|
let result = this.escape(text);
|
|
1167
1208
|
const f = node.formatting;
|
|
1168
1209
|
if (this.config.includeFormatting && f) {
|
|
1210
|
+
// Inline code: a monospace run becomes `<code>`, not a `font-family: monospace` span, so
|
|
1211
|
+
// an editor keying on <code> sees it and it re-imports as inline code (HtmlParser maps
|
|
1212
|
+
// <code> back to a monospace run). Innermost, so bold/italic wrap it (`<b><code>…`).
|
|
1213
|
+
if (f.font === 'monospace')
|
|
1214
|
+
result = `<code>${result}</code>`;
|
|
1169
1215
|
// Inside an `<hN>`, the heading's own styling is authoritative. A run that also carries
|
|
1170
1216
|
// bold and a font size - the normal case for ODF, where a heading's paragraph style is
|
|
1171
1217
|
// inherited by its runs - would wrap the text in `<b>` the heading already implies and,
|
|
@@ -1220,7 +1266,8 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
1220
1266
|
else if (meta?.link) {
|
|
1221
1267
|
const isInternal = meta.linkType !== 'external';
|
|
1222
1268
|
if (!this.config.ignoreInternalLinks || !isInternal) {
|
|
1223
|
-
|
|
1269
|
+
const linkTitle = meta.title ? ` title="${this.escape(meta.title)}"` : '';
|
|
1270
|
+
result = `<a href="${(0, sanitize_js_1.sanitizeUrl)(meta.link)}"${linkTitle}${meta.linkType === 'external' ? ' target="_blank"' : ''}>${result}</a>`;
|
|
1224
1271
|
}
|
|
1225
1272
|
}
|
|
1226
1273
|
if (meta?.abbreviationTitle) {
|
|
@@ -1250,6 +1297,12 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
1250
1297
|
const meta = node.metadata;
|
|
1251
1298
|
if (meta.alignment)
|
|
1252
1299
|
pushSafe('text-align', meta.alignment);
|
|
1300
|
+
// A table cell's column alignment (GFM `:---`/`:---:`/`---:`) lives on
|
|
1301
|
+
// `CellMetadata.align`, not `alignment`. Emit it as `text-align` on the `<th>`/`<td>`
|
|
1302
|
+
// so `HtmlParser` reads it back and the pipe-table markers survive AST -> HTML -> AST.
|
|
1303
|
+
// An unaligned cell (no `align`) adds nothing, keeping its HTML byte-identical.
|
|
1304
|
+
if (node.type === 'cell' && meta.align)
|
|
1305
|
+
pushSafe('text-align', meta.align);
|
|
1253
1306
|
if (meta.backgroundColor)
|
|
1254
1307
|
pushSafe('background-color', meta.backgroundColor);
|
|
1255
1308
|
if (meta.verticalAlign)
|
|
@@ -1274,7 +1327,9 @@ class HtmlGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
1274
1327
|
pushSafe('background-color', f.backgroundColor);
|
|
1275
1328
|
if (f.size && !options.skipFontSize)
|
|
1276
1329
|
pushSafe('font-size', f.size);
|
|
1277
|
-
|
|
1330
|
+
// A monospace run is emitted as <code> by formatText, so it must not also become a
|
|
1331
|
+
// font-family style here (that was the old, non-semantic inline-code shape).
|
|
1332
|
+
if (f.font && f.font !== 'monospace') {
|
|
1278
1333
|
const safeFont = (0, sanitize_js_1.sanitizeCssValue)(f.font);
|
|
1279
1334
|
if (safeFont)
|
|
1280
1335
|
styles.push(`font-family: ${safeFont}, sans-serif`);
|
|
@@ -47,6 +47,7 @@ export declare class MarkdownGenerator extends BaseGenerator<'md'> {
|
|
|
47
47
|
private collectedAbbreviations;
|
|
48
48
|
private resolvedDialect;
|
|
49
49
|
private resolvedFallbackToHtml;
|
|
50
|
+
private resolvedEmbeds;
|
|
50
51
|
constructor(ast: OfficeParserAST, config?: GeneratorConfig<'md'>);
|
|
51
52
|
/**
|
|
52
53
|
* Renders anchor tags if HTML fallback is allowed.
|