pageproof 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 +90 -0
- package/THIRD_PARTY_NOTICES.md +74 -0
- package/assets/SKILL.md +58 -0
- package/assets/_paged.css +82 -0
- package/assets/chicago.csl +6006 -0
- package/assets/default.html +496 -0
- package/assets/doublespaced.css +8 -0
- package/assets/european-journal-of-international-law.csl +404 -0
- package/assets/favicon.svg +5 -0
- package/assets/footnotes-inline.lua +56 -0
- package/assets/latex.css +142 -0
- package/assets/msword.css +100 -0
- package/assets/numbered.css +75 -0
- package/assets/vendor/mathjax/LICENSE +202 -0
- package/assets/vendor/mathjax/sre/mathmaps/af.json +146 -0
- package/assets/vendor/mathjax/sre/mathmaps/base.json +140 -0
- package/assets/vendor/mathjax/sre/mathmaps/ca.json +140 -0
- package/assets/vendor/mathjax/sre/mathmaps/da.json +140 -0
- package/assets/vendor/mathjax/sre/mathmaps/de.json +146 -0
- package/assets/vendor/mathjax/sre/mathmaps/en.json +158 -0
- package/assets/vendor/mathjax/sre/mathmaps/es.json +140 -0
- package/assets/vendor/mathjax/sre/mathmaps/euro.json +32 -0
- package/assets/vendor/mathjax/sre/mathmaps/fr.json +146 -0
- package/assets/vendor/mathjax/sre/mathmaps/hi.json +146 -0
- package/assets/vendor/mathjax/sre/mathmaps/it.json +146 -0
- package/assets/vendor/mathjax/sre/mathmaps/ko.json +146 -0
- package/assets/vendor/mathjax/sre/mathmaps/nb.json +146 -0
- package/assets/vendor/mathjax/sre/mathmaps/nemeth.json +125 -0
- package/assets/vendor/mathjax/sre/mathmaps/nn.json +146 -0
- package/assets/vendor/mathjax/sre/mathmaps/sv.json +146 -0
- package/assets/vendor/mathjax/sre/speech-worker.js +1 -0
- package/assets/vendor/mathjax/tex-mml-chtml-nofont.js +18 -0
- package/assets/vendor/mathjax-fonts/LICENSE +202 -0
- package/assets/vendor/mathjax-fonts/mathjax-tex-font/chtml/woff2/mjx-tex-b.woff2 +0 -0
- package/assets/vendor/mathjax-fonts/mathjax-tex-font/chtml/woff2/mjx-tex-bi.woff2 +0 -0
- package/assets/vendor/mathjax-fonts/mathjax-tex-font/chtml/woff2/mjx-tex-brk.woff2 +0 -0
- package/assets/vendor/mathjax-fonts/mathjax-tex-font/chtml/woff2/mjx-tex-c.woff2 +0 -0
- package/assets/vendor/mathjax-fonts/mathjax-tex-font/chtml/woff2/mjx-tex-cb.woff2 +0 -0
- package/assets/vendor/mathjax-fonts/mathjax-tex-font/chtml/woff2/mjx-tex-f.woff2 +0 -0
- package/assets/vendor/mathjax-fonts/mathjax-tex-font/chtml/woff2/mjx-tex-fb.woff2 +0 -0
- package/assets/vendor/mathjax-fonts/mathjax-tex-font/chtml/woff2/mjx-tex-i.woff2 +0 -0
- package/assets/vendor/mathjax-fonts/mathjax-tex-font/chtml/woff2/mjx-tex-lo.woff2 +0 -0
- package/assets/vendor/mathjax-fonts/mathjax-tex-font/chtml/woff2/mjx-tex-m.woff2 +0 -0
- package/assets/vendor/mathjax-fonts/mathjax-tex-font/chtml/woff2/mjx-tex-mi.woff2 +0 -0
- package/assets/vendor/mathjax-fonts/mathjax-tex-font/chtml/woff2/mjx-tex-n.woff2 +0 -0
- package/assets/vendor/mathjax-fonts/mathjax-tex-font/chtml/woff2/mjx-tex-ob.woff2 +0 -0
- package/assets/vendor/mathjax-fonts/mathjax-tex-font/chtml/woff2/mjx-tex-os.woff2 +0 -0
- package/assets/vendor/mathjax-fonts/mathjax-tex-font/chtml/woff2/mjx-tex-s3.woff2 +0 -0
- package/assets/vendor/mathjax-fonts/mathjax-tex-font/chtml/woff2/mjx-tex-s4.woff2 +0 -0
- package/assets/vendor/mathjax-fonts/mathjax-tex-font/chtml/woff2/mjx-tex-so.woff2 +0 -0
- package/assets/vendor/mathjax-fonts/mathjax-tex-font/chtml/woff2/mjx-tex-ss.woff2 +0 -0
- package/assets/vendor/mathjax-fonts/mathjax-tex-font/chtml/woff2/mjx-tex-ssb.woff2 +0 -0
- package/assets/vendor/mathjax-fonts/mathjax-tex-font/chtml/woff2/mjx-tex-ssi.woff2 +0 -0
- package/assets/vendor/mathjax-fonts/mathjax-tex-font/chtml/woff2/mjx-tex-v.woff2 +0 -0
- package/assets/vendor/mathjax-fonts/mathjax-tex-font/chtml/woff2/mjx-tex-zero.woff2 +0 -0
- package/assets/vendor/mathjax-fonts/mathjax-tex-font/chtml.js +1 -0
- package/assets/vendor/mathjax-fonts/mathjax-tex-font/package.json +88 -0
- package/assets/vendor/paged.polyfill.js +33251 -0
- package/bin/mdpreview.js +8 -0
- package/bin/pageproof.js +8 -0
- package/package.json +42 -0
- package/src/assets.js +246 -0
- package/src/cli.js +166 -0
- package/src/lifecycle.js +445 -0
- package/src/pandoc.js +346 -0
- package/src/server.js +228 -0
- package/src/util.js +43 -0
package/src/lifecycle.js
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import chokidar from 'chokidar';
|
|
6
|
+
import { materializeAssets, resolveAssets, watchedAssetPaths } from './assets.js';
|
|
7
|
+
import { buildWithPandoc, writeErrorPage } from './pandoc.js';
|
|
8
|
+
import { commandExists, delay, isWsl, writeJson } from './util.js';
|
|
9
|
+
import { createPreviewServer } from './server.js';
|
|
10
|
+
|
|
11
|
+
const mediaExtensions = new Set([
|
|
12
|
+
'.svg',
|
|
13
|
+
'.png',
|
|
14
|
+
'.jpg',
|
|
15
|
+
'.jpeg',
|
|
16
|
+
'.gif',
|
|
17
|
+
'.webp',
|
|
18
|
+
'.pdf',
|
|
19
|
+
'.woff',
|
|
20
|
+
'.woff2',
|
|
21
|
+
'.ttf',
|
|
22
|
+
'.otf'
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
export async function runSession(options) {
|
|
26
|
+
const sourceFile = path.resolve(options.file);
|
|
27
|
+
if (!commandExists('pandoc')) throw new Error('Missing required program: pandoc');
|
|
28
|
+
if (isWsl() && !commandExists('cmd.exe')) throw new Error('Missing required program: cmd.exe');
|
|
29
|
+
|
|
30
|
+
const sourceDir = path.dirname(sourceFile);
|
|
31
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mdpreview-'));
|
|
32
|
+
const debug = Boolean(options.debug);
|
|
33
|
+
const logFile = options.logFile ?? (debug ? path.join(tempDir, 'session.log') : undefined);
|
|
34
|
+
const stdout = options.stdout ?? process.stdout;
|
|
35
|
+
const stderr = options.stderr ?? process.stderr;
|
|
36
|
+
const state = createSessionState({ sourceFile, logFile });
|
|
37
|
+
const reporter = createReporter({ sourceFile, debug, logFile, stdout });
|
|
38
|
+
const handleBrowserLog = createBrowserLogHandler({
|
|
39
|
+
tempDir,
|
|
40
|
+
logFile,
|
|
41
|
+
debugBrowser: debug,
|
|
42
|
+
onEntry: (entry) => {
|
|
43
|
+
if (entry.buildId === state.buildId) state.browserReport = entry;
|
|
44
|
+
if (!state.firstBrowserWarningReported && (entry.ok === false || entry.errors?.length)) {
|
|
45
|
+
state.firstBrowserWarningReported = true;
|
|
46
|
+
stdout.write(`Warning: ${entry.errors?.length || 1} browser errors while rendering\n`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
let shuttingDown = false;
|
|
51
|
+
let started = false;
|
|
52
|
+
let watcher;
|
|
53
|
+
let serverHandle;
|
|
54
|
+
let noClientTimer;
|
|
55
|
+
let closeGraceTimer;
|
|
56
|
+
let cssFiles = [];
|
|
57
|
+
|
|
58
|
+
const cleanup = async (exitCode = 0, cleanupOptions = {}) => {
|
|
59
|
+
if (shuttingDown) return;
|
|
60
|
+
shuttingDown = true;
|
|
61
|
+
clearTimeout(noClientTimer);
|
|
62
|
+
clearTimeout(closeGraceTimer);
|
|
63
|
+
await serverHandle?.close().catch(() => {});
|
|
64
|
+
await watcher?.close().catch(() => {});
|
|
65
|
+
if (started) {
|
|
66
|
+
const reason = cleanupOptions.reason ?? (exitCode === 0 ? 'browser closed' : 'error');
|
|
67
|
+
if (reason === 'browser closed' || reason === 'interrupted') stdout.write(`Stopped: ${reason}\n`);
|
|
68
|
+
await writeServerLog(logFile, { type: 'shutdown', reason });
|
|
69
|
+
}
|
|
70
|
+
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
71
|
+
if (!options.noExit && !cleanupOptions.noExit && cleanupOptions.exit !== false) process.exit(exitCode);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const build = createBuildScheduler(async () => {
|
|
75
|
+
const buildId = ++state.buildId;
|
|
76
|
+
const startedAt = new Date();
|
|
77
|
+
state.lastBuildStartedAt = startedAt.toISOString();
|
|
78
|
+
state.pandocSpawnError = undefined;
|
|
79
|
+
state.browserReport = undefined;
|
|
80
|
+
await writeServerLog(logFile, {
|
|
81
|
+
type: 'build-started',
|
|
82
|
+
buildId,
|
|
83
|
+
sourceFile,
|
|
84
|
+
startedAt: state.lastBuildStartedAt
|
|
85
|
+
});
|
|
86
|
+
let result;
|
|
87
|
+
try {
|
|
88
|
+
result = await buildWithPandoc({
|
|
89
|
+
sourceFile,
|
|
90
|
+
tempDir,
|
|
91
|
+
cssFiles,
|
|
92
|
+
log: null,
|
|
93
|
+
debugBrowser: debug,
|
|
94
|
+
buildId
|
|
95
|
+
});
|
|
96
|
+
} catch (error) {
|
|
97
|
+
state.pandocSpawnError = error?.message || String(error);
|
|
98
|
+
result = {
|
|
99
|
+
ok: false,
|
|
100
|
+
diagnostics: '',
|
|
101
|
+
stderr: ''
|
|
102
|
+
};
|
|
103
|
+
await writePandocFailurePage({ tempDir, sourceFile, cssFiles, debug, buildId, error: state.pandocSpawnError });
|
|
104
|
+
}
|
|
105
|
+
const finishedAt = new Date();
|
|
106
|
+
state.diagnostics = result.diagnostics;
|
|
107
|
+
state.stderr = result.stderr;
|
|
108
|
+
state.lastBuildOk = result.ok;
|
|
109
|
+
await writeServerLog(logFile, {
|
|
110
|
+
type: 'build-finished',
|
|
111
|
+
buildId,
|
|
112
|
+
ok: result.ok,
|
|
113
|
+
sourceFile,
|
|
114
|
+
startedAt: startedAt.toISOString(),
|
|
115
|
+
finishedAt: finishedAt.toISOString(),
|
|
116
|
+
durationMs: finishedAt.getTime() - startedAt.getTime(),
|
|
117
|
+
diagnostics: result.diagnostics,
|
|
118
|
+
stderr: result.stderr || undefined,
|
|
119
|
+
error: state.pandocSpawnError
|
|
120
|
+
});
|
|
121
|
+
if (serverHandle) serverHandle.broadcastReload();
|
|
122
|
+
return result;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const resolvedAssets = await resolveAssets(options.selections ?? {});
|
|
127
|
+
cssFiles = (await materializeAssets(resolvedAssets, tempDir)).cssFiles;
|
|
128
|
+
await build.runNow();
|
|
129
|
+
if (state.pandocSpawnError) throw new Error(state.pandocSpawnError);
|
|
130
|
+
if (state.lastBuildOk === false) throw new Error(initialBuildFailureMessage(state));
|
|
131
|
+
|
|
132
|
+
let url;
|
|
133
|
+
serverHandle = createPreviewServer({
|
|
134
|
+
tempDir,
|
|
135
|
+
sourceDir,
|
|
136
|
+
getDiagnostics: () => state.diagnostics,
|
|
137
|
+
getStatus: () => sessionStatus(state, serverHandle?.clients.size ?? 0),
|
|
138
|
+
onBrowserLog: handleBrowserLog,
|
|
139
|
+
onFirstClient: async () => {
|
|
140
|
+
clearTimeout(noClientTimer);
|
|
141
|
+
if (options.readyFile) await writeJson(options.readyFile, {
|
|
142
|
+
url,
|
|
143
|
+
tempDir,
|
|
144
|
+
logFile,
|
|
145
|
+
pid: process.pid
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
onClientConnected: () => clearTimeout(closeGraceTimer),
|
|
149
|
+
onAllClientsGone: () => {
|
|
150
|
+
clearTimeout(closeGraceTimer);
|
|
151
|
+
closeGraceTimer = setTimeout(() => cleanup(0, { reason: 'browser closed' }), 3000);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
const address = await serverHandle.listen();
|
|
155
|
+
url = `http://localhost:${address.port}/`;
|
|
156
|
+
started = true;
|
|
157
|
+
reporter.start(state, url);
|
|
158
|
+
|
|
159
|
+
watcher = chokidar.watch(watchTargets(sourceFile, resolvedAssets), {
|
|
160
|
+
ignoreInitial: true,
|
|
161
|
+
awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 25 }
|
|
162
|
+
});
|
|
163
|
+
watcher.on('all', (_eventName, changedPath) => {
|
|
164
|
+
if (mediaExtensions.has(path.extname(changedPath).toLowerCase())) {
|
|
165
|
+
serverHandle.broadcastReload();
|
|
166
|
+
} else {
|
|
167
|
+
const assetChanged = watchedAssetPaths(resolvedAssets).includes(changedPath);
|
|
168
|
+
if (assetChanged) {
|
|
169
|
+
materializeAssets(resolvedAssets, tempDir)
|
|
170
|
+
.then((materialized) => {
|
|
171
|
+
cssFiles = materialized.cssFiles;
|
|
172
|
+
build.schedule();
|
|
173
|
+
})
|
|
174
|
+
.catch((error) => reportOperationalError(error, { state, logFile, stderr }));
|
|
175
|
+
} else {
|
|
176
|
+
build.schedule();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await launchBrowser(url, options);
|
|
182
|
+
|
|
183
|
+
noClientTimer = setTimeout(() => {
|
|
184
|
+
const seconds = Math.round((options.attachTimeoutMs ?? 30000) / 1000);
|
|
185
|
+
stderr.write(`Error: browser failed to connect within ${seconds} seconds\n`);
|
|
186
|
+
cleanup(1, { reason: 'attach timeout' });
|
|
187
|
+
}, options.attachTimeoutMs ?? 30000);
|
|
188
|
+
|
|
189
|
+
process.once('SIGINT', () => cleanup(0, { reason: 'interrupted' }));
|
|
190
|
+
process.once('SIGTERM', () => cleanup(0, { reason: 'interrupted' }));
|
|
191
|
+
|
|
192
|
+
if (options.noExit) return { url, tempDir, cleanup, diagnostics: () => state.diagnostics, status: () => sessionStatus(state, serverHandle?.clients.size ?? 0) };
|
|
193
|
+
return await new Promise(() => {});
|
|
194
|
+
} catch (error) {
|
|
195
|
+
await writeServerLog(logFile, {
|
|
196
|
+
type: 'startup-error',
|
|
197
|
+
sourceFile,
|
|
198
|
+
error: error?.message || String(error)
|
|
199
|
+
});
|
|
200
|
+
await cleanup(1, { exit: false });
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function initialBuildFailureMessage(state) {
|
|
206
|
+
const stderr = String(state.stderr || '').trim();
|
|
207
|
+
const detail = stderr || String(state.diagnostics || '').trim() || 'Pandoc failed to produce a preview.';
|
|
208
|
+
return `Error compiling ${path.basename(state.sourceFile)}:\n${detail}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function createSessionState({ sourceFile, logFile }) {
|
|
212
|
+
return {
|
|
213
|
+
pid: process.pid,
|
|
214
|
+
sourceFile,
|
|
215
|
+
logFile,
|
|
216
|
+
buildId: 0,
|
|
217
|
+
diagnostics: '',
|
|
218
|
+
stderr: '',
|
|
219
|
+
pandocSpawnError: undefined,
|
|
220
|
+
lastBuildStartedAt: undefined,
|
|
221
|
+
lastBuildOk: undefined,
|
|
222
|
+
browserReport: undefined,
|
|
223
|
+
firstBrowserWarningReported: false
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function sessionStatus(state, clientCount) {
|
|
228
|
+
const browserErrors = state.browserReport
|
|
229
|
+
? [
|
|
230
|
+
...(state.browserReport.error ? [{ message: state.browserReport.error }] : []),
|
|
231
|
+
...(state.browserReport.errors ?? [])
|
|
232
|
+
]
|
|
233
|
+
: undefined;
|
|
234
|
+
const status = {
|
|
235
|
+
pid: state.pid,
|
|
236
|
+
browserConnected: clientCount >= 1,
|
|
237
|
+
timestamp: new Date().toISOString(),
|
|
238
|
+
pandocSpawnError: state.pandocSpawnError,
|
|
239
|
+
pandocDiagnostics: String(state.diagnostics || '').trim() || undefined,
|
|
240
|
+
lastBuildStartedAt: state.lastBuildStartedAt,
|
|
241
|
+
browserErrors
|
|
242
|
+
};
|
|
243
|
+
return Object.fromEntries(Object.entries(status).filter(([, value]) => value !== undefined));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function createReporter({ sourceFile, debug, logFile, stdout }) {
|
|
247
|
+
let started = false;
|
|
248
|
+
return {
|
|
249
|
+
start(state, url) {
|
|
250
|
+
if (started) return;
|
|
251
|
+
started = true;
|
|
252
|
+
if (debug) stdout.write(`Writing log to ${logFile}\n`);
|
|
253
|
+
if (state.stderr) stdout.write(state.stderr.endsWith('\n') ? state.stderr : `${state.stderr}\n`);
|
|
254
|
+
stdout.write(`Showing ${path.basename(sourceFile)} at ${url}\n`);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function reportOperationalError(error, { state, logFile, stderr }) {
|
|
260
|
+
const message = error?.message || String(error);
|
|
261
|
+
state.pandocSpawnError = message;
|
|
262
|
+
stderr.write(`${message}\n`);
|
|
263
|
+
await writeServerLog(logFile, { type: 'operational-error', error: message });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function writeServerLog(logFile, event) {
|
|
267
|
+
if (!logFile) return;
|
|
268
|
+
const entry = {
|
|
269
|
+
timestamp: new Date().toISOString(),
|
|
270
|
+
...event
|
|
271
|
+
};
|
|
272
|
+
await fs.appendFile(logFile, `[server] ${JSON.stringify(entry)}\n`).catch((error) => {
|
|
273
|
+
process.stderr.write(`Failed to write server log: ${error?.message || String(error)}\n`);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function writePandocFailurePage({ tempDir, sourceFile, cssFiles, debug, buildId, error }) {
|
|
278
|
+
await writeErrorPage(
|
|
279
|
+
path.join(tempDir, 'index.html'),
|
|
280
|
+
error,
|
|
281
|
+
{
|
|
282
|
+
debugBrowser: debug,
|
|
283
|
+
cssFiles,
|
|
284
|
+
browserTitle: path.basename(sourceFile),
|
|
285
|
+
buildId
|
|
286
|
+
}
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function createBrowserLogHandler(options) {
|
|
291
|
+
const { tempDir, logFile, debugBrowser = false, onEntry } = options;
|
|
292
|
+
let sequence = 0;
|
|
293
|
+
|
|
294
|
+
return async function handleBrowserLog(payload = {}) {
|
|
295
|
+
sequence += 1;
|
|
296
|
+
const entry = await browserLogEntry({
|
|
297
|
+
payload,
|
|
298
|
+
tempDir,
|
|
299
|
+
sequence,
|
|
300
|
+
debugBrowser
|
|
301
|
+
});
|
|
302
|
+
const line = `[browser] ${JSON.stringify(entry)}\n`;
|
|
303
|
+
|
|
304
|
+
if (logFile) {
|
|
305
|
+
await fs.appendFile(logFile, line).catch((error) => {
|
|
306
|
+
console.error(`Failed to write browser log: ${error?.message || String(error)}`);
|
|
307
|
+
});
|
|
308
|
+
await onEntry?.(entry);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (debugBrowser) {
|
|
313
|
+
process.stdout.write(line);
|
|
314
|
+
await onEntry?.(entry);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
await onEntry?.(entry);
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function browserLogEntry({ payload, tempDir, sequence, debugBrowser }) {
|
|
323
|
+
const entry = {
|
|
324
|
+
timestamp: new Date().toISOString(),
|
|
325
|
+
type: stringOr(payload.type, 'browser-event'),
|
|
326
|
+
buildId: numberOr(payload.buildId),
|
|
327
|
+
phase: stringOr(payload.phase),
|
|
328
|
+
ok: typeof payload.ok === 'boolean' ? payload.ok : undefined,
|
|
329
|
+
url: stringOr(payload.url),
|
|
330
|
+
pageCount: numberOr(payload.pageCount),
|
|
331
|
+
renderedTextLength: numberOr(payload.renderedTextLength),
|
|
332
|
+
sourceTextLength: numberOr(payload.sourceTextLength),
|
|
333
|
+
renderedTextHead: debugBrowser ? stringOr(payload.renderedTextHead) : undefined,
|
|
334
|
+
renderedTextTail: stringOr(payload.renderedTextTail),
|
|
335
|
+
error: stringOr(payload.error),
|
|
336
|
+
errors: Array.isArray(payload.errors) ? payload.errors.slice(-20).map(normalizeBrowserError) : []
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
if (debugBrowser) {
|
|
340
|
+
entry.snapshots = {};
|
|
341
|
+
await writeSnapshot(entry.snapshots, tempDir, sequence, 'document', payload.documentHtml);
|
|
342
|
+
await writeSnapshot(entry.snapshots, tempDir, sequence, 'pages', payload.pagesHtml);
|
|
343
|
+
await writeSnapshot(entry.snapshots, tempDir, sequence, 'source', payload.sourceHtml);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return Object.fromEntries(Object.entries(entry).filter(([, value]) => value !== undefined));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function writeSnapshot(snapshots, tempDir, sequence, name, value) {
|
|
350
|
+
if (typeof value !== 'string' || !value) return;
|
|
351
|
+
const filePath = path.join(tempDir, `browser-${String(sequence).padStart(4, '0')}-${name}.html`);
|
|
352
|
+
await fs.writeFile(filePath, value);
|
|
353
|
+
snapshots[name] = filePath;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function normalizeBrowserError(error) {
|
|
357
|
+
return {
|
|
358
|
+
message: stringOr(error?.message, 'Unknown browser error'),
|
|
359
|
+
source: stringOr(error?.source),
|
|
360
|
+
line: numberOr(error?.line),
|
|
361
|
+
column: numberOr(error?.column)
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function stringOr(value, fallback) {
|
|
366
|
+
if (value === undefined || value === null) return fallback;
|
|
367
|
+
return String(value);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function numberOr(value) {
|
|
371
|
+
const number = Number(value);
|
|
372
|
+
return Number.isFinite(number) ? number : undefined;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function watchTargets(sourceFile, resolvedAssets) {
|
|
376
|
+
const sourceDir = path.dirname(sourceFile);
|
|
377
|
+
const references = path.join(sourceDir, 'references.bib');
|
|
378
|
+
return [sourceFile, references, sourceDir, ...watchedAssetPaths(resolvedAssets)];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function createBuildScheduler(buildFn, debounceMs = 100) {
|
|
382
|
+
let timer;
|
|
383
|
+
let running = false;
|
|
384
|
+
let pending = false;
|
|
385
|
+
|
|
386
|
+
async function run() {
|
|
387
|
+
clearTimeout(timer);
|
|
388
|
+
if (running) {
|
|
389
|
+
pending = true;
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
running = true;
|
|
393
|
+
try {
|
|
394
|
+
do {
|
|
395
|
+
pending = false;
|
|
396
|
+
await buildFn();
|
|
397
|
+
} while (pending);
|
|
398
|
+
} finally {
|
|
399
|
+
running = false;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
schedule() {
|
|
405
|
+
clearTimeout(timer);
|
|
406
|
+
timer = setTimeout(run, debounceMs);
|
|
407
|
+
},
|
|
408
|
+
async runNow() {
|
|
409
|
+
await run();
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function launchBrowser(url, options = {}) {
|
|
415
|
+
if (options.noBrowser || process.env.MDPREVIEW_NO_BROWSER === '1') return;
|
|
416
|
+
if (isWsl()) {
|
|
417
|
+
await spawnAndWait('cmd.exe', ['/c', 'start', '', url]);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
await spawnAndWait('xdg-open', [url]);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function spawnAndWait(command, args) {
|
|
424
|
+
return new Promise((resolve, reject) => {
|
|
425
|
+
const child = spawn(command, args, { stdio: 'ignore', detached: true });
|
|
426
|
+
child.on('error', reject);
|
|
427
|
+
child.on('close', (code) => {
|
|
428
|
+
if (code === 0) resolve();
|
|
429
|
+
else reject(new Error(`${command} exited with code ${code}`));
|
|
430
|
+
});
|
|
431
|
+
child.unref();
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export async function waitForReadyFile(filePath, timeoutMs = 10000) {
|
|
436
|
+
const started = Date.now();
|
|
437
|
+
while (Date.now() - started < timeoutMs) {
|
|
438
|
+
try {
|
|
439
|
+
return JSON.parse(await fs.readFile(filePath, 'utf8'));
|
|
440
|
+
} catch {
|
|
441
|
+
await delay(50);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
throw new Error(`Timed out waiting for preview session readiness: ${filePath}`);
|
|
445
|
+
}
|