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