Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/command/render/output.ts
3584 views
1
/*
2
* output.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import {
8
basename,
9
dirname,
10
extname,
11
isAbsolute,
12
join,
13
relative,
14
SEP_PATTERN,
15
} from "../../deno_ral/path.ts";
16
17
import { writeFileToStdout } from "../../core/console.ts";
18
import { dirAndStem, expandPath, safeRemoveSync } from "../../core/path.ts";
19
import {
20
parse as parseYaml,
21
partitionYamlFrontMatter,
22
stringify as stringifyYaml,
23
} from "../../core/yaml.ts";
24
25
import {
26
kOutputExt,
27
kOutputFile,
28
kPreserveYaml,
29
kVariant,
30
} from "../../config/constants.ts";
31
32
import {
33
quartoLatexmkOutputRecipe,
34
useQuartoLatexmk,
35
} from "./latexmk/latexmk.ts";
36
37
import { kStdOut, replacePandocOutputArg } from "./flags.ts";
38
import { OutputRecipe, RenderContext } from "./types.ts";
39
import { resolveKeepSource } from "./codetools.ts";
40
import {
41
contextPdfOutputRecipe,
42
useContextPdfOutputRecipe,
43
} from "./output-tex.ts";
44
import { formatOutputFile } from "../../core/render.ts";
45
import { kYamlMetadataBlock } from "../../core/pandoc/pandoc-formats.ts";
46
import {
47
typstPdfOutputRecipe,
48
useTypstPdfOutputRecipe,
49
} from "./output-typst.ts";
50
51
// render commands imply the --output argument for pandoc and the final
52
// output file to create for the user, but we need a 'recipe' to go from
53
// this spec to what we should actually pass to pandoc on the command line.
54
// considerations include providing the default extension, dealing with
55
// output to stdout, and rendering pdfs (which can require an additional
56
// step after pandoc e.g. for latexmk)
57
58
export function outputRecipe(
59
context: RenderContext,
60
): OutputRecipe {
61
// alias
62
const input = context.target.input;
63
const options = context.options;
64
const format = context.format;
65
66
// determine if an output file was specified (could be on the command line or
67
// could be within metadata)
68
let output = options.flags?.output;
69
if (!output) {
70
const outputFile = formatOutputFile(format);
71
if (outputFile) {
72
// https://github.com/quarto-dev/quarto-cli/issues/2440
73
if (outputFile.match(SEP_PATTERN)) {
74
throw new Error(
75
`\nIn file ${context.target.source}\n Invalid value for \`output-file\`: paths are not allowed`,
76
);
77
}
78
output = join(dirname(input), outputFile);
79
} else {
80
output = "";
81
}
82
}
83
84
if (useQuartoLatexmk(format, options.flags)) {
85
return quartoLatexmkOutputRecipe(input, output, options, format);
86
} else if (useContextPdfOutputRecipe(format, options.flags)) {
87
return contextPdfOutputRecipe(input, output, options, format);
88
} else if (useTypstPdfOutputRecipe(format)) {
89
return typstPdfOutputRecipe(
90
input,
91
output,
92
options,
93
format,
94
context.project,
95
);
96
} else {
97
// default recipe spec based on user input
98
const completeActions: VoidFunction[] = [];
99
100
const recipe: OutputRecipe = {
101
output,
102
keepYaml: false,
103
args: options.pandocArgs || [],
104
format: { ...format },
105
complete: (): Promise<string | void> => {
106
completeActions.forEach((action) => action());
107
return Promise.resolve();
108
},
109
};
110
111
// keep source if requested (via keep-source or code-tools), we are targeting html,
112
// and engine can keep it (e.g. we wouldn't keep an .ipynb file as source)
113
resolveKeepSource(recipe.format, context.engine, context.target);
114
115
// helper function to re-write output
116
const updateOutput = (output: string) => {
117
recipe.output = output;
118
if (options.flags?.output) {
119
recipe.args = replacePandocOutputArg(recipe.args, output);
120
} else {
121
recipe.format.pandoc[kOutputFile] = output;
122
}
123
};
124
125
// determine ext
126
const ext = format.render[kOutputExt] || "html";
127
128
// compute dir and stem
129
const [inputDir, inputStem] = dirAndStem(input);
130
131
// tweak pandoc writer if we have extensions declared
132
if (format.render[kVariant]) {
133
const to = format.pandoc.to;
134
const variant = format.render[kVariant];
135
136
recipe.format = {
137
...recipe.format,
138
pandoc: {
139
...recipe.format.pandoc,
140
to: `${to}${variant}`,
141
},
142
};
143
144
// we implement +yaml_metadata_block internally to prevent
145
// gunk from the quarto rendering pipeline from showing up
146
if (recipe.format.pandoc.to?.includes(`+${kYamlMetadataBlock}`)) {
147
recipe.keepYaml = true;
148
}
149
}
150
151
// complete hook for keep-yaml
152
// workaround for https://github.com/quarto-dev/quarto-cli/issues/5079
153
if (recipe.keepYaml || recipe.format.render[kPreserveYaml]) {
154
completeActions.push(() => {
155
// read yaml and output markdown
156
const inputMd = partitionYamlFrontMatter(context.target.markdown.value);
157
if (inputMd) {
158
const outputFile = isAbsolute(recipe.output)
159
? recipe.output
160
: join(dirname(context.target.input), recipe.output);
161
const output = Deno.readTextFileSync(outputFile);
162
const outputMd = partitionYamlFrontMatter(
163
Deno.readTextFileSync(outputFile),
164
);
165
// remove _quarto metadata
166
//
167
// this is required to avoid tests breaking due to the
168
// _quarto regexp tests finding themselves in the output
169
const yaml = parseYaml(
170
inputMd.yaml.replace(/^---+\n/m, "").replace(/\n---+\n*$/m, "\n"),
171
) as Record<string, unknown>;
172
delete yaml._quarto;
173
const yamlString = `---\n${stringifyYaml(yaml)}---\n`;
174
175
const markdown = outputMd?.markdown || output;
176
Deno.writeTextFileSync(
177
outputFile,
178
yamlString + "\n\n" + markdown,
179
);
180
}
181
});
182
}
183
184
const deriveAutoOutput = () => {
185
// no output specified: derive an output path from the extension
186
187
// derive new output file
188
let output = inputStem + "." + ext;
189
// special case for .md to .md, need to append the writer to create a
190
// non-conflicting filename
191
if (extname(input) === ".md" && ext === "md") {
192
output = `${inputStem}-${format.identifier["base-format"]}.md`;
193
}
194
195
// special case if the source will overwrite the destination (note: this
196
// behavior can be customized with a custom output-ext)
197
if (output === basename(context.target.source)) {
198
output = inputStem + `.${kOutExt}.` + ext;
199
}
200
201
// assign output
202
updateOutput(output);
203
};
204
205
if (!recipe.output) {
206
deriveAutoOutput();
207
} else if (recipe.output === kStdOut) {
208
deriveAutoOutput();
209
recipe.isOutputTransient = true;
210
completeActions.push(() => {
211
writeFileToStdout(join(inputDir, recipe.output));
212
safeRemoveSync(join(inputDir, recipe.output));
213
});
214
} else if (!isAbsolute(recipe.output)) {
215
// relatve output file on the command line: make it relative to the input dir
216
// for pandoc (which will run in the input dir)
217
updateOutput(relative(inputDir, recipe.output));
218
} else {
219
// absolute path may need ~ substitution
220
updateOutput(expandPath(recipe.output));
221
}
222
223
// return
224
return recipe;
225
}
226
}
227
228
const kOutExt = "out";
229
230