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