Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/command/render/cmd.ts
3583 views
1
/*
2
* cmd.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import { dirname, relative } from "../../deno_ral/path.ts";
8
import { expandGlobSync } from "../../deno_ral/fs.ts";
9
import { Command } from "cliffy/command/mod.ts";
10
import { debug, info, warning } from "../../deno_ral/log.ts";
11
12
import { fixupPandocArgs, kStdOut, parseRenderFlags } from "./flags.ts";
13
14
import { renderResultFinalOutput } from "./render.ts";
15
import { render } from "./render-shared.ts";
16
import { renderServices } from "./render-services.ts";
17
18
import { RenderResult } from "./types.ts";
19
import { kCliffyImplicitCwd } from "../../config/constants.ts";
20
import { InternalError } from "../../core/lib/error.ts";
21
import { notebookContext } from "../../render/notebook/notebook-context.ts";
22
23
export const renderCommand = new Command()
24
.name("render")
25
.stopEarly()
26
.arguments("[input:string] [...args]")
27
.description(
28
"Render files or projects to various document types.",
29
)
30
.option(
31
"-t, --to",
32
"Specify output format(s).",
33
)
34
.option(
35
"-o, --output",
36
"Write output to FILE (use '--output -' for stdout).",
37
)
38
.option(
39
"--output-dir",
40
"Write output to DIR (path is input/project relative)",
41
)
42
.option(
43
"-M, --metadata",
44
"Metadata value (KEY:VALUE).",
45
)
46
.option(
47
"--site-url",
48
"Override site-url for website or book output",
49
)
50
.option(
51
"--execute",
52
"Execute code (--no-execute to skip execution).",
53
)
54
.option(
55
"-P, --execute-param",
56
"Execution parameter (KEY:VALUE).",
57
)
58
.option(
59
"--execute-params",
60
"YAML file with execution parameters.",
61
)
62
.option(
63
"--execute-dir",
64
"Working directory for code execution.",
65
)
66
.option(
67
"--execute-daemon",
68
"Keep Jupyter kernel alive (defaults to 300 seconds).",
69
)
70
.option(
71
"--execute-daemon-restart",
72
"Restart keepalive Jupyter kernel before render.",
73
)
74
.option(
75
"--execute-debug",
76
"Show debug output when executing computations.",
77
)
78
.option(
79
"--use-freezer",
80
"Force use of frozen computations for an incremental file render.",
81
)
82
.option(
83
"--cache",
84
"Cache execution output (--no-cache to prevent cache).",
85
)
86
.option(
87
"--cache-refresh",
88
"Force refresh of execution cache.",
89
)
90
.option(
91
"--no-clean",
92
"Do not clean project output-dir prior to render",
93
)
94
.option(
95
"--debug",
96
"Leave intermediate files in place after render.",
97
)
98
.option(
99
"pandoc-args...",
100
"Additional pandoc command line arguments.",
101
)
102
.example(
103
"Render Markdown",
104
"quarto render document.qmd\n" +
105
"quarto render document.qmd --to html\n" +
106
"quarto render document.qmd --to pdf --toc",
107
)
108
.example(
109
"Render Notebook",
110
"quarto render notebook.ipynb\n" +
111
"quarto render notebook.ipynb --to docx\n" +
112
"quarto render notebook.ipynb --to pdf --toc",
113
)
114
.example(
115
"Render Project",
116
"quarto render\n" +
117
"quarto render projdir",
118
)
119
.example(
120
"Render w/ Metadata",
121
"quarto render document.qmd -M echo:false\n" +
122
"quarto render document.qmd -M code-fold:true",
123
)
124
.example(
125
"Render to Stdout",
126
"quarto render document.qmd --output -",
127
)
128
// deno-lint-ignore no-explicit-any
129
.action(async (options: any, input?: string, ...args: string[]) => {
130
// remove implicit clean argument (re-injected based on what the user
131
// actually passes in flags.ts)
132
if (options === undefined) {
133
throw new InternalError("Expected `options` to be an object");
134
}
135
delete options.clean;
136
137
// if an option got defined then this was mis-parsed as an 'option'
138
// rather than an 'arg' because no input was passed. reshuffle
139
// things to make them work
140
if (Object.keys(options).length === 1) {
141
const option = Object.keys(options)[0];
142
const optionArg = option.replaceAll(
143
/([A-Z])/g,
144
(_match: string, p1: string) => `-${p1.toLowerCase()}`,
145
);
146
if (input) {
147
args.unshift(input);
148
input = undefined;
149
}
150
args.unshift("--" + optionArg);
151
delete options[option];
152
}
153
154
// show help if requested
155
if (args.length > 0 && args[0] === "--help" || args[0] === "-h") {
156
renderCommand.showHelp();
157
return;
158
}
159
160
// if input is missing but there exists an args parameter which is a .qmd or .ipynb file,
161
// issue a warning.
162
if (!input || input === kCliffyImplicitCwd) {
163
input = Deno.cwd();
164
debug(`Render: Using current directory (${input}) as implicit input`);
165
const firstArg = args.find((arg) =>
166
arg.endsWith(".qmd") || arg.endsWith(".ipynb")
167
);
168
if (firstArg) {
169
warning(
170
"`quarto render` invoked with no input file specified (the parameter order matters).\nQuarto will render the current directory by default.\n" +
171
`Did you mean to run \`quarto render ${firstArg} ${
172
args.filter((arg) => arg !== firstArg).join(" ")
173
}\`?\n` +
174
"Use `quarto render --help` for more information.",
175
);
176
}
177
}
178
const inputs = [input!];
179
const firstPandocArg = args.findIndex((arg) => arg.startsWith("-"));
180
if (firstPandocArg !== -1) {
181
inputs.push(...args.slice(0, firstPandocArg));
182
args = args.slice(firstPandocArg);
183
}
184
185
// found by
186
// $ pandoc --help | grep '\[='
187
// cf https://github.com/jgm/pandoc/issues/8013#issuecomment-1094162866
188
189
const pandocArgsWithOptionalValues = [
190
"--file-scope",
191
"--sandbox",
192
"--standalone",
193
"--ascii",
194
"--toc",
195
"--preserve-tabs",
196
"--self-contained",
197
"--embed-resources",
198
"--no-check-certificate",
199
"--strip-comments",
200
"--reference-links",
201
"--list-tables",
202
"--listings",
203
"--incremental",
204
"--section-divs",
205
"--html-q-tags",
206
"--epub-title-page",
207
"--webtex",
208
"--mathjax",
209
"--katex",
210
"--trace",
211
"--dump-args",
212
"--ignore-args",
213
"--fail-if-warnings",
214
"--list-extensions",
215
];
216
217
// normalize args (to deal with args like --foo=bar)
218
const normalizedArgs = [];
219
for (const arg of args) {
220
const equalSignIndex = arg.indexOf("=");
221
if (
222
equalSignIndex > 0 && arg.startsWith("-") &&
223
!pandocArgsWithOptionalValues.includes(arg.slice(0, equalSignIndex))
224
) {
225
// Split the arg at the first equal sign
226
normalizedArgs.push(arg.slice(0, equalSignIndex));
227
normalizedArgs.push(arg.slice(equalSignIndex + 1));
228
} else {
229
normalizedArgs.push(arg);
230
}
231
}
232
args = normalizedArgs;
233
234
// extract pandoc flag values we know/care about, then fixup args as
235
// necessary (remove our flags that pandoc doesn't know about)
236
const flags = await parseRenderFlags(args);
237
args = fixupPandocArgs(args, flags);
238
239
// run render on input files
240
241
let renderResult: RenderResult | undefined;
242
let renderResultInput: string | undefined;
243
for (const input of inputs) {
244
for (const walk of expandGlobSync(input)) {
245
const services = renderServices(notebookContext());
246
try {
247
renderResultInput = relative(Deno.cwd(), walk.path) || ".";
248
if (renderResult) {
249
renderResult.context.cleanup();
250
}
251
renderResult = await render(renderResultInput, {
252
services,
253
flags,
254
pandocArgs: args,
255
useFreezer: flags.useFreezer === true,
256
setProjectDir: true,
257
});
258
259
// check for error
260
if (renderResult.error) {
261
renderResult.context.cleanup();
262
throw renderResult.error;
263
}
264
} finally {
265
services.cleanup();
266
}
267
}
268
}
269
if (renderResult && renderResultInput) {
270
// report output created
271
if (!options.flags?.quiet && options.flags?.output !== kStdOut) {
272
const finalOutput = renderResultFinalOutput(
273
renderResult,
274
Deno.statSync(renderResultInput).isDirectory
275
? renderResultInput
276
: dirname(renderResultInput),
277
);
278
279
if (finalOutput) {
280
info("Output created: " + finalOutput + "\n");
281
}
282
283
if (renderResult) {
284
renderResult.context.cleanup();
285
}
286
}
287
} else {
288
throw new Error(`No valid input files passed to render`);
289
}
290
});
291
292