Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/format/reveal/format-reveal-theme.ts
6452 views
1
/*
2
* format-reveal-theme.ts
3
*
4
* Copyright (C) 2021-2022 Posit Software, PBC
5
*/
6
7
import { dirname, join, relative } from "../../deno_ral/path.ts";
8
import { existsSync } from "../../deno_ral/fs.ts";
9
10
import { kTheme } from "../../config/constants.ts";
11
import {
12
Format,
13
kTextHighlightingMode,
14
Metadata,
15
SassBundleLayers,
16
SassBundleLayersWithBrand,
17
SassLayer,
18
} from "../../config/types.ts";
19
20
import { isFileRef } from "../../core/http.ts";
21
import { pathWithForwardSlashes } from "../../core/path.ts";
22
import { formatResourcePath, resourcePath } from "../../core/resources.ts";
23
import {
24
cleanSourceMappingUrl,
25
compileSass,
26
mergeLayers,
27
outputVariable,
28
sassLayerFile,
29
SassVariable,
30
sassVariable,
31
} from "../../core/sass.ts";
32
33
import { kCodeBlockHeight, kRevealJsUrl } from "./constants.ts";
34
import { resolveTextHighlightingLayer } from "../html/format-html-scss.ts";
35
import { quartoBaseLayer } from "../html/format-html-shared.ts";
36
import { TempContext } from "../../core/temp.ts";
37
import { hasAdaptiveTheme } from "../../quarto-core/text-highlighting.ts";
38
import { copyMinimal, copyTo } from "../../core/copy.ts";
39
import { titleSlideScss } from "./format-reveal-title.ts";
40
import { asCssFont, asCssNumber } from "../../core/css.ts";
41
import { cssHasDarkModeSentinel } from "../../core/pandoc/css.ts";
42
import { pandocNativeStr } from "../../core/pandoc/codegen.ts";
43
import { ProjectContext } from "../../project/types.ts";
44
import { brandRevealSassLayers } from "../../core/sass/brand.ts";
45
import { md5HashBytes } from "../../core/hash.ts";
46
47
export const kRevealLightThemes = [
48
"white",
49
"beige",
50
"sky",
51
"serif",
52
"simple",
53
"solarized",
54
];
55
56
export const kRevealDarkThemes = [
57
"black",
58
"league",
59
"night",
60
"blood",
61
"moon",
62
"dracula",
63
];
64
65
export const kRevealThemes = [...kRevealLightThemes, ...kRevealDarkThemes];
66
67
export async function revealTheme(
68
format: Format,
69
input: string,
70
libDir: string,
71
project: ProjectContext,
72
) {
73
// metadata override to return
74
const metadata: Metadata = {};
75
76
// target revealDir
77
const revealDir = join(libDir, "revealjs");
78
79
// revealurl (optional)
80
const revealJsUrl = format.metadata[kRevealJsUrl] as string | undefined;
81
82
// we don't support remote versions b/c we need to compile the scss
83
if (revealJsUrl && !isFileRef(revealJsUrl)) {
84
throw new Error(
85
"Invalid revealjs-url: " + revealJsUrl +
86
" (remote urls are not supported)",
87
);
88
}
89
90
// compute reveal url
91
const revealUrl = pathWithForwardSlashes(revealDir);
92
// escape to avoid pandoc markdown parsing from YAML default file
93
// https://github.com/quarto-dev/quarto-cli/issues/9117
94
metadata[kRevealJsUrl] = pandocNativeStr(revealUrl).mappedString().value;
95
96
// copy reveal dir
97
const revealSrcDir = revealJsUrl ||
98
formatResourcePath("revealjs", "reveal");
99
const revealDestDir = join(dirname(input), libDir, "revealjs");
100
["dist", "plugin"].forEach((dir) => {
101
copyMinimal(join(revealSrcDir, dir), join(revealDestDir, dir));
102
});
103
104
// Resolve load paths
105
const cssThemeDir = join(revealSrcDir, "css", "theme");
106
const loadPaths = [
107
join(cssThemeDir, "source"),
108
join(cssThemeDir, "template"),
109
];
110
111
const brandLayers: SassLayer[] = await brandRevealSassLayers(
112
input,
113
format,
114
project,
115
);
116
117
// theme is either user provided scss or something in our 'themes' dir
118
// (note that standard reveal scss themes must be converted to quarto
119
// theme format so they can participate in the pipeline)
120
const themeConfig =
121
(format.metadata?.[kTheme] as string | string[] | undefined) || "default";
122
let usedBrandLayers = false;
123
const themeLayers = (Array.isArray(themeConfig) ? themeConfig : [themeConfig])
124
.map(
125
(theme) => {
126
const themePath = join(relative(Deno.cwd(), dirname(input)), theme);
127
if (theme === "brand") {
128
usedBrandLayers = true;
129
return brandLayers;
130
} else if (existsSync(themePath)) {
131
loadPaths.unshift(join(dirname(input), dirname(theme)));
132
return [themeLayer(themePath)];
133
} else {
134
// alias revealjs theme names
135
if (theme === "white") {
136
theme = "default";
137
} else if (theme === "black") {
138
theme = "dark";
139
}
140
// read theme
141
theme = formatResourcePath(
142
"revealjs",
143
join("themes", `${theme}.scss`),
144
);
145
return [themeLayer(theme)];
146
}
147
},
148
).flat();
149
if (!usedBrandLayers) {
150
themeLayers.unshift(...brandLayers);
151
}
152
// get any variables defined in yaml
153
const yamlLayer: SassLayer = {
154
uses: "",
155
defaults: pandocVariablesToThemeScss(format.metadata, true),
156
functions: "",
157
mixins: "",
158
rules: "",
159
};
160
161
// Inject the highlighting theme, if not adaptive
162
const highlightingLayer = !hasAdaptiveTheme(format.pandoc)
163
? resolveTextHighlightingLayer(
164
input,
165
format,
166
"light",
167
)
168
: undefined;
169
const userLayers = [yamlLayer];
170
if (highlightingLayer) {
171
userLayers.push(highlightingLayer);
172
}
173
userLayers.push(...themeLayers);
174
175
// Quarto layers
176
const quartoLayers = [
177
quartoBaseLayer(format, true, true, false, true),
178
quartoLayer(),
179
quartoRevealBrandLayer(),
180
];
181
const titleSlideLayer = titleSlideScss(format);
182
if (titleSlideLayer) {
183
userLayers.unshift(titleSlideLayer);
184
}
185
186
// create sass bundle layers
187
const bundleLayers: SassBundleLayers = {
188
key: "reveal-theme",
189
user: userLayers,
190
quarto: mergeLayers(
191
...quartoLayers,
192
),
193
framework: revealFrameworkLayer(revealSrcDir),
194
loadPaths,
195
};
196
197
// compile sass
198
const css = await compileSass([bundleLayers], project);
199
// Remove sourcemap information
200
cleanSourceMappingUrl(css);
201
// convert from string to bytes
202
const hash = await md5HashBytes(Deno.readFileSync(css));
203
const fileName = `quarto-${hash}`;
204
copyTo(
205
css,
206
join(revealDestDir, "dist", "theme", `${fileName}.css`),
207
);
208
metadata[kTheme] = fileName;
209
210
const highlightingMode: "light" | "dark" =
211
cssHasDarkModeSentinel(Deno.readTextFileSync(css)) ? "dark" : "light";
212
213
// return
214
return {
215
revealUrl,
216
revealDestDir,
217
metadata,
218
[kTextHighlightingMode]: highlightingMode,
219
};
220
}
221
222
// Revealjs framework layer is supposed to be more files but:
223
// - Only mixins.scss and theme.scss are needed here
224
// - settings.scss is manually included in the quarto.scss file
225
// - exposer.scss is loaded in theme.scss and found through the loadPaths
226
function revealFrameworkLayer(revealDir: string): SassLayer {
227
const readTemplate = (template: string) => {
228
return Deno.readTextFileSync(
229
join(revealDir, "css", "theme", "template", template),
230
);
231
};
232
return {
233
uses: "",
234
defaults: "",
235
functions: "",
236
mixins: readTemplate("mixins.scss"),
237
rules: readTemplate("theme.scss"),
238
};
239
}
240
241
export function pandocVariablesToThemeScss(
242
metadata: Metadata,
243
asDefaults = false,
244
) {
245
return pandocVariablesToRevealDefaults(metadata).map(
246
(variable) => {
247
return outputVariable(variable, asDefaults);
248
},
249
).join("\n");
250
}
251
252
function pandocVariablesToRevealDefaults(
253
metadata: Metadata,
254
): SassVariable[] {
255
const explicitVars: SassVariable[] = [];
256
257
// Helper for adding explicitly set variables
258
const add = (
259
defaults: SassVariable[],
260
name: string,
261
value?: unknown,
262
formatter?: (val: unknown) => unknown,
263
) => {
264
if (value) {
265
const sassVar = sassVariable(name, value, formatter);
266
defaults.push(sassVar);
267
}
268
};
269
270
// Pass through to some theme variables variables
271
add(explicitVars, "font-family-sans-serif", metadata["mainfont"], asCssFont);
272
add(explicitVars, "font-family-monospace", metadata["monofont"], asCssFont);
273
add(explicitVars, "presentation-font-size-root", metadata["fontsize"]);
274
add(
275
explicitVars,
276
"presentation-line-height",
277
metadata["linestretch"],
278
asCssNumber,
279
);
280
add(explicitVars, "code-block-bg", metadata["monobackgroundcolor"]);
281
282
// Non-pandoc options from front matter
283
add(explicitVars, "code-block-height", metadata[kCodeBlockHeight]);
284
return explicitVars;
285
}
286
287
function quartoLayer(): SassLayer {
288
return sassLayerFile(formatResourcePath("revealjs", "quarto.scss"));
289
}
290
291
function themeLayer(theme: string): SassLayer {
292
return sassLayerFile(theme);
293
}
294
295
function quartoRevealBrandLayer(): SassLayer {
296
return sassLayerFile(
297
resourcePath(join("formats", "revealjs", "brand", "brand.scss")),
298
);
299
}
300
301