@publisle/research 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -8
- package/dist/article.d.ts.map +1 -1
- package/dist/article.js +14 -10
- package/dist/article.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/native-install.d.ts +9 -0
- package/dist/native-install.d.ts.map +1 -0
- package/dist/native-install.js +76 -0
- package/dist/native-install.js.map +1 -0
- package/dist/native-node.d.ts +24 -0
- package/dist/native-node.d.ts.map +1 -0
- package/dist/native-node.js +153 -0
- package/dist/native-node.js.map +1 -0
- package/dist/native.d.ts +48 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +121 -0
- package/dist/native.js.map +1 -0
- package/dist/node.d.ts +6 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +102 -32
- package/dist/node.js.map +1 -1
- package/package.json +22 -6
- package/src/article.ts +13 -8
- package/src/index.ts +11 -0
- package/src/native-install.ts +96 -0
- package/src/native-node.ts +236 -0
- package/src/native.ts +184 -0
- package/src/node.ts +206 -95
package/src/node.ts
CHANGED
|
@@ -13,14 +13,67 @@ import {
|
|
|
13
13
|
writeFile,
|
|
14
14
|
} from "node:fs/promises";
|
|
15
15
|
import { homedir, tmpdir } from "node:os";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
basename,
|
|
18
|
+
dirname,
|
|
19
|
+
isAbsolute,
|
|
20
|
+
join,
|
|
21
|
+
relative,
|
|
22
|
+
resolve,
|
|
23
|
+
} from "node:path";
|
|
17
24
|
import { promisify } from "node:util";
|
|
25
|
+
import {
|
|
26
|
+
compileNativePdf,
|
|
27
|
+
type NativeCompileOptions,
|
|
28
|
+
type NativeCompileResult,
|
|
29
|
+
} from "./native-node.ts";
|
|
30
|
+
export {
|
|
31
|
+
compileNativePdf,
|
|
32
|
+
type NativeCompileOptions,
|
|
33
|
+
type NativeCompileResult,
|
|
34
|
+
};
|
|
18
35
|
import type { ResearchLoss } from "./article.ts";
|
|
19
|
-
import
|
|
36
|
+
import {
|
|
37
|
+
parseDocument,
|
|
38
|
+
type Block,
|
|
39
|
+
type BlockType,
|
|
40
|
+
type Document,
|
|
41
|
+
} from "@publisle/schema";
|
|
20
42
|
import { createLatexPackage, type LatexPackageOptions } from "./package.ts";
|
|
21
43
|
import type { LatexPackage } from "./package.ts";
|
|
22
44
|
|
|
23
45
|
const execute = promisify(execFile);
|
|
46
|
+
|
|
47
|
+
/** One kpsewhich invocation resolves every requirement at once. kpsewhich
|
|
48
|
+
* exits non-zero when a file is missing but still lists the found paths on
|
|
49
|
+
* stdout, so partial results survive; output basenames match the arguments. */
|
|
50
|
+
async function kpsewhichAll(
|
|
51
|
+
names: readonly string[],
|
|
52
|
+
): Promise<Map<string, string>> {
|
|
53
|
+
const paths = new Map<string, string>();
|
|
54
|
+
if (!names.length) return paths;
|
|
55
|
+
let stdout: string;
|
|
56
|
+
try {
|
|
57
|
+
stdout = (await execute("kpsewhich", [...names], { timeout: 10_000 }))
|
|
58
|
+
.stdout;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
stdout = (error as { stdout?: string }).stdout ?? "";
|
|
61
|
+
}
|
|
62
|
+
for (const line of stdout.split("\n")) {
|
|
63
|
+
const path = line.trim();
|
|
64
|
+
if (path) paths.set(basename(path), path);
|
|
65
|
+
}
|
|
66
|
+
return paths;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** In-process probe caches. Native TeX installations do not change while the
|
|
70
|
+
* process runs, so successful probes are reused across compilations. Failed
|
|
71
|
+
* probes are retried so an installation completed mid-process is picked up. */
|
|
72
|
+
const taggingFormatCache = new Map<string, string | undefined>();
|
|
73
|
+
const doctorCache = new Map<string, CompilerStatus>();
|
|
74
|
+
const engineVersionCache = new Map<string, string>();
|
|
75
|
+
const dependencyDigestCache = new Map<string, string>();
|
|
76
|
+
|
|
24
77
|
export interface CompileLatexOptions {
|
|
25
78
|
/** Base directory for local manuscript assets. */
|
|
26
79
|
readonly sourceDirectory: string;
|
|
@@ -118,31 +171,35 @@ export async function compileLatexPackage(
|
|
|
118
171
|
...source.resources,
|
|
119
172
|
};
|
|
120
173
|
try {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
174
|
+
await Promise.all(
|
|
175
|
+
Object.entries({
|
|
176
|
+
...source.files,
|
|
177
|
+
...source.resources,
|
|
178
|
+
}).map(async ([name, value]) => {
|
|
179
|
+
const path = packagePath(directory, name);
|
|
180
|
+
await mkdir(dirname(path), { recursive: true });
|
|
181
|
+
await writeFile(path, value, { flag: "wx" });
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
129
184
|
const base = await realpath(options.sourceDirectory);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
185
|
+
await Promise.all(
|
|
186
|
+
source.assets.map(async (asset) => {
|
|
187
|
+
const path = await realpath(resolve(base, asset.source));
|
|
188
|
+
const local = relative(base, path);
|
|
189
|
+
if (
|
|
190
|
+
isAbsolute(local) ||
|
|
191
|
+
local === ".." ||
|
|
192
|
+
local.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`)
|
|
193
|
+
)
|
|
194
|
+
throw new Error(
|
|
195
|
+
`Figure asset escapes manuscript directory: ${asset.source}`,
|
|
196
|
+
);
|
|
197
|
+
const destination = packagePath(directory, asset.destination);
|
|
198
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
199
|
+
await copyFile(path, destination, constants.COPYFILE_EXCL);
|
|
200
|
+
files[asset.destination] = new Uint8Array(await readFile(destination));
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
146
203
|
let stdout = "";
|
|
147
204
|
const containerName = `publisle-${randomUUID()}`;
|
|
148
205
|
try {
|
|
@@ -238,43 +295,65 @@ export async function compileLatexPackage(
|
|
|
238
295
|
files["build.log"] = stdout;
|
|
239
296
|
const engineVersion =
|
|
240
297
|
backend.kind === "native"
|
|
241
|
-
? (
|
|
242
|
-
|
|
243
|
-
|
|
298
|
+
? await (async () => {
|
|
299
|
+
const cached = engineVersionCache.get(source.engine);
|
|
300
|
+
if (cached) return cached;
|
|
301
|
+
const version = (
|
|
302
|
+
await execute(source.engine, ["--version"], { timeout: 5000 })
|
|
303
|
+
).stdout.split("\n")[0];
|
|
304
|
+
engineVersionCache.set(source.engine, version ?? "");
|
|
305
|
+
return version;
|
|
306
|
+
})()
|
|
244
307
|
: backend.image;
|
|
245
308
|
const fontVersions: Record<string, string> = {};
|
|
246
309
|
if (backend.kind === "native")
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
310
|
+
await Promise.all(
|
|
311
|
+
Object.entries(source.fonts).map(async ([script, font]) => {
|
|
312
|
+
fontVersions[script] = (
|
|
313
|
+
await execute(
|
|
314
|
+
"fc-match",
|
|
315
|
+
["-f", "%{family}: %{fontversion}\n", font],
|
|
316
|
+
{ timeout: 5000 },
|
|
317
|
+
)
|
|
318
|
+
).stdout.trim();
|
|
319
|
+
}),
|
|
320
|
+
);
|
|
255
321
|
const dependencyDigests: Record<string, string> = {};
|
|
256
322
|
const dependencyVersions: Record<string, string> = {};
|
|
257
|
-
if (backend.kind === "native")
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
|
|
323
|
+
if (backend.kind === "native") {
|
|
324
|
+
const resolvedPaths = await kpsewhichAll(
|
|
325
|
+
source.requirements.filter(
|
|
326
|
+
(name) =>
|
|
327
|
+
source.resources?.[name] === undefined &&
|
|
328
|
+
source.files[name] === undefined,
|
|
329
|
+
),
|
|
330
|
+
);
|
|
331
|
+
await Promise.all(
|
|
332
|
+
source.requirements.map(async (name) => {
|
|
333
|
+
const provided = source.resources?.[name] ?? source.files[name];
|
|
334
|
+
const path = resolvedPaths.get(name);
|
|
335
|
+
const bytes =
|
|
336
|
+
provided === undefined
|
|
337
|
+
? await readFile(path ?? "")
|
|
338
|
+
: typeof provided === "string"
|
|
339
|
+
? Buffer.from(provided)
|
|
340
|
+
: provided;
|
|
341
|
+
const digest = dependencyDigestCache.get(path ?? name);
|
|
342
|
+
if (digest) {
|
|
343
|
+
dependencyDigests[name] = digest;
|
|
344
|
+
} else {
|
|
345
|
+
dependencyDigests[name] =
|
|
346
|
+
"sha256:" + createHash("sha256").update(bytes).digest("hex");
|
|
347
|
+
dependencyDigestCache.set(path ?? name, dependencyDigests[name]);
|
|
348
|
+
}
|
|
349
|
+
const version =
|
|
350
|
+
/\\Provides(?:Package|Class)\{[^}]+\}\s*\[([^\]]+)\]/u.exec(
|
|
351
|
+
new TextDecoder().decode(bytes),
|
|
352
|
+
)?.[1];
|
|
353
|
+
if (version) dependencyVersions[name] = version;
|
|
354
|
+
}),
|
|
355
|
+
);
|
|
356
|
+
}
|
|
278
357
|
files["manifest.json"] =
|
|
279
358
|
JSON.stringify(
|
|
280
359
|
{
|
|
@@ -333,6 +412,8 @@ export interface CompilerStatus {
|
|
|
333
412
|
readonly latexFormat?: string;
|
|
334
413
|
}
|
|
335
414
|
async function taggingFormat(image?: string): Promise<string | undefined> {
|
|
415
|
+
const key = image ?? "native";
|
|
416
|
+
if (taggingFormatCache.has(key)) return taggingFormatCache.get(key);
|
|
336
417
|
const directory = await mkdtemp(join(tmpdir(), "publisle-format-"));
|
|
337
418
|
const containerName = `publisle-format-${randomUUID()}`;
|
|
338
419
|
try {
|
|
@@ -379,11 +460,16 @@ async function taggingFormat(image?: string): Promise<string | undefined> {
|
|
|
379
460
|
openin_any: "p",
|
|
380
461
|
openout_any: "p",
|
|
381
462
|
},
|
|
382
|
-
|
|
463
|
+
// Cold container startup can initialize LuaLaTeX caches.
|
|
464
|
+
timeout: image ? 60_000 : 15_000,
|
|
383
465
|
maxBuffer: 1024 * 1024,
|
|
384
466
|
},
|
|
385
467
|
);
|
|
386
|
-
|
|
468
|
+
const detected = /PUBLISLE-FORMAT:(\d{4}-\d{2}-\d{2})/u.exec(
|
|
469
|
+
output.stdout,
|
|
470
|
+
)?.[1];
|
|
471
|
+
taggingFormatCache.set(key, detected);
|
|
472
|
+
return detected;
|
|
387
473
|
} finally {
|
|
388
474
|
if (image)
|
|
389
475
|
await execute("docker", ["rm", "--force", containerName], {
|
|
@@ -395,6 +481,18 @@ async function taggingFormat(image?: string): Promise<string | undefined> {
|
|
|
395
481
|
export async function doctorCompiler(
|
|
396
482
|
source: LatexPackage,
|
|
397
483
|
): Promise<CompilerStatus> {
|
|
484
|
+
const cacheKey = [
|
|
485
|
+
source.engine,
|
|
486
|
+
source.pdfStandard ?? "",
|
|
487
|
+
Object.values(source.fonts).join(","),
|
|
488
|
+
...source.requirements.map((name) =>
|
|
489
|
+
source.files[name] === undefined && source.resources?.[name] === undefined
|
|
490
|
+
? name
|
|
491
|
+
: `${name}:bundled`,
|
|
492
|
+
),
|
|
493
|
+
].join("|");
|
|
494
|
+
const cached = doctorCache.get(cacheKey);
|
|
495
|
+
if (cached) return cached;
|
|
398
496
|
const missing: string[] = [];
|
|
399
497
|
let latexFormat: string | undefined;
|
|
400
498
|
if (source.engine === "lualatex") {
|
|
@@ -404,44 +502,45 @@ export async function doctorCompiler(
|
|
|
404
502
|
else if (source.pdfStandard && latexFormat < "2025-11-01")
|
|
405
503
|
missing.push("LaTeX format >= 2025-11-01 (PDF/UA tagging)");
|
|
406
504
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
)
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
if (!matched.toLowerCase().includes(font.toLowerCase()))
|
|
505
|
+
await Promise.all(
|
|
506
|
+
["latexmk", source.engine, "bibtex", "kpsewhich"].map(async (command) => {
|
|
507
|
+
try {
|
|
508
|
+
await execute(command, ["--version"], { timeout: 5000 });
|
|
509
|
+
} catch {
|
|
510
|
+
missing.push(command);
|
|
511
|
+
}
|
|
512
|
+
}),
|
|
513
|
+
);
|
|
514
|
+
const external = source.requirements.filter(
|
|
515
|
+
(dependency) =>
|
|
516
|
+
source.files[dependency] === undefined &&
|
|
517
|
+
source.resources?.[dependency] === undefined,
|
|
518
|
+
);
|
|
519
|
+
const resolved = await kpsewhichAll(external);
|
|
520
|
+
for (const dependency of external)
|
|
521
|
+
if (!resolved.has(dependency)) missing.push(dependency);
|
|
522
|
+
await Promise.all(
|
|
523
|
+
Object.values(source.fonts).map(async (font) => {
|
|
524
|
+
try {
|
|
525
|
+
const matched = (
|
|
526
|
+
await execute("fc-match", ["-f", "%{family}", font], {
|
|
527
|
+
timeout: 5000,
|
|
528
|
+
})
|
|
529
|
+
).stdout;
|
|
530
|
+
if (!matched.toLowerCase().includes(font.toLowerCase()))
|
|
531
|
+
missing.push(`font:${font}`);
|
|
532
|
+
} catch {
|
|
436
533
|
missing.push(`font:${font}`);
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
534
|
+
}
|
|
535
|
+
}),
|
|
536
|
+
);
|
|
537
|
+
const status: CompilerStatus = {
|
|
441
538
|
available: missing.length === 0,
|
|
442
539
|
missing,
|
|
443
540
|
...(latexFormat ? { latexFormat } : {}),
|
|
444
541
|
};
|
|
542
|
+
if (status.available) doctorCache.set(cacheKey, status);
|
|
543
|
+
return status;
|
|
445
544
|
}
|
|
446
545
|
async function selectCompiler(
|
|
447
546
|
source: LatexPackage,
|
|
@@ -589,10 +688,22 @@ export async function setupCompiler(
|
|
|
589
688
|
return details.Id;
|
|
590
689
|
}
|
|
591
690
|
/** Unicode-safe default PDF API. Never falls back to the legacy text exporter. */
|
|
592
|
-
export
|
|
691
|
+
export function exportPdf(
|
|
692
|
+
document: Document<Block<BlockType, unknown>>,
|
|
693
|
+
options: NativeCompileOptions & { readonly engine: "rust" },
|
|
694
|
+
): Promise<NativeCompileResult>;
|
|
695
|
+
export function exportPdf(
|
|
593
696
|
document: Document<Block<BlockType, unknown>>,
|
|
594
697
|
options: LatexPackageOptions & CompileLatexOptions,
|
|
595
|
-
): Promise<CompiledLatexPackage
|
|
698
|
+
): Promise<CompiledLatexPackage>;
|
|
699
|
+
export async function exportPdf(
|
|
700
|
+
document: Document<Block<BlockType, unknown>>,
|
|
701
|
+
options:
|
|
702
|
+
| (LatexPackageOptions & CompileLatexOptions)
|
|
703
|
+
| (NativeCompileOptions & { readonly engine: "rust" }),
|
|
704
|
+
): Promise<CompiledLatexPackage | NativeCompileResult> {
|
|
705
|
+
if (options.engine === "rust")
|
|
706
|
+
return compileNativePdf(parseDocument(document), options);
|
|
596
707
|
return compileLatexPackage(createLatexPackage(document, options), options);
|
|
597
708
|
}
|
|
598
709
|
|