Skip to content

Commit 1e95b2c

Browse files
committed
[feat] add QUARTO_CAPTURE_RENDER_COMMAND to capture all information needed to recreate a pandoc render call outside of Quarto
1 parent 634916c commit 1e95b2c

File tree

3 files changed

+161
-9
lines changed

3 files changed

+161
-9
lines changed

src/command/render/pandoc.ts

Lines changed: 85 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ import { info } from "../../deno_ral/log.ts";
1010

1111
import { ensureDir, existsSync, expandGlobSync } from "../../deno_ral/fs.ts";
1212

13-
import { stringify } from "../../core/yaml.ts";
14-
import { encodeBase64 } from "encoding/base64";
13+
import { parse as parseYml, stringify } from "../../core/yaml.ts";
14+
import { copyTo } from "../../core/copy.ts";
15+
import { decodeBase64, encodeBase64 } from "encoding/base64";
1516

1617
import * as ld from "../../core/lodash.ts";
1718

@@ -205,6 +206,7 @@ import { kFieldCategories } from "../../project/types/website/listing/website-li
205206
import { isWindows } from "../../deno_ral/platform.ts";
206207
import { appendToCombinedLuaProfile } from "../../core/performance/perfetto-utils.ts";
207208
import { makeTimedFunctionAsync } from "../../core/performance/function-times.ts";
209+
import { walkJson } from "../../core/json.ts";
208210

209211
// in case we are running multiple pandoc processes
210212
// we need to make sure we capture all of the trace files
@@ -238,6 +240,76 @@ const handleCombinedLuaProfiles = (
238240
};
239241
};
240242

243+
function captureRenderCommand(
244+
args: Deno.RunOptions,
245+
temp: TempContext,
246+
outputDir: string,
247+
) {
248+
Deno.mkdirSync(outputDir, { recursive: true });
249+
const newArgs = [
250+
args.cmd[0],
251+
...args.cmd.slice(1).map((_arg) => {
252+
const arg = _arg as string; // we know it's a string, TypeScript doesn't somehow
253+
if (!arg.startsWith(temp.baseDir)) {
254+
return arg;
255+
}
256+
const newArg = join(outputDir, basename(arg));
257+
if (arg.match(/^.*quarto\-defaults.*.yml$/)) {
258+
// we need to correct the defaults YML because it contains a reference to a template in a temp directory
259+
const ymlDefaults = Deno.readTextFileSync(arg);
260+
const defaults = parseYml(ymlDefaults);
261+
262+
const templateDirectory = dirname(defaults.template);
263+
const newTemplateDirectory = join(
264+
outputDir,
265+
basename(templateDirectory),
266+
);
267+
copyTo(templateDirectory, newTemplateDirectory);
268+
defaults.template = join(
269+
newTemplateDirectory,
270+
basename(defaults.template),
271+
);
272+
const defaultsOutputFile = join(outputDir, basename(arg));
273+
Deno.writeTextFileSync(defaultsOutputFile, stringify(defaults));
274+
return defaultsOutputFile;
275+
}
276+
Deno.copyFileSync(arg, newArg);
277+
return newArg;
278+
}),
279+
] as typeof args.cmd;
280+
281+
// now we need to correct entries in filterParams
282+
const filterParams = JSON.parse(
283+
new TextDecoder().decode(decodeBase64(args.env!["QUARTO_FILTER_PARAMS"])),
284+
);
285+
walkJson(
286+
filterParams,
287+
(v: unknown) => typeof v === "string" && v.startsWith(temp.baseDir),
288+
(_v: unknown) => {
289+
const v = _v as string;
290+
const newV = join(outputDir, basename(v));
291+
Deno.copyFileSync(v, newV);
292+
return newV;
293+
},
294+
);
295+
296+
Deno.writeTextFileSync(
297+
join(outputDir, "render-command.json"),
298+
JSON.stringify(
299+
{
300+
...args,
301+
args: newArgs,
302+
env: {
303+
...args.env,
304+
"QUARTO_FILTER_PARAMS": encodeBase64(JSON.stringify(filterParams)),
305+
},
306+
},
307+
undefined,
308+
2,
309+
),
310+
);
311+
}
312+
241313
export async function runPandoc(
242314
options: PandocOptions,
243315
sysFilters: string[],
@@ -1234,14 +1306,18 @@ export async function runPandoc(
12341306

12351307
setupPandocEnv();
12361308

1309+
const params = {
1310+
cmd,
1311+
cwd,
1312+
env: pandocEnv,
1313+
ourEnv: Deno.env.toObject(),
1314+
};
1315+
const captureCommand = Deno.env.get("QUARTO_CAPTURE_RENDER_COMMAND");
1316+
if (captureCommand) {
1317+
captureRenderCommand(params, options.services.temp, captureCommand);
1318+
}
12371319
const pandocRender = makeTimedFunctionAsync("pandoc-render", async () => {
1238-
return await execProcess(
1239-
{
1240-
cmd,
1241-
cwd,
1242-
env: pandocEnv,
1243-
},
1244-
);
1320+
return await execProcess(params);
12451321
});
12461322

12471323
// run pandoc

src/core/json.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* json.ts
3+
*
4+
* Copyright (C) 2025 Posit Software, PBC
5+
*/
6+
7+
export const walkJson = (
8+
// deno-lint-ignore no-explicit-any
9+
obj: any,
10+
test: (v: unknown) => boolean,
11+
process: (v: unknown) => unknown,
12+
// deno-lint-ignore no-explicit-any
13+
): any => {
14+
if (test(obj)) {
15+
return process(obj);
16+
}
17+
if (Array.isArray(obj)) {
18+
for (let i = 0; i < obj.length; i++) {
19+
const v = obj[i];
20+
const result = test(v);
21+
if (!result) {
22+
walkJson(v, test, process);
23+
} else {
24+
obj[i] = walkJson(v, test, process);
25+
}
26+
}
27+
} else if (typeof obj === "object" && obj) {
28+
for (const key in obj) {
29+
const v = obj[key];
30+
const result = test(v);
31+
if (!result) {
32+
walkJson(v, test, process);
33+
} else {
34+
obj[key] = process(v);
35+
}
36+
}
37+
}
38+
return obj;
39+
};

tools/run-pandoc-capture.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// NOTE: THIS ISN'T A QUARTO RUN SCRIPT TO CUT DOWN ON THE OVERHEAD
2+
import { resolve, dirname } from "https://deno.land/std/path/mod.ts";
3+
4+
const arg = Deno.args[0];
5+
if (!arg) {
6+
console.error("Please provide a file name");
7+
Deno.exit(1);
8+
}
9+
const input = JSON.parse(Deno.readTextFileSync(arg));
10+
Deno.chdir(input.cwd);
11+
const runArgs = {
12+
cmd: [...input.args],
13+
env: {
14+
...input.ourEnv,
15+
...input.env,
16+
"QUARTO_FILTER_DEPENDENCY_FILE": "/dev/null",
17+
},
18+
cwd: input.cwd,
19+
}
20+
const params = {
21+
...runArgs,
22+
stdout: "piped",
23+
stderr: "piped",
24+
} as any;
25+
const p = Deno.run(params);
26+
27+
const [status, stdout, stderr] = await Promise.all([
28+
p.status(),
29+
p.output(),
30+
p.stderrOutput()
31+
]);
32+
p.close();
33+
34+
if (status.code !== 0) {
35+
console.error(new TextDecoder().decode(stderr));
36+
Deno.exit(status.code);
37+
}

0 commit comments

Comments
 (0)