Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/tests/utils.ts
12923 views
1
/*
2
* utils.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*
6
*/
7
8
import { basename, dirname, extname, join, relative } from "../src/deno_ral/path.ts";
9
import { parseFormatString } from "../src/core/pandoc/pandoc-formats.ts";
10
import { kMetadataFormat, kOutputExt, kOutputFile } from "../src/config/constants.ts";
11
import { pathWithForwardSlashes, safeExistsSync } from "../src/core/path.ts";
12
import { readYaml } from "../src/core/yaml.ts";
13
import { isWindows } from "../src/deno_ral/platform.ts";
14
import { bookOutputStem } from "../src/project/types/book/book-shared.ts";
15
import { ProjectConfig } from "../src/project/types.ts";
16
17
// caller is responsible for cleanup!
18
export function inTempDirectory(fn: (dir: string) => unknown): unknown {
19
const dir = Deno.makeTempDirSync();
20
return fn(dir);
21
}
22
23
export async function withTempDir<T>(
24
fn: (dir: string) => T | Promise<T>,
25
prefix = "quarto-test",
26
): Promise<T> {
27
const dir = Deno.makeTempDirSync({ prefix });
28
try {
29
return await fn(dir);
30
} finally {
31
Deno.removeSync(dir, { recursive: true });
32
}
33
}
34
35
// Find a _quarto.yaml file in the directory hierarchy of the input file
36
export function findProjectDir(input: string, until?: RegExp | undefined): string | undefined {
37
let dir = dirname(input);
38
// This is used for smoke-all tests and should stop there
39
// to avoid side effect of _quarto.yml outside of Quarto tests folders
40
while (dir !== "" && dir !== "." && (until ? !until.test(pathWithForwardSlashes(dir)) : true)) {
41
const filename = ["_quarto.yml", "_quarto.yaml"].find((file) => {
42
const yamlPath = join(dir, file);
43
if (safeExistsSync(yamlPath)) {
44
return true;
45
}
46
});
47
if (filename) {
48
return dir;
49
}
50
51
const newDir = dirname(dir); // stops at the root for both Windows and Posix
52
if (newDir === dir) {
53
return;
54
}
55
dir = newDir;
56
}
57
}
58
59
export function findProjectOutputDir(projectdir: string | undefined) {
60
if (!projectdir) {
61
return;
62
}
63
const yaml = readYaml(join(projectdir, "_quarto.yml"));
64
let type = undefined;
65
try {
66
// deno-lint-ignore no-explicit-any
67
type = ((yaml as any).project as any).type;
68
} catch (error) {
69
throw new Error("Failed to read quarto project YAML" + String(error));
70
}
71
if (type === "book") {
72
return "_book";
73
}
74
if (type === "website") {
75
return (yaml as any)?.project?.["output-dir"] || "_site";
76
}
77
if (type === "manuscript") {
78
return (yaml as any)?.project?.["output-dir"] || "_manuscript";
79
}
80
// type default explicit or just unset
81
return (yaml as any)?.project?.["output-dir"] || "";
82
}
83
84
// Get the book output stem using the real bookOutputStem() from book-shared.ts
85
export function findBookOutputStem(projectdir: string | undefined): string | undefined {
86
if (!projectdir) {
87
return undefined;
88
}
89
const yaml = readYaml(join(projectdir, "_quarto.yml")) as Record<string, unknown>;
90
// deno-lint-ignore no-explicit-any
91
const projectType = ((yaml as any).project as any)?.type;
92
if (projectType !== "book") {
93
return undefined;
94
}
95
// Pass the yaml as ProjectConfig - it has { project: {...}, book: {...} }
96
return bookOutputStem(projectdir, yaml as ProjectConfig);
97
}
98
99
// Gets output that should be created for this input file and target format
100
export function outputForInput(
101
input: string,
102
to: string,
103
projectOutDir?: string,
104
projectRoot?: string,
105
// deno-lint-ignore no-explicit-any
106
metadata?: Record<string, any>,
107
) {
108
// TODO: Consider improving this (e.g. for cases like Beamer, or typst)
109
projectRoot = projectRoot ?? findProjectDir(input);
110
projectOutDir = projectOutDir ?? findProjectOutputDir(projectRoot);
111
112
// For book projects with single-file output (PDF, Typst, EPUB), use the book title as stem
113
// Multi-file formats (HTML) produce individual chapter files, so use input filename
114
// Single-file formats only produce merged output when rendering the index file
115
const inputBasename = basename(input, extname(input));
116
const isMultiFileFormat = to.startsWith("html") || to === "revealjs";
117
const isSingleFileBookRender = !isMultiFileFormat && inputBasename === "index";
118
const bookStem = isSingleFileBookRender ? findBookOutputStem(projectRoot) : undefined;
119
const dir = bookStem ? "" : (projectRoot ? relative(projectRoot, dirname(input)) : dirname(input));
120
let stem = bookStem || metadata?.[kMetadataFormat]?.[to]?.[kOutputFile] || inputBasename;
121
let ext = metadata?.[kMetadataFormat]?.[to]?.[kOutputExt];
122
123
// TODO: there's a bug where output-ext keys from a custom format are
124
// not recognized (specifically this happens for confluence)
125
//
126
// we hack it here for the time being.
127
//
128
if (to === "confluence-publish") {
129
ext = "xml";
130
}
131
if (to === "docusaurus-md") {
132
ext = "mdx";
133
}
134
135
136
const formatDesc = parseFormatString(to);
137
const baseFormat = formatDesc.baseFormat;
138
if (formatDesc.baseFormat === "pdf") {
139
stem = `${stem}${formatDesc.variants.join("")}${
140
formatDesc.modifiers.join("")
141
}`;
142
}
143
144
let outputExt;
145
if (ext) {
146
outputExt = ext
147
} else {
148
outputExt = baseFormat || "html";
149
if (baseFormat === "latex" || baseFormat == "context") {
150
outputExt = "tex";
151
}
152
if (baseFormat === "beamer") {
153
outputExt = "pdf";
154
}
155
if (baseFormat === "revealjs") {
156
outputExt = "html";
157
}
158
if (["commonmark", "gfm", "markdown", "markdown_strict"].some((f) => f === baseFormat)) {
159
outputExt = "md";
160
}
161
if (baseFormat === "csljson") {
162
outputExt = "csl";
163
}
164
if (baseFormat === "bibtex" || baseFormat === "biblatex") {
165
outputExt = "bib";
166
}
167
if (baseFormat === "jats") {
168
outputExt = "xml";
169
}
170
if (baseFormat === "asciidoc") {
171
outputExt = "adoc";
172
}
173
if (baseFormat === "typst") {
174
outputExt = "pdf";
175
}
176
if (baseFormat === "dashboard") {
177
outputExt = "html";
178
}
179
if (baseFormat === "email") {
180
outputExt = "html";
181
}
182
}
183
184
const outputPath: string = projectRoot && projectOutDir !== undefined
185
? join(projectRoot, projectOutDir, dir, `${stem}.${outputExt}`)
186
: projectOutDir !== undefined
187
? join(projectOutDir, dir, `${stem}.${outputExt}`)
188
: join(dir, `${stem}.${outputExt}`);
189
const supportPath: string = projectRoot && projectOutDir !== undefined
190
? join(projectRoot, projectOutDir, dir, `${stem}_files`)
191
: projectOutDir !== undefined
192
? join(projectOutDir, dir, `${stem}_files`)
193
: join(dir, `${stem}_files`);
194
195
// For book projects with typst format, the intermediate .typ file is at project root
196
// as index.typ (from the merged book content), not derived from the PDF path
197
let intermediateTypstPath: string | undefined;
198
if (baseFormat === "typst" && projectRoot && projectOutDir === "_book") {
199
// Book projects place the merged .typ at project root as index.typ
200
intermediateTypstPath = join(projectRoot, "index.typ");
201
}
202
203
return {
204
outputPath,
205
supportPath,
206
intermediateTypstPath,
207
};
208
}
209
210
export function projectOutputForInput(input: string) {
211
const projectDir = findProjectDir(input);
212
const projectOutDir = findProjectOutputDir(projectDir);
213
if (!projectDir) {
214
throw new Error("No project directory found");
215
}
216
const dir = join(projectDir, projectOutDir, relative(projectDir, dirname(input)));
217
const stem = basename(input, extname(input));
218
219
const outputPath = join(dir, `${stem}.html`);
220
const supportPath = join(dir, `site_libs`);
221
222
return {
223
outputPath,
224
supportPath,
225
};
226
}
227
228
export function docs(path: string): string {
229
return join("docs", path);
230
}
231
232
export function fileLoader(...path: string[]) {
233
return (file: string, to: string) => {
234
const input = docs(join(...path, file));
235
const output = outputForInput(input, to);
236
return {
237
input,
238
output,
239
};
240
};
241
}
242
243
// On Windows, `quarto.cmd` needs to be explicit in `execProcess()`
244
export function quartoDevCmd(): string {
245
return isWindows ? "quarto.cmd" : "quarto";
246
}
247
248
249