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