Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/command/render/latexmk/latex.ts
3587 views
1
/*
2
* latex.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import { basename, join } from "../../../deno_ral/path.ts";
8
import { existsSync, safeRemoveSync } from "../../../deno_ral/fs.ts";
9
import { error, info } from "../../../deno_ral/log.ts";
10
11
import { PdfEngine } from "../../../config/types.ts";
12
13
import { dirAndStem } from "../../../core/path.ts";
14
import { execProcess, ExecProcessOptions } from "../../../core/process.ts";
15
import { ProcessResult } from "../../../core/process-types.ts";
16
17
import { PackageManager } from "./pkgmgr.ts";
18
import { kLatexBodyMessageOptions } from "./types.ts";
19
import { hasTexLive, texLiveCmd, TexLiveContext } from "./texlive.ts";
20
import { withPath } from "../../../core/env.ts";
21
import { logProgress } from "../../../core/log.ts";
22
23
export interface LatexCommandReponse {
24
log: string;
25
result: ProcessResult;
26
output?: string;
27
}
28
29
export async function hasLatexDistribution() {
30
try {
31
const result = await execProcess({
32
cmd: "pdftex",
33
args: ["--version"],
34
stdout: "piped",
35
stderr: "piped",
36
});
37
return result.code === 0;
38
} catch {
39
return false;
40
}
41
}
42
43
const kLatexMkEngineFlags = [
44
"-pdf",
45
"-pdfdvi",
46
"-pdfps",
47
"-pdflua",
48
"-pdfxe",
49
"-pdf-",
50
];
51
52
// Runs the Pdf engine
53
export async function runPdfEngine(
54
input: string,
55
engine: PdfEngine,
56
texLive: TexLiveContext,
57
outputDir?: string,
58
texInputDirs?: string[],
59
pkgMgr?: PackageManager,
60
quiet?: boolean,
61
): Promise<LatexCommandReponse> {
62
// Input and log paths
63
const [cwd, stem] = dirAndStem(input);
64
const targetDir = outputDir ? join(cwd, outputDir) : cwd;
65
const output = join(targetDir, `${stem}.pdf`);
66
const log = join(targetDir, `${stem}.log`);
67
68
// Clean any log file or output from previous runs
69
[log, output].forEach((file) => {
70
if (existsSync(file)) {
71
safeRemoveSync(file);
72
}
73
});
74
75
// build pdf engine command line
76
// ensure that we provide latexmk with its require custom options
77
// Note that users may control the latexmk engine options, but
78
// if not specified, we should provide a default
79
const computeEngineArgs = () => {
80
if (engine.pdfEngine === "latexmk") {
81
const engineArgs = ["-interaction=batchmode", "-halt-on-error"];
82
if (
83
!engine.pdfEngineOpts || engine.pdfEngineOpts.find((opt) => {
84
return kLatexMkEngineFlags.includes(opt);
85
}) === undefined
86
) {
87
engineArgs.push("-pdf");
88
}
89
engineArgs.push("-quiet");
90
return engineArgs;
91
} else {
92
return ["-interaction=batchmode", "-halt-on-error"];
93
}
94
};
95
const args = computeEngineArgs();
96
97
// output directory
98
if (outputDir !== undefined) {
99
args.push(`-output-directory=${outputDir}`);
100
}
101
102
// pdf engine opts
103
if (engine.pdfEngineOpts) {
104
args.push(...engine.pdfEngineOpts);
105
}
106
107
// input file
108
args.push(basename(input));
109
110
// Run the command
111
const result = await runLatexCommand(
112
engine.pdfEngine,
113
args,
114
{
115
pkgMgr,
116
cwd,
117
texInputDirs,
118
texLive,
119
},
120
quiet,
121
);
122
123
// Success, return result
124
return {
125
result,
126
output,
127
log,
128
};
129
}
130
131
// Run the index generation engine (currently hard coded to makeindex)
132
export async function runIndexEngine(
133
input: string,
134
texLive: TexLiveContext,
135
engine?: string,
136
args?: string[],
137
pkgMgr?: PackageManager,
138
quiet?: boolean,
139
) {
140
const [cwd, stem] = dirAndStem(input);
141
const log = join(cwd, `${stem}.ilg`);
142
143
// Clean any log file from previous runs
144
if (existsSync(log)) {
145
safeRemoveSync(log);
146
}
147
148
const result = await runLatexCommand(
149
engine || "makeindex",
150
[...(args || []), basename(input)],
151
{
152
cwd,
153
pkgMgr,
154
texLive,
155
},
156
quiet,
157
);
158
159
return {
160
result,
161
log,
162
};
163
}
164
165
// Runs the bibengine to process citations
166
export async function runBibEngine(
167
engine: string,
168
input: string,
169
cwd: string,
170
texLive: TexLiveContext,
171
pkgMgr?: PackageManager,
172
texInputDirs?: string[],
173
quiet?: boolean,
174
): Promise<LatexCommandReponse> {
175
const [dir, stem] = dirAndStem(input);
176
const log = join(dir, `${stem}.blg`);
177
178
// Clean any log file from previous runs
179
if (existsSync(log)) {
180
safeRemoveSync(log);
181
}
182
183
const result = await runLatexCommand(
184
engine,
185
[input],
186
{
187
pkgMgr,
188
cwd,
189
texInputDirs,
190
texLive,
191
},
192
quiet,
193
);
194
return {
195
result,
196
log,
197
};
198
}
199
200
export interface LatexCommandContext {
201
pkgMgr?: PackageManager;
202
cwd?: string;
203
texInputDirs?: string[];
204
texLive: TexLiveContext;
205
}
206
207
async function runLatexCommand(
208
latexCmd: string,
209
args: string[],
210
context: LatexCommandContext,
211
quiet?: boolean,
212
): Promise<ProcessResult> {
213
const fullLatexCmd = texLiveCmd(latexCmd, context.texLive);
214
215
const runOptions: ExecProcessOptions = {
216
cmd: fullLatexCmd.fullPath,
217
args,
218
stdout: "piped",
219
stderr: "piped",
220
};
221
222
//Ensure that the bin directory is available as a part of PDF compilation
223
if (context.texLive.binDir) {
224
runOptions.env = runOptions.env || {};
225
runOptions.env["PATH"] = withPath({ prepend: [context.texLive.binDir] });
226
}
227
228
// Set the working directory
229
if (context.cwd) {
230
runOptions.cwd = context.cwd;
231
}
232
233
// Add a tex search path
234
// The // means that TeX programs will search recursively in that folder;
235
// the trailing colon means "append the standard value of TEXINPUTS" (which you don't need to provide).
236
if (context.texInputDirs && context.texInputDirs.length > 0) {
237
// note this //
238
runOptions.env = runOptions.env || {};
239
runOptions.env["TEXINPUTS"] = `${context.texInputDirs.join(";")};`;
240
runOptions.env["BSTINPUTS"] = `${context.texInputDirs.join(";")};`;
241
}
242
243
// Run the command
244
const runCmd = async () => {
245
const result = await execProcess(runOptions, undefined, "stdout>stderr");
246
if (!quiet && result.stderr) {
247
info(result.stderr, kLatexBodyMessageOptions);
248
}
249
return result;
250
};
251
252
try {
253
// Try running the command
254
return await runCmd();
255
} catch (_e) {
256
// First confirm that there is a TeX installation available
257
const tex = await hasTexLive() || await hasLatexDistribution();
258
if (!tex) {
259
info(
260
"\nNo TeX installation was detected.\n\nPlease run 'quarto install tinytex' to install TinyTex.\nIf you prefer, you may install TexLive or another TeX distribution.\n",
261
);
262
return Promise.reject();
263
} else if (context.pkgMgr && context.pkgMgr.autoInstall) {
264
// If the command itself can't be found, try installing the command
265
// if auto installation is enabled
266
if (!quiet) {
267
logProgress(
268
`command ${latexCmd} not found, attempting install`,
269
);
270
}
271
272
// Search for a package for this command
273
const packageForCommand = await context.pkgMgr.searchPackages([latexCmd]);
274
if (packageForCommand) {
275
// try to install it
276
await context.pkgMgr.installPackages(packagesForCommand(latexCmd));
277
}
278
// Try running the command again
279
return await runCmd();
280
} else {
281
// Some other error has occurred
282
error(
283
`Error executing ${latexCmd}`,
284
);
285
286
return Promise.reject();
287
}
288
}
289
}
290
291
// Convert any commands to their
292
function packagesForCommand(cmd: string): string[] {
293
if (cmd === "texindy") {
294
return ["xindy"];
295
} else {
296
return [cmd];
297
}
298
}
299
300