Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/command/render/template.ts
3583 views
1
/*
2
* template.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
import { basename, isAbsolute, join } from "../../deno_ral/path.ts";
7
import {
8
kEmbedResources,
9
kHtmlMathMethod,
10
kSelfContained,
11
kSelfContainedMath,
12
} from "../../config/constants.ts";
13
14
import {
15
Format,
16
FormatExtras,
17
FormatTemplateContext,
18
Metadata,
19
} from "../../config/types.ts";
20
import { copyTo } from "../../core/copy.ts";
21
import { PandocOptions, RenderFlags } from "./types.ts";
22
import * as ld from "../../core/lodash.ts";
23
import { isHtmlDocOutput, isRevealjsOutput } from "../../config/format.ts";
24
import { expandGlobSync } from "../../deno_ral/fs.ts";
25
import { normalizePath } from "../../core/path.ts";
26
import { isGlob } from "../../core/lib/glob.ts";
27
import { ProjectContext } from "../../project/types.ts";
28
import { isWindows } from "../../deno_ral/platform.ts";
29
30
export const kPatchedTemplateExt = ".patched";
31
export const kTemplatePartials = "template-partials";
32
33
/**
34
* read and expand template partial globs
35
*
36
* @param metadata
37
* @param cwd current working directory for glob expansion
38
*/
39
export function readPartials(
40
metadata: Metadata,
41
inputDir?: string,
42
) {
43
if (typeof (metadata?.[kTemplatePartials]) === "string") {
44
metadata[kTemplatePartials] = [metadata[kTemplatePartials]];
45
}
46
const result = (metadata?.[kTemplatePartials] || []) as string[];
47
48
inputDir = inputDir ? normalizePath(inputDir) : undefined;
49
const resolvePath = (path: string) => {
50
if (!inputDir || isAbsolute(path)) {
51
return path;
52
} else {
53
return join(inputDir, path);
54
}
55
};
56
57
return result.flatMap((path) => {
58
const result = [];
59
for (const walk of expandGlobSync(resolvePath(path))) {
60
result.push(walk.path);
61
}
62
if (!isGlob(path) && result.length === 0) {
63
throw new Error(
64
`Template partial ${path} was not found. Please confirm that the path to the file is correct.`,
65
);
66
}
67
return result;
68
});
69
}
70
71
export function resolveTemplatePartialPaths(
72
metadata: Metadata,
73
inputDir?: string,
74
project?: ProjectContext,
75
) {
76
if (typeof (metadata?.[kTemplatePartials]) === "string") {
77
metadata[kTemplatePartials] = [metadata[kTemplatePartials]];
78
}
79
const result = (metadata?.[kTemplatePartials] || []) as string[];
80
metadata[kTemplatePartials] = result.map((path) => {
81
if (project && (path.startsWith("/") || path.startsWith("\\"))) {
82
return join(project.dir, path.slice(1));
83
} else if (!inputDir || isAbsolute(path)) {
84
return path;
85
} else {
86
if (isAbsolute(inputDir)) {
87
return join(inputDir, path);
88
} else {
89
return join(Deno.cwd(), inputDir, path);
90
}
91
}
92
});
93
}
94
95
export async function stageTemplate(
96
options: PandocOptions,
97
extras: FormatExtras,
98
userContext?: FormatTemplateContext,
99
) {
100
const stagingDir = options.services.temp.createDir();
101
const template = "template.patched";
102
103
const stageContext = (
104
dir: string,
105
template: string,
106
context?: FormatTemplateContext,
107
) => {
108
if (context) {
109
if (context.template) {
110
const targetFile = join(dir, template);
111
copyTo(context.template, targetFile);
112
// Ensure that file is writable
113
if (!isWindows) {
114
Deno.chmodSync(targetFile, 0o666);
115
}
116
}
117
118
if (context.partials) {
119
for (const partial of context.partials) {
120
// TODO: Confirm that partial is a file not a directory
121
copyTo(partial, join(stagingDir, basename(partial)));
122
}
123
}
124
return true;
125
} else {
126
return false;
127
}
128
};
129
130
const formatStaged = stageContext(
131
stagingDir,
132
template,
133
extras.templateContext,
134
);
135
const userStaged = await stageContext(stagingDir, template, userContext);
136
if (formatStaged || userStaged) {
137
// The path to the newly staged template
138
const stagedTemplatePath = join(stagingDir, template);
139
140
// Apply any patches now that the template is staged
141
applyTemplatePatches(stagedTemplatePath, options.format, options.flags);
142
143
// Return the path to the template
144
return stagedTemplatePath;
145
} else {
146
return undefined;
147
}
148
}
149
150
export function cleanTemplatePartialMetadata(
151
metadata: Metadata,
152
builtIns: string[],
153
) {
154
const partials = metadata[kTemplatePartials] as string[] | undefined;
155
if (partials) {
156
const cleansed = partials.filter((part) => builtIns.includes(part));
157
if (cleansed.length === 0) {
158
delete metadata[kTemplatePartials];
159
} else {
160
metadata[kTemplatePartials] = cleansed;
161
}
162
}
163
}
164
165
interface TemplatePatch {
166
searchValue: RegExp;
167
contents: string;
168
}
169
170
function applyTemplatePatches(
171
template: string,
172
format: Format,
173
flags?: RenderFlags,
174
) {
175
// The patches to apply
176
const patches: TemplatePatch[] = [];
177
178
// make math evade self-contained for HTML and Reveal
179
if (isHtmlDocOutput(format.pandoc) || isRevealjsOutput(format.pandoc)) {
180
if (
181
((flags && flags[kSelfContained]) || format.pandoc[kSelfContained] ||
182
(flags && flags[kEmbedResources]) || format.pandoc[kEmbedResources]) &&
183
!format.render[kSelfContainedMath]
184
) {
185
const math = mathConfig(format, flags);
186
if (math) {
187
const mathTemplate = math.method === "mathjax"
188
? mathjaxScript(math.url)
189
: math.method == "katex"
190
? katexScript(math.url)
191
: "";
192
193
if (mathTemplate) {
194
patches.push({
195
searchValue: /\$math\$/,
196
contents: mathTemplate,
197
});
198
}
199
}
200
}
201
}
202
203
// Apply any patches
204
if (patches.length) {
205
let templateContents = Deno.readTextFileSync(template);
206
patches.forEach((patch) => {
207
templateContents = templateContents.replace(
208
patch.searchValue,
209
patch.contents,
210
);
211
});
212
Deno.writeTextFileSync(template, templateContents);
213
}
214
}
215
216
function mathConfig(format: Format, flags?: RenderFlags) {
217
// if any command line math flags were passed then bail
218
if (
219
flags?.mathjax || flags?.katex || flags?.webtex || flags?.gladtex ||
220
flags?.mathml
221
) {
222
return undefined;
223
}
224
225
const math = format.pandoc[kHtmlMathMethod];
226
if (math === undefined || math === "mathjax") {
227
return {
228
method: "mathjax",
229
url: "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml-full.js",
230
};
231
} else if (math === "katex") {
232
return {
233
method: "katex",
234
url: "https://cdn.jsdelivr.net/npm/[email protected]/dist/",
235
};
236
} else if (ld.isObject(math)) {
237
const mathMethod = math as { method: string; url: string };
238
if (
239
(mathMethod.method === "mathjax" || mathMethod.method === "katex") &&
240
typeof (mathMethod.url) === "string"
241
) {
242
return mathMethod;
243
}
244
}
245
}
246
247
function mathjaxScript(url: string) {
248
return `
249
<script>
250
(function () {
251
var script = document.createElement("script");
252
script.type = "text/javascript";
253
script.src = "${url}";
254
document.getElementsByTagName("head")[0].appendChild(script);
255
})();
256
</script>
257
`;
258
}
259
260
function katexScript(url: string) {
261
url = url.trim();
262
if (!url.endsWith("/")) {
263
url += "/";
264
}
265
return `
266
<script>
267
document.addEventListener("DOMContentLoaded", function () {
268
var head = document.getElementsByTagName("head")[0];
269
var link = document.createElement("link");
270
link.rel = "stylesheet";
271
link.href = "${url}katex.min.css";
272
head.appendChild(link);
273
274
function renderMathElements() {
275
var mathElements = document.getElementsByClassName("math");
276
var macros = [];
277
for (var i = 0; i < mathElements.length; i++) {
278
var texText = mathElements[i].firstChild;
279
if (mathElements[i].tagName == "SPAN") {
280
if (window.katex) {
281
window.katex.render(texText.data, mathElements[i], {
282
displayMode: mathElements[i].classList.contains('display'),
283
throwOnError: false,
284
macros: macros,
285
fleqn: false
286
});
287
} else {
288
console.error("KaTeX has not been loaded correctly, as not found globally.");
289
}
290
}
291
}
292
}
293
294
var script = document.createElement("script");
295
script.src = "${url}katex.min.js";
296
script.onload = renderMathElements;
297
298
// Check for RequireJS and AMD detection as it conflicts with KaTeX loading.
299
if (typeof require === 'function' && typeof define === 'function' && define.amd) {
300
// Disable require.js AMD detection temporarily, as it conflicts with KaTeX loading using CommonJS
301
var disableAmdScript = document.createElement("script");
302
disableAmdScript.textContent = 'window._amd_backup = window.define.amd; window.define.amd = false;';
303
head.appendChild(disableAmdScript);
304
305
// overwrite onload to restore Require.js AMD detection
306
script.onload = function() {
307
// Restore Require.js AMD detection
308
window.define.amd = window._amd_backup;
309
delete window._amd_backup;
310
renderMathElements();
311
};
312
}
313
314
head.appendChild(script);
315
});
316
</script>
317
`;
318
}
319
320