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
|
@@ -27,12 +27,12 @@ const foldLines = (value) => String(value ?? '').replace(/[\r\n]+/g, ' ');
|
|
|
27
27
|
* exactly (every feature on, GitHub-style admonitions) - the backward-compatibility anchor.
|
|
28
28
|
*/
|
|
29
29
|
const MARKDOWN_DIALECT_PRESETS = {
|
|
30
|
-
extended: { admonitions: '
|
|
31
|
-
github: { admonitions: '
|
|
32
|
-
gitlab: { admonitions: '
|
|
33
|
-
obsidian: { admonitions: '
|
|
34
|
-
pandoc: { admonitions: '
|
|
35
|
-
commonmark: { admonitions: 'none', definitionLists:
|
|
30
|
+
extended: { admonitions: 'blockquote', definitionLists: 'colon', footnotes: 'caret', citations: 'at', wikilinks: 'double-bracket', math: 'dollar', attributeLists: 'brace', strikethrough: 'tilde', highlight: 'equals', bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'native' },
|
|
31
|
+
github: { admonitions: 'blockquote', definitionLists: 'none', footnotes: 'caret', citations: 'none', wikilinks: 'none', math: 'dollar', attributeLists: 'none', strikethrough: 'tilde', highlight: 'none', bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'native' },
|
|
32
|
+
gitlab: { admonitions: 'fence', definitionLists: 'none', footnotes: 'caret', citations: 'none', wikilinks: 'none', math: 'dollar', attributeLists: 'none', strikethrough: 'tilde', highlight: 'none', bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'native' },
|
|
33
|
+
obsidian: { admonitions: 'blockquote', definitionLists: 'none', footnotes: 'caret', citations: 'none', wikilinks: 'double-bracket', math: 'dollar', attributeLists: 'none', strikethrough: 'tilde', highlight: 'equals', bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'native' },
|
|
34
|
+
pandoc: { admonitions: 'fence-attribute', definitionLists: 'colon', footnotes: 'caret', citations: 'at', wikilinks: 'none', math: 'dollar', attributeLists: 'brace', strikethrough: 'tilde', highlight: 'none', bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'native' },
|
|
35
|
+
commonmark: { admonitions: 'none', definitionLists: 'none', footnotes: 'none', citations: 'none', wikilinks: 'none', math: 'none', attributeLists: 'none', strikethrough: 'none', highlight: 'none', bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'html' },
|
|
36
36
|
};
|
|
37
37
|
/**
|
|
38
38
|
* Normalizes `MdGeneratorConfig.dialect` into a fully-resolved preset. A string names a preset
|
|
@@ -40,6 +40,30 @@ const MARKDOWN_DIALECT_PRESETS = {
|
|
|
40
40
|
* omitted field falls back to - NOT "whatever preset was ambient before", since config merging
|
|
41
41
|
* replaces the whole `dialect` field rather than layering an object on top of a prior string.
|
|
42
42
|
*/
|
|
43
|
+
/**
|
|
44
|
+
* Coerces a per-capability field to its canonical syntax variant. An omitted value inherits `base`;
|
|
45
|
+
* a deprecated boolean maps `true` -> `onValue` and `false` -> `'none'` (the two are the only legacy
|
|
46
|
+
* inputs, dropped next major); an explicit syntax string passes through unchanged.
|
|
47
|
+
*/
|
|
48
|
+
function resolveToggle(value, onValue, base) {
|
|
49
|
+
if (value === undefined)
|
|
50
|
+
return base;
|
|
51
|
+
if (value === true)
|
|
52
|
+
return onValue;
|
|
53
|
+
if (value === false)
|
|
54
|
+
return 'none';
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
/** Maps the deprecated admonition flavor aliases to their syntax names; passes syntax names through. */
|
|
58
|
+
function resolveAdmonitions(value, base) {
|
|
59
|
+
switch (value) {
|
|
60
|
+
case undefined: return base;
|
|
61
|
+
case 'github': return 'blockquote';
|
|
62
|
+
case 'gitlab': return 'fence';
|
|
63
|
+
case 'pandoc': return 'fence-attribute';
|
|
64
|
+
default: return value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
43
67
|
function resolveDialect(dialect) {
|
|
44
68
|
if (dialect === undefined)
|
|
45
69
|
return MARKDOWN_DIALECT_PRESETS.extended;
|
|
@@ -47,14 +71,15 @@ function resolveDialect(dialect) {
|
|
|
47
71
|
return MARKDOWN_DIALECT_PRESETS[dialect] ?? MARKDOWN_DIALECT_PRESETS.extended;
|
|
48
72
|
const base = MARKDOWN_DIALECT_PRESETS[dialect.extends ?? 'extended'] ?? MARKDOWN_DIALECT_PRESETS.extended;
|
|
49
73
|
return {
|
|
50
|
-
admonitions: dialect.admonitions
|
|
51
|
-
definitionLists: dialect.definitionLists
|
|
52
|
-
footnotes: dialect.footnotes
|
|
53
|
-
citations: dialect.citations
|
|
54
|
-
wikilinks: dialect.wikilinks
|
|
74
|
+
admonitions: resolveAdmonitions(dialect.admonitions, base.admonitions),
|
|
75
|
+
definitionLists: resolveToggle(dialect.definitionLists, 'colon', base.definitionLists),
|
|
76
|
+
footnotes: resolveToggle(dialect.footnotes, 'caret', base.footnotes),
|
|
77
|
+
citations: resolveToggle(dialect.citations, 'at', base.citations),
|
|
78
|
+
wikilinks: resolveToggle(dialect.wikilinks, 'double-bracket', base.wikilinks),
|
|
55
79
|
math: dialect.math ?? base.math,
|
|
56
|
-
attributeLists: dialect.attributeLists
|
|
57
|
-
strikethrough: dialect.strikethrough
|
|
80
|
+
attributeLists: resolveToggle(dialect.attributeLists, 'brace', base.attributeLists),
|
|
81
|
+
strikethrough: resolveToggle(dialect.strikethrough, 'tilde', base.strikethrough),
|
|
82
|
+
highlight: dialect.highlight ?? base.highlight,
|
|
58
83
|
bulletListMarker: dialect.bulletListMarker ?? base.bulletListMarker,
|
|
59
84
|
orderedListMarker: dialect.orderedListMarker ?? base.orderedListMarker,
|
|
60
85
|
emphasisMarker: dialect.emphasisMarker ?? base.emphasisMarker,
|
|
@@ -71,6 +96,7 @@ function resolveFallbackToHtml(fallbackToHtml) {
|
|
|
71
96
|
// inlineFormatting is opt-in only: it is never enabled by the boolean form, since it changes
|
|
72
97
|
// default output. Every other field follows the boolean.
|
|
73
98
|
textFormatting: on, alignment: on, anchors: on, tables: on, embeds: on, cellLineBreaks: on,
|
|
99
|
+
itemLineBreaks: on,
|
|
74
100
|
inlineFormatting: false,
|
|
75
101
|
});
|
|
76
102
|
if (fallbackToHtml === undefined || typeof fallbackToHtml === 'boolean')
|
|
@@ -83,6 +109,7 @@ function resolveFallbackToHtml(fallbackToHtml) {
|
|
|
83
109
|
tables: fallbackToHtml.tables ?? on.tables,
|
|
84
110
|
embeds: fallbackToHtml.embeds ?? on.embeds,
|
|
85
111
|
cellLineBreaks: fallbackToHtml.cellLineBreaks ?? on.cellLineBreaks,
|
|
112
|
+
itemLineBreaks: fallbackToHtml.itemLineBreaks ?? on.itemLineBreaks,
|
|
86
113
|
inlineFormatting: fallbackToHtml.inlineFormatting ?? on.inlineFormatting,
|
|
87
114
|
};
|
|
88
115
|
}
|
|
@@ -133,10 +160,19 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
133
160
|
collectedAbbreviations = new Map();
|
|
134
161
|
resolvedDialect;
|
|
135
162
|
resolvedFallbackToHtml;
|
|
163
|
+
resolvedEmbeds;
|
|
136
164
|
constructor(ast, config) {
|
|
137
165
|
super('md', ast, config);
|
|
138
166
|
this.resolvedDialect = resolveDialect(this.config.mdConfig.dialect);
|
|
139
167
|
this.resolvedFallbackToHtml = resolveFallbackToHtml(this.config.mdConfig.fallbackToHtml);
|
|
168
|
+
// `dialect.embeds` is the authority for embed form. It lives in a different config object
|
|
169
|
+
// than the deprecated `fallbackToHtml.embeds` boolean, and an explicit boolean `false` must
|
|
170
|
+
// still win over the preset default, so it is resolved here rather than in `resolveDialect`:
|
|
171
|
+
// an embeds value set on the dialect OBJECT wins; otherwise the boolean maps (`true`/unset ->
|
|
172
|
+
// `'html'`, `false` -> `'link'`); otherwise the default `'html'`.
|
|
173
|
+
const dialectCfg = this.config.mdConfig.dialect;
|
|
174
|
+
const explicitEmbeds = (dialectCfg && typeof dialectCfg === 'object') ? dialectCfg.embeds : undefined;
|
|
175
|
+
this.resolvedEmbeds = explicitEmbeds ?? (this.resolvedFallbackToHtml.embeds ? 'html' : 'link');
|
|
140
176
|
}
|
|
141
177
|
/**
|
|
142
178
|
* Renders anchor tags if HTML fallback is allowed.
|
|
@@ -163,10 +199,13 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
163
199
|
* ImageMetadata/TableMetadata's width/align fields - the canonical form is always
|
|
164
200
|
* `key=value`, matching MarkdownParser's own vocabulary (MARKDOWN_DIALECT.md §15).
|
|
165
201
|
*/
|
|
166
|
-
renderAttributeList(meta) {
|
|
167
|
-
if (
|
|
202
|
+
renderAttributeList(meta, options = {}) {
|
|
203
|
+
if (this.resolvedDialect.attributeLists === 'none')
|
|
168
204
|
return '';
|
|
169
|
-
if (!meta
|
|
205
|
+
if (!meta)
|
|
206
|
+
return '';
|
|
207
|
+
const align = options.skipAlign ? undefined : meta.align;
|
|
208
|
+
if (!meta.width && !align)
|
|
170
209
|
return '';
|
|
171
210
|
const parts = [];
|
|
172
211
|
// Allowlist, not escape. These land in `metadata.width`/`align` on reparse, which the
|
|
@@ -182,8 +221,8 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
182
221
|
if (meta.width && MD_LENGTH_PATTERN.test(String(meta.width).trim())) {
|
|
183
222
|
parts.push(`width=${String(meta.width).trim()}`);
|
|
184
223
|
}
|
|
185
|
-
if (
|
|
186
|
-
parts.push(`align=${String(
|
|
224
|
+
if (align && MD_ALIGN_VALUES.has(String(align).trim().toLowerCase())) {
|
|
225
|
+
parts.push(`align=${String(align).trim().toLowerCase()}`);
|
|
187
226
|
}
|
|
188
227
|
if (parts.length === 0)
|
|
189
228
|
return '';
|
|
@@ -283,8 +322,19 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
283
322
|
text = emphasisAsterisk ? `**${text}**` : `__${text}__`;
|
|
284
323
|
if (node.formatting.italic)
|
|
285
324
|
text = emphasisAsterisk ? `*${text}*` : `_${text}_`;
|
|
286
|
-
if (node.formatting.strikethrough && this.resolvedDialect.strikethrough)
|
|
325
|
+
if (node.formatting.strikethrough && this.resolvedDialect.strikethrough !== 'none')
|
|
287
326
|
text = `~~${text}~~`;
|
|
327
|
+
// `==text==` highlight, in dialects that define it (Obsidian/extended). A plain
|
|
328
|
+
// highlight (the default yellow) always becomes `==text==`; a highlight carrying
|
|
329
|
+
// a SPECIFIC colour stays a background-color <span> when `inlineFormatting` is on,
|
|
330
|
+
// so its exact colour survives. With `inlineFormatting` off (no span to hold it)
|
|
331
|
+
// even a coloured highlight degrades to `==` rather than being dropped. In
|
|
332
|
+
// GFM/CommonMark `==` is literal, so a highlight falls through to the <span> path.
|
|
333
|
+
const isDefaultHighlight = node.formatting.backgroundColor === '#ffff00';
|
|
334
|
+
const emitHighlightMark = !!node.formatting.backgroundColor && this.resolvedDialect.highlight !== 'none'
|
|
335
|
+
&& (isDefaultHighlight || !this.resolvedFallbackToHtml.inlineFormatting);
|
|
336
|
+
if (emitHighlightMark)
|
|
337
|
+
text = `==${text}==`;
|
|
288
338
|
// Use HTML tags for formatting not natively supported by standard Markdown
|
|
289
339
|
if (this.resolvedFallbackToHtml.textFormatting) {
|
|
290
340
|
if (node.formatting.underline)
|
|
@@ -307,14 +357,18 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
307
357
|
styles.push(`${prop}: ${safe}`);
|
|
308
358
|
};
|
|
309
359
|
pushStyle('color', node.formatting.color);
|
|
310
|
-
|
|
360
|
+
// Skip the background-color only when it was already emitted as `==text==`
|
|
361
|
+
// above; a specific-colour highlight in a highlight dialect still keeps its
|
|
362
|
+
// exact colour here.
|
|
363
|
+
if (!emitHighlightMark)
|
|
364
|
+
pushStyle('background-color', node.formatting.backgroundColor);
|
|
311
365
|
pushStyle('font-size', node.formatting.size);
|
|
312
366
|
if (styles.length)
|
|
313
367
|
text = `<span style="${styles.join('; ')}">${text}</span>`;
|
|
314
368
|
}
|
|
315
369
|
}
|
|
316
370
|
const meta = node.metadata;
|
|
317
|
-
if (meta?.wikilink && this.resolvedDialect.wikilinks) {
|
|
371
|
+
if (meta?.wikilink && this.resolvedDialect.wikilinks !== 'none') {
|
|
318
372
|
// Obsidian syntax: bare page name, or page|alias when the display
|
|
319
373
|
// text differs from the page name. Strip the `[]|`/newline chars
|
|
320
374
|
// that would break out of the `[[...]]` wrapper.
|
|
@@ -340,8 +394,11 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
340
394
|
link = '#' + this.slugify(target);
|
|
341
395
|
}
|
|
342
396
|
// Reject javascript:/data: schemes and encode `()`/whitespace so the
|
|
343
|
-
// URL can't break out of `](...)` or inject a script link.
|
|
344
|
-
|
|
397
|
+
// URL can't break out of `](...)` or inject a script link. An advisory
|
|
398
|
+
// title follows as `"title"` (quotes inside it escaped), matching what
|
|
399
|
+
// the parser reads back.
|
|
400
|
+
const linkTitle = meta.title ? ` "${meta.title.replace(/"/g, '\\"')}"` : '';
|
|
401
|
+
text = `[${text}](${(0, sanitize_js_1.sanitizeMarkdownUrl)(link)}${linkTitle})`;
|
|
345
402
|
}
|
|
346
403
|
}
|
|
347
404
|
if (meta?.abbreviationTitle) {
|
|
@@ -358,7 +415,7 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
358
415
|
// this branch also replaces `text` wholesale, so a strip that left `<`
|
|
359
416
|
// behind discarded the escaping applied earlier.
|
|
360
417
|
const key = String(meta.citationKey).replace(/[^a-zA-Z0-9_:.-]/g, '');
|
|
361
|
-
text = this.resolvedDialect.citations ? `[@${key}]` : `[${key}]`;
|
|
418
|
+
text = this.resolvedDialect.citations !== 'none' ? `[@${key}]` : `[${key}]`;
|
|
362
419
|
}
|
|
363
420
|
return text;
|
|
364
421
|
}
|
|
@@ -408,7 +465,17 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
408
465
|
? (meta.checked ? `${bullet}[x] ` : `${bullet}[ ] `)
|
|
409
466
|
: (meta?.listType === 'ordered' ? `${(meta.itemIndex ?? 0) + 1}${this.resolvedDialect.orderedListMarker} ` : bullet);
|
|
410
467
|
const anchors = this.renderAnchors(meta);
|
|
411
|
-
|
|
468
|
+
// A list item is a single Markdown line. HTML-origin items carry `paragraph`
|
|
469
|
+
// children (e.g. `<li><p>a</p><ul>...`), whose renderer appends `\n\n`; dumped
|
|
470
|
+
// verbatim that produces `- a\n\n\n - a1`, whose blank line splits the list
|
|
471
|
+
// apart and whose indent is then stripped on reparse, flattening the nesting.
|
|
472
|
+
// Collapse the item's internal breaks the same way table cells do (see the
|
|
473
|
+
// `cellLineBreaks` handling in renderMarkdownTable): join with `<br>` when the
|
|
474
|
+
// fallback is on, a space when off. Block children (code fences, tables) inside
|
|
475
|
+
// an item degrade under this join, exactly as they do inside a cell.
|
|
476
|
+
const br = this.resolvedFallbackToHtml.itemLineBreaks ? '<br>' : ' ';
|
|
477
|
+
const content = childrenOutput.trim().replace(/[ \t]*\n+/g, br);
|
|
478
|
+
return `${indent}${marker}${anchors}${content}\n`;
|
|
412
479
|
}
|
|
413
480
|
case 'image': {
|
|
414
481
|
if (!this.config.includeImages)
|
|
@@ -427,7 +494,8 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
427
494
|
// Strip `[]` from alt (would close the `![...]`) and neutralize the URL scheme.
|
|
428
495
|
const safeAlt = (0, sanitize_js_1.markdownEscapeText)(alt).replace(/[[\]]/g, '');
|
|
429
496
|
const safeSrc = (0, sanitize_js_1.sanitizeMarkdownUrl)(src, { allowDataImage: true });
|
|
430
|
-
|
|
497
|
+
const imgTitle = meta?.title ? ` "${meta.title.replace(/"/g, '\\"')}"` : '';
|
|
498
|
+
return `${anchors}${anchors ? '\n' : ''}${this.renderAttributeList(meta)}`;
|
|
431
499
|
}
|
|
432
500
|
case 'table': {
|
|
433
501
|
const anchors = this.renderAnchors(node.metadata);
|
|
@@ -437,7 +505,7 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
437
505
|
// only the plain pipe-table form needs the attribute-list syntax for alignment.
|
|
438
506
|
const usedHtmlFallback = this.resolvedDialect.tables === 'html' ||
|
|
439
507
|
(this.resolvedFallbackToHtml.tables && (this.hasNestedTable(node) || this.hasColspanOrRowspan(node)));
|
|
440
|
-
const attrList = usedHtmlFallback ? '' : this.renderAttributeList(node.metadata);
|
|
508
|
+
const attrList = usedHtmlFallback ? '' : this.renderAttributeList(node.metadata, { skipAlign: true });
|
|
441
509
|
if (attrList) {
|
|
442
510
|
// Must glue directly below the last row with no blank line, or
|
|
443
511
|
// MarkdownParser's block splitter won't see it as part of the same block.
|
|
@@ -530,7 +598,7 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
530
598
|
case 'note': {
|
|
531
599
|
const meta = node.metadata;
|
|
532
600
|
if (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') {
|
|
533
|
-
if (
|
|
601
|
+
if (this.resolvedDialect.footnotes === 'none') {
|
|
534
602
|
// Dialect has no footnote syntax - the caller inlines this bare body
|
|
535
603
|
// as a parenthetical at the reference point instead of collecting it
|
|
536
604
|
// into an end-of-document "### Notes" section under a [^id] marker.
|
|
@@ -544,32 +612,67 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
544
612
|
return `> **Note:** ${childrenOutput.trim()}\n\n`;
|
|
545
613
|
}
|
|
546
614
|
case 'embed': {
|
|
547
|
-
// Markdown has no native embed syntax.
|
|
548
|
-
//
|
|
549
|
-
//
|
|
615
|
+
// Markdown has no native embed syntax. `this.resolvedEmbeds` (from
|
|
616
|
+
// `dialect.embeds`, honoring the deprecated `fallbackToHtml.embeds` boolean)
|
|
617
|
+
// selects the form: 'html' (the single-line block this library has always
|
|
618
|
+
// emitted and re-recognises), 'directive' (a remark-directive leaf), 'link'
|
|
619
|
+
// (a plain link), 'thumbnail' (YouTube-only clickable preview).
|
|
550
620
|
const meta = node.metadata;
|
|
621
|
+
const mode = this.resolvedEmbeds;
|
|
622
|
+
// A directive label sits inside `::name[...]`; strip the `[]`/newline chars that
|
|
623
|
+
// would break out of it. An attribute value sits inside `{...}`; percent-encode
|
|
624
|
+
// the space/brace chars that would break out (widths/aligns/ids never contain
|
|
625
|
+
// them, but a src can).
|
|
626
|
+
const dirLabel = (meta?.label || '').replace(/[[\]\r\n]+/g, ' ').trim();
|
|
627
|
+
const dirUrl = (u) => (0, sanitize_js_1.sanitizeMarkdownUrl)(u).replace(/[{}\s]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0'));
|
|
628
|
+
const attrList = (pairs) => {
|
|
629
|
+
const kv = pairs.filter(([, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${v}`);
|
|
630
|
+
return kv.length ? `{${kv.join(' ')}}` : '';
|
|
631
|
+
};
|
|
551
632
|
if (meta?.embedType === 'iframe') {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
633
|
+
const rawUrl = meta?.url || '';
|
|
634
|
+
if (mode === 'directive') {
|
|
635
|
+
const src = dirUrl(rawUrl);
|
|
636
|
+
if (!src)
|
|
637
|
+
return '';
|
|
638
|
+
const lbl = dirLabel ? `[${dirLabel}]` : '';
|
|
639
|
+
return `::embed${lbl}${attrList([['src', src], ['width', meta?.width], ['height', meta?.height], ['align', meta?.align]])}\n\n`;
|
|
640
|
+
}
|
|
641
|
+
if (mode === 'html') {
|
|
642
|
+
// sanitizeUrl scheme-checks and HTML-escapes the src (hostile schemes drop
|
|
643
|
+
// the node). The single-line <iframe> is what MarkdownParser recognises on
|
|
644
|
+
// reimport, gated there on preserveIframes.
|
|
645
|
+
const safe = (0, sanitize_js_1.sanitizeUrl)(rawUrl);
|
|
646
|
+
if (!safe)
|
|
647
|
+
return '';
|
|
559
648
|
const w = meta?.width ? ` width="${(0, sanitize_js_1.escapeHtml)(meta.width)}"` : '';
|
|
560
649
|
const h = meta?.height ? ` height="${(0, sanitize_js_1.escapeHtml)(meta.height)}"` : '';
|
|
561
650
|
return `\n<iframe src="${safe}"${w}${h}></iframe>\n\n`;
|
|
562
651
|
}
|
|
563
|
-
|
|
652
|
+
// 'link' and 'thumbnail' (thumbnail is YouTube-only, so a generic iframe
|
|
653
|
+
// degrades to a link) both emit a plain link.
|
|
654
|
+
const safe = (0, sanitize_js_1.sanitizeMarkdownUrl)(rawUrl);
|
|
655
|
+
return safe ? `[${meta?.label || 'Embed'}](${safe})\n\n` : '';
|
|
564
656
|
}
|
|
565
657
|
const id = meta?.videoId || '';
|
|
566
|
-
if (
|
|
658
|
+
if (mode === 'directive') {
|
|
659
|
+
const lbl = dirLabel ? `[${dirLabel}]` : '';
|
|
660
|
+
return `::youtube${lbl}${attrList([['id', id], ['width', meta?.width], ['align', meta?.align]])}\n\n`;
|
|
661
|
+
}
|
|
662
|
+
if (mode === 'html') {
|
|
567
663
|
const width = meta?.width ? ` data-width="${(0, sanitize_js_1.escapeHtml)(meta.width)}"` : '';
|
|
568
664
|
const align = meta?.align ? ` data-align="${(0, sanitize_js_1.escapeHtml)(meta.align)}"` : '';
|
|
569
|
-
|
|
665
|
+
const lbl = meta?.label ? ` data-embed-label="${(0, sanitize_js_1.escapeHtml)(meta.label)}"` : '';
|
|
666
|
+
return `\n<div data-youtube-video="${(0, sanitize_js_1.escapeHtml)(id)}"${width}${align}${lbl}></div>\n\n`;
|
|
570
667
|
}
|
|
668
|
+
if (mode === 'thumbnail' && id) {
|
|
669
|
+
const watch = (0, sanitize_js_1.sanitizeMarkdownUrl)(`https://www.youtube.com/watch?v=${id}`);
|
|
670
|
+
const thumb = (0, sanitize_js_1.sanitizeMarkdownUrl)(`https://img.youtube.com/vi/${id}/hqdefault.jpg`);
|
|
671
|
+
return `[](${watch})\n\n`;
|
|
672
|
+
}
|
|
673
|
+
// 'link' (and 'thumbnail' with no id): a plain link.
|
|
571
674
|
const url = meta?.url || (id ? `https://youtu.be/${id}` : '');
|
|
572
|
-
return url ? `[YouTube](${(0, sanitize_js_1.sanitizeMarkdownUrl)(url)})\n\n` : '';
|
|
675
|
+
return url ? `[${meta?.label || 'YouTube'}](${(0, sanitize_js_1.sanitizeMarkdownUrl)(url)})\n\n` : '';
|
|
573
676
|
}
|
|
574
677
|
case 'admonition': {
|
|
575
678
|
const meta = node.metadata;
|
|
@@ -586,12 +689,12 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
586
689
|
const title = meta?.title ? (0, sanitize_js_1.markdownEscapeText)(foldLines(meta.title)) : '';
|
|
587
690
|
const body = childrenOutput.trim();
|
|
588
691
|
switch (this.resolvedDialect.admonitions) {
|
|
589
|
-
case '
|
|
692
|
+
case 'fence':
|
|
590
693
|
// GLFM fenced-div: no dedicated title syntax, so a custom title (if
|
|
591
694
|
// any) is folded into the body as a bold first line.
|
|
592
695
|
return `:::${type}\n${title ? `**${title}**\n\n` : ''}${body}\n:::\n\n`;
|
|
593
|
-
case '
|
|
594
|
-
// Pandoc's own fenced-div-with-class syntax; same title handling as
|
|
696
|
+
case 'fence-attribute':
|
|
697
|
+
// Pandoc's own fenced-div-with-class syntax; same title handling as fence.
|
|
595
698
|
return `::: {.${type}}\n${title ? `**${title}**\n\n` : ''}${body}\n:::\n\n`;
|
|
596
699
|
case 'none': {
|
|
597
700
|
// Degrade to a plain bold-labeled blockquote, no special marker.
|
|
@@ -599,7 +702,7 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
599
702
|
const heading = title || label.charAt(0) + label.slice(1).toLowerCase();
|
|
600
703
|
return `> **${heading}:**\n${quotedLines}\n\n`;
|
|
601
704
|
}
|
|
602
|
-
case '
|
|
705
|
+
case 'blockquote':
|
|
603
706
|
default: {
|
|
604
707
|
// Canonical GitHub blockquote form. No dedicated title syntax either
|
|
605
708
|
// (matches this library's historical output).
|
|
@@ -609,15 +712,15 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
609
712
|
}
|
|
610
713
|
}
|
|
611
714
|
case 'definitionList':
|
|
612
|
-
if (
|
|
715
|
+
if (this.resolvedDialect.definitionLists === 'none')
|
|
613
716
|
return `${childrenOutput}\n`;
|
|
614
717
|
return `${childrenOutput}\n`;
|
|
615
718
|
case 'definitionTerm':
|
|
616
|
-
if (
|
|
719
|
+
if (this.resolvedDialect.definitionLists === 'none')
|
|
617
720
|
return `**${childrenOutput}**\n\n`;
|
|
618
721
|
return `${childrenOutput}\n`;
|
|
619
722
|
case 'definitionDescription':
|
|
620
|
-
if (
|
|
723
|
+
if (this.resolvedDialect.definitionLists === 'none')
|
|
621
724
|
return `${childrenOutput}\n\n`;
|
|
622
725
|
return `: ${childrenOutput}\n`;
|
|
623
726
|
case 'chart':
|
|
@@ -732,7 +835,7 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
732
835
|
// end-of-document "### Notes" section, or its content would be duplicated.
|
|
733
836
|
const isInlinedFootnote = (note) => {
|
|
734
837
|
const meta = note.metadata;
|
|
735
|
-
return (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') &&
|
|
838
|
+
return (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') && this.resolvedDialect.footnotes === 'none';
|
|
736
839
|
};
|
|
737
840
|
this.collectNotesFrom(node);
|
|
738
841
|
let result = await processor(node, childrenOutput);
|
|
@@ -848,7 +951,7 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
848
951
|
return;
|
|
849
952
|
const isInlinedFootnote = (note) => {
|
|
850
953
|
const meta = note.metadata;
|
|
851
|
-
return (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') &&
|
|
954
|
+
return (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') && this.resolvedDialect.footnotes === 'none';
|
|
852
955
|
};
|
|
853
956
|
this.collectedNotes.push(...node.notes.filter(note => !isInlinedFootnote(note)));
|
|
854
957
|
}
|
|
@@ -892,7 +995,9 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
892
995
|
let cellContent = await this.processNodeRecursive(cellNode, processor);
|
|
893
996
|
// Use <br> fallback only if allowed, otherwise space
|
|
894
997
|
const br = this.resolvedFallbackToHtml.cellLineBreaks ? '<br>' : ' ';
|
|
895
|
-
|
|
998
|
+
// Consume any trailing spaces before the newline(s) too, so a hard-break's
|
|
999
|
+
// ` \n` collapses to a single `<br>` instead of leaving ` <br>` in the cell.
|
|
1000
|
+
cellContent = cellContent.trim().replace(/[ \t]*\n+/g, br).replace(/\|/g, '\\|');
|
|
896
1001
|
rowCells.push(cellContent);
|
|
897
1002
|
// Handle colspan by adding empty cells
|
|
898
1003
|
const colSpan = cellNode.metadata?.colSpan || 1;
|
|
@@ -909,7 +1014,17 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
909
1014
|
this.inImplicitBold = wasInImplicitBold;
|
|
910
1015
|
}
|
|
911
1016
|
}
|
|
912
|
-
// Second pass: Build table string with separator
|
|
1017
|
+
// Second pass: Build table string with separator. The separator carries standard GFM
|
|
1018
|
+
// per-column alignment (`:---`/`:---:`/`---:`) from columnAlignments, or the single table-level
|
|
1019
|
+
// align applied to every column, rather than a non-standard trailing `{align}` attribute list.
|
|
1020
|
+
const tableMeta = node.metadata;
|
|
1021
|
+
// Column alignment lives on each cell (CellMetadata.align); read it off the header row.
|
|
1022
|
+
// Fall back to the single table-level align (an editor's data-align) for every column.
|
|
1023
|
+
const headerCells = (node.children?.[0]?.children || []).filter(c => c.type === 'cell');
|
|
1024
|
+
const alignMarker = (i) => {
|
|
1025
|
+
const a = headerCells[i]?.metadata?.align ?? tableMeta?.align;
|
|
1026
|
+
return a === 'center' ? ':---:' : a === 'left' ? ':---' : a === 'right' ? '---:' : '---';
|
|
1027
|
+
};
|
|
913
1028
|
for (let i = 0; i < processedRows.length; i++) {
|
|
914
1029
|
const row = processedRows[i];
|
|
915
1030
|
// Pad row with empty cells if it has fewer than maxCols
|
|
@@ -918,7 +1033,7 @@ class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
|
918
1033
|
tableOutput += `| ${row.join(' | ')} |\n`;
|
|
919
1034
|
if (i === 0) {
|
|
920
1035
|
// Header separator
|
|
921
|
-
tableOutput += `| ${Array(maxCols
|
|
1036
|
+
tableOutput += `| ${Array.from({ length: maxCols }, (_, i) => alignMarker(i)).join(' | ')} |\n`;
|
|
922
1037
|
}
|
|
923
1038
|
}
|
|
924
1039
|
return `\n${tableOutput}\n`;
|