Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/command/render/output-tex.ts
12925 views
1
/*
2
* output-tex.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import { dirname, join, normalize, relative } from "../../deno_ral/path.ts";
8
import { ensureDirSync, safeRemoveSync } from "../../deno_ral/fs.ts";
9
10
import { writeFileToStdout } from "../../core/console.ts";
11
import { dirAndStem, expandPath } from "../../core/path.ts";
12
import { texSafeFilename } from "../../core/tex.ts";
13
14
import {
15
kKeepTex,
16
kOutputExt,
17
kOutputFile,
18
kPdfStandard,
19
kPdfStandardApplied,
20
kTargetFormat,
21
pdfStandardEnv,
22
} from "../../config/constants.ts";
23
import { Format } from "../../config/types.ts";
24
import { asArray } from "../../core/array.ts";
25
import { validatePdfStandards } from "../../core/verapdf.ts";
26
27
import { PandocOptions, RenderFlags, RenderOptions } from "./types.ts";
28
import { kStdOut, replacePandocOutputArg } from "./flags.ts";
29
import { OutputRecipe } from "./types.ts";
30
import { pdfEngine } from "../../config/pdf.ts";
31
import { execProcess } from "../../core/process.ts";
32
import { parseFormatString } from "../../core/pandoc/pandoc-formats.ts";
33
import { normalizeOutputPath } from "./output-shared.ts";
34
35
export interface PdfGenerator {
36
generate: (
37
input: string,
38
format: Format,
39
pandocOptions: PandocOptions,
40
) => Promise<string>;
41
computePath: (texStem: string, inputDir: string, format: Format) => string;
42
}
43
44
export function texToPdfOutputRecipe(
45
input: string,
46
finalOutput: string,
47
options: RenderOptions,
48
format: Format,
49
pdfIntermediateTo: string,
50
pdfGenerator: PdfGenerator,
51
pdfOutputDir?: string | null,
52
): OutputRecipe {
53
// break apart input file
54
const [inputDir, inputStem] = dirAndStem(input);
55
56
// there are many characters that give tex trouble in filenames, create
57
// a target stem that replaces them with the '-' character
58
59
// include variants in the tex stem if they are present to avoid
60
// overwriting files
61
let fixupInputName = "";
62
if (format.identifier[kTargetFormat]) {
63
const formatDesc = parseFormatString(format.identifier[kTargetFormat]);
64
fixupInputName = `${formatDesc.variants.join("")}${
65
formatDesc.modifiers.join("")
66
}`;
67
}
68
69
const texStem = texSafeFilename(`${inputStem}${fixupInputName}`);
70
71
// calculate output and args for pandoc (this is an intermediate file
72
// which we will then compile to a pdf and rename to .tex)
73
const output = texStem + ".tex";
74
let args = options.pandocArgs || [];
75
const pandoc = { ...format.pandoc };
76
if (options.flags?.output) {
77
args = replacePandocOutputArg(args, output);
78
} else {
79
pandoc[kOutputFile] = output;
80
}
81
82
// when pandoc is done, we need to run the pdf generator and then copy the
83
// ouptut to the user's requested destination
84
const complete = async (pandocOptions: PandocOptions) => {
85
const input = join(inputDir, output);
86
const pdfOutput = await pdfGenerator.generate(input, format, pandocOptions);
87
88
// Validate PDF against applied standards using verapdf (if available)
89
// Use kPdfStandardApplied from pandocOptions.format.metadata (filtered by LaTeX support)
90
// if available, otherwise fall back to original kPdfStandard list
91
const pdfStandards = asArray(
92
pandocOptions.format.metadata?.[kPdfStandardApplied] ??
93
format.render?.[kPdfStandard] ??
94
format.metadata?.[kPdfStandard] ??
95
pdfStandardEnv(),
96
) as string[];
97
if (pdfStandards.length > 0) {
98
await validatePdfStandards(pdfOutput, pdfStandards, {
99
quiet: pandocOptions.flags?.quiet,
100
});
101
}
102
103
// keep tex if requested
104
const compileTex = join(inputDir, output);
105
if (!format.render[kKeepTex]) {
106
safeRemoveSync(compileTex);
107
}
108
109
// copy (or write for stdout) compiled pdf to final output location
110
if (finalOutput) {
111
if (finalOutput === kStdOut) {
112
writeFileToStdout(pdfOutput);
113
safeRemoveSync(pdfOutput);
114
} else {
115
const outputPdf = expandPath(finalOutput);
116
117
if (normalize(pdfOutput) !== normalize(outputPdf)) {
118
// ensure the target directory exists
119
ensureDirSync(dirname(outputPdf));
120
121
Deno.renameSync(pdfOutput, outputPdf);
122
}
123
}
124
125
// Clean the output directory if it is empty
126
if (pdfOutputDir) {
127
console.log({ pdfOutputDir });
128
try {
129
// Remove the outputDir if it is empty
130
safeRemoveSync(pdfOutputDir, { recursive: false });
131
} catch {
132
// This is ok, just means the directory wasn't empty
133
}
134
}
135
136
// final output needs to either absolute or input dir relative
137
// (however it may be working dir relative when it is passed in)
138
return normalizeOutputPath(input, finalOutput);
139
} else {
140
return normalizeOutputPath(input, pdfOutput);
141
}
142
};
143
144
const pdfOutput = finalOutput
145
? finalOutput === kStdOut
146
? undefined
147
: normalizeOutputPath(input, finalOutput)
148
: normalizeOutputPath(
149
input,
150
pdfGenerator.computePath(texStem, dirname(input), format),
151
);
152
153
// tweak writer if it's pdf
154
const to = format.pandoc.to === "pdf" ? pdfIntermediateTo : format.pandoc.to;
155
156
// return recipe
157
return {
158
output,
159
keepYaml: false,
160
args,
161
format: {
162
...format,
163
pandoc: {
164
...pandoc,
165
to,
166
},
167
},
168
complete,
169
finalOutput: pdfOutput ? relative(inputDir, pdfOutput) : undefined,
170
};
171
}
172
173
export function useContextPdfOutputRecipe(
174
format: Format,
175
flags?: RenderFlags,
176
) {
177
const kContextPdfEngine = "context";
178
if (format.pandoc.to === "pdf" && format.render[kOutputExt] === "pdf") {
179
const engine = pdfEngine(format.pandoc, format.render, flags);
180
return engine.pdfEngine === kContextPdfEngine;
181
} else {
182
return false;
183
}
184
}
185
186
// based on: https://github.com/rstudio/rmarkdown/blob/main/R/context_document.R
187
188
export function contextPdfOutputRecipe(
189
input: string,
190
finalOutput: string,
191
options: RenderOptions,
192
format: Format,
193
): OutputRecipe {
194
const computePath = (stem: string, dir: string, _format: Format) => {
195
return join(dir, stem + ".pdf");
196
};
197
198
const generate = async (
199
input: string,
200
format: Format,
201
pandocOptions: PandocOptions,
202
): Promise<string> => {
203
// derive engine (parse opts, etc.)
204
const engine = pdfEngine(format.pandoc, format.render, pandocOptions.flags);
205
206
// build context command
207
const cmd = "context";
208
const args = [input];
209
if (engine.pdfEngineOpts) {
210
args.push(...engine.pdfEngineOpts);
211
}
212
args.push(
213
// ConTeXt produces some auxiliary files:
214
// direct PDF generation by Pandoc never produces these auxiliary
215
// files because Pandoc runs ConTeXt in a temporary directory.
216
// Replicate Pandoc's behavior using "--purgeall" option
217
"--purgeall",
218
// Pandoc runs ConteXt with "--batchmode" option. Do the same.
219
"--batchmode",
220
);
221
222
// run context
223
const result = await execProcess({
224
cmd,
225
args,
226
});
227
if (result.success) {
228
const [dir, stem] = dirAndStem(input);
229
return computePath(stem, dir, format);
230
} else {
231
throw new Error();
232
}
233
};
234
235
return texToPdfOutputRecipe(
236
input,
237
finalOutput,
238
options,
239
format,
240
"context",
241
{
242
generate,
243
computePath,
244
},
245
);
246
}
247
248