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.
@@ -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: 'github', definitionLists: true, footnotes: true, citations: true, wikilinks: true, math: 'dollar', attributeLists: true, strikethrough: true, bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'native' },
31
- github: { admonitions: 'github', definitionLists: false, footnotes: true, citations: false, wikilinks: false, math: 'dollar', attributeLists: false, strikethrough: true, bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'native' },
32
- gitlab: { admonitions: 'gitlab', definitionLists: false, footnotes: true, citations: false, wikilinks: false, math: 'dollar', attributeLists: false, strikethrough: true, bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'native' },
33
- obsidian: { admonitions: 'github', definitionLists: false, footnotes: true, citations: false, wikilinks: true, math: 'dollar', attributeLists: false, strikethrough: true, bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'native' },
34
- pandoc: { admonitions: 'pandoc', definitionLists: true, footnotes: true, citations: true, wikilinks: false, math: 'dollar', attributeLists: true, strikethrough: true, bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'native' },
35
- commonmark: { admonitions: 'none', definitionLists: false, footnotes: false, citations: false, wikilinks: false, math: 'none', attributeLists: false, strikethrough: false, bulletListMarker: '-', orderedListMarker: '.', emphasisMarker: 'asterisk', tables: 'html' },
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 ?? base.admonitions,
51
- definitionLists: dialect.definitionLists ?? base.definitionLists,
52
- footnotes: dialect.footnotes ?? base.footnotes,
53
- citations: dialect.citations ?? base.citations,
54
- wikilinks: dialect.wikilinks ?? base.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 ?? base.attributeLists,
57
- strikethrough: dialect.strikethrough ?? base.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 (!this.resolvedDialect.attributeLists)
202
+ renderAttributeList(meta, options = {}) {
203
+ if (this.resolvedDialect.attributeLists === 'none')
168
204
  return '';
169
- if (!meta?.width && !meta?.align)
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 (meta.align && MD_ALIGN_VALUES.has(String(meta.align).trim().toLowerCase())) {
186
- parts.push(`align=${String(meta.align).trim().toLowerCase()}`);
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
- pushStyle('background-color', node.formatting.backgroundColor);
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
- text = `[${text}](${(0, sanitize_js_1.sanitizeMarkdownUrl)(link)})`;
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
- return `${indent}${marker}${anchors}${childrenOutput}\n`;
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
- return `${anchors}${anchors ? '\n' : ''}![${safeAlt}](${safeSrc})${this.renderAttributeList(meta)}`;
497
+ const imgTitle = meta?.title ? ` "${meta.title.replace(/"/g, '\\"')}"` : '';
498
+ return `${anchors}${anchors ? '\n' : ''}![${safeAlt}](${safeSrc}${imgTitle})${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 (!this.resolvedDialect.footnotes) {
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. When fallbackToHtml.embeds is on (our
548
- // save default), emit the exact single-line div MarkdownParser recognises on
549
- // reimport; otherwise degrade to a plain link.
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
- // sanitizeUrl scheme-checks and HTML-escapes the src (hostile schemes drop
553
- // the node). The single-line <iframe> is what MarkdownParser recognises on
554
- // reimport, gated there on preserveIframes.
555
- const safe = (0, sanitize_js_1.sanitizeUrl)(meta?.url || '');
556
- if (!safe)
557
- return '';
558
- if (this.resolvedFallbackToHtml.embeds) {
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
- return `[Embed](${(0, sanitize_js_1.sanitizeMarkdownUrl)(meta?.url || '')})\n\n`;
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 (this.resolvedFallbackToHtml.embeds) {
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
- return `\n<div data-youtube-video="${(0, sanitize_js_1.escapeHtml)(id)}"${width}${align}></div>\n\n`;
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 `[![${meta?.label || 'YouTube'}](${thumb})](${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 'gitlab':
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 'pandoc':
594
- // Pandoc's own fenced-div-with-class syntax; same title handling as gitlab.
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 'github':
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 (!this.resolvedDialect.definitionLists)
715
+ if (this.resolvedDialect.definitionLists === 'none')
613
716
  return `${childrenOutput}\n`;
614
717
  return `${childrenOutput}\n`;
615
718
  case 'definitionTerm':
616
- if (!this.resolvedDialect.definitionLists)
719
+ if (this.resolvedDialect.definitionLists === 'none')
617
720
  return `**${childrenOutput}**\n\n`;
618
721
  return `${childrenOutput}\n`;
619
722
  case 'definitionDescription':
620
- if (!this.resolvedDialect.definitionLists)
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') && !this.resolvedDialect.footnotes;
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') && !this.resolvedDialect.footnotes;
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
- cellContent = cellContent.trim().replace(/\n+/g, br).replace(/\|/g, '\\|');
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).fill(' --- ').join(' | ')} |\n`;
1036
+ tableOutput += `| ${Array.from({ length: maxCols }, (_, i) => alignMarker(i)).join(' | ')} |\n`;
922
1037
  }
923
1038
  }
924
1039
  return `\n${tableOutput}\n`;