@gmickel/gno 1.46.0 → 2.1.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 +17 -5
- package/THIRD_PARTY_NOTICES.md +46 -0
- package/assets/skill/SKILL.md +7 -6
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.1.0.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.1.0.zip.sha256 +1 -0
- package/browser-extension/dist/chunk-4tc9v0ja.js +74 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/browser-extension/dist/preview.html +1 -1
- package/browser-extension/dist/service-worker.js +32 -33
- package/bunfig.toml +2 -0
- package/package.json +40 -26
- package/spec/cli.md +29 -4
- package/spec/db/schema.sql +146 -1
- package/spec/mcp.md +26 -0
- package/src/app/context-runtime-types.ts +3 -0
- package/src/app/context-runtime.ts +2 -0
- package/src/cli/commands/ask.ts +6 -1
- package/src/cli/commands/daemon.ts +21 -8
- package/src/cli/commands/embed.ts +77 -41
- package/src/cli/detach.ts +3 -2
- package/src/config/types.ts +3 -3
- package/src/converters/adapters/markitdownTs/adapter.ts +1 -2
- package/src/converters/adapters/officeparser/adapter.ts +1 -2
- package/src/converters/versions.ts +6 -8
- package/src/core/context-evidence.ts +8 -4
- package/src/core/job-manager.ts +95 -13
- package/src/core/network-boundary-inventory.ts +10 -0
- package/src/core/shutdown-budget.ts +45 -0
- package/src/embed/backlog.ts +107 -4
- package/src/embed/batch.ts +42 -2
- package/src/embed/fingerprint.ts +16 -0
- package/src/embed/retry.ts +113 -5
- package/src/embed/variant-backlog.ts +105 -0
- package/src/embed/variant-plan.ts +62 -0
- package/src/embed/variant-retry.ts +113 -0
- package/src/ingestion/graph-reconciliation.ts +327 -0
- package/src/ingestion/sync.ts +9 -272
- package/src/llm/http-inference.ts +6 -0
- package/src/llm/httpEmbedding.ts +37 -6
- package/src/llm/httpGeneration.ts +18 -3
- package/src/llm/httpRerank.ts +23 -5
- package/src/llm/inference-cancellation.ts +168 -0
- package/src/llm/inference-scope.ts +202 -0
- package/src/llm/lazy-ports.ts +115 -0
- package/src/llm/native-worker/client.ts +541 -0
- package/src/llm/native-worker/dispatcher.ts +228 -0
- package/src/llm/native-worker/embedding-identity.ts +33 -0
- package/src/llm/native-worker/entry.ts +173 -0
- package/src/llm/native-worker/errors.ts +32 -0
- package/src/llm/native-worker/evaluation.ts +16 -0
- package/src/llm/native-worker/owned-exit.ts +108 -0
- package/src/llm/native-worker/owner.ts +141 -0
- package/src/llm/native-worker/ports.ts +317 -0
- package/src/llm/native-worker/protocol.ts +442 -0
- package/src/llm/native-worker/runtime-config.ts +92 -0
- package/src/llm/nodeLlamaCpp/adapter.ts +77 -20
- package/src/llm/nodeLlamaCpp/embedding.ts +130 -46
- package/src/llm/nodeLlamaCpp/generation.ts +34 -5
- package/src/llm/nodeLlamaCpp/lifecycle-options.ts +99 -0
- package/src/llm/nodeLlamaCpp/lifecycle.ts +209 -204
- package/src/llm/nodeLlamaCpp/rerank-capacity.ts +111 -0
- package/src/llm/nodeLlamaCpp/rerank.ts +118 -27
- package/src/llm/nodeLlamaCpp/simulator-handle.ts +73 -0
- package/src/llm/nodeLlamaCpp/simulator-install.ts +124 -0
- package/src/llm/nodeLlamaCpp/simulator-session.ts +240 -0
- package/src/llm/nodeLlamaCpp/simulator-types.ts +80 -0
- package/src/llm/types.ts +35 -5
- package/src/mcp/context.ts +27 -0
- package/src/mcp/http-transport.ts +12 -10
- package/src/mcp/server.ts +3 -0
- package/src/mcp/tool-profile.ts +30 -8
- package/src/mcp/tools/context.ts +8 -11
- package/src/mcp/tools/embed.ts +1 -1
- package/src/mcp/tools/index-cmd.ts +1 -1
- package/src/mcp/tools/index.ts +10 -8
- package/src/mcp/tools/query.ts +14 -30
- package/src/mcp/tools/vsearch.ts +1 -1
- package/src/pipeline/answer.ts +23 -3
- package/src/pipeline/claim-verifier.ts +6 -0
- package/src/pipeline/expansion.ts +43 -40
- package/src/pipeline/explain.ts +6 -2
- package/src/pipeline/filters.ts +63 -0
- package/src/pipeline/fusion.ts +29 -9
- package/src/pipeline/graph-retrieval.ts +29 -9
- package/src/pipeline/hybrid.ts +198 -55
- package/src/pipeline/hydration.ts +161 -0
- package/src/pipeline/owner-fusion.ts +87 -0
- package/src/pipeline/rerank.ts +35 -11
- package/src/pipeline/search.ts +13 -2
- package/src/pipeline/types.ts +5 -3
- package/src/pipeline/vsearch.ts +87 -7
- package/src/sdk/client.ts +47 -3
- package/src/sdk/embed.ts +63 -39
- package/src/serve/background-runtime.ts +1 -1
- package/src/serve/context.ts +41 -56
- package/src/serve/embed-scheduler.ts +58 -35
- package/src/serve/public/components/IndexingProgress.tsx +46 -60
- package/src/serve/public/components/PublishExportDialog.tsx +266 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/globals.css +35 -0
- package/src/serve/public/lib/publish-export.ts +81 -1
- package/src/serve/public/lib/shiki-language-ids.ts +14 -0
- package/src/serve/public/pages/Collections.tsx +12 -46
- package/src/serve/public/pages/DocView.tsx +14 -52
- package/src/serve/resident-admission.ts +36 -36
- package/src/serve/resident-background-work.ts +20 -2
- package/src/serve/resident-request.ts +11 -5
- package/src/serve/resident-runtime.ts +97 -61
- package/src/serve/resident-shutdown.ts +153 -0
- package/src/serve/routes/api.ts +3 -1
- package/src/serve/server.ts +47 -26
- package/src/store/migrations/028-vector-variants.ts +54 -0
- package/src/store/migrations/029-graph-reference-state.ts +77 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +251 -183
- package/src/store/sqlite/eligibility.ts +174 -0
- package/src/store/sqlite/graph-edge-application.ts +66 -0
- package/src/store/sqlite/graph-reference-state.ts +194 -0
- package/src/store/sqlite/legacy-vector-ownership.ts +79 -0
- package/src/store/types.ts +80 -12
- package/src/store/vector/eligibility.ts +36 -0
- package/src/store/vector/freshness.ts +33 -6
- package/src/store/vector/lazy.ts +81 -0
- package/src/store/vector/sqlite-vec.ts +106 -54
- package/src/store/vector/stats.ts +14 -3
- package/src/store/vector/types.ts +35 -2
- package/src/store/vector/variant-search.ts +192 -0
- package/src/store/vector/variants.ts +451 -0
- package/vendor/converters/markitdown-ts/LICENSE +21 -0
- package/vendor/converters/markitdown-ts/dist/index.cjs +1180 -0
- package/vendor/converters/markitdown-ts/dist/index.d.cts +46 -0
- package/vendor/converters/markitdown-ts/dist/index.d.mts +46 -0
- package/vendor/converters/markitdown-ts/dist/index.d.ts +46 -0
- package/vendor/converters/markitdown-ts/dist/index.mjs +1152 -0
- package/vendor/converters/markitdown-ts/package.json +77 -0
- package/vendor/converters/officeparser/LICENSE +21 -0
- package/vendor/converters/officeparser/dist/OfficeConverter.d.ts +47 -0
- package/vendor/converters/officeparser/dist/OfficeConverter.js +76 -0
- package/vendor/converters/officeparser/dist/OfficeGenerator.d.ts +23 -0
- package/vendor/converters/officeparser/dist/OfficeGenerator.js +73 -0
- package/vendor/converters/officeparser/dist/OfficeParser.d.ts +106 -0
- package/vendor/converters/officeparser/dist/OfficeParser.js +332 -0
- package/vendor/converters/officeparser/dist/cli.d.ts +28 -0
- package/vendor/converters/officeparser/dist/cli.js +381 -0
- package/vendor/converters/officeparser/dist/defaults.d.ts +41 -0
- package/vendor/converters/officeparser/dist/defaults.js +218 -0
- package/vendor/converters/officeparser/dist/generators/BaseGenerator.d.ts +107 -0
- package/vendor/converters/officeparser/dist/generators/BaseGenerator.js +248 -0
- package/vendor/converters/officeparser/dist/generators/ChunkingGenerator.d.ts +82 -0
- package/vendor/converters/officeparser/dist/generators/ChunkingGenerator.js +797 -0
- package/vendor/converters/officeparser/dist/generators/CsvGenerator.d.ts +38 -0
- package/vendor/converters/officeparser/dist/generators/CsvGenerator.js +245 -0
- package/vendor/converters/officeparser/dist/generators/EpubGenerator.d.ts +43 -0
- package/vendor/converters/officeparser/dist/generators/EpubGenerator.js +315 -0
- package/vendor/converters/officeparser/dist/generators/HtmlGenerator.d.ts +59 -0
- package/vendor/converters/officeparser/dist/generators/HtmlGenerator.js +1942 -0
- package/vendor/converters/officeparser/dist/generators/MarkdownGenerator.d.ts +96 -0
- package/vendor/converters/officeparser/dist/generators/MarkdownGenerator.js +1175 -0
- package/vendor/converters/officeparser/dist/generators/PdfGenerator.d.ts +22 -0
- package/vendor/converters/officeparser/dist/generators/PdfGenerator.js +194 -0
- package/vendor/converters/officeparser/dist/generators/RtfGenerator.d.ts +29 -0
- package/vendor/converters/officeparser/dist/generators/RtfGenerator.js +316 -0
- package/vendor/converters/officeparser/dist/generators/TextGenerator.d.ts +13 -0
- package/vendor/converters/officeparser/dist/generators/TextGenerator.js +201 -0
- package/vendor/converters/officeparser/dist/index.d.ts +60 -0
- package/vendor/converters/officeparser/dist/index.js +72 -0
- package/vendor/converters/officeparser/dist/index.mjs +18 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.d.ts +2621 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.iife.js +1336 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.mjs +1335 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.slim.d.ts +2621 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.slim.iife.js +1336 -0
- package/vendor/converters/officeparser/dist/officeparser.browser.slim.mjs +1335 -0
- package/vendor/converters/officeparser/dist/parsers/CsvParser.d.ts +9 -0
- package/vendor/converters/officeparser/dist/parsers/CsvParser.js +115 -0
- package/vendor/converters/officeparser/dist/parsers/EpubParser.d.ts +8 -0
- package/vendor/converters/officeparser/dist/parsers/EpubParser.js +217 -0
- package/vendor/converters/officeparser/dist/parsers/ExcelParser.d.ts +32 -0
- package/vendor/converters/officeparser/dist/parsers/ExcelParser.js +736 -0
- package/vendor/converters/officeparser/dist/parsers/HtmlParser.d.ts +2 -0
- package/vendor/converters/officeparser/dist/parsers/HtmlParser.js +1287 -0
- package/vendor/converters/officeparser/dist/parsers/MarkdownParser.d.ts +2 -0
- package/vendor/converters/officeparser/dist/parsers/MarkdownParser.js +1272 -0
- package/vendor/converters/officeparser/dist/parsers/OpenOfficeParser.d.ts +31 -0
- package/vendor/converters/officeparser/dist/parsers/OpenOfficeParser.js +1819 -0
- package/vendor/converters/officeparser/dist/parsers/PdfParser.d.ts +67 -0
- package/vendor/converters/officeparser/dist/parsers/PdfParser.js +848 -0
- package/vendor/converters/officeparser/dist/parsers/PowerPointParser.d.ts +32 -0
- package/vendor/converters/officeparser/dist/parsers/PowerPointParser.js +950 -0
- package/vendor/converters/officeparser/dist/parsers/RtfParser.d.ts +187 -0
- package/vendor/converters/officeparser/dist/parsers/RtfParser.js +1801 -0
- package/vendor/converters/officeparser/dist/parsers/WordParser.d.ts +79 -0
- package/vendor/converters/officeparser/dist/parsers/WordParser.js +1177 -0
- package/vendor/converters/officeparser/dist/sbom.cdx.json +1763 -0
- package/vendor/converters/officeparser/dist/types.d.ts +2507 -0
- package/vendor/converters/officeparser/dist/types.js +107 -0
- package/vendor/converters/officeparser/dist/utils/astUtils.d.ts +16 -0
- package/vendor/converters/officeparser/dist/utils/astUtils.js +33 -0
- package/vendor/converters/officeparser/dist/utils/chartUtils.d.ts +6 -0
- package/vendor/converters/officeparser/dist/utils/chartUtils.js +257 -0
- package/vendor/converters/officeparser/dist/utils/configUtils.d.ts +44 -0
- package/vendor/converters/officeparser/dist/utils/configUtils.js +315 -0
- package/vendor/converters/officeparser/dist/utils/dateUtils.d.ts +17 -0
- package/vendor/converters/officeparser/dist/utils/dateUtils.js +69 -0
- package/vendor/converters/officeparser/dist/utils/envUtils.d.ts +29 -0
- package/vendor/converters/officeparser/dist/utils/envUtils.js +152 -0
- package/vendor/converters/officeparser/dist/utils/errorUtils.d.ts +72 -0
- package/vendor/converters/officeparser/dist/utils/errorUtils.js +245 -0
- package/vendor/converters/officeparser/dist/utils/imageUtils.d.ts +66 -0
- package/vendor/converters/officeparser/dist/utils/imageUtils.js +133 -0
- package/vendor/converters/officeparser/dist/utils/mathUtils.d.ts +42 -0
- package/vendor/converters/officeparser/dist/utils/mathUtils.js +385 -0
- package/vendor/converters/officeparser/dist/utils/moduleLoader.d.ts +18 -0
- package/vendor/converters/officeparser/dist/utils/moduleLoader.js +106 -0
- package/vendor/converters/officeparser/dist/utils/ocrUtils.d.ts +42 -0
- package/vendor/converters/officeparser/dist/utils/ocrUtils.js +428 -0
- package/vendor/converters/officeparser/dist/utils/sanitize.d.ts +148 -0
- package/vendor/converters/officeparser/dist/utils/sanitize.js +344 -0
- package/vendor/converters/officeparser/dist/utils/sheetUtils.d.ts +7 -0
- package/vendor/converters/officeparser/dist/utils/sheetUtils.js +35 -0
- package/vendor/converters/officeparser/dist/utils/styleMapper.d.ts +36 -0
- package/vendor/converters/officeparser/dist/utils/styleMapper.js +224 -0
- package/vendor/converters/officeparser/dist/utils/xmlUtils.d.ts +163 -0
- package/vendor/converters/officeparser/dist/utils/xmlUtils.js +461 -0
- package/vendor/converters/officeparser/dist/utils/zipUtils.d.ts +134 -0
- package/vendor/converters/officeparser/dist/utils/zipUtils.js +337 -0
- package/vendor/converters/officeparser/package.json +147 -0
- package/vendor/converters/upstream-manifest.json +124 -0
- package/vendor/dependency-fixes/README.md +77 -0
- package/vendor/dependency-fixes/vendor-converters.py +83 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.46.0.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.46.0.zip.sha256 +0 -1
- package/browser-extension/dist/chunk-627emwpj.js +0 -75
- /package/browser-extension/dist/{chunk-ydfx5d7p.css → chunk-z74y8n8c.css} +0 -0
|
@@ -0,0 +1,1175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MarkdownGenerator = void 0;
|
|
4
|
+
const sanitize_js_1 = require("../utils/sanitize.js");
|
|
5
|
+
const BaseGenerator_js_1 = require("./BaseGenerator.js");
|
|
6
|
+
const errorUtils_js_1 = require("../utils/errorUtils.js");
|
|
7
|
+
/**
|
|
8
|
+
* Values accepted for an attribute-list `align=`. Matches what `MarkdownParser`'s own
|
|
9
|
+
* `parseAttributeList` allowlists on import (plus `justify`, which HTML sources can supply),
|
|
10
|
+
* so this is lossless for anything the parser produced.
|
|
11
|
+
*/
|
|
12
|
+
const MD_ALIGN_VALUES = new Set(['left', 'center', 'right', 'justify']);
|
|
13
|
+
/** A CSS length or percentage - the only shape `width=` legitimately carries. */
|
|
14
|
+
const MD_LENGTH_PATTERN = /^\d+(?:\.\d+)?(?:px|%|em|rem|pt|pc|in|cm|mm|ex|ch|vw|vh)?$/;
|
|
15
|
+
/** Admonition kinds, mirroring the union declared on `AdmonitionMetadata` in types.ts. */
|
|
16
|
+
const MD_ADMONITION_TYPES = new Set(['note', 'tip', 'important', 'warning', 'caution']);
|
|
17
|
+
/**
|
|
18
|
+
* Folds line breaks to spaces.
|
|
19
|
+
*
|
|
20
|
+
* Used on values that sit inside a single-line construct (an abbreviation definition, an
|
|
21
|
+
* admonition's bold title). A raw newline there does not merely look wrong: it terminates the
|
|
22
|
+
* construct and exposes whatever follows as document-level Markdown.
|
|
23
|
+
*/
|
|
24
|
+
const foldLines = (value) => String(value ?? '').replace(/[\r\n]+/g, ' ');
|
|
25
|
+
/**
|
|
26
|
+
* Named Markdown dialect presets. `extended` reproduces this library's historical output
|
|
27
|
+
* exactly (every feature on, GitHub-style admonitions) - the backward-compatibility anchor.
|
|
28
|
+
*/
|
|
29
|
+
const MARKDOWN_DIALECT_PRESETS = {
|
|
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
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Normalizes `MdGeneratorConfig.dialect` into a fully-resolved preset. A string names a preset
|
|
39
|
+
* directly; an object's `extends` field (default `'extended'`) names the base preset that any
|
|
40
|
+
* omitted field falls back to - NOT "whatever preset was ambient before", since config merging
|
|
41
|
+
* replaces the whole `dialect` field rather than layering an object on top of a prior string.
|
|
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
|
+
}
|
|
67
|
+
function resolveDialect(dialect) {
|
|
68
|
+
if (dialect === undefined)
|
|
69
|
+
return MARKDOWN_DIALECT_PRESETS.extended;
|
|
70
|
+
if (typeof dialect === 'string')
|
|
71
|
+
return MARKDOWN_DIALECT_PRESETS[dialect] ?? MARKDOWN_DIALECT_PRESETS.extended;
|
|
72
|
+
const base = MARKDOWN_DIALECT_PRESETS[dialect.extends ?? 'extended'] ?? MARKDOWN_DIALECT_PRESETS.extended;
|
|
73
|
+
return {
|
|
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),
|
|
79
|
+
math: dialect.math ?? base.math,
|
|
80
|
+
attributeLists: resolveToggle(dialect.attributeLists, 'brace', base.attributeLists),
|
|
81
|
+
strikethrough: resolveToggle(dialect.strikethrough, 'tilde', base.strikethrough),
|
|
82
|
+
highlight: dialect.highlight ?? base.highlight,
|
|
83
|
+
bulletListMarker: dialect.bulletListMarker ?? base.bulletListMarker,
|
|
84
|
+
orderedListMarker: dialect.orderedListMarker ?? base.orderedListMarker,
|
|
85
|
+
emphasisMarker: dialect.emphasisMarker ?? base.emphasisMarker,
|
|
86
|
+
tables: dialect.tables ?? base.tables,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Normalizes `MdGeneratorConfig.fallbackToHtml` into a fully resolved object, mirroring
|
|
91
|
+
* `HtmlGenerator`'s `resolveStandalone()` pattern: `true`/undefined turns every part on; `false`
|
|
92
|
+
* turns every part off; an object's omitted fields default to on.
|
|
93
|
+
*/
|
|
94
|
+
function resolveFallbackToHtml(fallbackToHtml) {
|
|
95
|
+
const uniform = (on) => ({
|
|
96
|
+
// inlineFormatting is opt-in only: it is never enabled by the boolean form, since it changes
|
|
97
|
+
// default output. Every other field follows the boolean.
|
|
98
|
+
textFormatting: on, alignment: on, anchors: on, tables: on, embeds: on, cellLineBreaks: on,
|
|
99
|
+
itemLineBreaks: on,
|
|
100
|
+
inlineFormatting: false,
|
|
101
|
+
});
|
|
102
|
+
if (fallbackToHtml === undefined || typeof fallbackToHtml === 'boolean')
|
|
103
|
+
return uniform(fallbackToHtml ?? true);
|
|
104
|
+
const on = uniform(true);
|
|
105
|
+
return {
|
|
106
|
+
textFormatting: fallbackToHtml.textFormatting ?? on.textFormatting,
|
|
107
|
+
alignment: fallbackToHtml.alignment ?? on.alignment,
|
|
108
|
+
anchors: fallbackToHtml.anchors ?? on.anchors,
|
|
109
|
+
tables: fallbackToHtml.tables ?? on.tables,
|
|
110
|
+
embeds: fallbackToHtml.embeds ?? on.embeds,
|
|
111
|
+
cellLineBreaks: fallbackToHtml.cellLineBreaks ?? on.cellLineBreaks,
|
|
112
|
+
itemLineBreaks: fallbackToHtml.itemLineBreaks ?? on.itemLineBreaks,
|
|
113
|
+
inlineFormatting: fallbackToHtml.inlineFormatting ?? on.inlineFormatting,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Generates Markdown from an AST.
|
|
118
|
+
*
|
|
119
|
+
* DESIGN PRINCIPLES:
|
|
120
|
+
* 1. **Strict Native Preference**: Always utilize native Markdown syntax for features that
|
|
121
|
+
* are natively supported (headings, lists, bold/italic, etc.). HTML tags should NEVER
|
|
122
|
+
* be used for these features.
|
|
123
|
+
*
|
|
124
|
+
* 2. **Fidelity vs. Purity (The `fallbackToHtml` Principle)**:
|
|
125
|
+
* - When a given `fallbackToHtml` part is TRUE: The generator prioritizes high-fidelity
|
|
126
|
+
* document conversion for that part. It will use HTML tags for features that Markdown
|
|
127
|
+
* cannot natively represent (e.g., `<u>` for underline, `<div>` for alignment, `<table>`
|
|
128
|
+
* for nested structures or merged cells).
|
|
129
|
+
* - When FALSE: The generator prioritizes "pure" Markdown for that part.
|
|
130
|
+
* Unsupported features are either:
|
|
131
|
+
* - **Skipped**: Non-essential formatting like underline, subscript, superscript,
|
|
132
|
+
* or text alignment is omitted.
|
|
133
|
+
* - **Simplified/Hoisted**: Complex structures like nested tables are hoisted out
|
|
134
|
+
* of their parent cells and rendered as separate sequential tables to maintain
|
|
135
|
+
* valid Markdown syntax.
|
|
136
|
+
*
|
|
137
|
+
* 3. **Consistency**: All similar structural or formatting ideological problems must be
|
|
138
|
+
* resolved using these same rules to ensure predictable output.
|
|
139
|
+
*
|
|
140
|
+
* 4. **Dialect (`MdGeneratorConfig.dialect`)**: A second, independent axis from `fallbackToHtml` -
|
|
141
|
+
* which *native* Markdown syntax to emit for constructs with more than one real-world
|
|
142
|
+
* convention (admonitions, definition lists, footnotes, citations, wikilinks, math, list/
|
|
143
|
+
* emphasis markers, tables). See `resolveDialect()` and `MARKDOWN_DIALECT_PRESETS` above.
|
|
144
|
+
*/
|
|
145
|
+
class MarkdownGenerator extends BaseGenerator_js_1.BaseGenerator {
|
|
146
|
+
isInsideTable = false;
|
|
147
|
+
/**
|
|
148
|
+
* Set while rendering the children of a heading, or the cells of a table's header row.
|
|
149
|
+
*
|
|
150
|
+
* Markdown already conveys "this is a heading" with `#` and "this is a header row" with the
|
|
151
|
+
* separator line, so a run inside one that also carries bold - the normal case for ODF, whose
|
|
152
|
+
* heading and header-row paragraph styles are bold and are now inherited by their runs - would
|
|
153
|
+
* render as `# **Heading**` and `| **ITEM** |`. That is redundant rather than wrong, but it
|
|
154
|
+
* also round-trips back into bold text nodes nested inside a heading, so the noise compounds
|
|
155
|
+
* on every parse/generate cycle. Emphasis the node type already implies is dropped; every
|
|
156
|
+
* other formatting flag still comes through.
|
|
157
|
+
*/
|
|
158
|
+
inImplicitBold = false;
|
|
159
|
+
hoistedContent = [];
|
|
160
|
+
collectedAbbreviations = new Map();
|
|
161
|
+
resolvedDialect;
|
|
162
|
+
resolvedFallbackToHtml;
|
|
163
|
+
resolvedEmbeds;
|
|
164
|
+
constructor(ast, config) {
|
|
165
|
+
super('md', ast, config);
|
|
166
|
+
this.resolvedDialect = resolveDialect(this.config.mdConfig.dialect);
|
|
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');
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Renders anchor tags if HTML fallback is allowed.
|
|
179
|
+
*/
|
|
180
|
+
renderAnchors(metadata) {
|
|
181
|
+
if (!this.resolvedFallbackToHtml.anchors || this.config.ignoreInternalLinks)
|
|
182
|
+
return '';
|
|
183
|
+
const ids = metadata?.anchorIds || [];
|
|
184
|
+
return ids.map((aid) => `<a id="${this.slugify(aid)}"></a>`).join('');
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Serializes a frontmatter array as a YAML flow sequence (e.g. `[a, b]`), matching
|
|
188
|
+
* MarkdownParser's frontmatter array handling. Plain strings are left bare; anything
|
|
189
|
+
* that would break flow-array syntax (or isn't a string) falls back to JSON encoding.
|
|
190
|
+
*/
|
|
191
|
+
serializeFrontmatterArray(arr) {
|
|
192
|
+
const items = arr.map(item => (typeof item === 'string' && item.trim() === item && !/[,[\]]/.test(item))
|
|
193
|
+
? item
|
|
194
|
+
: JSON.stringify(item));
|
|
195
|
+
return `[${items.join(', ')}]`;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Renders a Pandoc-style attribute list (e.g. `{width=50% align=left}`) from
|
|
199
|
+
* ImageMetadata/TableMetadata's width/align fields - the canonical form is always
|
|
200
|
+
* `key=value`, matching MarkdownParser's own vocabulary (MARKDOWN_DIALECT.md §15).
|
|
201
|
+
*/
|
|
202
|
+
renderAttributeList(meta, options = {}) {
|
|
203
|
+
if (this.resolvedDialect.attributeLists === 'none')
|
|
204
|
+
return '';
|
|
205
|
+
if (!meta)
|
|
206
|
+
return '';
|
|
207
|
+
const align = options.skipAlign ? undefined : meta.align;
|
|
208
|
+
if (!meta.width && !align)
|
|
209
|
+
return '';
|
|
210
|
+
const parts = [];
|
|
211
|
+
// Allowlist, not escape. These land in `metadata.width`/`align` on reparse, which the
|
|
212
|
+
// parser does NOT entity-decode, so encoding here would not round-trip - and stripping
|
|
213
|
+
// alone is not enough: the previous `[{}\s]+` guard removed whitespace, which stops
|
|
214
|
+
// `<img src=x onerror=…>` but not the slash-separated `<img/src=x/onerror=…>`.
|
|
215
|
+
// Both values have a small, fully-known shape, so matching that shape is both safer and
|
|
216
|
+
// lossless for anything a parser can produce.
|
|
217
|
+
//
|
|
218
|
+
// (`isValidContainerWidth` in utils/configUtils.ts is a near-identical regex, but it is a
|
|
219
|
+
// config validator that also accepts 'auto' and numbers; importing configUtils here for
|
|
220
|
+
// one pattern would be a worse coupling than this local constant.)
|
|
221
|
+
if (meta.width && MD_LENGTH_PATTERN.test(String(meta.width).trim())) {
|
|
222
|
+
parts.push(`width=${String(meta.width).trim()}`);
|
|
223
|
+
}
|
|
224
|
+
if (align && MD_ALIGN_VALUES.has(String(align).trim().toLowerCase())) {
|
|
225
|
+
parts.push(`align=${String(align).trim().toLowerCase()}`);
|
|
226
|
+
}
|
|
227
|
+
if (parts.length === 0)
|
|
228
|
+
return '';
|
|
229
|
+
return `{${parts.join(' ')}}`;
|
|
230
|
+
}
|
|
231
|
+
/** Converts a document-supplied date to an ISO string, or '' if invalid
|
|
232
|
+
* (a malformed date would otherwise throw a RangeError and abort generation). */
|
|
233
|
+
toIsoDate(value) {
|
|
234
|
+
if (value === undefined || value === null || value === '')
|
|
235
|
+
return '';
|
|
236
|
+
const d = new Date(value);
|
|
237
|
+
return isNaN(d.getTime()) ? '' : d.toISOString();
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Generates Markdown string from the provided AST.
|
|
241
|
+
*
|
|
242
|
+
* @returns A Markdown string
|
|
243
|
+
*/
|
|
244
|
+
async generate() {
|
|
245
|
+
let output = '';
|
|
246
|
+
// Add Metadata (YAML Front Matter)
|
|
247
|
+
const meta = this.effectiveMetadata;
|
|
248
|
+
if (meta) {
|
|
249
|
+
// Build the field lines first. JSON-encode scalar values so a title/author/description
|
|
250
|
+
// containing a quote or newline can't break out of the YAML string and inject arbitrary
|
|
251
|
+
// front-matter keys. (JSON.stringify of a benign value yields the same `"..."` form as
|
|
252
|
+
// before, so normal output is unchanged.)
|
|
253
|
+
let fields = '';
|
|
254
|
+
if (meta.title)
|
|
255
|
+
fields += `title: ${JSON.stringify(meta.title)}\n`;
|
|
256
|
+
if (meta.author)
|
|
257
|
+
fields += `author: ${JSON.stringify(meta.author)}\n`;
|
|
258
|
+
const createdIso = this.toIsoDate(meta.created);
|
|
259
|
+
if (createdIso)
|
|
260
|
+
fields += `created: ${createdIso}\n`;
|
|
261
|
+
const modifiedIso = this.toIsoDate(meta.modified);
|
|
262
|
+
if (modifiedIso)
|
|
263
|
+
fields += `modified: ${modifiedIso}\n`;
|
|
264
|
+
if (meta.description)
|
|
265
|
+
fields += `description: ${JSON.stringify(meta.description)}\n`;
|
|
266
|
+
if (meta.subject)
|
|
267
|
+
fields += `subject: ${JSON.stringify(meta.subject)}\n`;
|
|
268
|
+
if (meta.keywords)
|
|
269
|
+
fields += `keywords: ${JSON.stringify(meta.keywords)}\n`;
|
|
270
|
+
if (meta.customProperties) {
|
|
271
|
+
for (const [key, val] of Object.entries(meta.customProperties)) {
|
|
272
|
+
// Strip newlines/colons from the key so it can't inject a new mapping.
|
|
273
|
+
const safeKey = String(key).replace(/[\r\n:]+/g, ' ').trim();
|
|
274
|
+
fields += `${safeKey}: ${Array.isArray(val) ? this.serializeFrontmatterArray(val) : JSON.stringify(val)}\n`;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// Only emit the frontmatter fence when at least one field is present. Empty metadata
|
|
278
|
+
// (a bare Tiptap/HTML fragment with no <head>) would otherwise emit `---\n---`, which
|
|
279
|
+
// reparses as a setext `## ---` heading and corrupts the document on every save/reload.
|
|
280
|
+
if (fields) {
|
|
281
|
+
output += `---\n${fields}---\n\n`;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const processor = async (node, childrenOutput) => {
|
|
285
|
+
// Handle Style Mapping for Markdown using the semantic mapping helper
|
|
286
|
+
const mapping = this.getSemanticMapping(node);
|
|
287
|
+
if (mapping) {
|
|
288
|
+
// Map common HTML tags to Markdown equivalents
|
|
289
|
+
if (mapping.tag === 'blockquote')
|
|
290
|
+
return `> ${childrenOutput}\n\n`;
|
|
291
|
+
if (mapping.tag === 'code')
|
|
292
|
+
return `\`${childrenOutput}\` `;
|
|
293
|
+
if (mapping.tag === 'pre')
|
|
294
|
+
return `\`\`\`\n${childrenOutput}\n\`\`\`\n\n`;
|
|
295
|
+
const hMatch = mapping.tag.match(/^h([1-6])$/);
|
|
296
|
+
if (hMatch) {
|
|
297
|
+
const level = parseInt(hMatch[1]);
|
|
298
|
+
return `${'#'.repeat(level)} ${childrenOutput}\n\n`;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
switch (node.type) {
|
|
302
|
+
case 'text': {
|
|
303
|
+
// Entity-encode angle brackets so document text can't inject a raw
|
|
304
|
+
// HTML tag (e.g. <script>) when the Markdown is rendered to HTML.
|
|
305
|
+
let text = (0, sanitize_js_1.markdownEscapeText)(node.text || '');
|
|
306
|
+
if (this.config.includeFormatting && node.formatting) {
|
|
307
|
+
// Inline code: re-wrap the RAW text in backticks. The content is literal
|
|
308
|
+
// inside a code span, so the entity-escaped form above must not show through.
|
|
309
|
+
// The fence is one backtick longer than the longest embedded run so an inner
|
|
310
|
+
// backtick can't close the span early, padded when the content touches a
|
|
311
|
+
// backtick. Done before emphasis so bold/italic wrap the span (`**`code`**`).
|
|
312
|
+
// Previously a monospace text node emitted its bare text, dropping the code.
|
|
313
|
+
if (node.formatting.font === 'monospace') {
|
|
314
|
+
const raw = node.text || '';
|
|
315
|
+
const longestRun = Math.max(0, ...(raw.match(/`+/g) || []).map(s => s.length));
|
|
316
|
+
const fence = '`'.repeat(longestRun + 1);
|
|
317
|
+
const pad = (raw.startsWith('`') || raw.endsWith('`')) ? ' ' : '';
|
|
318
|
+
text = `${fence}${pad}${raw}${pad}${fence}`;
|
|
319
|
+
}
|
|
320
|
+
const emphasisAsterisk = this.resolvedDialect.emphasisMarker === 'asterisk';
|
|
321
|
+
if (node.formatting.bold && !this.inImplicitBold)
|
|
322
|
+
text = emphasisAsterisk ? `**${text}**` : `__${text}__`;
|
|
323
|
+
if (node.formatting.italic)
|
|
324
|
+
text = emphasisAsterisk ? `*${text}*` : `_${text}_`;
|
|
325
|
+
if (node.formatting.strikethrough && this.resolvedDialect.strikethrough !== 'none')
|
|
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}==`;
|
|
338
|
+
// Use HTML tags for formatting not natively supported by standard Markdown
|
|
339
|
+
if (this.resolvedFallbackToHtml.textFormatting) {
|
|
340
|
+
if (node.formatting.underline)
|
|
341
|
+
text = `<u>${text}</u>`;
|
|
342
|
+
if (node.formatting.subscript)
|
|
343
|
+
text = `<sub>${text}</sub>`;
|
|
344
|
+
if (node.formatting.superscript)
|
|
345
|
+
text = `<sup>${text}</sup>`;
|
|
346
|
+
}
|
|
347
|
+
// Inline color / highlight / font size have no Markdown syntax; emit a styled
|
|
348
|
+
// <span> (outermost, so the inner Markdown markers survive) only when opted in,
|
|
349
|
+
// so default output is unchanged. Values are CSS-sanitized against injection.
|
|
350
|
+
if (this.resolvedFallbackToHtml.inlineFormatting) {
|
|
351
|
+
const styles = [];
|
|
352
|
+
const pushStyle = (prop, val) => {
|
|
353
|
+
if (!val)
|
|
354
|
+
return;
|
|
355
|
+
const safe = (0, sanitize_js_1.sanitizeCssValue)(val); // drops url()/expression()/<>/quotes
|
|
356
|
+
if (safe)
|
|
357
|
+
styles.push(`${prop}: ${safe}`);
|
|
358
|
+
};
|
|
359
|
+
pushStyle('color', node.formatting.color);
|
|
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);
|
|
365
|
+
pushStyle('font-size', node.formatting.size);
|
|
366
|
+
if (styles.length)
|
|
367
|
+
text = `<span style="${styles.join('; ')}">${text}</span>`;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const meta = node.metadata;
|
|
371
|
+
if (meta?.wikilink && this.resolvedDialect.wikilinks !== 'none') {
|
|
372
|
+
// Obsidian syntax: bare page name, or page|alias when the display
|
|
373
|
+
// text differs from the page name. Strip the `[]|`/newline chars
|
|
374
|
+
// that would break out of the `[[...]]` wrapper.
|
|
375
|
+
// The alias must be built from the ESCAPED text, not from raw node.text.
|
|
376
|
+
// Rebuilding from the raw value here discarded the markdownEscapeText()
|
|
377
|
+
// applied above, so a wikilink was the one place document text reached
|
|
378
|
+
// the output unescaped. Escaping is lossless for the alias specifically,
|
|
379
|
+
// because it lands back in a text node, which the parser entity-decodes.
|
|
380
|
+
const alias = (0, sanitize_js_1.markdownEscapeText)(node.text || '').replace(/[[\]|\r\n]+/g, '');
|
|
381
|
+
// `page` lands in metadata.link, which is NOT entity-decoded on reparse,
|
|
382
|
+
// so it gets `<` dropped rather than encoded - a page name is an
|
|
383
|
+
// identifier, and `<` carries no meaning in one.
|
|
384
|
+
const page = (meta.link || '').replace(/[[\]|<\r\n]+/g, '');
|
|
385
|
+
text = (node.text && node.text !== (meta.link || '')) ? `[[${page}|${alias}]]` : `[[${page}]]`;
|
|
386
|
+
}
|
|
387
|
+
else if (meta?.link) {
|
|
388
|
+
const isInternal = meta.linkType !== 'external';
|
|
389
|
+
if (!this.config.ignoreInternalLinks || !isInternal) {
|
|
390
|
+
let link = meta.link;
|
|
391
|
+
// Slugify internal link targets to match heading IDs if generating IDs
|
|
392
|
+
if (isInternal && link.startsWith('#') && (this.config.generateIds || this.resolvedFallbackToHtml.anchors)) {
|
|
393
|
+
const target = link.substring(1);
|
|
394
|
+
link = '#' + this.slugify(target);
|
|
395
|
+
}
|
|
396
|
+
// Reject javascript:/data: schemes and encode `()`/whitespace so the
|
|
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})`;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (meta?.abbreviationTitle) {
|
|
405
|
+
// Markdown Extra's abbreviation syntax has no inline marker - the bare
|
|
406
|
+
// word round-trips as-is, with its expansion collected at the document
|
|
407
|
+
// end via `*[abbr]: title`.
|
|
408
|
+
this.collectedAbbreviations.set(node.text || '', meta.abbreviationTitle);
|
|
409
|
+
}
|
|
410
|
+
if (meta?.citationKey) {
|
|
411
|
+
// Allowlist to exactly the character class MarkdownParser's own citation
|
|
412
|
+
// recognizer accepts, so this is provably lossless for anything it
|
|
413
|
+
// produced - while fully neutralizing a key arriving from HtmlParser's
|
|
414
|
+
// `data-citation-key`, which accepts any string. Like the wikilink above,
|
|
415
|
+
// this branch also replaces `text` wholesale, so a strip that left `<`
|
|
416
|
+
// behind discarded the escaping applied earlier.
|
|
417
|
+
const key = String(meta.citationKey).replace(/[^a-zA-Z0-9_:.-]/g, '');
|
|
418
|
+
text = this.resolvedDialect.citations !== 'none' ? `[@${key}]` : `[${key}]`;
|
|
419
|
+
}
|
|
420
|
+
return text;
|
|
421
|
+
}
|
|
422
|
+
case 'heading': {
|
|
423
|
+
const meta = node.metadata;
|
|
424
|
+
const level = Math.min(Math.max(meta?.level || 1, 1), 6);
|
|
425
|
+
const prefix = '#'.repeat(level) + ' ';
|
|
426
|
+
let id = '';
|
|
427
|
+
let remainingAnchors = [];
|
|
428
|
+
if (!this.config.ignoreInternalLinks && meta?.anchorIds && meta.anchorIds.length > 0) {
|
|
429
|
+
const ids = [...meta.anchorIds];
|
|
430
|
+
const lastId = ids.pop();
|
|
431
|
+
// Slugify the explicit ID to ensure it's a valid Markdown identifier
|
|
432
|
+
id = ` {#${this.slugify(lastId)}}`;
|
|
433
|
+
remainingAnchors = ids;
|
|
434
|
+
}
|
|
435
|
+
else if (this.config.generateIds) {
|
|
436
|
+
id = ` {#${this.slugify(this.getNodeText(node))}}`;
|
|
437
|
+
}
|
|
438
|
+
const anchors = this.resolvedFallbackToHtml.anchors
|
|
439
|
+
? remainingAnchors.map(aid => `<a name="${this.slugify(aid)}"></a>`).join('')
|
|
440
|
+
: '';
|
|
441
|
+
let content = `${prefix}${childrenOutput}${id}`;
|
|
442
|
+
// Alignment fallback via HTML div/p
|
|
443
|
+
if (this.resolvedFallbackToHtml.alignment && meta?.alignment && meta.alignment !== 'left') {
|
|
444
|
+
// Use extra newlines to ensure Markdown inside the div is parsed
|
|
445
|
+
content = `<div style="text-align: ${(0, sanitize_js_1.sanitizeCssValue)(meta.alignment)}">\n\n${content}\n\n</div>`;
|
|
446
|
+
}
|
|
447
|
+
return `${anchors}${anchors ? '\n' : ''}${content}\n\n`;
|
|
448
|
+
}
|
|
449
|
+
case 'paragraph': {
|
|
450
|
+
const meta = node.metadata;
|
|
451
|
+
const anchors = this.renderAnchors(meta);
|
|
452
|
+
let content = childrenOutput;
|
|
453
|
+
// Alignment fallback via HTML div/p
|
|
454
|
+
if (this.resolvedFallbackToHtml.alignment && meta?.alignment && meta.alignment !== 'left') {
|
|
455
|
+
content = `<div style="text-align: ${(0, sanitize_js_1.sanitizeCssValue)(meta.alignment)}">${content}</div>`;
|
|
456
|
+
}
|
|
457
|
+
return childrenOutput ? `${anchors}${content}\n\n` : '';
|
|
458
|
+
}
|
|
459
|
+
case 'list': {
|
|
460
|
+
const meta = node.metadata;
|
|
461
|
+
const indentSpaces = ' '.repeat(4);
|
|
462
|
+
const indent = indentSpaces.repeat(meta?.indentation || 0);
|
|
463
|
+
const bullet = `${this.resolvedDialect.bulletListMarker} `;
|
|
464
|
+
const marker = meta?.isTask
|
|
465
|
+
? (meta.checked ? `${bullet}[x] ` : `${bullet}[ ] `)
|
|
466
|
+
: (meta?.listType === 'ordered' ? `${(meta.itemIndex ?? 0) + 1}${this.resolvedDialect.orderedListMarker} ` : bullet);
|
|
467
|
+
const anchors = this.renderAnchors(meta);
|
|
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`;
|
|
479
|
+
}
|
|
480
|
+
case 'image': {
|
|
481
|
+
if (!this.config.includeImages)
|
|
482
|
+
return '';
|
|
483
|
+
const meta = node.metadata;
|
|
484
|
+
const alt = meta?.altText || 'image';
|
|
485
|
+
let src = meta?.url || meta?.attachmentName || '';
|
|
486
|
+
// Resolve attachment to data URI if no external URL is provided
|
|
487
|
+
if (!meta?.url && meta?.attachmentName && this.ast) {
|
|
488
|
+
const attachment = this.ast.attachments.find(a => a.name === meta.attachmentName);
|
|
489
|
+
if (attachment) {
|
|
490
|
+
src = `data:${attachment.mimeType || 'image/png'};base64,${attachment.data}`;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const anchors = this.renderAnchors(meta);
|
|
494
|
+
// Strip `[]` from alt (would close the `![...]`) and neutralize the URL scheme.
|
|
495
|
+
const safeAlt = (0, sanitize_js_1.markdownEscapeText)(alt).replace(/[[\]]/g, '');
|
|
496
|
+
const safeSrc = (0, sanitize_js_1.sanitizeMarkdownUrl)(src, { allowDataImage: true });
|
|
497
|
+
const imgTitle = meta?.title ? ` "${meta.title.replace(/"/g, '\\"')}"` : '';
|
|
498
|
+
return `${anchors}${anchors ? '\n' : ''}${this.renderAttributeList(meta)}`;
|
|
499
|
+
}
|
|
500
|
+
case 'table': {
|
|
501
|
+
const anchors = this.renderAnchors(node.metadata);
|
|
502
|
+
const tableOutput = await this.renderMarkdownTable(node, processor);
|
|
503
|
+
// The HTML-fallback path (merged cells/nested tables, or a dialect that forces
|
|
504
|
+
// HTML tables outright) already carries data-align on the <table> tag directly -
|
|
505
|
+
// only the plain pipe-table form needs the attribute-list syntax for alignment.
|
|
506
|
+
const usedHtmlFallback = this.resolvedDialect.tables === 'html' ||
|
|
507
|
+
(this.resolvedFallbackToHtml.tables && (this.hasNestedTable(node) || this.hasColspanOrRowspan(node)));
|
|
508
|
+
const attrList = usedHtmlFallback ? '' : this.renderAttributeList(node.metadata, { skipAlign: true });
|
|
509
|
+
if (attrList) {
|
|
510
|
+
// Must glue directly below the last row with no blank line, or
|
|
511
|
+
// MarkdownParser's block splitter won't see it as part of the same block.
|
|
512
|
+
return `${anchors}${anchors ? '\n' : ''}${tableOutput.replace(/\n+$/, '\n')}${attrList}\n`;
|
|
513
|
+
}
|
|
514
|
+
return `${anchors}${anchors ? '\n' : ''}${tableOutput}`;
|
|
515
|
+
}
|
|
516
|
+
case 'row':
|
|
517
|
+
case 'cell': {
|
|
518
|
+
// These are handled manually in the 'table' case above
|
|
519
|
+
return childrenOutput;
|
|
520
|
+
}
|
|
521
|
+
case 'break': {
|
|
522
|
+
// A hard line break (CommonMark: two trailing spaces before the newline)
|
|
523
|
+
// round-trips back to a distinct 'break' node on reparse. A thematic break
|
|
524
|
+
// emits `---` as its own block (the top-level loop supplies the surrounding
|
|
525
|
+
// blank lines), so a Markdown `---` / HTML `<hr>` survives a save instead of
|
|
526
|
+
// collapsing to whitespace. Every other breakType - notably 'page', which
|
|
527
|
+
// Markdown has no syntax for - keeps emitting a bare newline, unchanged.
|
|
528
|
+
const meta = node.metadata;
|
|
529
|
+
if (meta?.breakType === 'carriageReturn')
|
|
530
|
+
return ' \n';
|
|
531
|
+
if (meta?.breakType === 'thematic')
|
|
532
|
+
return '---';
|
|
533
|
+
return '\n';
|
|
534
|
+
}
|
|
535
|
+
case 'code': {
|
|
536
|
+
const meta = node.metadata;
|
|
537
|
+
// Math content reached the output completely raw, which mattered most under
|
|
538
|
+
// `math: 'none'` (the commonmark preset), where there is no `$` wrapper at
|
|
539
|
+
// all and the text lands directly in the document body.
|
|
540
|
+
//
|
|
541
|
+
// Encode rather than drop: `$a < b$` is ordinary LaTeX, and dropping `<`
|
|
542
|
+
// would silently corrupt real formulae. markdownEscapeText only touches `<`
|
|
543
|
+
// followed by a letter/`/`/`!`/`?`, which is not idiomatic math, and it is
|
|
544
|
+
// idempotent - so output is stable across repeated round-trips even though
|
|
545
|
+
// the first cycle shifts an anomalous `<img` to `<img`. (Fully lossless
|
|
546
|
+
// would mean teaching MarkdownParser.decodeHtmlEntities to cover math `code`
|
|
547
|
+
// nodes; that is a parser behaviour change with its own baseline
|
|
548
|
+
// consequences and must not gate a security fix.)
|
|
549
|
+
if (meta?.math === 'block') {
|
|
550
|
+
// A content line of exactly `$$` would close the block early.
|
|
551
|
+
const mathBlock = (0, sanitize_js_1.markdownEscapeText)(node.text || '')
|
|
552
|
+
.split('\n').map(l => (l.trim() === '$$' ? ` ${l}` : l)).join('\n');
|
|
553
|
+
return this.resolvedDialect.math === 'dollar' ? `\n$$\n${mathBlock}\n$$\n\n` : `\n${mathBlock}\n\n`;
|
|
554
|
+
}
|
|
555
|
+
if (meta?.math === 'inline') {
|
|
556
|
+
// Dropping `$` and newlines is lossless here: the parser's own inline-math
|
|
557
|
+
// recognizer is `\$(?!\s)([^$\n]+?)(?<!\s)\$`, which can never capture either.
|
|
558
|
+
const mathInline = (0, sanitize_js_1.markdownEscapeText)(node.text || '').replace(/[$\r\n]+/g, '');
|
|
559
|
+
return this.resolvedDialect.math === 'dollar' ? `$${mathInline}$` : mathInline;
|
|
560
|
+
}
|
|
561
|
+
const lang = (meta?.language || '').replace(/[\r\n`]+/g, '');
|
|
562
|
+
// A `code` node is always block-level: genuinely inline code is a monospace
|
|
563
|
+
// text node, never a `code` node. So emit a fenced block whenever the node
|
|
564
|
+
// carries a language OR spans multiple lines. Previously the decision keyed only
|
|
565
|
+
// off a line break, so a single-line code node with a language - `const x = 1;`
|
|
566
|
+
// tagged `js`, or a one-line `mermaid` diagram - collapsed to an inline span,
|
|
567
|
+
// silently dropping both its language and its block-ness. (Testing `[\r\n]`, not
|
|
568
|
+
// just `\n`, still routes a CR-only body to the fenced branch, where a renderer
|
|
569
|
+
// that normalizes `\r` to a line ending would otherwise kill an inline span.)
|
|
570
|
+
if (lang || (node.text && /[\r\n]/.test(node.text))) {
|
|
571
|
+
// Fence with one more backtick than the longest run inside the content
|
|
572
|
+
// so an embedded ``` can't close the block early and inject markup.
|
|
573
|
+
const longestRun = Math.max(0, ...((node.text || '').match(/`+/g) || []).map(s => s.length));
|
|
574
|
+
const fence = '`'.repeat(Math.max(3, longestRun + 1));
|
|
575
|
+
return `\n${fence}${lang}\n${node.text || ''}\n${fence}\n\n`;
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
const t = node.text || '';
|
|
579
|
+
const longestRun = Math.max(0, ...(t.match(/`+/g) || []).map(s => s.length));
|
|
580
|
+
const fence = '`'.repeat(Math.max(1, longestRun + 1));
|
|
581
|
+
const pad = (t.startsWith('`') || t.endsWith('`')) ? ' ' : '';
|
|
582
|
+
return `${fence}${pad}${t}${pad}${fence} `;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
case 'sheet': {
|
|
586
|
+
const anchors = this.renderAnchors(node.metadata);
|
|
587
|
+
const tableOutput = await this.renderMarkdownTable(node, processor);
|
|
588
|
+
return `\n---\n\n${anchors}${anchors ? '\n' : ''}${tableOutput}\n\n`;
|
|
589
|
+
}
|
|
590
|
+
case 'slide': {
|
|
591
|
+
const anchors = this.renderAnchors(node.metadata);
|
|
592
|
+
return `\n---\n\n${anchors}${anchors ? '\n' : ''}${childrenOutput}\n\n`;
|
|
593
|
+
}
|
|
594
|
+
case 'page': {
|
|
595
|
+
const anchors = this.renderAnchors(node.metadata);
|
|
596
|
+
return `\n---\n\n${anchors}${anchors ? '\n' : ''}${childrenOutput}\n\n`;
|
|
597
|
+
}
|
|
598
|
+
case 'note': {
|
|
599
|
+
const meta = node.metadata;
|
|
600
|
+
if (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') {
|
|
601
|
+
if (this.resolvedDialect.footnotes === 'none') {
|
|
602
|
+
// Dialect has no footnote syntax - the caller inlines this bare body
|
|
603
|
+
// as a parenthetical at the reference point instead of collecting it
|
|
604
|
+
// into an end-of-document "### Notes" section under a [^id] marker.
|
|
605
|
+
return childrenOutput.trim();
|
|
606
|
+
}
|
|
607
|
+
// Indent continuation lines one level so a multi-line body re-parses as a
|
|
608
|
+
// single definition (a bare newline would end it). Single-line bodies, the
|
|
609
|
+
// common case, are unaffected.
|
|
610
|
+
return `[^${this.getFootnoteKey(node)}]: ${childrenOutput.trim().replace(/\n/g, '\n ')}\n\n`;
|
|
611
|
+
}
|
|
612
|
+
return `> **Note:** ${childrenOutput.trim()}\n\n`;
|
|
613
|
+
}
|
|
614
|
+
case 'embed': {
|
|
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).
|
|
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
|
+
};
|
|
632
|
+
if (meta?.embedType === 'iframe') {
|
|
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 '';
|
|
648
|
+
const w = meta?.width ? ` width="${(0, sanitize_js_1.escapeHtml)(meta.width)}"` : '';
|
|
649
|
+
const h = meta?.height ? ` height="${(0, sanitize_js_1.escapeHtml)(meta.height)}"` : '';
|
|
650
|
+
return `\n<iframe src="${safe}"${w}${h}></iframe>\n\n`;
|
|
651
|
+
}
|
|
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` : '';
|
|
656
|
+
}
|
|
657
|
+
const id = meta?.videoId || '';
|
|
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') {
|
|
663
|
+
const width = meta?.width ? ` data-width="${(0, sanitize_js_1.escapeHtml)(meta.width)}"` : '';
|
|
664
|
+
const align = meta?.align ? ` data-align="${(0, sanitize_js_1.escapeHtml)(meta.align)}"` : '';
|
|
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`;
|
|
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.
|
|
674
|
+
const url = meta?.url || (id ? `https://youtu.be/${id}` : '');
|
|
675
|
+
return url ? `[${meta?.label || 'YouTube'}](${(0, sanitize_js_1.sanitizeMarkdownUrl)(url)})\n\n` : '';
|
|
676
|
+
}
|
|
677
|
+
case 'admonition': {
|
|
678
|
+
const meta = node.metadata;
|
|
679
|
+
// `admonitionType` is a closed union in types.ts and both parsers already
|
|
680
|
+
// allowlist on import, so enforcing it here is a no-op for any conforming
|
|
681
|
+
// AST - it closes the gap for a programmatically-built one, where the type is
|
|
682
|
+
// interpolated straight into `:::TYPE` / `::: {.TYPE}` / `[!TYPE]`.
|
|
683
|
+
const rawType = String(meta?.admonitionType || 'note').toLowerCase();
|
|
684
|
+
const type = MD_ADMONITION_TYPES.has(rawType) ? rawType : 'note';
|
|
685
|
+
const label = type.toUpperCase();
|
|
686
|
+
// A newline in the title would close the `**...**` and, in the fenced-div
|
|
687
|
+
// branches, could emit a stray `:::` line. `title` is never parser-set, so
|
|
688
|
+
// there is no round-trip to preserve and escaping is free.
|
|
689
|
+
const title = meta?.title ? (0, sanitize_js_1.markdownEscapeText)(foldLines(meta.title)) : '';
|
|
690
|
+
const body = childrenOutput.trim();
|
|
691
|
+
switch (this.resolvedDialect.admonitions) {
|
|
692
|
+
case 'fence':
|
|
693
|
+
// GLFM fenced-div: no dedicated title syntax, so a custom title (if
|
|
694
|
+
// any) is folded into the body as a bold first line.
|
|
695
|
+
return `:::${type}\n${title ? `**${title}**\n\n` : ''}${body}\n:::\n\n`;
|
|
696
|
+
case 'fence-attribute':
|
|
697
|
+
// Pandoc's own fenced-div-with-class syntax; same title handling as fence.
|
|
698
|
+
return `::: {.${type}}\n${title ? `**${title}**\n\n` : ''}${body}\n:::\n\n`;
|
|
699
|
+
case 'none': {
|
|
700
|
+
// Degrade to a plain bold-labeled blockquote, no special marker.
|
|
701
|
+
const quotedLines = body.split('\n').map(l => l.length > 0 ? `> ${l}` : '>').join('\n');
|
|
702
|
+
const heading = title || label.charAt(0) + label.slice(1).toLowerCase();
|
|
703
|
+
return `> **${heading}:**\n${quotedLines}\n\n`;
|
|
704
|
+
}
|
|
705
|
+
case 'blockquote':
|
|
706
|
+
default: {
|
|
707
|
+
// Canonical GitHub blockquote form. No dedicated title syntax either
|
|
708
|
+
// (matches this library's historical output).
|
|
709
|
+
const quotedLines = body.split('\n').map(l => l.length > 0 ? `> ${l}` : '>').join('\n');
|
|
710
|
+
return `> [!${label}]\n${quotedLines}\n\n`;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
case 'definitionList':
|
|
715
|
+
if (this.resolvedDialect.definitionLists === 'none')
|
|
716
|
+
return `${childrenOutput}\n`;
|
|
717
|
+
return `${childrenOutput}\n`;
|
|
718
|
+
case 'definitionTerm':
|
|
719
|
+
if (this.resolvedDialect.definitionLists === 'none')
|
|
720
|
+
return `**${childrenOutput}**\n\n`;
|
|
721
|
+
return `${childrenOutput}\n`;
|
|
722
|
+
case 'definitionDescription':
|
|
723
|
+
if (this.resolvedDialect.definitionLists === 'none')
|
|
724
|
+
return `${childrenOutput}\n\n`;
|
|
725
|
+
return `: ${childrenOutput}\n`;
|
|
726
|
+
case 'chart':
|
|
727
|
+
case 'drawing':
|
|
728
|
+
case 'comment':
|
|
729
|
+
case 'header':
|
|
730
|
+
case 'footer':
|
|
731
|
+
case 'slideMaster':
|
|
732
|
+
return childrenOutput;
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
const optimizedContent = this.optimizeNodes(this.ast.content);
|
|
736
|
+
for (let i = 0; i < optimizedContent.length; i++) {
|
|
737
|
+
const node = optimizedContent[i];
|
|
738
|
+
const nextNode = optimizedContent[i + 1];
|
|
739
|
+
// A top-level footnote/endnote note is an orphan definition (unreferenced `[^id]: ...`
|
|
740
|
+
// the MarkdownParser recovered). Collect it so it's emitted with the other definitions
|
|
741
|
+
// at the document end rather than inline before them.
|
|
742
|
+
const orphanNoteType = node.metadata?.noteType;
|
|
743
|
+
if (node.type === 'note' && (orphanNoteType === 'footnote' || orphanNoteType === 'endnote')) {
|
|
744
|
+
this.collectedNotes.push(node);
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
let result = await this.processNodeRecursive(node, processor);
|
|
748
|
+
// Ensure lists and other block elements are separated from non-similar content by a blank line
|
|
749
|
+
if (nextNode) {
|
|
750
|
+
const isBothLists = node.type === 'list' && nextNode.type === 'list';
|
|
751
|
+
if (!isBothLists) {
|
|
752
|
+
if (!result.endsWith('\n\n')) {
|
|
753
|
+
if (result.endsWith('\n'))
|
|
754
|
+
result += '\n';
|
|
755
|
+
else
|
|
756
|
+
result += '\n\n';
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
output += result;
|
|
761
|
+
}
|
|
762
|
+
if (this.collectedNotes.length > 0) {
|
|
763
|
+
// No decorative `---\n\n### Notes` preamble: `[^id]:` definitions are valid on their own
|
|
764
|
+
// (GitHub/Pandoc render the footnotes section and its rule automatically), and the
|
|
765
|
+
// literal heading round-tripped as a real `###` node - so every save/reload re-emitted
|
|
766
|
+
// the parsed heading AND a fresh one, growing the document unbounded. Emitting the bare
|
|
767
|
+
// definitions makes the cycle byte-stable. Behaviour change, noted in the changelog.
|
|
768
|
+
// Collapse the preceding block's trailing blank lines so exactly one blank line separates
|
|
769
|
+
// the body from the definitions (rather than the doubled `\n\n\n\n` the concatenation
|
|
770
|
+
// would otherwise leave).
|
|
771
|
+
output = output.replace(/\n+$/, '');
|
|
772
|
+
let notesMd = '\n\n';
|
|
773
|
+
// De-duplicate by node identity: a footnote referenced more than once shares a single
|
|
774
|
+
// note object (see MarkdownParser), pushed here once per reference. Emit its definition
|
|
775
|
+
// just once. Distinct notes - even two office notes that happen to share a numeric id -
|
|
776
|
+
// are separate objects and are all kept.
|
|
777
|
+
for (const note of [...new Set(this.collectedNotes)]) {
|
|
778
|
+
notesMd += await this.processNodeRecursive(note, processor);
|
|
779
|
+
}
|
|
780
|
+
output += notesMd;
|
|
781
|
+
}
|
|
782
|
+
if (this.collectedAbbreviations.size > 0) {
|
|
783
|
+
output += '\n\n';
|
|
784
|
+
for (const [abbr, title] of this.collectedAbbreviations) {
|
|
785
|
+
output += `*[${(0, sanitize_js_1.markdownEscapeText)(String(abbr).replace(/[[\]\r\n]+/g, ''))}]: ${(0, sanitize_js_1.markdownEscapeText)(foldLines(title))}\n`;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
// Only a run of literal "\n" at either end is ever a generator artifact here: block
|
|
789
|
+
// separators, the notes/abbreviations sections, the unconditional '\n\n' before
|
|
790
|
+
// hoistedContent (added even when hoistedContent is empty), and renderMarkdownTable's
|
|
791
|
+
// HTML-fallback branches, which unconditionally wrap in a leading+trailing '\n' as
|
|
792
|
+
// separators from whatever precedes/follows (in practice this rarely surfaces at the very
|
|
793
|
+
// start of `output` today since frontmatter's own "---" almost always precedes real
|
|
794
|
+
// content first - see the type doc on `ast.metadata` - but the strip is correct regardless
|
|
795
|
+
// of what precedes it). Nothing else at either end is a generator artifact: not leading
|
|
796
|
+
// whitespace, and not any other kind of trailing whitespace, both of which would be real
|
|
797
|
+
// document content. See the identical reasoning in TextGenerator.generate().
|
|
798
|
+
return {
|
|
799
|
+
value: (output + '\n\n' + this.hoistedContent.join('\n\n')).replace(/^\n+|\n+$/g, ''),
|
|
800
|
+
messages: this.messages
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Recursively processes nodes and builds output.
|
|
805
|
+
* Overridden to provide AST optimization (merging adjacent text nodes).
|
|
806
|
+
*/
|
|
807
|
+
async processNodeRecursive(node, processor) {
|
|
808
|
+
// Mirrors the check in BaseGenerator.processNodeRecursive. This override replaces that
|
|
809
|
+
// method entirely, so without repeating the check here the signal would be silently
|
|
810
|
+
// inert for this generator - which is exactly how it was missed.
|
|
811
|
+
(0, errorUtils_js_1.checkAbortSignal)(this.config.abortSignal);
|
|
812
|
+
// Allow user to completely override rendering or skip via onNode
|
|
813
|
+
const override = await this.handleOnNode(node);
|
|
814
|
+
if (override === false) {
|
|
815
|
+
return '';
|
|
816
|
+
}
|
|
817
|
+
if (typeof override === 'string') {
|
|
818
|
+
return override;
|
|
819
|
+
}
|
|
820
|
+
const walkedByProcessor = node.type === 'table' || node.type === 'sheet';
|
|
821
|
+
const wasInImplicitBold = this.inImplicitBold;
|
|
822
|
+
if (node.type === 'heading' && this.hasUniformFormatting(node, f => f?.bold === true))
|
|
823
|
+
this.inImplicitBold = true;
|
|
824
|
+
let childrenOutput = '';
|
|
825
|
+
if (!walkedByProcessor && node.children && node.children.length > 0) {
|
|
826
|
+
// Optimization: Merge adjacent text nodes with identical formatting
|
|
827
|
+
const optimizedChildren = this.optimizeNodes(node.children);
|
|
828
|
+
for (const child of optimizedChildren) {
|
|
829
|
+
childrenOutput += await this.processNodeRecursive(child, processor);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
this.inImplicitBold = wasInImplicitBold;
|
|
833
|
+
// When the dialect has no footnote syntax, a footnote/endnote is inlined right at its
|
|
834
|
+
// reference point instead (see below) - so it must not also be collected into the
|
|
835
|
+
// end-of-document "### Notes" section, or its content would be duplicated.
|
|
836
|
+
const isInlinedFootnote = (note) => {
|
|
837
|
+
const meta = note.metadata;
|
|
838
|
+
return (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') && this.resolvedDialect.footnotes === 'none';
|
|
839
|
+
};
|
|
840
|
+
this.collectNotesFrom(node);
|
|
841
|
+
let result = await processor(node, childrenOutput);
|
|
842
|
+
if (node.type === 'slide' && node.notes && node.notes.length > 0) {
|
|
843
|
+
for (const note of node.notes) {
|
|
844
|
+
result += await this.processNodeRecursive(note, processor);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
else if (node.notes && node.notes.length > 0) {
|
|
848
|
+
for (const note of node.notes) {
|
|
849
|
+
const meta = note.metadata;
|
|
850
|
+
if (meta?.noteType !== 'footnote' && meta?.noteType !== 'endnote')
|
|
851
|
+
continue;
|
|
852
|
+
if (isInlinedFootnote(note)) {
|
|
853
|
+
// Markdown-specific degrade (not RTF/plain-text's "drop the marker, just
|
|
854
|
+
// append at the end" convention): inline the note's rendered body as a
|
|
855
|
+
// parenthetical right where it's referenced, since Markdown readers benefit
|
|
856
|
+
// from an inline association those simpler formats don't need in the same way.
|
|
857
|
+
const body = await this.processNodeRecursive(note, processor);
|
|
858
|
+
result += ` (Note: ${body})`;
|
|
859
|
+
}
|
|
860
|
+
else {
|
|
861
|
+
// Emit the [^id] reference marker at the point of reference. Without this,
|
|
862
|
+
// a footnote/endnote would only ever show up in the collected ### Notes
|
|
863
|
+
// section at the end, with no indication of where it was originally cited.
|
|
864
|
+
result += `[^${this.getFootnoteKey(note)}]`;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return result;
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Merges adjacent text nodes with identical formatting and metadata.
|
|
872
|
+
*/
|
|
873
|
+
optimizeNodes(nodes) {
|
|
874
|
+
if (nodes.length <= 1)
|
|
875
|
+
return nodes;
|
|
876
|
+
const result = [];
|
|
877
|
+
let current = null;
|
|
878
|
+
for (const node of nodes) {
|
|
879
|
+
if (node.type === 'text' && current && current.type === 'text' &&
|
|
880
|
+
// A note anchors to the end of its text run and its `[^id]` marker is emitted there;
|
|
881
|
+
// merging a following run onto a note-carrying run would slide the marker past it
|
|
882
|
+
// (`Body[^1].` -> `Body.[^1]`). Keep such runs separate so the marker stays put and
|
|
883
|
+
// matches where HtmlGenerator emits it.
|
|
884
|
+
(!current.notes || current.notes.length === 0) &&
|
|
885
|
+
(!node.notes || node.notes.length === 0) &&
|
|
886
|
+
this.areFormattingEqual(node.formatting, current.formatting) &&
|
|
887
|
+
JSON.stringify(node.metadata) === JSON.stringify(current.metadata)) {
|
|
888
|
+
current.text = (current.text || '') + (node.text || '');
|
|
889
|
+
if (current.rawContent && node.rawContent)
|
|
890
|
+
current.rawContent += node.rawContent;
|
|
891
|
+
if (node.notes && node.notes.length > 0) {
|
|
892
|
+
if (!current.notes)
|
|
893
|
+
current.notes = [];
|
|
894
|
+
current.notes.push(...node.notes);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
else {
|
|
898
|
+
current = { ...node }; // Clone
|
|
899
|
+
if (node.notes) {
|
|
900
|
+
current.notes = [...node.notes];
|
|
901
|
+
}
|
|
902
|
+
result.push(current);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return result;
|
|
906
|
+
}
|
|
907
|
+
areFormattingEqual(f1, f2) {
|
|
908
|
+
if (f1 === f2)
|
|
909
|
+
return true;
|
|
910
|
+
if (!f1 || !f2)
|
|
911
|
+
return false;
|
|
912
|
+
const keys1 = Object.keys(f1);
|
|
913
|
+
const keys2 = Object.keys(f2);
|
|
914
|
+
if (keys1.length !== keys2.length)
|
|
915
|
+
return false;
|
|
916
|
+
return keys1.every(key => f1[key] === f2[key]);
|
|
917
|
+
}
|
|
918
|
+
async renderMarkdownTable(node, processor) {
|
|
919
|
+
if (!node.children || node.children.length === 0)
|
|
920
|
+
return '';
|
|
921
|
+
// A dialect that has no native table syntax at all (e.g. strict CommonMark) always
|
|
922
|
+
// renders as HTML, regardless of complexity - this is a separate axis from the
|
|
923
|
+
// nested/merged-cell HTML fallback below, which only applies to otherwise-native tables.
|
|
924
|
+
if (this.resolvedDialect.tables === 'html') {
|
|
925
|
+
return '\n' + await this.renderTableAsHtml(node) + '\n';
|
|
926
|
+
}
|
|
927
|
+
// If table is complex, nested, or uses merges, fallback to HTML for high fidelity if allowed
|
|
928
|
+
const isComplex = this.hasNestedTable(node) || this.hasColspanOrRowspan(node);
|
|
929
|
+
if (this.resolvedFallbackToHtml.tables && isComplex) {
|
|
930
|
+
return '\n' + await this.renderTableAsHtml(node) + '\n';
|
|
931
|
+
}
|
|
932
|
+
// Handle nested tables in pure Markdown by hoisting them out
|
|
933
|
+
if (this.isInsideTable && !this.resolvedFallbackToHtml.tables) {
|
|
934
|
+
const wasInside = this.isInsideTable;
|
|
935
|
+
this.isInsideTable = false; // Reset to allow rendering the hoisted table correctly
|
|
936
|
+
const hoistedId = this.hoistedContent.length + 1;
|
|
937
|
+
const tableOutput = await this.renderMarkdownTableInternal(node, processor);
|
|
938
|
+
this.hoistedContent.push(`**Table ${hoistedId} (Hoisted from cell content):**\n${tableOutput}`);
|
|
939
|
+
this.isInsideTable = wasInside;
|
|
940
|
+
return `*(See Table ${hoistedId} below)*`;
|
|
941
|
+
}
|
|
942
|
+
this.isInsideTable = true;
|
|
943
|
+
const result = await this.renderMarkdownTableInternal(node, processor);
|
|
944
|
+
this.isInsideTable = false;
|
|
945
|
+
return result;
|
|
946
|
+
}
|
|
947
|
+
collectNotesFrom(node) {
|
|
948
|
+
if (!node.notes || node.notes.length === 0)
|
|
949
|
+
return;
|
|
950
|
+
if (node.type === 'slide')
|
|
951
|
+
return;
|
|
952
|
+
const isInlinedFootnote = (note) => {
|
|
953
|
+
const meta = note.metadata;
|
|
954
|
+
return (meta?.noteType === 'footnote' || meta?.noteType === 'endnote') && this.resolvedDialect.footnotes === 'none';
|
|
955
|
+
};
|
|
956
|
+
this.collectedNotes.push(...node.notes.filter(note => !isInlinedFootnote(note)));
|
|
957
|
+
}
|
|
958
|
+
async renderMarkdownTableInternal(node, processor) {
|
|
959
|
+
let tableOutput = '';
|
|
960
|
+
let maxCols = 0;
|
|
961
|
+
// First pass: Process rows and determine max columns (accounting for colspans)
|
|
962
|
+
const processedRows = [];
|
|
963
|
+
for (const rowNode of (node.children ?? [])) {
|
|
964
|
+
// The first row becomes the header row - the `| --- |` separator emitted below marks
|
|
965
|
+
// it as such - so bold inside it is already implied.
|
|
966
|
+
const wasInImplicitBold = this.inImplicitBold;
|
|
967
|
+
if (processedRows.length === 0 && this.hasUniformFormatting(rowNode, f => f?.bold === true))
|
|
968
|
+
this.inImplicitBold = true;
|
|
969
|
+
try {
|
|
970
|
+
const override = await this.handleOnNode(rowNode);
|
|
971
|
+
if (override === false)
|
|
972
|
+
continue;
|
|
973
|
+
if (typeof override === 'string') {
|
|
974
|
+
processedRows.push([override]);
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
// After the override checks, not before: a row the caller skipped via `onNode` must not
|
|
978
|
+
// still contribute its footnote to the end-of-document Notes section, where it would
|
|
979
|
+
// appear with no `[^id]` marker anywhere in the document pointing at it.
|
|
980
|
+
// `renderTableAsHtml` gets this right by returning early, so collecting here keeps the
|
|
981
|
+
// pipe and HTML paths agreeing on what a skipped row means.
|
|
982
|
+
this.collectNotesFrom(rowNode);
|
|
983
|
+
const rowCells = [];
|
|
984
|
+
let lastCol = -1;
|
|
985
|
+
if (rowNode.children) {
|
|
986
|
+
const cellNodes = rowNode.children.filter(c => c.type === 'cell');
|
|
987
|
+
for (const cellNode of cellNodes) {
|
|
988
|
+
const currentCol = cellNode.metadata?.col ?? (lastCol + 1);
|
|
989
|
+
// Fill gaps with empty cells
|
|
990
|
+
while (lastCol < currentCol - 1) {
|
|
991
|
+
rowCells.push(' ');
|
|
992
|
+
lastCol++;
|
|
993
|
+
}
|
|
994
|
+
// Process cell content
|
|
995
|
+
let cellContent = await this.processNodeRecursive(cellNode, processor);
|
|
996
|
+
// Use <br> fallback only if allowed, otherwise space
|
|
997
|
+
const br = this.resolvedFallbackToHtml.cellLineBreaks ? '<br>' : ' ';
|
|
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, '\\|');
|
|
1001
|
+
rowCells.push(cellContent);
|
|
1002
|
+
// Handle colspan by adding empty cells
|
|
1003
|
+
const colSpan = cellNode.metadata?.colSpan || 1;
|
|
1004
|
+
for (let i = 1; i < colSpan; i++) {
|
|
1005
|
+
rowCells.push(' ');
|
|
1006
|
+
}
|
|
1007
|
+
lastCol = currentCol + colSpan - 1;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
processedRows.push(rowCells);
|
|
1011
|
+
maxCols = Math.max(maxCols, rowCells.length);
|
|
1012
|
+
}
|
|
1013
|
+
finally {
|
|
1014
|
+
this.inImplicitBold = wasInImplicitBold;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
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
|
+
};
|
|
1028
|
+
for (let i = 0; i < processedRows.length; i++) {
|
|
1029
|
+
const row = processedRows[i];
|
|
1030
|
+
// Pad row with empty cells if it has fewer than maxCols
|
|
1031
|
+
while (row.length < maxCols)
|
|
1032
|
+
row.push(' ');
|
|
1033
|
+
tableOutput += `| ${row.join(' | ')} |\n`;
|
|
1034
|
+
if (i === 0) {
|
|
1035
|
+
// Header separator
|
|
1036
|
+
tableOutput += `| ${Array.from({ length: maxCols }, (_, i) => alignMarker(i)).join(' | ')} |\n`;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return `\n${tableOutput}\n`;
|
|
1040
|
+
}
|
|
1041
|
+
hasNestedTable(node) {
|
|
1042
|
+
if (!node.children)
|
|
1043
|
+
return false;
|
|
1044
|
+
for (const child of node.children) {
|
|
1045
|
+
if (child.type === 'table')
|
|
1046
|
+
return true;
|
|
1047
|
+
if (this.hasNestedTable(child))
|
|
1048
|
+
return true;
|
|
1049
|
+
}
|
|
1050
|
+
return false;
|
|
1051
|
+
}
|
|
1052
|
+
hasColspanOrRowspan(node) {
|
|
1053
|
+
if (!node.children)
|
|
1054
|
+
return false;
|
|
1055
|
+
for (const row of node.children) {
|
|
1056
|
+
if (row.type === 'row' && row.children) {
|
|
1057
|
+
for (const cell of row.children) {
|
|
1058
|
+
if (cell.type === 'cell') {
|
|
1059
|
+
const meta = cell.metadata;
|
|
1060
|
+
if ((meta?.colSpan && meta.colSpan > 1) || (meta?.rowSpan && meta.rowSpan > 1)) {
|
|
1061
|
+
return true;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
return false;
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Renders a complex table as HTML since Markdown doesn't support nested tables or rowspans.
|
|
1071
|
+
*/
|
|
1072
|
+
async renderTableAsHtml(node, override) {
|
|
1073
|
+
if (override === false)
|
|
1074
|
+
return '';
|
|
1075
|
+
if (typeof override === 'string') {
|
|
1076
|
+
if (node.type === 'row')
|
|
1077
|
+
return ` <tr><td colspan="100%">${override}</td></tr>\n`;
|
|
1078
|
+
if (node.type === 'cell')
|
|
1079
|
+
return `<td>${override}</td>`;
|
|
1080
|
+
return override;
|
|
1081
|
+
}
|
|
1082
|
+
if (node.type === 'table') {
|
|
1083
|
+
let rows = '';
|
|
1084
|
+
if (node.children) {
|
|
1085
|
+
for (const row of node.children) {
|
|
1086
|
+
rows += await this.renderTableAsHtml(row, await this.handleOnNode(row));
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
// Carry table-layout alignment through the HTML fallback so it isn't lost
|
|
1090
|
+
// just because the table also needed HTML for merged cells.
|
|
1091
|
+
const tableMeta = node.metadata;
|
|
1092
|
+
const alignAttr = tableMeta?.align ? ` data-align="${(0, sanitize_js_1.escapeHtml)(tableMeta.align)}"` : '';
|
|
1093
|
+
return `<table${alignAttr}>\n${rows}</table>\n`;
|
|
1094
|
+
}
|
|
1095
|
+
else if (node.type === 'row') {
|
|
1096
|
+
this.collectNotesFrom(node);
|
|
1097
|
+
let cells = '';
|
|
1098
|
+
if (node.children) {
|
|
1099
|
+
for (const cell of node.children) {
|
|
1100
|
+
cells += await this.renderTableAsHtml(cell, await this.handleOnNode(cell));
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
return ` <tr>\n${cells} </tr>\n`;
|
|
1104
|
+
}
|
|
1105
|
+
else if (node.type === 'cell') {
|
|
1106
|
+
this.collectNotesFrom(node);
|
|
1107
|
+
const meta = node.metadata;
|
|
1108
|
+
const rs = meta?.rowSpan > 1 ? ` rowspan="${meta.rowSpan}"` : '';
|
|
1109
|
+
const cs = meta?.colSpan > 1 ? ` colspan="${meta.colSpan}"` : '';
|
|
1110
|
+
let content = '';
|
|
1111
|
+
if (node.children) {
|
|
1112
|
+
// Use a simplified HTML processor for cell content
|
|
1113
|
+
for (const child of this.optimizeNodes(node.children)) {
|
|
1114
|
+
content += await this.processNodeRecursive(child, async (n, co) => {
|
|
1115
|
+
switch (n.type) {
|
|
1116
|
+
case 'text': {
|
|
1117
|
+
if (n.metadata) {
|
|
1118
|
+
const m = n.metadata;
|
|
1119
|
+
if (m.abbreviationTitle) {
|
|
1120
|
+
this.collectedAbbreviations.set(n.text || '', m.abbreviationTitle);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
// Inside HTML table cells, entity-encode angle brackets so cell
|
|
1124
|
+
// text can't inject a raw tag (e.g. </td><script>).
|
|
1125
|
+
let text = (0, sanitize_js_1.markdownEscapeText)(n.text || '');
|
|
1126
|
+
if (n.formatting?.bold && !this.inImplicitBold)
|
|
1127
|
+
text = `<b>${text}</b>`;
|
|
1128
|
+
if (n.formatting?.italic)
|
|
1129
|
+
text = `<i>${text}</i>`;
|
|
1130
|
+
if (n.formatting?.underline)
|
|
1131
|
+
text = `<u>${text}</u>`;
|
|
1132
|
+
if (n.formatting?.subscript)
|
|
1133
|
+
text = `<sub>${text}</sub>`;
|
|
1134
|
+
if (n.formatting?.superscript)
|
|
1135
|
+
text = `<sup>${text}</sup>`;
|
|
1136
|
+
return text;
|
|
1137
|
+
}
|
|
1138
|
+
case 'paragraph': return `<p>${co}</p>`;
|
|
1139
|
+
case 'heading': {
|
|
1140
|
+
const level = Math.min(Math.max(Number(n.metadata?.level) || 1, 1), 6);
|
|
1141
|
+
return `<h${level}>${co}</h${level}>`;
|
|
1142
|
+
}
|
|
1143
|
+
case 'table': return await this.renderTableAsHtml(n);
|
|
1144
|
+
case 'list':
|
|
1145
|
+
case 'image':
|
|
1146
|
+
case 'chart':
|
|
1147
|
+
case 'drawing':
|
|
1148
|
+
case 'slide':
|
|
1149
|
+
case 'note':
|
|
1150
|
+
case 'sheet':
|
|
1151
|
+
case 'row':
|
|
1152
|
+
case 'cell':
|
|
1153
|
+
case 'page':
|
|
1154
|
+
case 'break':
|
|
1155
|
+
case 'code':
|
|
1156
|
+
case 'comment':
|
|
1157
|
+
case 'header':
|
|
1158
|
+
case 'footer':
|
|
1159
|
+
case 'slideMaster':
|
|
1160
|
+
case 'embed':
|
|
1161
|
+
case 'admonition':
|
|
1162
|
+
case 'definitionList':
|
|
1163
|
+
case 'definitionTerm':
|
|
1164
|
+
case 'definitionDescription':
|
|
1165
|
+
return co;
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return ` <td${rs}${cs}>${content}</td>\n`;
|
|
1171
|
+
}
|
|
1172
|
+
return '';
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
exports.MarkdownGenerator = MarkdownGenerator;
|