@karmaniverous/jeeves-server 3.0.0-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/.env.local +13 -0
- package/.env.local.template +13 -0
- package/.tsbuildinfo +1 -0
- package/CHANGELOG.md +450 -0
- package/about.md +82 -0
- package/client/README.md +73 -0
- package/client/eslint.config.js +23 -0
- package/client/index.html +14 -0
- package/client/package-lock.json +5181 -0
- package/client/package.json +60 -0
- package/client/public/vite.svg +1 -0
- package/client/src/App.tsx +22 -0
- package/client/src/components/AccountMenu.tsx +167 -0
- package/client/src/components/ActionDropdown.tsx +120 -0
- package/client/src/components/CodeEditor.tsx +143 -0
- package/client/src/components/CodeViewer.tsx +113 -0
- package/client/src/components/ConfirmDialog.tsx +32 -0
- package/client/src/components/DirectoryRow.tsx +62 -0
- package/client/src/components/DirectoryTable.tsx +42 -0
- package/client/src/components/DownloadDropdown.tsx +116 -0
- package/client/src/components/DriveList.tsx +54 -0
- package/client/src/components/EmbeddedDiagramPanzoom.ts +28 -0
- package/client/src/components/FileContentView.tsx +155 -0
- package/client/src/components/InlineSvgPanzoom.ts +60 -0
- package/client/src/components/LazyDiagram.ts +93 -0
- package/client/src/components/LinkDropdown.tsx +134 -0
- package/client/src/components/MarkdownView.tsx +115 -0
- package/client/src/components/MermaidViewer.tsx +21 -0
- package/client/src/components/PlantUmlViewer.tsx +21 -0
- package/client/src/components/SearchModal.tsx +424 -0
- package/client/src/components/SvgViewer.tsx +107 -0
- package/client/src/components/TabBar.tsx +96 -0
- package/client/src/components/layout/Header.tsx +270 -0
- package/client/src/components/panzoom.ts +203 -0
- package/client/src/components/renderableUtils.ts +15 -0
- package/client/src/components/runner/JobTable.tsx +153 -0
- package/client/src/components/runner/RunHistory.tsx +140 -0
- package/client/src/components/runner/StatsBar.tsx +43 -0
- package/client/src/components/runner/StatusPill.tsx +27 -0
- package/client/src/components/runner/jobTableUtils.ts +65 -0
- package/client/src/components/scrollUtils.ts +39 -0
- package/client/src/components/ui/alert-dialog.tsx +107 -0
- package/client/src/components/ui/button.tsx +40 -0
- package/client/src/components/ui/dropdown-menu.tsx +79 -0
- package/client/src/components/ui/input.tsx +26 -0
- package/client/src/components/useActionState.ts +43 -0
- package/client/src/hooks/useFileBrowser.ts +102 -0
- package/client/src/hooks/useFileData.ts +78 -0
- package/client/src/hooks/useScrollAnchor.ts +70 -0
- package/client/src/hooks/useShareSettings.ts +22 -0
- package/client/src/hooks/useTopBar.ts +27 -0
- package/client/src/index.css +281 -0
- package/client/src/lib/AuthContext.ts +27 -0
- package/client/src/lib/api.ts +239 -0
- package/client/src/lib/auth.tsx +50 -0
- package/client/src/lib/codeBlockCm6.ts +129 -0
- package/client/src/lib/codeBlockCopy.ts +43 -0
- package/client/src/lib/codemirror.ts +77 -0
- package/client/src/lib/runner-api.ts +172 -0
- package/client/src/lib/svg.ts +50 -0
- package/client/src/lib/theme.ts +34 -0
- package/client/src/lib/utils.ts +6 -0
- package/client/src/main.tsx +11 -0
- package/client/src/pages/FileBrowser.tsx +135 -0
- package/client/src/pages/Home.tsx +46 -0
- package/client/src/pages/Runner.tsx +151 -0
- package/client/src/pages/RunnerJob.tsx +170 -0
- package/client/tsconfig.app.json +32 -0
- package/client/tsconfig.json +7 -0
- package/client/tsconfig.node.json +26 -0
- package/client/vite.config.ts +35 -0
- package/content/privacy.md +61 -0
- package/content/terms.md +41 -0
- package/dist/client/assets/CodeEditor-0XHVI8Nu.js +1 -0
- package/dist/client/assets/CodeViewer-CykMVsfX.js +1 -0
- package/dist/client/assets/index--MBieNJA.js +1 -0
- package/dist/client/assets/index-BENeXQI_.js +1 -0
- package/dist/client/assets/index-BbBpoOxz.js +1 -0
- package/dist/client/assets/index-BdV9g5AM.js +6 -0
- package/dist/client/assets/index-BjAilRri.js +2 -0
- package/dist/client/assets/index-BqbhWo2I.js +3 -0
- package/dist/client/assets/index-CVbycZ0H.js +1 -0
- package/dist/client/assets/index-Cs5oz2oJ.js +5 -0
- package/dist/client/assets/index-D8KZVveX.js +1 -0
- package/dist/client/assets/index-DC4HMHxY.js +13 -0
- package/dist/client/assets/index-DbMebkkd.css +1 -0
- package/dist/client/assets/index-DcY2RXqX.js +1 -0
- package/dist/client/assets/index-Duy-tZYV.js +1 -0
- package/dist/client/assets/index-Dw7rDFmE.js +7 -0
- package/dist/client/assets/index-FlCUvrjv.js +2 -0
- package/dist/client/assets/index-K6OVmfhg.js +1 -0
- package/dist/client/assets/index-LjwgzZ7F.js +62 -0
- package/dist/client/assets/index-MLwyFRN0.js +1 -0
- package/dist/client/assets/index-OpqBpSjn.js +1 -0
- package/dist/client/assets/index-SsHei0HE.js +1 -0
- package/dist/client/assets/index-uQa2yckk.js +1 -0
- package/dist/client/assets/index-udkXoIER.js +1 -0
- package/dist/client/index.html +15 -0
- package/dist/client/vite.svg +1 -0
- package/dist/src/auth/google.js +57 -0
- package/dist/src/auth/keys.js +185 -0
- package/dist/src/auth/resolve.js +102 -0
- package/dist/src/auth/session.js +57 -0
- package/dist/src/cli/commands/config.js +100 -0
- package/dist/src/cli/commands/config.test.js +84 -0
- package/dist/src/cli/commands/service.js +93 -0
- package/dist/src/cli/commands/start.js +24 -0
- package/dist/src/cli/index.js +20 -0
- package/dist/src/config/index.js +90 -0
- package/dist/src/config/loadConfig.test.js +127 -0
- package/dist/src/config/resolve.js +134 -0
- package/dist/src/config/resolve.test.js +148 -0
- package/dist/src/config/schema.js +159 -0
- package/dist/src/config/substituteEnvVars.js +45 -0
- package/dist/src/config/substituteEnvVars.test.js +51 -0
- package/dist/src/config/types.js +5 -0
- package/dist/src/routes/api/auth-status.js +56 -0
- package/dist/src/routes/api/diagrams.js +35 -0
- package/dist/src/routes/api/directory.js +93 -0
- package/dist/src/routes/api/drives.js +15 -0
- package/dist/src/routes/api/export.js +218 -0
- package/dist/src/routes/api/fileContent.js +286 -0
- package/dist/src/routes/api/index.js +33 -0
- package/dist/src/routes/api/linkInfo.js +71 -0
- package/dist/src/routes/api/linkInfo.test.js +104 -0
- package/dist/src/routes/api/middleware.js +117 -0
- package/dist/src/routes/api/raw.js +38 -0
- package/dist/src/routes/api/runner.js +59 -0
- package/dist/src/routes/api/search.js +236 -0
- package/dist/src/routes/api/sharing.js +203 -0
- package/dist/src/routes/api/status.js +68 -0
- package/dist/src/routes/api/status.test.js +62 -0
- package/dist/src/routes/auth.js +99 -0
- package/dist/src/routes/event.js +77 -0
- package/dist/src/routes/event.test.js +206 -0
- package/dist/src/routes/health.js +10 -0
- package/dist/src/routes/keys.js +129 -0
- package/dist/src/routes/path/index.js +17 -0
- package/dist/src/routes/static.js +30 -0
- package/dist/src/server.js +90 -0
- package/dist/src/services/deepShareLinks.js +163 -0
- package/dist/src/services/diagramCache.js +104 -0
- package/dist/src/services/embeddedDiagrams.js +136 -0
- package/dist/src/services/eventLog.js +55 -0
- package/dist/src/services/eventLog.test.js +113 -0
- package/dist/src/services/eventQueue.js +154 -0
- package/dist/src/services/eventQueue.test.js +104 -0
- package/dist/src/services/export.js +220 -0
- package/dist/src/services/exportCache.js +196 -0
- package/dist/src/services/markdown.js +147 -0
- package/dist/src/services/mermaid.js +97 -0
- package/dist/src/services/plantuml.js +145 -0
- package/dist/src/services/puppeteer.js +156 -0
- package/dist/src/util/breadcrumbs.js +22 -0
- package/dist/src/util/crypto.js +56 -0
- package/dist/src/util/crypto.test.js +99 -0
- package/dist/src/util/fileDetection.js +66 -0
- package/dist/src/util/fileDetection.test.js +89 -0
- package/dist/src/util/formatters.js +43 -0
- package/dist/src/util/formatters.test.js +83 -0
- package/dist/src/util/packageVersion.js +25 -0
- package/dist/src/util/platform.js +148 -0
- package/dist/src/util/state.js +46 -0
- package/dist/vitest.config.js +12 -0
- package/favicon.svg +3 -0
- package/guides/access-decision-flow.mmd +24 -0
- package/guides/access-decision-flow.svg +1 -0
- package/guides/api-integration.md +236 -0
- package/guides/deployment.md +287 -0
- package/guides/event-gateway.md +204 -0
- package/guides/event-gateway.mmd +17 -0
- package/guides/event-gateway.svg +1 -0
- package/guides/exports.md +239 -0
- package/guides/setup.md +313 -0
- package/guides/sharing.md +204 -0
- package/jeeves-server.config.template.json +25 -0
- package/package.json +124 -0
- package/scripts/download-plantuml.js +70 -0
- package/src/auth/google.ts +93 -0
- package/src/auth/keys.ts +252 -0
- package/src/auth/resolve.ts +157 -0
- package/src/auth/session.ts +77 -0
- package/src/cli/commands/config.test.ts +107 -0
- package/src/cli/commands/config.ts +113 -0
- package/src/cli/commands/service.ts +129 -0
- package/src/cli/commands/start.ts +27 -0
- package/src/cli/index.ts +25 -0
- package/src/config/index.ts +113 -0
- package/src/config/loadConfig.test.ts +155 -0
- package/src/config/resolve.test.ts +192 -0
- package/src/config/resolve.ts +173 -0
- package/src/config/schema.ts +179 -0
- package/src/config/substituteEnvVars.test.ts +64 -0
- package/src/config/substituteEnvVars.ts +52 -0
- package/src/config/types.ts +129 -0
- package/src/routes/api/auth-status.ts +85 -0
- package/src/routes/api/diagrams.ts +53 -0
- package/src/routes/api/directory.ts +123 -0
- package/src/routes/api/drives.ts +23 -0
- package/src/routes/api/export.ts +314 -0
- package/src/routes/api/fileContent.ts +414 -0
- package/src/routes/api/index.ts +37 -0
- package/src/routes/api/linkInfo.test.ts +132 -0
- package/src/routes/api/linkInfo.ts +83 -0
- package/src/routes/api/middleware.ts +156 -0
- package/src/routes/api/raw.ts +54 -0
- package/src/routes/api/runner.ts +107 -0
- package/src/routes/api/search.ts +321 -0
- package/src/routes/api/sharing.ts +259 -0
- package/src/routes/api/status.test.ts +72 -0
- package/src/routes/api/status.ts +82 -0
- package/src/routes/auth.ts +143 -0
- package/src/routes/event.test.ts +248 -0
- package/src/routes/event.ts +109 -0
- package/src/routes/health.ts +13 -0
- package/src/routes/keys.ts +192 -0
- package/src/routes/path/index.ts +24 -0
- package/src/routes/static.ts +54 -0
- package/src/server.ts +104 -0
- package/src/services/deepShareLinks.ts +203 -0
- package/src/services/diagramCache.ts +128 -0
- package/src/services/embeddedDiagrams.ts +168 -0
- package/src/services/eventLog.test.ts +144 -0
- package/src/services/eventLog.ts +68 -0
- package/src/services/eventQueue.test.ts +127 -0
- package/src/services/eventQueue.ts +196 -0
- package/src/services/export.ts +267 -0
- package/src/services/exportCache.ts +216 -0
- package/src/services/markdown.ts +189 -0
- package/src/services/mermaid.ts +113 -0
- package/src/services/plantuml.ts +172 -0
- package/src/services/puppeteer.ts +188 -0
- package/src/types/fastify.d.ts +13 -0
- package/src/types/jsonmap.d.ts +10 -0
- package/src/types/plantuml-encoder.d.ts +4 -0
- package/src/util/breadcrumbs.ts +33 -0
- package/src/util/crypto.test.ts +132 -0
- package/src/util/crypto.ts +79 -0
- package/src/util/fileDetection.test.ts +115 -0
- package/src/util/fileDetection.ts +70 -0
- package/src/util/formatters.test.ts +105 -0
- package/src/util/formatters.ts +44 -0
- package/src/util/packageVersion.ts +30 -0
- package/src/util/platform.ts +178 -0
- package/src/util/state.ts +55 -0
- package/test-docs/diagram-retry-test.md +18 -0
- package/test-docs/embedded-diagrams.md +52 -0
- package/test-docs/lazy-diagrams-test.md +333 -0
- package/test-docs/page-a.md +7 -0
- package/test-docs/page-b.md +7 -0
- package/test-docs/page-c.md +7 -0
- package/test-docs/sub/page-d.md +7 -0
- package/test-docs/test-diagram.puml +13 -0
- package/test-docs/validate-deep-share.js +318 -0
- package/tsconfig.json +37 -0
- package/tsdoc.json +13 -0
- package/vendor/.plantuml-version +1 -0
- package/vendor/plantuml.jar +0 -0
- package/vitest.config.js +12 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export service for PDF and DOCX generation.
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates Puppeteer page loading, print styling, and format-specific
|
|
5
|
+
* export logic. Heavy lifting delegated to puppeteer.ts utilities.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import HtmlToDocx from '@turbodocx/html-to-docx';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
addPrintStyles,
|
|
12
|
+
captureSvgsAsPng,
|
|
13
|
+
launchBrowser,
|
|
14
|
+
SVG_CONTAINER_SELECTORS,
|
|
15
|
+
waitForSpaContent,
|
|
16
|
+
} from './puppeteer.js';
|
|
17
|
+
|
|
18
|
+
export type ExportFormat = 'pdf' | 'docx';
|
|
19
|
+
|
|
20
|
+
export interface ExportOptions {
|
|
21
|
+
url: string;
|
|
22
|
+
fileName: string;
|
|
23
|
+
format: ExportFormat;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Max bounds for images in DOCX (6 inches × 8 inches at 96dpi)
|
|
27
|
+
const MAX_WIDTH_PX = 576;
|
|
28
|
+
const MAX_HEIGHT_PX = 768;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Export page as PDF.
|
|
32
|
+
*/
|
|
33
|
+
export async function exportPDF(options: ExportOptions): Promise<Buffer> {
|
|
34
|
+
const browser = await launchBrowser();
|
|
35
|
+
try {
|
|
36
|
+
const page = await browser.newPage();
|
|
37
|
+
await page.goto(options.url, { waitUntil: 'networkidle0' });
|
|
38
|
+
await waitForSpaContent(page);
|
|
39
|
+
await addPrintStyles(page);
|
|
40
|
+
|
|
41
|
+
const pdfBuffer = await page.pdf({
|
|
42
|
+
format: 'A4',
|
|
43
|
+
margin: { top: '1cm', right: '1cm', bottom: '1cm', left: '1cm' },
|
|
44
|
+
printBackground: true,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return Buffer.from(pdfBuffer);
|
|
48
|
+
} finally {
|
|
49
|
+
await browser.close();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Export page as DOCX.
|
|
55
|
+
*/
|
|
56
|
+
export async function exportDOCX(options: ExportOptions): Promise<Buffer> {
|
|
57
|
+
const browser = await launchBrowser();
|
|
58
|
+
try {
|
|
59
|
+
const page = await browser.newPage();
|
|
60
|
+
await page.setViewport({ width: 1200, height: 800 });
|
|
61
|
+
await page.goto(options.url, { waitUntil: 'networkidle0' });
|
|
62
|
+
await waitForSpaContent(page);
|
|
63
|
+
await addPrintStyles(page);
|
|
64
|
+
|
|
65
|
+
// Capture SVGs as PNGs for DOCX embedding
|
|
66
|
+
const svgPngDataUrls = await captureSvgsAsPng(browser, page);
|
|
67
|
+
|
|
68
|
+
// Get processed HTML with SVGs replaced by PNGs
|
|
69
|
+
const processedHtml = await page.evaluate(
|
|
70
|
+
(
|
|
71
|
+
pngUrls: typeof svgPngDataUrls,
|
|
72
|
+
maxW: number,
|
|
73
|
+
maxH: number,
|
|
74
|
+
selectors: string,
|
|
75
|
+
) => {
|
|
76
|
+
function calcScaled(
|
|
77
|
+
origW: number,
|
|
78
|
+
origH: number,
|
|
79
|
+
): { width: number; height: number } {
|
|
80
|
+
let w = origW,
|
|
81
|
+
h = origH;
|
|
82
|
+
if (w > maxW) {
|
|
83
|
+
const s = maxW / w;
|
|
84
|
+
w = maxW;
|
|
85
|
+
h = Math.round(h * s);
|
|
86
|
+
}
|
|
87
|
+
if (h > maxH) {
|
|
88
|
+
const s = maxH / h;
|
|
89
|
+
h = maxH;
|
|
90
|
+
w = Math.round(w * s);
|
|
91
|
+
}
|
|
92
|
+
return { width: w, height: h };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const content =
|
|
96
|
+
document.querySelector('article.prose') ??
|
|
97
|
+
document.querySelector('.content');
|
|
98
|
+
if (!content) return '<p>No content</p>';
|
|
99
|
+
|
|
100
|
+
const contentClone = content.cloneNode(true) as HTMLElement;
|
|
101
|
+
contentClone.querySelectorAll('a.anchor').forEach((el) => {
|
|
102
|
+
el.remove();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Replace SVG containers with PNG images
|
|
106
|
+
const svgContainers = contentClone.querySelectorAll(selectors);
|
|
107
|
+
svgContainers.forEach((container, i) => {
|
|
108
|
+
const pngInfo = pngUrls.find((p) => p.index === i);
|
|
109
|
+
if (pngInfo) {
|
|
110
|
+
const img = document.createElement('img');
|
|
111
|
+
img.src = pngInfo.dataUrl;
|
|
112
|
+
img.alt = 'Diagram';
|
|
113
|
+
const dims = calcScaled(pngInfo.width, pngInfo.height);
|
|
114
|
+
img.setAttribute('width', String(dims.width));
|
|
115
|
+
img.setAttribute('height', String(dims.height));
|
|
116
|
+
container.replaceWith(img);
|
|
117
|
+
} else {
|
|
118
|
+
const p = document.createElement('p');
|
|
119
|
+
p.textContent = '[Diagram]';
|
|
120
|
+
p.style.fontStyle = 'italic';
|
|
121
|
+
container.replaceWith(p);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Fix image URLs and set explicit dimensions
|
|
126
|
+
contentClone.querySelectorAll('img').forEach((img) => {
|
|
127
|
+
if (img.src && !img.src.startsWith('data:')) {
|
|
128
|
+
img.src = new URL(img.src, window.location.origin).href;
|
|
129
|
+
}
|
|
130
|
+
const origW = img.naturalWidth || img.width || 400;
|
|
131
|
+
const origH = img.naturalHeight || img.height || 300;
|
|
132
|
+
const dims = calcScaled(origW, origH);
|
|
133
|
+
img.setAttribute('width', String(dims.width));
|
|
134
|
+
img.setAttribute('height', String(dims.height));
|
|
135
|
+
img.style.maxWidth = '';
|
|
136
|
+
img.style.maxHeight = '';
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Inline styles for tables
|
|
140
|
+
contentClone.querySelectorAll('table').forEach((t) => {
|
|
141
|
+
t.setAttribute('border', '1');
|
|
142
|
+
(t as HTMLElement).style.borderCollapse = 'collapse';
|
|
143
|
+
(t as HTMLElement).style.width = '100%';
|
|
144
|
+
});
|
|
145
|
+
contentClone.querySelectorAll('th').forEach((th) => {
|
|
146
|
+
(th as HTMLElement).style.backgroundColor = '#f0f0f0';
|
|
147
|
+
(th as HTMLElement).style.fontWeight = 'bold';
|
|
148
|
+
(th as HTMLElement).style.padding = '8px';
|
|
149
|
+
(th as HTMLElement).style.border = '1px solid #999';
|
|
150
|
+
});
|
|
151
|
+
contentClone.querySelectorAll('td').forEach((td) => {
|
|
152
|
+
(td as HTMLElement).style.padding = '8px';
|
|
153
|
+
(td as HTMLElement).style.border = '1px solid #999';
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Inline styles for code blocks — wrap in single-cell table for
|
|
157
|
+
// consistent block background in DOCX (html-to-docx doesn't support
|
|
158
|
+
// background-color on pre as a block fill)
|
|
159
|
+
contentClone.querySelectorAll('pre').forEach((pre) => {
|
|
160
|
+
const text = pre.textContent || '';
|
|
161
|
+
const table = document.createElement('table');
|
|
162
|
+
table.setAttribute('border', '1');
|
|
163
|
+
table.style.borderCollapse = 'collapse';
|
|
164
|
+
table.style.width = '100%';
|
|
165
|
+
table.style.marginTop = '8px';
|
|
166
|
+
table.style.marginBottom = '8px';
|
|
167
|
+
const tr = document.createElement('tr');
|
|
168
|
+
const td = document.createElement('td');
|
|
169
|
+
td.style.backgroundColor = '#f5f5f5';
|
|
170
|
+
td.style.border = '1px solid #ddd';
|
|
171
|
+
td.style.padding = '12px';
|
|
172
|
+
// Build one <p> per line with monospace styling and for
|
|
173
|
+
// indentation. html-to-docx converts each <p> to a Word paragraph
|
|
174
|
+
// cleanly, without the double-spacing that <br> causes.
|
|
175
|
+
const lines = text.split('\n');
|
|
176
|
+
lines.forEach((line) => {
|
|
177
|
+
const p = document.createElement('p');
|
|
178
|
+
p.style.fontFamily = 'Consolas, monospace';
|
|
179
|
+
p.style.fontSize = '9pt';
|
|
180
|
+
p.style.margin = '0';
|
|
181
|
+
p.style.lineHeight = '1.3';
|
|
182
|
+
// Preserve leading whitespace with
|
|
183
|
+
const leadingSpaces = line.match(/^( +)/);
|
|
184
|
+
if (leadingSpaces) {
|
|
185
|
+
const nbsp = '\u00A0'.repeat(leadingSpaces[1].length);
|
|
186
|
+
p.textContent = nbsp + line.slice(leadingSpaces[1].length);
|
|
187
|
+
} else {
|
|
188
|
+
p.textContent = line || '\u00A0'; // empty lines need content
|
|
189
|
+
}
|
|
190
|
+
td.appendChild(p);
|
|
191
|
+
});
|
|
192
|
+
tr.appendChild(td);
|
|
193
|
+
table.appendChild(tr);
|
|
194
|
+
pre.replaceWith(table);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
return contentClone.innerHTML;
|
|
198
|
+
},
|
|
199
|
+
svgPngDataUrls,
|
|
200
|
+
MAX_WIDTH_PX,
|
|
201
|
+
MAX_HEIGHT_PX,
|
|
202
|
+
SVG_CONTAINER_SELECTORS,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
await browser.close();
|
|
206
|
+
|
|
207
|
+
const fullHtml = buildDocxHtml(processedHtml);
|
|
208
|
+
const baseName = options.fileName.replace(/\.md$/i, '');
|
|
209
|
+
|
|
210
|
+
const docxBuffer = await HtmlToDocx(fullHtml, null, {
|
|
211
|
+
title: baseName,
|
|
212
|
+
creator: 'Jeeves Server',
|
|
213
|
+
table: { row: { cantSplit: true } },
|
|
214
|
+
imageProcessing: {
|
|
215
|
+
svgHandling: 'native',
|
|
216
|
+
maxRetries: 2,
|
|
217
|
+
downloadTimeout: 15000,
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return Buffer.isBuffer(docxBuffer)
|
|
222
|
+
? docxBuffer
|
|
223
|
+
: Buffer.from(docxBuffer as ArrayBuffer);
|
|
224
|
+
} finally {
|
|
225
|
+
await browser.close();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Export page based on format.
|
|
231
|
+
*/
|
|
232
|
+
export async function exportPage(options: ExportOptions): Promise<Buffer> {
|
|
233
|
+
return options.format === 'pdf' ? exportPDF(options) : exportDOCX(options);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Build a clean HTML document for DOCX conversion. */
|
|
237
|
+
function buildDocxHtml(bodyContent: string): string {
|
|
238
|
+
return `<!DOCTYPE html>
|
|
239
|
+
<html>
|
|
240
|
+
<head>
|
|
241
|
+
<meta charset="UTF-8">
|
|
242
|
+
<style>
|
|
243
|
+
body { font-family: Calibri, Arial, sans-serif; font-size: 10pt; line-height: 1.6; }
|
|
244
|
+
h1 { font-size: 18pt; font-weight: bold; color: #1a1a1a; margin-top: 20pt; margin-bottom: 10pt; }
|
|
245
|
+
h2 { font-size: 14pt; font-weight: bold; color: #2a2a2a; margin-top: 16pt; margin-bottom: 8pt; }
|
|
246
|
+
h3 { font-size: 12pt; font-weight: bold; color: #3a3a3a; margin-top: 12pt; margin-bottom: 6pt; }
|
|
247
|
+
h4 { font-size: 10pt; font-weight: bold; color: #4a4a4a; margin-top: 10pt; margin-bottom: 5pt; }
|
|
248
|
+
p { margin: 5pt 0; }
|
|
249
|
+
code { font-family: Consolas, 'Courier New', monospace; font-size: 9pt; background-color: #f4f4f4; padding: 2pt 4pt; }
|
|
250
|
+
pre { font-family: Consolas, 'Courier New', monospace; font-size: 8pt; background-color: #f8f8f8; border: 1pt solid #ddd; padding: 10pt; margin: 10pt 0; white-space: pre-wrap; word-wrap: break-word; }
|
|
251
|
+
pre code { background-color: transparent; padding: 0; }
|
|
252
|
+
table { border-collapse: collapse; width: 100%; margin: 12pt 0; }
|
|
253
|
+
th { background-color: #f0f0f0; font-weight: bold; border: 1pt solid #999; padding: 8pt; text-align: left; }
|
|
254
|
+
td { border: 1pt solid #999; padding: 8pt; text-align: left; }
|
|
255
|
+
tr:nth-child(even) td { background-color: #fafafa; }
|
|
256
|
+
blockquote { border-left: 4pt solid #ddd; margin: 12pt 0; padding: 6pt 12pt; color: #666; }
|
|
257
|
+
ul, ol { margin: 6pt 0; padding-left: 24pt; }
|
|
258
|
+
li { margin: 4pt 0; }
|
|
259
|
+
a { color: #0066cc; }
|
|
260
|
+
img, svg { margin: 12pt 0; display: block; }
|
|
261
|
+
</style>
|
|
262
|
+
</head>
|
|
263
|
+
<body>
|
|
264
|
+
${bodyContent}
|
|
265
|
+
</body>
|
|
266
|
+
</html>`;
|
|
267
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export cache for PDF/DOCX renders.
|
|
3
|
+
*
|
|
4
|
+
* Cache key = sha256(normalizedFsPath + NUL + format).
|
|
5
|
+
* Invalidation: source file mtime greater than cache file mtime = miss.
|
|
6
|
+
* Also maintains a reverse index (fsPath to diagram cache hashes) so
|
|
7
|
+
* "Clear Cache" can purge both export and diagram caches for a file.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import crypto from 'node:crypto';
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
|
|
14
|
+
let cacheDir: string | null = null;
|
|
15
|
+
|
|
16
|
+
/** Diagram type for a given file extension. */
|
|
17
|
+
const DIAGRAM_EXT_MAP: Record<string, string> = {
|
|
18
|
+
'.mmd': 'mermaid',
|
|
19
|
+
'.puml': 'plantuml',
|
|
20
|
+
'.plantuml': 'plantuml',
|
|
21
|
+
'.pu': 'plantuml',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Initialize the export cache directory. Call once at startup. */
|
|
25
|
+
export function initExportCache(dir?: string): void {
|
|
26
|
+
cacheDir = dir ?? path.resolve('.export-cache');
|
|
27
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function cacheKey(fsPath: string, format: string): string {
|
|
31
|
+
const normalized = fsPath.replace(/\\/g, '/').toLowerCase();
|
|
32
|
+
return crypto
|
|
33
|
+
.createHash('sha256')
|
|
34
|
+
.update(`${normalized}\0${format}`)
|
|
35
|
+
.digest('hex');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function cacheFilePath(fsPath: string, format: string): string | null {
|
|
39
|
+
if (!cacheDir) return null;
|
|
40
|
+
return path.join(cacheDir, `${cacheKey(fsPath, format)}.${format}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get a cached export buffer if it exists and is fresh.
|
|
45
|
+
* Returns null on miss or stale cache.
|
|
46
|
+
*/
|
|
47
|
+
export function getCachedExport(fsPath: string, format: string): Buffer | null {
|
|
48
|
+
const file = cacheFilePath(fsPath, format);
|
|
49
|
+
if (!file) return null;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const sourceStat = fs.statSync(fsPath);
|
|
53
|
+
const cacheStat = fs.statSync(file);
|
|
54
|
+
if (cacheStat.mtimeMs >= sourceStat.mtimeMs) {
|
|
55
|
+
return fs.readFileSync(file);
|
|
56
|
+
}
|
|
57
|
+
// Stale
|
|
58
|
+
return null;
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Store an export buffer in the cache. */
|
|
65
|
+
export function cacheExport(
|
|
66
|
+
fsPath: string,
|
|
67
|
+
format: string,
|
|
68
|
+
buffer: Buffer,
|
|
69
|
+
): void {
|
|
70
|
+
const file = cacheFilePath(fsPath, format);
|
|
71
|
+
if (!file) return;
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
fs.writeFileSync(file, buffer);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error('[exportCache] write failed:', (err as Error).message);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Clear all cached exports for a given file path.
|
|
82
|
+
* Returns the number of files deleted.
|
|
83
|
+
*/
|
|
84
|
+
export function clearExportCache(fsPath: string): number {
|
|
85
|
+
if (!cacheDir) return 0;
|
|
86
|
+
|
|
87
|
+
let count = 0;
|
|
88
|
+
for (const format of ['pdf', 'docx']) {
|
|
89
|
+
const file = cacheFilePath(fsPath, format);
|
|
90
|
+
if (file) {
|
|
91
|
+
try {
|
|
92
|
+
fs.unlinkSync(file);
|
|
93
|
+
count++;
|
|
94
|
+
} catch {
|
|
95
|
+
// Not cached, that's fine
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return count;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Clear standalone diagram cache entries for a diagram source file.
|
|
104
|
+
* Reads the file, computes the content-addressed hash, and removes
|
|
105
|
+
* all format variants (svg, png, pdf, eps) from the diagram cache.
|
|
106
|
+
*/
|
|
107
|
+
export function clearStandaloneDiagramCache(
|
|
108
|
+
fsPath: string,
|
|
109
|
+
diagramCacheDir: string | null,
|
|
110
|
+
): number {
|
|
111
|
+
if (!diagramCacheDir) return 0;
|
|
112
|
+
|
|
113
|
+
const ext = path.extname(fsPath).toLowerCase();
|
|
114
|
+
const diagramType = DIAGRAM_EXT_MAP[ext];
|
|
115
|
+
if (!diagramType) return 0;
|
|
116
|
+
|
|
117
|
+
let source: string;
|
|
118
|
+
try {
|
|
119
|
+
source = fs.readFileSync(fsPath, 'utf8');
|
|
120
|
+
} catch {
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const hash = crypto
|
|
125
|
+
.createHash('sha256')
|
|
126
|
+
.update(`${diagramType}\0${source}`)
|
|
127
|
+
.digest('hex');
|
|
128
|
+
|
|
129
|
+
let count = 0;
|
|
130
|
+
for (const fmt of ['svg', 'png', 'pdf', 'eps']) {
|
|
131
|
+
try {
|
|
132
|
+
fs.unlinkSync(path.join(diagramCacheDir, `${hash}.${fmt}`));
|
|
133
|
+
count++;
|
|
134
|
+
} catch {
|
|
135
|
+
// Not present
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return count;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// --- Reverse index: fsPath → diagram cache hashes ---
|
|
142
|
+
|
|
143
|
+
const reverseIndexFile = (): string | null =>
|
|
144
|
+
cacheDir ? path.join(cacheDir, '_diagram-index.json') : null;
|
|
145
|
+
|
|
146
|
+
type DiagramIndex = Record<string, string[]>;
|
|
147
|
+
|
|
148
|
+
function loadDiagramIndex(): DiagramIndex {
|
|
149
|
+
const file = reverseIndexFile();
|
|
150
|
+
if (!file) return {};
|
|
151
|
+
try {
|
|
152
|
+
return JSON.parse(fs.readFileSync(file, 'utf8')) as DiagramIndex;
|
|
153
|
+
} catch {
|
|
154
|
+
return {};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function saveDiagramIndex(index: DiagramIndex): void {
|
|
159
|
+
const file = reverseIndexFile();
|
|
160
|
+
if (!file) return;
|
|
161
|
+
try {
|
|
162
|
+
fs.writeFileSync(file, JSON.stringify(index, null, 2), 'utf8');
|
|
163
|
+
} catch (err) {
|
|
164
|
+
console.error('[exportCache] index write failed:', (err as Error).message);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Register diagram cache hashes associated with a file.
|
|
170
|
+
* Called during diagram rendering to build the reverse index.
|
|
171
|
+
*/
|
|
172
|
+
export function registerDiagramHashes(fsPath: string, hashes: string[]): void {
|
|
173
|
+
if (!cacheDir || hashes.length === 0) return;
|
|
174
|
+
const key = fsPath.replace(/\\/g, '/').toLowerCase();
|
|
175
|
+
const index = loadDiagramIndex();
|
|
176
|
+
const existing = new Set(index[key] ?? []);
|
|
177
|
+
for (const h of hashes) existing.add(h);
|
|
178
|
+
index[key] = [...existing];
|
|
179
|
+
saveDiagramIndex(index);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Clear diagram cache entries associated with a file.
|
|
184
|
+
* Returns the number of diagram cache files deleted.
|
|
185
|
+
*/
|
|
186
|
+
export function clearDiagramCacheForFile(
|
|
187
|
+
fsPath: string,
|
|
188
|
+
diagramCacheDir: string | null,
|
|
189
|
+
): number {
|
|
190
|
+
if (!cacheDir || !diagramCacheDir) return 0;
|
|
191
|
+
|
|
192
|
+
const key = fsPath.replace(/\\/g, '/').toLowerCase();
|
|
193
|
+
const index = loadDiagramIndex();
|
|
194
|
+
const hashes = index[key] as string[] | undefined;
|
|
195
|
+
if (!hashes || hashes.length === 0) return 0;
|
|
196
|
+
|
|
197
|
+
let count = 0;
|
|
198
|
+
for (const hash of hashes) {
|
|
199
|
+
// Diagram cache files can be .svg, .png, or .pdf
|
|
200
|
+
for (const ext of ['svg', 'png', 'pdf']) {
|
|
201
|
+
try {
|
|
202
|
+
fs.unlinkSync(path.join(diagramCacheDir, `${hash}.${ext}`));
|
|
203
|
+
count++;
|
|
204
|
+
} catch {
|
|
205
|
+
// Not present
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Remove from index
|
|
211
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
212
|
+
const { [key]: _removed, ...rest } = index;
|
|
213
|
+
saveDiagramIndex(rest);
|
|
214
|
+
|
|
215
|
+
return count;
|
|
216
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown rendering with TOC generation, Windows path linking, and syntax highlighting
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
|
|
7
|
+
import { marked } from 'marked';
|
|
8
|
+
|
|
9
|
+
import { registerDiagram } from './embeddedDiagrams.js';
|
|
10
|
+
|
|
11
|
+
interface Heading {
|
|
12
|
+
level: number;
|
|
13
|
+
text: string;
|
|
14
|
+
slug: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a filesystem path for linking (skip non-existent and templated paths)
|
|
19
|
+
*/
|
|
20
|
+
function resolvePathForLink(fsPath: string): string | null {
|
|
21
|
+
// Skip templated paths
|
|
22
|
+
if (fsPath.includes('{') || fsPath.includes('}')) return null;
|
|
23
|
+
|
|
24
|
+
if (!fs.existsSync(fsPath)) return null;
|
|
25
|
+
|
|
26
|
+
return fsPath;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Convert platform-native filesystem paths in markdown text to clickable browse links.
|
|
33
|
+
* Windows: C:\\foo\\bar → [C:\\foo\\bar](/browse/c/foo/bar)
|
|
34
|
+
* Linux: /home/user/docs → [/home/user/docs](/browse/home/user/docs)
|
|
35
|
+
*/
|
|
36
|
+
function linkifyFilesystemPaths(markdown: string): string {
|
|
37
|
+
// Platform-specific path regex
|
|
38
|
+
const pathRegex = IS_WINDOWS
|
|
39
|
+
? /([A-Z]):\\(?:[^\s"'`<>\\]+\\)*[^\s"'`<>\\]+/g
|
|
40
|
+
: /(?<=\s|^)(\/(?:home|opt|var|tmp|etc|usr|srv|mnt|media)\/[^\s"'`<>]+)/gm;
|
|
41
|
+
|
|
42
|
+
const linkifyPath = (fsPath: string): string => {
|
|
43
|
+
const resolved = resolvePathForLink(fsPath);
|
|
44
|
+
if (!resolved) return fsPath;
|
|
45
|
+
|
|
46
|
+
let urlPath: string;
|
|
47
|
+
if (IS_WINDOWS) {
|
|
48
|
+
urlPath =
|
|
49
|
+
'/' +
|
|
50
|
+
resolved
|
|
51
|
+
.replace(/\\/g, '/')
|
|
52
|
+
.replace(/^([A-Z]):/, (_m: string, d: string) => d.toLowerCase());
|
|
53
|
+
} else {
|
|
54
|
+
urlPath = resolved;
|
|
55
|
+
}
|
|
56
|
+
return `[${fsPath}](/browse${urlPath})`;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Split by code blocks and inline code
|
|
60
|
+
const codeBlockRegex = /(```[\s\S]*?```|`[^`\n]+`)/g;
|
|
61
|
+
const parts = markdown.split(codeBlockRegex);
|
|
62
|
+
|
|
63
|
+
return parts
|
|
64
|
+
.map((part) => {
|
|
65
|
+
if (part.startsWith('```') || part.startsWith('`')) {
|
|
66
|
+
return part; // Don't modify code
|
|
67
|
+
}
|
|
68
|
+
return part.replace(pathRegex, linkifyPath);
|
|
69
|
+
})
|
|
70
|
+
.join('');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Extract YAML frontmatter from markdown if present.
|
|
75
|
+
* Returns the frontmatter content (without delimiters) and the remaining markdown.
|
|
76
|
+
*/
|
|
77
|
+
function extractFrontmatter(markdown: string): {
|
|
78
|
+
frontmatter: string | null;
|
|
79
|
+
body: string;
|
|
80
|
+
} {
|
|
81
|
+
const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
|
|
82
|
+
if (!match) return { frontmatter: null, body: markdown };
|
|
83
|
+
return { frontmatter: match[1], body: markdown.slice(match[0].length) };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Parse markdown to HTML with heading extraction
|
|
88
|
+
*/
|
|
89
|
+
export function parseMarkdown(
|
|
90
|
+
markdown: string,
|
|
91
|
+
options: { linkWindowsPaths?: boolean; basePath?: string } = {},
|
|
92
|
+
): { html: string; headings: Heading[] } {
|
|
93
|
+
// Extract frontmatter before processing
|
|
94
|
+
const { frontmatter, body } = extractFrontmatter(markdown);
|
|
95
|
+
let processedMarkdown = body;
|
|
96
|
+
|
|
97
|
+
// Optionally linkify Windows paths
|
|
98
|
+
if (options.linkWindowsPaths) {
|
|
99
|
+
processedMarkdown = linkifyFilesystemPaths(processedMarkdown);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const headings: Heading[] = [];
|
|
103
|
+
|
|
104
|
+
// Custom renderer to extract headings and add anchors
|
|
105
|
+
const renderer = new marked.Renderer();
|
|
106
|
+
|
|
107
|
+
renderer.heading = function (
|
|
108
|
+
args: string | { text: string; raw?: string; depth: number },
|
|
109
|
+
) {
|
|
110
|
+
const text = typeof args === 'object' ? args.text : args;
|
|
111
|
+
const raw = typeof args === 'object' && args.raw ? args.raw : text;
|
|
112
|
+
const level = typeof args === 'object' ? args.depth : 1;
|
|
113
|
+
|
|
114
|
+
const slug = raw
|
|
115
|
+
.toLowerCase()
|
|
116
|
+
.replace(/<[^>]+>/g, '')
|
|
117
|
+
.replace(/[^\w\s-]/g, '')
|
|
118
|
+
.replace(/\s+/g, '-')
|
|
119
|
+
.replace(/-+/g, '-')
|
|
120
|
+
.replace(/^-|-$/g, '');
|
|
121
|
+
|
|
122
|
+
headings.push({ level, text: text.replace(/<[^>]+>/g, ''), slug });
|
|
123
|
+
|
|
124
|
+
return `<h${String(level)} id="${slug}">${text} <a href="#${slug}" class="anchor">#</a></h${String(level)}>\n`;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Rewrite relative image src to /path/ URLs
|
|
128
|
+
if (options.basePath) {
|
|
129
|
+
const base = options.basePath;
|
|
130
|
+
renderer.image = function (
|
|
131
|
+
args: string | { href: string; title: string | null; text: string },
|
|
132
|
+
) {
|
|
133
|
+
const href = typeof args === 'object' ? args.href : args;
|
|
134
|
+
const title = typeof args === 'object' ? args.title : '';
|
|
135
|
+
const text = typeof args === 'object' ? args.text : '';
|
|
136
|
+
let src = href;
|
|
137
|
+
if (src && !src.startsWith('http') && !src.startsWith('data:')) {
|
|
138
|
+
// Rewrite relative paths to /api/raw/ for file serving
|
|
139
|
+
if (!src.startsWith('/')) {
|
|
140
|
+
src = `/api/raw/${base}/${src}`;
|
|
141
|
+
} else if (src.startsWith('/path/')) {
|
|
142
|
+
// Legacy /path/ references → /api/raw/
|
|
143
|
+
src = src.replace('/path/', '/api/raw/');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const titleAttr = title ? ` title="${title}"` : '';
|
|
147
|
+
return `<img src="${src}" alt="${text}"${titleAttr} />`;
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Syntax-highlight fenced code blocks; render mermaid/plantuml as diagrams
|
|
152
|
+
renderer.code = function (
|
|
153
|
+
args: string | { text: string; lang?: string; escaped?: boolean },
|
|
154
|
+
) {
|
|
155
|
+
const text = typeof args === 'object' ? args.text : args;
|
|
156
|
+
const lang = (
|
|
157
|
+
typeof args === 'object' ? args.lang : undefined
|
|
158
|
+
)?.toLowerCase();
|
|
159
|
+
|
|
160
|
+
// Diagram code blocks → register for async rendering (GitHub convention)
|
|
161
|
+
if (lang === 'mermaid') {
|
|
162
|
+
return registerDiagram('mermaid', text);
|
|
163
|
+
}
|
|
164
|
+
if (lang === 'plantuml' || lang === 'puml') {
|
|
165
|
+
return registerDiagram('plantuml', text);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const escaped = text
|
|
169
|
+
.replace(/&/g, '&')
|
|
170
|
+
.replace(/</g, '<')
|
|
171
|
+
.replace(/>/g, '>');
|
|
172
|
+
const langClass = lang ? ` class="language-${lang}"` : '';
|
|
173
|
+
return `<pre><code${langClass}>${escaped}</code></pre>\n`;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
marked.setOptions({ renderer });
|
|
177
|
+
let html = marked(processedMarkdown) as string;
|
|
178
|
+
|
|
179
|
+
// Prepend frontmatter as a rendered YAML code block
|
|
180
|
+
if (frontmatter) {
|
|
181
|
+
const escaped = frontmatter
|
|
182
|
+
.replace(/&/g, '&')
|
|
183
|
+
.replace(/</g, '<')
|
|
184
|
+
.replace(/>/g, '>');
|
|
185
|
+
html = `<div class="frontmatter-block"><pre><code class="language-yaml">${escaped}</code></pre></div>\n${html}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return { html, headings };
|
|
189
|
+
}
|