@publisle/research 0.1.0 → 0.2.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 +1 -1
- 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-node.d.ts +24 -0
- package/dist/native-node.d.ts.map +1 -0
- package/dist/native-node.js +121 -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 +100 -31
- package/dist/node.js.map +1 -1
- package/package.json +15 -6
- package/src/article.ts +13 -8
- package/src/index.ts +11 -0
- package/src/native-node.ts +189 -0
- package/src/native.ts +179 -0
- package/src/node.ts +204 -94
package/src/native.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { isPlainObject, type Document } from "@publisle/schema";
|
|
2
|
+
import { citationClusters, projectArticle } from "./article.ts";
|
|
3
|
+
import { resolveDocument, type ResolvedDocument } from "./resolve.ts";
|
|
4
|
+
import type { ArticleBlock, ArticleInline } from "./article.ts";
|
|
5
|
+
|
|
6
|
+
export const NATIVE_PDF_INPUT_VERSION = 1 as const;
|
|
7
|
+
export const NATIVE_PDF_TEMPLATES = [
|
|
8
|
+
"article",
|
|
9
|
+
"article-arabic",
|
|
10
|
+
"ieee-journal",
|
|
11
|
+
"acm-journal",
|
|
12
|
+
"springer-journal",
|
|
13
|
+
"elsevier-numeric",
|
|
14
|
+
"elsevier-author-date",
|
|
15
|
+
] as const;
|
|
16
|
+
export type NativePdfTemplate = (typeof NATIVE_PDF_TEMPLATES)[number];
|
|
17
|
+
export interface NativeFont {
|
|
18
|
+
readonly path: string;
|
|
19
|
+
readonly role: "regular" | "bold" | "italic" | "mono" | "fallback";
|
|
20
|
+
readonly index?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface NativePdfInput {
|
|
23
|
+
readonly schemaVersion: typeof NATIVE_PDF_INPUT_VERSION;
|
|
24
|
+
readonly template: NativePdfTemplate;
|
|
25
|
+
readonly templateVersion: string;
|
|
26
|
+
readonly pdfStandard: "ua-2" | null;
|
|
27
|
+
readonly article: ResolvedDocument["article"] & { readonly title: string };
|
|
28
|
+
readonly citations: readonly string[];
|
|
29
|
+
readonly bibliography: ResolvedDocument["bibliography"];
|
|
30
|
+
readonly assets: readonly {
|
|
31
|
+
readonly source: string;
|
|
32
|
+
readonly path: string;
|
|
33
|
+
}[];
|
|
34
|
+
readonly fonts: readonly NativeFont[];
|
|
35
|
+
readonly templateData: { readonly country?: string };
|
|
36
|
+
}
|
|
37
|
+
export interface NativePdfOptions {
|
|
38
|
+
readonly template?: NativePdfTemplate;
|
|
39
|
+
readonly style?: string;
|
|
40
|
+
readonly pdfUa?: "ua-2" | false;
|
|
41
|
+
readonly fonts: readonly NativeFont[];
|
|
42
|
+
readonly assets?: NativePdfInput["assets"];
|
|
43
|
+
readonly data?: { readonly country?: string };
|
|
44
|
+
}
|
|
45
|
+
export interface NativePdfPackage {
|
|
46
|
+
readonly input: NativePdfInput;
|
|
47
|
+
/** Maps bounded compiler block indices back to authored source IDs. */
|
|
48
|
+
readonly blockIds: readonly string[];
|
|
49
|
+
readonly diagnostics: ResolvedDocument["losses"];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Pure lowering shared by the website worker and Research CLI. No filesystem or network access. */
|
|
53
|
+
export function createNativePdfPackage(
|
|
54
|
+
document: Document,
|
|
55
|
+
options: NativePdfOptions,
|
|
56
|
+
): NativePdfPackage {
|
|
57
|
+
const template = options.template ?? "article";
|
|
58
|
+
if (!NATIVE_PDF_TEMPLATES.includes(template))
|
|
59
|
+
throw new Error(`Unsupported native PDF template: ${template}`);
|
|
60
|
+
const resolved = resolveDocument(
|
|
61
|
+
document,
|
|
62
|
+
options.style ??
|
|
63
|
+
(template === "elsevier-author-date" ? "author-date" : "numeric"),
|
|
64
|
+
);
|
|
65
|
+
if (resolved.unresolved.length)
|
|
66
|
+
throw new Error(`Unresolved citations: ${resolved.unresolved.join(", ")}`);
|
|
67
|
+
if (
|
|
68
|
+
["ieee-journal", "springer-journal"].includes(template) &&
|
|
69
|
+
resolved.form !== "numeric"
|
|
70
|
+
)
|
|
71
|
+
throw new Error("This journal profile requires numeric citations.");
|
|
72
|
+
if (template === "elsevier-author-date" && resolved.form !== "author-date")
|
|
73
|
+
throw new Error("This journal profile requires author-date citations.");
|
|
74
|
+
if (
|
|
75
|
+
options.data !== undefined &&
|
|
76
|
+
(!isPlainObject(options.data) ||
|
|
77
|
+
Object.keys(options.data).some((key) => key !== "country") ||
|
|
78
|
+
(options.data.country !== undefined &&
|
|
79
|
+
(typeof options.data.country !== "string" ||
|
|
80
|
+
options.data.country.length > 100)))
|
|
81
|
+
)
|
|
82
|
+
throw new Error("Unsupported native template metadata.");
|
|
83
|
+
if (
|
|
84
|
+
template === "acm-journal" &&
|
|
85
|
+
resolved.article.authorDetails?.some((author) => author.affiliation) &&
|
|
86
|
+
!options.data?.country?.trim()
|
|
87
|
+
)
|
|
88
|
+
throw new Error("ACM affiliations require template data: country.");
|
|
89
|
+
if (!resolved.article.title?.trim() || !resolved.article.authors.length)
|
|
90
|
+
throw new Error("PDF export requires a title and at least one author.");
|
|
91
|
+
if (options.fonts.length === 0 || options.fonts.length > 16)
|
|
92
|
+
throw new Error("Native PDF export requires one to sixteen pinned fonts.");
|
|
93
|
+
const article = structuredClone(resolved.article);
|
|
94
|
+
// CSL resolves cited footnotes at their first reference, rather than at the
|
|
95
|
+
// definition's physical position. Bind results before the native layout runs.
|
|
96
|
+
const notes = new Map(
|
|
97
|
+
article.blocks
|
|
98
|
+
.filter((block) => block.kind === "footnote")
|
|
99
|
+
.map((block) => [block.id, block]),
|
|
100
|
+
);
|
|
101
|
+
const visited = new Set<string>();
|
|
102
|
+
let cursor = 0;
|
|
103
|
+
const bind = (inlines: readonly ArticleInline[]): void => {
|
|
104
|
+
for (const inline of inlines) {
|
|
105
|
+
if (inline.type === "citation") {
|
|
106
|
+
const value = resolved.citations[cursor++];
|
|
107
|
+
if (value === undefined)
|
|
108
|
+
throw new Error(
|
|
109
|
+
"Resolved citation count does not match the article.",
|
|
110
|
+
);
|
|
111
|
+
Object.assign(inline, { text: value });
|
|
112
|
+
} else if (
|
|
113
|
+
inline.type === "text" &&
|
|
114
|
+
inline.reference?.kind === "footnote" &&
|
|
115
|
+
!visited.has(inline.reference.target)
|
|
116
|
+
) {
|
|
117
|
+
visited.add(inline.reference.target);
|
|
118
|
+
notes.get(inline.reference.target)?.paragraphs.forEach(bind);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
for (const block of article.blocks) visitInlines(block, bind);
|
|
123
|
+
if (cursor !== resolved.citations.length)
|
|
124
|
+
throw new Error("Resolved citation count does not match the article.");
|
|
125
|
+
// Each projection may emit multiple printable blocks (for example callouts).
|
|
126
|
+
const blockIds = document.blocks.flatMap((block) =>
|
|
127
|
+
projectArticle({ ...document, blocks: [block] }).blocks.map(() => block.id),
|
|
128
|
+
);
|
|
129
|
+
if (blockIds.length !== article.blocks.length)
|
|
130
|
+
throw new Error("Native projection source mapping is inconsistent.");
|
|
131
|
+
const clusters = citationClusters(resolved.article);
|
|
132
|
+
if (clusters.length !== resolved.citations.length)
|
|
133
|
+
throw new Error("Resolved citation clusters are inconsistent.");
|
|
134
|
+
return {
|
|
135
|
+
input: {
|
|
136
|
+
schemaVersion: NATIVE_PDF_INPUT_VERSION,
|
|
137
|
+
template,
|
|
138
|
+
templateVersion: "0.1.0-experimental",
|
|
139
|
+
pdfStandard: options.pdfUa === false ? null : "ua-2",
|
|
140
|
+
article: { ...article, title: resolved.article.title },
|
|
141
|
+
citations: resolved.citations,
|
|
142
|
+
bibliography: resolved.bibliography,
|
|
143
|
+
assets: options.assets ?? [],
|
|
144
|
+
fonts: options.fonts,
|
|
145
|
+
templateData: options.data ?? {},
|
|
146
|
+
},
|
|
147
|
+
blockIds,
|
|
148
|
+
diagnostics: resolved.losses,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function visitInlines(
|
|
153
|
+
block: ArticleBlock,
|
|
154
|
+
visit: (inlines: readonly ArticleInline[]) => void,
|
|
155
|
+
): void {
|
|
156
|
+
switch (block.kind) {
|
|
157
|
+
case "heading":
|
|
158
|
+
case "paragraph":
|
|
159
|
+
visit(block.inlines);
|
|
160
|
+
break;
|
|
161
|
+
case "list":
|
|
162
|
+
block.items.forEach(visit);
|
|
163
|
+
break;
|
|
164
|
+
case "quote":
|
|
165
|
+
block.paragraphs.forEach(visit);
|
|
166
|
+
break;
|
|
167
|
+
case "figure":
|
|
168
|
+
visit(block.caption);
|
|
169
|
+
break;
|
|
170
|
+
case "table":
|
|
171
|
+
if (block.caption) visit(block.caption);
|
|
172
|
+
block.rows.forEach((row) => {
|
|
173
|
+
row.forEach(visit);
|
|
174
|
+
});
|
|
175
|
+
break;
|
|
176
|
+
default:
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
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 {
|
|
@@ -383,7 +464,11 @@ async function taggingFormat(image?: string): Promise<string | undefined> {
|
|
|
383
464
|
maxBuffer: 1024 * 1024,
|
|
384
465
|
},
|
|
385
466
|
);
|
|
386
|
-
|
|
467
|
+
const detected = /PUBLISLE-FORMAT:(\d{4}-\d{2}-\d{2})/u.exec(
|
|
468
|
+
output.stdout,
|
|
469
|
+
)?.[1];
|
|
470
|
+
taggingFormatCache.set(key, detected);
|
|
471
|
+
return detected;
|
|
387
472
|
} finally {
|
|
388
473
|
if (image)
|
|
389
474
|
await execute("docker", ["rm", "--force", containerName], {
|
|
@@ -395,6 +480,18 @@ async function taggingFormat(image?: string): Promise<string | undefined> {
|
|
|
395
480
|
export async function doctorCompiler(
|
|
396
481
|
source: LatexPackage,
|
|
397
482
|
): Promise<CompilerStatus> {
|
|
483
|
+
const cacheKey = [
|
|
484
|
+
source.engine,
|
|
485
|
+
source.pdfStandard ?? "",
|
|
486
|
+
Object.values(source.fonts).join(","),
|
|
487
|
+
...source.requirements.map((name) =>
|
|
488
|
+
source.files[name] === undefined && source.resources?.[name] === undefined
|
|
489
|
+
? name
|
|
490
|
+
: `${name}:bundled`,
|
|
491
|
+
),
|
|
492
|
+
].join("|");
|
|
493
|
+
const cached = doctorCache.get(cacheKey);
|
|
494
|
+
if (cached) return cached;
|
|
398
495
|
const missing: string[] = [];
|
|
399
496
|
let latexFormat: string | undefined;
|
|
400
497
|
if (source.engine === "lualatex") {
|
|
@@ -404,44 +501,45 @@ export async function doctorCompiler(
|
|
|
404
501
|
else if (source.pdfStandard && latexFormat < "2025-11-01")
|
|
405
502
|
missing.push("LaTeX format >= 2025-11-01 (PDF/UA tagging)");
|
|
406
503
|
}
|
|
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()))
|
|
504
|
+
await Promise.all(
|
|
505
|
+
["latexmk", source.engine, "bibtex", "kpsewhich"].map(async (command) => {
|
|
506
|
+
try {
|
|
507
|
+
await execute(command, ["--version"], { timeout: 5000 });
|
|
508
|
+
} catch {
|
|
509
|
+
missing.push(command);
|
|
510
|
+
}
|
|
511
|
+
}),
|
|
512
|
+
);
|
|
513
|
+
const external = source.requirements.filter(
|
|
514
|
+
(dependency) =>
|
|
515
|
+
source.files[dependency] === undefined &&
|
|
516
|
+
source.resources?.[dependency] === undefined,
|
|
517
|
+
);
|
|
518
|
+
const resolved = await kpsewhichAll(external);
|
|
519
|
+
for (const dependency of external)
|
|
520
|
+
if (!resolved.has(dependency)) missing.push(dependency);
|
|
521
|
+
await Promise.all(
|
|
522
|
+
Object.values(source.fonts).map(async (font) => {
|
|
523
|
+
try {
|
|
524
|
+
const matched = (
|
|
525
|
+
await execute("fc-match", ["-f", "%{family}", font], {
|
|
526
|
+
timeout: 5000,
|
|
527
|
+
})
|
|
528
|
+
).stdout;
|
|
529
|
+
if (!matched.toLowerCase().includes(font.toLowerCase()))
|
|
530
|
+
missing.push(`font:${font}`);
|
|
531
|
+
} catch {
|
|
436
532
|
missing.push(`font:${font}`);
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
533
|
+
}
|
|
534
|
+
}),
|
|
535
|
+
);
|
|
536
|
+
const status: CompilerStatus = {
|
|
441
537
|
available: missing.length === 0,
|
|
442
538
|
missing,
|
|
443
539
|
...(latexFormat ? { latexFormat } : {}),
|
|
444
540
|
};
|
|
541
|
+
if (status.available) doctorCache.set(cacheKey, status);
|
|
542
|
+
return status;
|
|
445
543
|
}
|
|
446
544
|
async function selectCompiler(
|
|
447
545
|
source: LatexPackage,
|
|
@@ -589,10 +687,22 @@ export async function setupCompiler(
|
|
|
589
687
|
return details.Id;
|
|
590
688
|
}
|
|
591
689
|
/** Unicode-safe default PDF API. Never falls back to the legacy text exporter. */
|
|
592
|
-
export
|
|
690
|
+
export function exportPdf(
|
|
691
|
+
document: Document<Block<BlockType, unknown>>,
|
|
692
|
+
options: NativeCompileOptions & { readonly engine: "rust" },
|
|
693
|
+
): Promise<NativeCompileResult>;
|
|
694
|
+
export function exportPdf(
|
|
593
695
|
document: Document<Block<BlockType, unknown>>,
|
|
594
696
|
options: LatexPackageOptions & CompileLatexOptions,
|
|
595
|
-
): Promise<CompiledLatexPackage
|
|
697
|
+
): Promise<CompiledLatexPackage>;
|
|
698
|
+
export async function exportPdf(
|
|
699
|
+
document: Document<Block<BlockType, unknown>>,
|
|
700
|
+
options:
|
|
701
|
+
| (LatexPackageOptions & CompileLatexOptions)
|
|
702
|
+
| (NativeCompileOptions & { readonly engine: "rust" }),
|
|
703
|
+
): Promise<CompiledLatexPackage | NativeCompileResult> {
|
|
704
|
+
if (options.engine === "rust")
|
|
705
|
+
return compileNativePdf(parseDocument(document), options);
|
|
596
706
|
return compileLatexPackage(createLatexPackage(document, options), options);
|
|
597
707
|
}
|
|
598
708
|
|