pandoc-glance 0.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/LICENSE +21 -0
- package/README.md +280 -0
- package/dist/browser.d.ts +3 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +25 -0
- package/dist/browser.js.map +1 -0
- package/dist/cli.d.ts +24 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +355 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/render.d.ts +62 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +899 -0
- package/dist/render.js.map +1 -0
- package/dist/server.d.ts +42 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +379 -0
- package/dist/server.js.map +1 -0
- package/dist/styles.d.ts +41 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +323 -0
- package/dist/styles.js.map +1 -0
- package/dist/watch-preview.d.ts +59 -0
- package/dist/watch-preview.d.ts.map +1 -0
- package/dist/watch-preview.js +175 -0
- package/dist/watch-preview.js.map +1 -0
- package/dist/watcher.d.ts +16 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +81 -0
- package/dist/watcher.js.map +1 -0
- package/docs/screenshots/dark-browser.png +0 -0
- package/package.json +64 -0
package/dist/render.js
ADDED
|
@@ -0,0 +1,899 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { realpath, stat } from "node:fs/promises";
|
|
4
|
+
import { basename, extname, isAbsolute, relative, resolve, sep, win32 } from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
import { buildPreviewCss, palettesForClient } from "./styles.js";
|
|
7
|
+
const MARKDOWN_EXTENSIONS = new Set([".md", ".markdown", ".mdown", ".mkd", ".qmd", ".rmd"]);
|
|
8
|
+
const LATEX_EXTENSIONS = new Set([".tex", ".latex"]);
|
|
9
|
+
const PANDOC_OUTPUT_LIMIT_BYTES = 50 * 1024 * 1024;
|
|
10
|
+
const PANDOC_TIMEOUT_MS = 30_000;
|
|
11
|
+
const MERMAID_BROWSER_VERSION = "11.16.0";
|
|
12
|
+
const MERMAID_BROWSER_ICON_PACKS = [
|
|
13
|
+
{ name: "lucide", url: "https://unpkg.com/@iconify-json/lucide@1/icons.json" },
|
|
14
|
+
{ name: "logos", url: "https://unpkg.com/@iconify-json/logos@1/icons.json" },
|
|
15
|
+
];
|
|
16
|
+
export class PandocError extends Error {
|
|
17
|
+
constructor(message, options) {
|
|
18
|
+
super(message, options);
|
|
19
|
+
this.name = "PandocError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function detectFormat(filePath, requested) {
|
|
23
|
+
if (requested !== "auto")
|
|
24
|
+
return requested;
|
|
25
|
+
const extension = extname(filePath).toLowerCase();
|
|
26
|
+
if (LATEX_EXTENSIONS.has(extension))
|
|
27
|
+
return "latex";
|
|
28
|
+
if (MARKDOWN_EXTENSIONS.has(extension))
|
|
29
|
+
return "markdown";
|
|
30
|
+
throw new Error(`Cannot detect the input format from ${extension || "a file with no extension"}. `
|
|
31
|
+
+ "Use --format markdown or --format latex.");
|
|
32
|
+
}
|
|
33
|
+
function pandocCommand() {
|
|
34
|
+
return process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
35
|
+
}
|
|
36
|
+
function pandocInstallHint() {
|
|
37
|
+
if (process.platform === "darwin")
|
|
38
|
+
return "Install it with `brew install pandoc`, or set PANDOC_PATH.";
|
|
39
|
+
if (process.platform === "win32") {
|
|
40
|
+
return "Install it with `winget install --id JohnMacFarlane.Pandoc`, or set PANDOC_PATH.";
|
|
41
|
+
}
|
|
42
|
+
return "Install it with your package manager (for example `sudo apt install pandoc`), or set PANDOC_PATH.";
|
|
43
|
+
}
|
|
44
|
+
async function runPandoc(args, input) {
|
|
45
|
+
const command = pandocCommand();
|
|
46
|
+
return await new Promise((resolvePromise, rejectPromise) => {
|
|
47
|
+
const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
48
|
+
const stdoutChunks = [];
|
|
49
|
+
const stderrChunks = [];
|
|
50
|
+
let stdoutBytes = 0;
|
|
51
|
+
let settled = false;
|
|
52
|
+
let timedOut = false;
|
|
53
|
+
const finishWithError = (error) => {
|
|
54
|
+
if (settled)
|
|
55
|
+
return;
|
|
56
|
+
settled = true;
|
|
57
|
+
clearTimeout(timeout);
|
|
58
|
+
rejectPromise(error);
|
|
59
|
+
};
|
|
60
|
+
const timeout = setTimeout(() => {
|
|
61
|
+
timedOut = true;
|
|
62
|
+
child.kill("SIGKILL");
|
|
63
|
+
}, PANDOC_TIMEOUT_MS);
|
|
64
|
+
timeout.unref();
|
|
65
|
+
child.stdout.on("data", (chunk) => {
|
|
66
|
+
const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
67
|
+
stdoutBytes += buffer.length;
|
|
68
|
+
if (stdoutBytes > PANDOC_OUTPUT_LIMIT_BYTES) {
|
|
69
|
+
child.kill("SIGKILL");
|
|
70
|
+
finishWithError(new PandocError("Pandoc HTML output exceeded 50 MB."));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
stdoutChunks.push(buffer);
|
|
74
|
+
});
|
|
75
|
+
child.stderr.on("data", (chunk) => {
|
|
76
|
+
stderrChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
77
|
+
});
|
|
78
|
+
child.once("error", (error) => {
|
|
79
|
+
const systemError = error;
|
|
80
|
+
if (systemError.code === "ENOENT") {
|
|
81
|
+
const configured = process.env.PANDOC_PATH?.trim();
|
|
82
|
+
const prefix = configured
|
|
83
|
+
? `Pandoc was not found at PANDOC_PATH=${configured}.`
|
|
84
|
+
: "Pandoc was not found.";
|
|
85
|
+
finishWithError(new PandocError(`${prefix} ${pandocInstallHint()}`, { cause: error }));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
finishWithError(new PandocError(`Failed to start Pandoc: ${error.message}`, { cause: error }));
|
|
89
|
+
});
|
|
90
|
+
child.once("close", (code, signal) => {
|
|
91
|
+
if (settled)
|
|
92
|
+
return;
|
|
93
|
+
settled = true;
|
|
94
|
+
clearTimeout(timeout);
|
|
95
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
|
|
96
|
+
if (timedOut) {
|
|
97
|
+
rejectPromise(new PandocError(`Pandoc timed out after ${PANDOC_TIMEOUT_MS / 1000} seconds.`));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (code !== 0) {
|
|
101
|
+
const status = code === null ? `signal ${signal ?? "unknown"}` : `exit code ${code}`;
|
|
102
|
+
rejectPromise(new PandocError(`Pandoc failed with ${status}${stderr ? `: ${stderr}` : "."}`));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
resolvePromise({
|
|
106
|
+
stdout: Buffer.concat(stdoutChunks).toString("utf8"),
|
|
107
|
+
stderr,
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
child.stdin.on("error", (error) => {
|
|
111
|
+
if (error.code !== "EPIPE")
|
|
112
|
+
finishWithError(new PandocError(`Could not send input to Pandoc: ${error.message}`));
|
|
113
|
+
});
|
|
114
|
+
child.stdin.end(input ?? "");
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
export async function assertPandocAvailable() {
|
|
118
|
+
await runPandoc(["--version"]);
|
|
119
|
+
}
|
|
120
|
+
function isLikelyMathExpression(expression) {
|
|
121
|
+
const content = expression.trim();
|
|
122
|
+
if (!content)
|
|
123
|
+
return false;
|
|
124
|
+
if (/\\[a-zA-Z]+/.test(content))
|
|
125
|
+
return true;
|
|
126
|
+
if (/[0-9]/.test(content))
|
|
127
|
+
return true;
|
|
128
|
+
if (/[=+\-*/^_<>≤≥±×÷]/u.test(content))
|
|
129
|
+
return true;
|
|
130
|
+
if (/[{}]/.test(content))
|
|
131
|
+
return true;
|
|
132
|
+
if (/[α-ωΑ-Ω]/u.test(content))
|
|
133
|
+
return true;
|
|
134
|
+
if (/^[A-Za-z]$/.test(content))
|
|
135
|
+
return true;
|
|
136
|
+
if (/^[A-Za-z][A-Za-z\s'".,:;!?-]*[A-Za-z]$/.test(content))
|
|
137
|
+
return false;
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
function normalizeMathInPlainSegment(markdown) {
|
|
141
|
+
let normalized = markdown.replace(/\\\[\s*([\s\S]*?)\s*\\\]/g, (match, expression) => {
|
|
142
|
+
const content = expression.trim();
|
|
143
|
+
if (!isLikelyMathExpression(content))
|
|
144
|
+
return match;
|
|
145
|
+
return `$$\n${content}\n$$`;
|
|
146
|
+
});
|
|
147
|
+
normalized = normalized.replace(/\\\(([\s\S]*?)\\\)/g, (match, expression) => {
|
|
148
|
+
if (!isLikelyMathExpression(expression))
|
|
149
|
+
return match;
|
|
150
|
+
return `$${expression.trim()}$`;
|
|
151
|
+
});
|
|
152
|
+
return normalized;
|
|
153
|
+
}
|
|
154
|
+
export function normalizeMathDelimiters(markdown) {
|
|
155
|
+
const lines = markdown.split("\n");
|
|
156
|
+
const output = [];
|
|
157
|
+
let plainLines = [];
|
|
158
|
+
let fenceCharacter;
|
|
159
|
+
let fenceLength = 0;
|
|
160
|
+
const flushPlain = () => {
|
|
161
|
+
if (plainLines.length === 0)
|
|
162
|
+
return;
|
|
163
|
+
output.push(normalizeMathInPlainSegment(plainLines.join("\n")));
|
|
164
|
+
plainLines = [];
|
|
165
|
+
};
|
|
166
|
+
for (const line of lines) {
|
|
167
|
+
const match = line.trimStart().match(/^(`{3,}|~{3,})/);
|
|
168
|
+
if (match) {
|
|
169
|
+
const marker = match[1];
|
|
170
|
+
const character = marker[0];
|
|
171
|
+
if (!fenceCharacter) {
|
|
172
|
+
flushPlain();
|
|
173
|
+
fenceCharacter = character;
|
|
174
|
+
fenceLength = marker.length;
|
|
175
|
+
output.push(line);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (character === fenceCharacter && marker.length >= fenceLength) {
|
|
179
|
+
fenceCharacter = undefined;
|
|
180
|
+
fenceLength = 0;
|
|
181
|
+
}
|
|
182
|
+
output.push(line);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (fenceCharacter)
|
|
186
|
+
output.push(line);
|
|
187
|
+
else
|
|
188
|
+
plainLines.push(line);
|
|
189
|
+
}
|
|
190
|
+
flushPlain();
|
|
191
|
+
return output.join("\n");
|
|
192
|
+
}
|
|
193
|
+
function formatMarkdownImageDestination(rawPath) {
|
|
194
|
+
const trimmed = rawPath.trim();
|
|
195
|
+
if (!trimmed)
|
|
196
|
+
return "";
|
|
197
|
+
const unwrapped = trimmed.startsWith("<") && trimmed.endsWith(">")
|
|
198
|
+
? trimmed.slice(1, -1).trim()
|
|
199
|
+
: trimmed;
|
|
200
|
+
return /[\s<>()]/.test(unwrapped) ? `<${unwrapped}>` : unwrapped;
|
|
201
|
+
}
|
|
202
|
+
export function normalizeObsidianImages(markdown) {
|
|
203
|
+
return markdown
|
|
204
|
+
.replace(/!\[\[([^|\]]+)\|([^\]]+)\]\]/g, (_match, imagePath, alt) => {
|
|
205
|
+
return `})`;
|
|
206
|
+
})
|
|
207
|
+
.replace(/!\[\[([^\]]+)\]\]/g, (_match, imagePath) => {
|
|
208
|
+
return `})`;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
function longestFenceRun(text, character) {
|
|
212
|
+
const pattern = character === "`" ? /`+/g : /~+/g;
|
|
213
|
+
let longest = 0;
|
|
214
|
+
let match;
|
|
215
|
+
while ((match = pattern.exec(text)) !== null)
|
|
216
|
+
longest = Math.max(longest, match[0].length);
|
|
217
|
+
return longest;
|
|
218
|
+
}
|
|
219
|
+
// Protect fenced blocks whose contents contain a run as long as the outer fence.
|
|
220
|
+
export function normalizeMarkdownFencedBlocks(markdown) {
|
|
221
|
+
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
|
222
|
+
const output = [];
|
|
223
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
224
|
+
const line = lines[index] ?? "";
|
|
225
|
+
const opening = line.match(/^(\s{0,3})(`{3,}|~{3,})([^\n]*)$/);
|
|
226
|
+
if (!opening) {
|
|
227
|
+
output.push(line);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const indent = opening[1] ?? "";
|
|
231
|
+
const openingFence = opening[2];
|
|
232
|
+
const suffix = opening[3] ?? "";
|
|
233
|
+
const character = openingFence[0];
|
|
234
|
+
let closingIndex = -1;
|
|
235
|
+
for (let candidate = index + 1; candidate < lines.length; candidate += 1) {
|
|
236
|
+
const closing = (lines[candidate] ?? "").match(/^\s{0,3}(`{3,}|~{3,})\s*$/);
|
|
237
|
+
if (!closing)
|
|
238
|
+
continue;
|
|
239
|
+
const closingFence = closing[1];
|
|
240
|
+
if (closingFence[0] === character && closingFence.length >= openingFence.length) {
|
|
241
|
+
closingIndex = candidate;
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (closingIndex === -1) {
|
|
246
|
+
output.push(line);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const contentLines = lines.slice(index + 1, closingIndex);
|
|
250
|
+
const content = contentLines.join("\n");
|
|
251
|
+
const backticks = longestFenceRun(content, "`");
|
|
252
|
+
const tildes = longestFenceRun(content, "~");
|
|
253
|
+
const currentLongest = character === "`" ? backticks : tildes;
|
|
254
|
+
if (currentLongest < openingFence.length) {
|
|
255
|
+
output.push(line, ...contentLines, lines[closingIndex] ?? "");
|
|
256
|
+
index = closingIndex;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const neededBackticks = Math.max(3, backticks + 1);
|
|
260
|
+
const neededTildes = Math.max(3, tildes + 1);
|
|
261
|
+
let replacementCharacter = character;
|
|
262
|
+
if (neededBackticks < neededTildes)
|
|
263
|
+
replacementCharacter = "`";
|
|
264
|
+
else if (neededTildes < neededBackticks)
|
|
265
|
+
replacementCharacter = "~";
|
|
266
|
+
else if (character === "`")
|
|
267
|
+
replacementCharacter = "~";
|
|
268
|
+
const replacementLength = replacementCharacter === "`" ? neededBackticks : neededTildes;
|
|
269
|
+
const replacement = replacementCharacter.repeat(replacementLength);
|
|
270
|
+
output.push(`${indent}${replacement}${suffix}`, ...contentLines, `${indent}${replacement}`);
|
|
271
|
+
index = closingIndex;
|
|
272
|
+
}
|
|
273
|
+
return output.join("\n");
|
|
274
|
+
}
|
|
275
|
+
export function prepareMarkdownForPandoc(markdown) {
|
|
276
|
+
return normalizeMarkdownFencedBlocks(normalizeObsidianImages(normalizeMathDelimiters(markdown)));
|
|
277
|
+
}
|
|
278
|
+
export async function renderPandocFragment(source, format, resourceRoot) {
|
|
279
|
+
const inputFormat = format === "latex"
|
|
280
|
+
? "latex"
|
|
281
|
+
: "markdown+lists_without_preceding_blankline-blank_before_blockquote-blank_before_header+tex_math_dollars+autolink_bare_uris-raw_html";
|
|
282
|
+
const pandocInput = format === "latex" ? source : prepareMarkdownForPandoc(source);
|
|
283
|
+
const args = [
|
|
284
|
+
"-f",
|
|
285
|
+
inputFormat,
|
|
286
|
+
"-t",
|
|
287
|
+
"html5",
|
|
288
|
+
"--mathml",
|
|
289
|
+
"--wrap=none",
|
|
290
|
+
`--resource-path=${resourceRoot}`,
|
|
291
|
+
];
|
|
292
|
+
if (format === "latex")
|
|
293
|
+
args.push("--standalone");
|
|
294
|
+
const result = await runPandoc(args, pandocInput);
|
|
295
|
+
let html = result.stdout;
|
|
296
|
+
if (format === "latex") {
|
|
297
|
+
const body = html.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
|
298
|
+
if (!body)
|
|
299
|
+
throw new PandocError("Pandoc did not return a complete HTML body for the LaTeX document.");
|
|
300
|
+
html = body[1].trimStart();
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
html,
|
|
304
|
+
warnings: result.stderr ? result.stderr.split(/\r?\n/).filter(Boolean) : [],
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
function escapeHtml(value) {
|
|
308
|
+
return value
|
|
309
|
+
.replace(/&/g, "&")
|
|
310
|
+
.replace(/</g, "<")
|
|
311
|
+
.replace(/>/g, ">")
|
|
312
|
+
.replace(/"/g, """)
|
|
313
|
+
.replace(/'/g, "'");
|
|
314
|
+
}
|
|
315
|
+
function escapeJsonForScript(value) {
|
|
316
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e");
|
|
317
|
+
}
|
|
318
|
+
function decodeHtmlAttribute(value) {
|
|
319
|
+
return value
|
|
320
|
+
.replace(/&/gi, "&")
|
|
321
|
+
.replace(/"/gi, "\"")
|
|
322
|
+
.replace(/'|'/gi, "'")
|
|
323
|
+
.replace(/&#x([0-9a-f]+);/gi, (_match, hex) => String.fromCodePoint(Number.parseInt(hex, 16)))
|
|
324
|
+
.replace(/&#([0-9]+);/g, (_match, decimal) => String.fromCodePoint(Number.parseInt(decimal, 10)));
|
|
325
|
+
}
|
|
326
|
+
function encodeHtmlAttribute(value) {
|
|
327
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'");
|
|
328
|
+
}
|
|
329
|
+
function decodeLocalReference(rawValue) {
|
|
330
|
+
const value = decodeHtmlAttribute(rawValue).trim();
|
|
331
|
+
if (!value || value.startsWith("#") || value.startsWith("//"))
|
|
332
|
+
return null;
|
|
333
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(value) && !/^file:/i.test(value) && !win32.isAbsolute(value))
|
|
334
|
+
return null;
|
|
335
|
+
const hashIndex = value.indexOf("#");
|
|
336
|
+
const hash = hashIndex >= 0 ? value.slice(hashIndex) : "";
|
|
337
|
+
const withoutHash = hashIndex >= 0 ? value.slice(0, hashIndex) : value;
|
|
338
|
+
const queryIndex = withoutHash.indexOf("?");
|
|
339
|
+
const withoutQuery = queryIndex >= 0 ? withoutHash.slice(0, queryIndex) : withoutHash;
|
|
340
|
+
try {
|
|
341
|
+
if (/^file:/i.test(withoutQuery))
|
|
342
|
+
return { path: fileURLToPath(withoutQuery), hash };
|
|
343
|
+
return { path: decodeURIComponent(withoutQuery), hash };
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return { path: withoutQuery, hash };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function pathIsWithin(root, candidate) {
|
|
350
|
+
const fromRoot = relative(resolve(root), resolve(candidate));
|
|
351
|
+
return fromRoot === "" || (!fromRoot.startsWith(`..${sep}`) && fromRoot !== ".." && !isAbsolute(fromRoot));
|
|
352
|
+
}
|
|
353
|
+
async function rewriteServerResourceUrls(fragmentHtml, resourceRoot, config) {
|
|
354
|
+
const assets = new Map();
|
|
355
|
+
const replacements = [];
|
|
356
|
+
const attributePattern = /\b(?:src|href|poster|data)=("([^"]*)"|'([^']*)')/gi;
|
|
357
|
+
let match;
|
|
358
|
+
while ((match = attributePattern.exec(fragmentHtml)) !== null) {
|
|
359
|
+
const quotedValue = match[1];
|
|
360
|
+
const rawValue = match[2] ?? match[3] ?? "";
|
|
361
|
+
const localReference = decodeLocalReference(rawValue);
|
|
362
|
+
if (!localReference)
|
|
363
|
+
continue;
|
|
364
|
+
const localPath = localReference.path;
|
|
365
|
+
const absolutePath = isAbsolute(localPath) || win32.isAbsolute(localPath);
|
|
366
|
+
let rewritten;
|
|
367
|
+
if (!absolutePath) {
|
|
368
|
+
rewritten = `${config.resourcePath}?path=${encodeURIComponent(localPath)}&v=${config.revision}${localReference.hash}`;
|
|
369
|
+
}
|
|
370
|
+
else if (pathIsWithin(resourceRoot, localPath)) {
|
|
371
|
+
const relativePath = relative(resolve(resourceRoot), resolve(localPath));
|
|
372
|
+
rewritten = `${config.resourcePath}?path=${encodeURIComponent(relativePath)}&v=${config.revision}${localReference.hash}`;
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
try {
|
|
376
|
+
const canonicalPath = await realpath(localPath);
|
|
377
|
+
const metadata = await stat(canonicalPath);
|
|
378
|
+
if (!metadata.isFile())
|
|
379
|
+
continue;
|
|
380
|
+
const assetId = createHash("sha256").update(canonicalPath).digest("base64url").slice(0, 24);
|
|
381
|
+
assets.set(assetId, canonicalPath);
|
|
382
|
+
rewritten = `${config.assetPath}/${assetId}?v=${config.revision}${localReference.hash}`;
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const quote = quotedValue[0] ?? "\"";
|
|
389
|
+
const replacement = `${quote}${encodeHtmlAttribute(rewritten)}${quote}`;
|
|
390
|
+
const valueOffset = match[0].indexOf(quotedValue);
|
|
391
|
+
replacements.push({
|
|
392
|
+
start: match.index + valueOffset,
|
|
393
|
+
end: match.index + valueOffset + quotedValue.length,
|
|
394
|
+
value: replacement,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
let rewrittenHtml = fragmentHtml;
|
|
398
|
+
for (const replacement of replacements.reverse()) {
|
|
399
|
+
rewrittenHtml = rewrittenHtml.slice(0, replacement.start) + replacement.value + rewrittenHtml.slice(replacement.end);
|
|
400
|
+
}
|
|
401
|
+
return { html: rewrittenHtml, assets };
|
|
402
|
+
}
|
|
403
|
+
// Adapted from pi-markdown-preview's MIT-licensed Mermaid icon and contrast handling.
|
|
404
|
+
function buildMermaidClientSource() {
|
|
405
|
+
const mermaidUrlJson = escapeJsonForScript(`https://cdn.jsdelivr.net/npm/mermaid@${MERMAID_BROWSER_VERSION}/dist/mermaid.esm.min.mjs`);
|
|
406
|
+
const iconPacksJson = escapeJsonForScript(MERMAID_BROWSER_ICON_PACKS);
|
|
407
|
+
return String.raw `
|
|
408
|
+
function setMermaidRenderResult(status, error) {
|
|
409
|
+
window.__mermaidRenderResult = error ? { status, error } : { status };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function renderMermaidFailure(entries, message) {
|
|
413
|
+
entries.forEach((entry) => {
|
|
414
|
+
const failure = document.createElement("div");
|
|
415
|
+
failure.className = "mermaid-error";
|
|
416
|
+
failure.setAttribute("role", "alert");
|
|
417
|
+
|
|
418
|
+
const summary = document.createElement("div");
|
|
419
|
+
summary.className = "mermaid-error-message";
|
|
420
|
+
summary.textContent = "Mermaid render failed: " + message;
|
|
421
|
+
|
|
422
|
+
const source = document.createElement("pre");
|
|
423
|
+
source.className = "mermaid-source";
|
|
424
|
+
const code = document.createElement("code");
|
|
425
|
+
code.textContent = entry.source;
|
|
426
|
+
source.appendChild(code);
|
|
427
|
+
failure.append(summary, source);
|
|
428
|
+
entry.wrapper.replaceChildren(failure);
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function renderMermaid() {
|
|
433
|
+
if (!root) {
|
|
434
|
+
setMermaidRenderResult("skipped");
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const blocks = Array.from(root.querySelectorAll("pre.mermaid"));
|
|
438
|
+
if (blocks.length === 0) {
|
|
439
|
+
setMermaidRenderResult("skipped");
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
setMermaidRenderResult("pending");
|
|
444
|
+
const entries = blocks.map((pre) => {
|
|
445
|
+
const code = pre.querySelector("code");
|
|
446
|
+
const source = String(code ? code.textContent : pre.textContent || "");
|
|
447
|
+
const wrapper = document.createElement("div");
|
|
448
|
+
wrapper.className = "mermaid-container";
|
|
449
|
+
const diagram = document.createElement("div");
|
|
450
|
+
diagram.className = "mermaid";
|
|
451
|
+
diagram.textContent = source;
|
|
452
|
+
wrapper.appendChild(diagram);
|
|
453
|
+
pre.replaceWith(wrapper);
|
|
454
|
+
return { wrapper, diagram, source };
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
const module = await import(${mermaidUrlJson});
|
|
459
|
+
const mermaid = module && module.default;
|
|
460
|
+
if (!mermaid) throw new Error("Mermaid did not expose a default export.");
|
|
461
|
+
|
|
462
|
+
const packs = ${iconPacksJson};
|
|
463
|
+
const pending = new Map();
|
|
464
|
+
let iconPackError = null;
|
|
465
|
+
const load = (pack) => {
|
|
466
|
+
if (!pending.has(pack.name)) {
|
|
467
|
+
pending.set(pack.name, fetch(pack.url).then((response) => {
|
|
468
|
+
if (!response.ok) {
|
|
469
|
+
throw new Error("Failed to load Mermaid icon pack " + pack.name + ": HTTP " + response.status);
|
|
470
|
+
}
|
|
471
|
+
return response.json();
|
|
472
|
+
}).catch((error) => {
|
|
473
|
+
iconPackError = iconPackError || (error instanceof Error ? error : new Error(String(error)));
|
|
474
|
+
throw error;
|
|
475
|
+
}));
|
|
476
|
+
}
|
|
477
|
+
return pending.get(pack.name);
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
mermaid.registerIconPacks(packs.map((pack) => ({ name: pack.name, loader: () => load(pack) })));
|
|
481
|
+
mermaid.initialize(mermaidConfig());
|
|
482
|
+
await mermaid.run({ nodes: entries.map((entry) => entry.diagram) });
|
|
483
|
+
if (iconPackError) throw iconPackError;
|
|
484
|
+
|
|
485
|
+
const parseRgb = (value) => {
|
|
486
|
+
const match = value.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/);
|
|
487
|
+
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
|
|
488
|
+
};
|
|
489
|
+
const isOpaqueColor = (value) => {
|
|
490
|
+
if (!parseRgb(value)) return false;
|
|
491
|
+
const alphaMatch = value.match(/^rgba\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+\s*,\s*([\d.]+)\s*\)$/);
|
|
492
|
+
return !alphaMatch || Number(alphaMatch[1]) >= 1;
|
|
493
|
+
};
|
|
494
|
+
const findOpaqueFill = (element) => {
|
|
495
|
+
if (!(element instanceof Element)) return null;
|
|
496
|
+
const shape = Array.from(element.querySelectorAll("rect, polygon, path, circle, ellipse")).find((candidate) => {
|
|
497
|
+
return isOpaqueColor(getComputedStyle(candidate).fill);
|
|
498
|
+
});
|
|
499
|
+
return shape ? getComputedStyle(shape).fill : null;
|
|
500
|
+
};
|
|
501
|
+
const findOpaqueBackground = (element, fallback) => {
|
|
502
|
+
let current = element instanceof Element ? element : null;
|
|
503
|
+
while (current) {
|
|
504
|
+
const background = getComputedStyle(current).backgroundColor;
|
|
505
|
+
if (current instanceof HTMLElement && isOpaqueColor(background)) return background;
|
|
506
|
+
current = current.parentElement;
|
|
507
|
+
}
|
|
508
|
+
return fallback;
|
|
509
|
+
};
|
|
510
|
+
const relativeLuminance = (color) => {
|
|
511
|
+
const linear = color.map((channel) => {
|
|
512
|
+
const value = channel / 255;
|
|
513
|
+
return value <= 0.04045 ? value / 12.92 : Math.pow((value + 0.055) / 1.055, 2.4);
|
|
514
|
+
});
|
|
515
|
+
return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];
|
|
516
|
+
};
|
|
517
|
+
const contrastRatio = (foreground, background) => {
|
|
518
|
+
const lighter = Math.max(relativeLuminance(foreground), relativeLuminance(background));
|
|
519
|
+
const darker = Math.min(relativeLuminance(foreground), relativeLuminance(background));
|
|
520
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
521
|
+
};
|
|
522
|
+
const toRgb = (color) => "rgb(" + color.map((channel) => Math.round(channel)).join(", ") + ")";
|
|
523
|
+
const ensureReadableColor = (foregroundCss, backgroundCss) => {
|
|
524
|
+
const foreground = parseRgb(foregroundCss);
|
|
525
|
+
const background = parseRgb(backgroundCss);
|
|
526
|
+
if (!foreground || !background || contrastRatio(foreground, background) >= 4.5) return foregroundCss;
|
|
527
|
+
const readableCandidates = [[0, 0, 0], [255, 255, 255]].flatMap((target) => {
|
|
528
|
+
for (let step = 1; step <= 20; step += 1) {
|
|
529
|
+
const amount = step / 20;
|
|
530
|
+
const color = foreground.map((channel, index) => channel + (target[index] - channel) * amount);
|
|
531
|
+
if (contrastRatio(color, background) >= 4.5) return [{ amount, color }];
|
|
532
|
+
}
|
|
533
|
+
return [];
|
|
534
|
+
});
|
|
535
|
+
readableCandidates.sort((left, right) => left.amount - right.amount);
|
|
536
|
+
if (readableCandidates.length > 0) return toRgb(readableCandidates[0].color);
|
|
537
|
+
const black = [0, 0, 0];
|
|
538
|
+
const white = [255, 255, 255];
|
|
539
|
+
return toRgb(contrastRatio(black, background) >= contrastRatio(white, background) ? black : white);
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const pageBackground = getComputedStyle(document.body).backgroundColor;
|
|
543
|
+
root.querySelectorAll(".mermaid-container .icon-shape").forEach((node) => {
|
|
544
|
+
const icon = node.querySelector("svg");
|
|
545
|
+
if (!icon) return;
|
|
546
|
+
const semanticColor = getComputedStyle(icon).color;
|
|
547
|
+
const iconSurface = findOpaqueFill(node.firstElementChild)
|
|
548
|
+
|| findOpaqueBackground(icon, pageBackground);
|
|
549
|
+
icon.style.setProperty("color", ensureReadableColor(semanticColor, iconSurface), "important");
|
|
550
|
+
node.querySelectorAll(".labelBkg, .nodeLabel").forEach((label) => {
|
|
551
|
+
if (!(label instanceof HTMLElement)) return;
|
|
552
|
+
const labelSurface = findOpaqueBackground(label, pageBackground);
|
|
553
|
+
label.style.setProperty("color", ensureReadableColor(semanticColor, labelSurface), "important");
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
root.querySelectorAll(".mermaid-container .node:not(.icon-shape)").forEach((node) => {
|
|
557
|
+
const shape = Array.from(node.querySelectorAll("rect, polygon, path, circle, ellipse")).find((candidate) => {
|
|
558
|
+
const fill = getComputedStyle(candidate).fill;
|
|
559
|
+
return fill && fill !== "none" && fill !== "rgba(0, 0, 0, 0)";
|
|
560
|
+
});
|
|
561
|
+
if (!shape) return;
|
|
562
|
+
const shapeFill = getComputedStyle(shape).fill;
|
|
563
|
+
node.querySelectorAll(".nodeLabel").forEach((label) => {
|
|
564
|
+
if (!(label instanceof HTMLElement)) return;
|
|
565
|
+
const labelColor = ensureReadableColor(getComputedStyle(label).color, shapeFill);
|
|
566
|
+
label.style.setProperty("color", labelColor, "important");
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
setMermaidRenderResult("success");
|
|
570
|
+
} catch (error) {
|
|
571
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
572
|
+
setMermaidRenderResult("failed", message);
|
|
573
|
+
renderMermaidFailure(entries, message);
|
|
574
|
+
appendWarning("preview-mermaid-warning", "Mermaid is unavailable. Showing the diagram source as code.");
|
|
575
|
+
console.error("Mermaid render failed:", error);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
`;
|
|
579
|
+
}
|
|
580
|
+
function buildClientScript(theme, liveReload) {
|
|
581
|
+
const clientConfig = {
|
|
582
|
+
theme,
|
|
583
|
+
palettes: palettesForClient(),
|
|
584
|
+
live: liveReload ?? null,
|
|
585
|
+
};
|
|
586
|
+
const configJson = escapeJsonForScript(clientConfig);
|
|
587
|
+
return `
|
|
588
|
+
(() => {
|
|
589
|
+
"use strict";
|
|
590
|
+
const CONFIG = ${configJson};
|
|
591
|
+
const MATHJAX_CDN_URL = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js";
|
|
592
|
+
const root = document.getElementById("preview-root");
|
|
593
|
+
const status = document.getElementById("preview-status");
|
|
594
|
+
let activeRenderError = CONFIG.live && CONFIG.live.initialError ? String(CONFIG.live.initialError) : "";
|
|
595
|
+
|
|
596
|
+
function showStatus(message, level) {
|
|
597
|
+
if (!status) return;
|
|
598
|
+
status.textContent = String(message || "");
|
|
599
|
+
status.dataset.level = level || "warning";
|
|
600
|
+
status.hidden = !message;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function appendWarning(className, message) {
|
|
604
|
+
if (!root || root.querySelector("." + className)) return;
|
|
605
|
+
const warning = document.createElement("div");
|
|
606
|
+
warning.className = "preview-warning " + className;
|
|
607
|
+
warning.textContent = message;
|
|
608
|
+
root.appendChild(warning);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function hashText(text) {
|
|
612
|
+
let hash = 2166136261;
|
|
613
|
+
const source = String(text || "");
|
|
614
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
615
|
+
hash ^= source.charCodeAt(index);
|
|
616
|
+
hash = Math.imul(hash, 16777619);
|
|
617
|
+
}
|
|
618
|
+
return (hash >>> 0).toString(36);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function assignStableAnchors() {
|
|
622
|
+
if (!root) return [];
|
|
623
|
+
const selector = "h1,h2,h3,h4,h5,h6,p,figure,blockquote,ul,ol,table,div.sourceCode,pre,math[display='block'],.mermaid-container";
|
|
624
|
+
const occurrences = new Map();
|
|
625
|
+
return Array.from(root.querySelectorAll(selector)).map((element) => {
|
|
626
|
+
if (element.id) {
|
|
627
|
+
element.dataset.previewAnchor = "id:" + element.id;
|
|
628
|
+
return element;
|
|
629
|
+
}
|
|
630
|
+
const text = String(element.textContent || "").replace(/\\s+/g, " ").trim().slice(0, 160);
|
|
631
|
+
const base = element.tagName.toLowerCase() + ":" + hashText(text);
|
|
632
|
+
const occurrence = occurrences.get(base) || 0;
|
|
633
|
+
occurrences.set(base, occurrence + 1);
|
|
634
|
+
element.dataset.previewAnchor = base + ":" + occurrence;
|
|
635
|
+
return element;
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function captureReadingPosition() {
|
|
640
|
+
if (!CONFIG.live || !root) return;
|
|
641
|
+
const elements = assignStableAnchors();
|
|
642
|
+
const targetLine = Math.max(24, window.innerHeight * 0.28);
|
|
643
|
+
let selected = elements[0] || null;
|
|
644
|
+
for (const element of elements) {
|
|
645
|
+
if (element.getBoundingClientRect().top <= targetLine) selected = element;
|
|
646
|
+
else break;
|
|
647
|
+
}
|
|
648
|
+
const maxScroll = Math.max(1, document.documentElement.scrollHeight - window.innerHeight);
|
|
649
|
+
const state = {
|
|
650
|
+
anchor: selected ? selected.dataset.previewAnchor || "" : "",
|
|
651
|
+
offset: selected ? selected.getBoundingClientRect().top : 0,
|
|
652
|
+
ratio: window.scrollY / maxScroll,
|
|
653
|
+
};
|
|
654
|
+
try { sessionStorage.setItem(CONFIG.live.storageKey, JSON.stringify(state)); } catch {}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function restoreReadingPosition() {
|
|
658
|
+
if (!CONFIG.live || !root) return;
|
|
659
|
+
let saved = null;
|
|
660
|
+
try { saved = JSON.parse(sessionStorage.getItem(CONFIG.live.storageKey) || "null"); } catch {}
|
|
661
|
+
if (!saved || typeof saved !== "object") return;
|
|
662
|
+
const elements = assignStableAnchors();
|
|
663
|
+
const anchored = elements.find((element) => element.dataset.previewAnchor === saved.anchor);
|
|
664
|
+
if (anchored) {
|
|
665
|
+
const top = window.scrollY + anchored.getBoundingClientRect().top - Number(saved.offset || 0);
|
|
666
|
+
window.scrollTo(0, Math.max(0, top));
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
const maxScroll = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
|
|
670
|
+
window.scrollTo(0, Math.max(0, Math.min(1, Number(saved.ratio || 0))) * maxScroll);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function currentPalette() {
|
|
674
|
+
if (CONFIG.theme === "dark") return CONFIG.palettes.dark;
|
|
675
|
+
if (CONFIG.theme === "light") return CONFIG.palettes.light;
|
|
676
|
+
return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
677
|
+
? CONFIG.palettes.dark
|
|
678
|
+
: CONFIG.palettes.light;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function mermaidConfig() {
|
|
682
|
+
const palette = currentPalette();
|
|
683
|
+
return {
|
|
684
|
+
startOnLoad: false,
|
|
685
|
+
theme: "base",
|
|
686
|
+
themeVariables: {
|
|
687
|
+
background: palette.bg,
|
|
688
|
+
primaryColor: palette.panel,
|
|
689
|
+
primaryTextColor: palette.text,
|
|
690
|
+
primaryBorderColor: palette.codeBorder,
|
|
691
|
+
secondaryColor: palette.card,
|
|
692
|
+
secondaryTextColor: palette.text,
|
|
693
|
+
secondaryBorderColor: palette.codeBorder,
|
|
694
|
+
tertiaryColor: palette.card,
|
|
695
|
+
tertiaryTextColor: palette.text,
|
|
696
|
+
tertiaryBorderColor: palette.codeBorder,
|
|
697
|
+
lineColor: palette.quote,
|
|
698
|
+
textColor: palette.text,
|
|
699
|
+
edgeLabelBackground: palette.panel,
|
|
700
|
+
nodeBorder: palette.codeBorder,
|
|
701
|
+
clusterBkg: palette.card,
|
|
702
|
+
clusterBorder: palette.codeBorder,
|
|
703
|
+
titleColor: palette.heading,
|
|
704
|
+
},
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
${buildMermaidClientSource()}
|
|
709
|
+
|
|
710
|
+
function fallbackMathTargets() {
|
|
711
|
+
if (!root) return [];
|
|
712
|
+
const targets = [];
|
|
713
|
+
const seen = new Set();
|
|
714
|
+
Array.from(root.querySelectorAll(".math.display, .math.inline")).forEach((node) => {
|
|
715
|
+
const display = node.classList.contains("display");
|
|
716
|
+
let source = String(node.textContent || "").trim();
|
|
717
|
+
if (!source) return;
|
|
718
|
+
if (display && source.startsWith("$$") && source.endsWith("$$")) source = source.slice(2, -2).trim();
|
|
719
|
+
else if (display && source.startsWith("\\\\[") && source.endsWith("\\\\]")) source = source.slice(2, -2).trim();
|
|
720
|
+
else if (!display && source.startsWith("\\\\(") && source.endsWith("\\\\)")) source = source.slice(2, -2).trim();
|
|
721
|
+
else if (!display && source.startsWith("$") && source.endsWith("$")) source = source.slice(1, -1).trim();
|
|
722
|
+
let renderTarget = node;
|
|
723
|
+
if (display && node.parentElement && node.parentElement.tagName === "P"
|
|
724
|
+
&& String(node.parentElement.textContent || "").trim() === String(node.textContent || "").trim()) {
|
|
725
|
+
renderTarget = node.parentElement;
|
|
726
|
+
}
|
|
727
|
+
if (!source || seen.has(renderTarget)) return;
|
|
728
|
+
seen.add(renderTarget);
|
|
729
|
+
targets.push({ renderTarget, display, source });
|
|
730
|
+
});
|
|
731
|
+
return targets;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
let mathJaxPromise = null;
|
|
735
|
+
function ensureMathJax() {
|
|
736
|
+
if (window.MathJax && typeof window.MathJax.typesetPromise === "function") return Promise.resolve(window.MathJax);
|
|
737
|
+
if (mathJaxPromise) return mathJaxPromise;
|
|
738
|
+
mathJaxPromise = new Promise((resolvePromise, rejectPromise) => {
|
|
739
|
+
window.MathJax = {
|
|
740
|
+
loader: { load: ["[tex]/ams", "[tex]/noerrors", "[tex]/noundefined"] },
|
|
741
|
+
tex: {
|
|
742
|
+
inlineMath: [["\\\\(", "\\\\)"], ["$", "$"]],
|
|
743
|
+
displayMath: [["\\\\[", "\\\\]"], ["$$", "$$"]],
|
|
744
|
+
packages: { "[+]": ["ams", "noerrors", "noundefined"] },
|
|
745
|
+
},
|
|
746
|
+
options: { skipHtmlTags: ["script", "noscript", "style", "textarea", "pre", "code"] },
|
|
747
|
+
startup: { typeset: false },
|
|
748
|
+
};
|
|
749
|
+
const script = document.createElement("script");
|
|
750
|
+
script.src = MATHJAX_CDN_URL;
|
|
751
|
+
script.async = true;
|
|
752
|
+
script.onload = () => {
|
|
753
|
+
const api = window.MathJax;
|
|
754
|
+
if (api && api.startup && api.startup.promise) api.startup.promise.then(() => resolvePromise(api)).catch(rejectPromise);
|
|
755
|
+
else if (api && typeof api.typesetPromise === "function") resolvePromise(api);
|
|
756
|
+
else rejectPromise(new Error("MathJax did not initialize."));
|
|
757
|
+
};
|
|
758
|
+
script.onerror = () => rejectPromise(new Error("Failed to load MathJax."));
|
|
759
|
+
document.head.appendChild(script);
|
|
760
|
+
}).catch((error) => {
|
|
761
|
+
mathJaxPromise = null;
|
|
762
|
+
throw error;
|
|
763
|
+
});
|
|
764
|
+
return mathJaxPromise;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
async function renderMathFallback() {
|
|
768
|
+
const targets = fallbackMathTargets();
|
|
769
|
+
if (targets.length === 0) return;
|
|
770
|
+
try {
|
|
771
|
+
const mathJax = await ensureMathJax();
|
|
772
|
+
targets.forEach((entry) => {
|
|
773
|
+
entry.renderTarget.textContent = entry.display
|
|
774
|
+
? "\\\\[\\n" + entry.source + "\\n\\\\]"
|
|
775
|
+
: "\\\\(" + entry.source + "\\\\)";
|
|
776
|
+
});
|
|
777
|
+
await mathJax.typesetPromise(targets.map((entry) => entry.renderTarget));
|
|
778
|
+
} catch (error) {
|
|
779
|
+
console.error("MathJax fallback failed:", error);
|
|
780
|
+
appendWarning("preview-math-warning", "MathJax fallback is unavailable. Unsupported equations may remain as TeX.");
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function connectLiveReload() {
|
|
785
|
+
if (!CONFIG.live || typeof EventSource !== "function") return;
|
|
786
|
+
if (activeRenderError) showStatus("Render failed; watching for a correction:\\n" + activeRenderError, "error");
|
|
787
|
+
const events = new EventSource(CONFIG.live.eventsPath);
|
|
788
|
+
events.addEventListener("preview", (event) => {
|
|
789
|
+
let update;
|
|
790
|
+
try { update = JSON.parse(event.data); } catch { return; }
|
|
791
|
+
if (update.status === "error") {
|
|
792
|
+
activeRenderError = String(update.error || "Unknown render error");
|
|
793
|
+
const prefix = Number(update.successfulRevision || 0) > 0
|
|
794
|
+
? "Render failed; the last successful preview is still shown:\\n"
|
|
795
|
+
: "Initial render failed; watching for a correction:\\n";
|
|
796
|
+
showStatus(prefix + activeRenderError, "error");
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
activeRenderError = "";
|
|
800
|
+
if (Number(update.revision || 0) > Number(CONFIG.live.revision || 0)) {
|
|
801
|
+
captureReadingPosition();
|
|
802
|
+
window.location.reload();
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
showStatus("", "");
|
|
806
|
+
});
|
|
807
|
+
events.addEventListener("open", () => {
|
|
808
|
+
if (!activeRenderError) showStatus("", "");
|
|
809
|
+
});
|
|
810
|
+
events.addEventListener("error", () => {
|
|
811
|
+
if (!activeRenderError) showStatus("Live reload disconnected; reconnecting…", "warning");
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
async function initialize() {
|
|
816
|
+
assignStableAnchors();
|
|
817
|
+
restoreReadingPosition();
|
|
818
|
+
connectLiveReload();
|
|
819
|
+
await Promise.all([renderMermaid(), renderMathFallback()]);
|
|
820
|
+
if (document.fonts && document.fonts.ready) {
|
|
821
|
+
try { await document.fonts.ready; } catch {}
|
|
822
|
+
}
|
|
823
|
+
requestAnimationFrame(() => requestAnimationFrame(restoreReadingPosition));
|
|
824
|
+
window.__pandocGlanceReady = true;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (CONFIG.theme === "auto" && window.matchMedia) {
|
|
828
|
+
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
|
829
|
+
media.addEventListener("change", () => {
|
|
830
|
+
if (root && root.querySelector(".mermaid-container")) {
|
|
831
|
+
captureReadingPosition();
|
|
832
|
+
window.location.reload();
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", () => { void initialize(); }, { once: true });
|
|
838
|
+
else void initialize();
|
|
839
|
+
})();
|
|
840
|
+
`;
|
|
841
|
+
}
|
|
842
|
+
export function buildPreviewHtml(options) {
|
|
843
|
+
const baseTag = options.resourceRoot
|
|
844
|
+
? `<base href="${pathToFileURL(resolve(options.resourceRoot) + sep).href}" />\n`
|
|
845
|
+
: "";
|
|
846
|
+
const clientScript = buildClientScript(options.theme, options.liveReload).replace(/<\/script/gi, "<\\/script");
|
|
847
|
+
const css = buildPreviewCss(options.theme, options.fontSizePx);
|
|
848
|
+
return `<!doctype html>
|
|
849
|
+
<html lang="en">
|
|
850
|
+
<head>
|
|
851
|
+
<meta charset="utf-8" />
|
|
852
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
853
|
+
${baseTag}<title>${escapeHtml(options.title)}</title>
|
|
854
|
+
<style>${css}</style>
|
|
855
|
+
</head>
|
|
856
|
+
<body>
|
|
857
|
+
<article id="preview-root">${options.fragmentHtml}</article>
|
|
858
|
+
<div id="preview-status" role="status" aria-live="polite" hidden></div>
|
|
859
|
+
<script type="module">${clientScript}</script>
|
|
860
|
+
</body>
|
|
861
|
+
</html>`;
|
|
862
|
+
}
|
|
863
|
+
export function buildInitialErrorHtml(options) {
|
|
864
|
+
const fragmentHtml = `<section class="initial-render-error"><h1>Preview render failed</h1><p>Fix the file and save it; this page will recover automatically.</p><pre>${escapeHtml(options.error)}</pre></section>`;
|
|
865
|
+
return buildPreviewHtml({
|
|
866
|
+
fragmentHtml,
|
|
867
|
+
title: options.title,
|
|
868
|
+
theme: options.theme,
|
|
869
|
+
fontSizePx: options.fontSizePx,
|
|
870
|
+
liveReload: { ...options.liveReload, initialError: options.error },
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
export async function renderDocument(options) {
|
|
874
|
+
const rendered = await renderPandocFragment(options.source, options.format, options.resourceRoot);
|
|
875
|
+
let fragmentHtml = rendered.html;
|
|
876
|
+
let assets = new Map();
|
|
877
|
+
if (options.serverResources) {
|
|
878
|
+
const rewritten = await rewriteServerResourceUrls(fragmentHtml, options.resourceRoot, options.serverResources);
|
|
879
|
+
fragmentHtml = rewritten.html;
|
|
880
|
+
assets = rewritten.assets;
|
|
881
|
+
}
|
|
882
|
+
const htmlOptions = {
|
|
883
|
+
fragmentHtml,
|
|
884
|
+
title: options.title ?? `${basename(options.sourcePath)} — pandoc-glance`,
|
|
885
|
+
theme: options.theme,
|
|
886
|
+
fontSizePx: options.fontSizePx,
|
|
887
|
+
};
|
|
888
|
+
if (!options.serverResources)
|
|
889
|
+
htmlOptions.resourceRoot = options.resourceRoot;
|
|
890
|
+
if (options.liveReload)
|
|
891
|
+
htmlOptions.liveReload = options.liveReload;
|
|
892
|
+
return {
|
|
893
|
+
html: buildPreviewHtml(htmlOptions),
|
|
894
|
+
fragmentHtml,
|
|
895
|
+
assets,
|
|
896
|
+
pandocWarnings: rendered.warnings,
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
//# sourceMappingURL=render.js.map
|