Path: blob/main/src/format/reveal/format-reveal-theme.ts
6452 views
/*1* format-reveal-theme.ts2*3* Copyright (C) 2021-2022 Posit Software, PBC4*/56import { dirname, join, relative } from "../../deno_ral/path.ts";7import { existsSync } from "../../deno_ral/fs.ts";89import { kTheme } from "../../config/constants.ts";10import {11Format,12kTextHighlightingMode,13Metadata,14SassBundleLayers,15SassBundleLayersWithBrand,16SassLayer,17} from "../../config/types.ts";1819import { isFileRef } from "../../core/http.ts";20import { pathWithForwardSlashes } from "../../core/path.ts";21import { formatResourcePath, resourcePath } from "../../core/resources.ts";22import {23cleanSourceMappingUrl,24compileSass,25mergeLayers,26outputVariable,27sassLayerFile,28SassVariable,29sassVariable,30} from "../../core/sass.ts";3132import { kCodeBlockHeight, kRevealJsUrl } from "./constants.ts";33import { resolveTextHighlightingLayer } from "../html/format-html-scss.ts";34import { quartoBaseLayer } from "../html/format-html-shared.ts";35import { TempContext } from "../../core/temp.ts";36import { hasAdaptiveTheme } from "../../quarto-core/text-highlighting.ts";37import { copyMinimal, copyTo } from "../../core/copy.ts";38import { titleSlideScss } from "./format-reveal-title.ts";39import { asCssFont, asCssNumber } from "../../core/css.ts";40import { cssHasDarkModeSentinel } from "../../core/pandoc/css.ts";41import { pandocNativeStr } from "../../core/pandoc/codegen.ts";42import { ProjectContext } from "../../project/types.ts";43import { brandRevealSassLayers } from "../../core/sass/brand.ts";44import { md5HashBytes } from "../../core/hash.ts";4546export const kRevealLightThemes = [47"white",48"beige",49"sky",50"serif",51"simple",52"solarized",53];5455export const kRevealDarkThemes = [56"black",57"league",58"night",59"blood",60"moon",61"dracula",62];6364export const kRevealThemes = [...kRevealLightThemes, ...kRevealDarkThemes];6566export async function revealTheme(67format: Format,68input: string,69libDir: string,70project: ProjectContext,71) {72// metadata override to return73const metadata: Metadata = {};7475// target revealDir76const revealDir = join(libDir, "revealjs");7778// revealurl (optional)79const revealJsUrl = format.metadata[kRevealJsUrl] as string | undefined;8081// we don't support remote versions b/c we need to compile the scss82if (revealJsUrl && !isFileRef(revealJsUrl)) {83throw new Error(84"Invalid revealjs-url: " + revealJsUrl +85" (remote urls are not supported)",86);87}8889// compute reveal url90const revealUrl = pathWithForwardSlashes(revealDir);91// escape to avoid pandoc markdown parsing from YAML default file92// https://github.com/quarto-dev/quarto-cli/issues/911793metadata[kRevealJsUrl] = pandocNativeStr(revealUrl).mappedString().value;9495// copy reveal dir96const revealSrcDir = revealJsUrl ||97formatResourcePath("revealjs", "reveal");98const revealDestDir = join(dirname(input), libDir, "revealjs");99["dist", "plugin"].forEach((dir) => {100copyMinimal(join(revealSrcDir, dir), join(revealDestDir, dir));101});102103// Resolve load paths104const cssThemeDir = join(revealSrcDir, "css", "theme");105const loadPaths = [106join(cssThemeDir, "source"),107join(cssThemeDir, "template"),108];109110const brandLayers: SassLayer[] = await brandRevealSassLayers(111input,112format,113project,114);115116// theme is either user provided scss or something in our 'themes' dir117// (note that standard reveal scss themes must be converted to quarto118// theme format so they can participate in the pipeline)119const themeConfig =120(format.metadata?.[kTheme] as string | string[] | undefined) || "default";121let usedBrandLayers = false;122const themeLayers = (Array.isArray(themeConfig) ? themeConfig : [themeConfig])123.map(124(theme) => {125const themePath = join(relative(Deno.cwd(), dirname(input)), theme);126if (theme === "brand") {127usedBrandLayers = true;128return brandLayers;129} else if (existsSync(themePath)) {130loadPaths.unshift(join(dirname(input), dirname(theme)));131return [themeLayer(themePath)];132} else {133// alias revealjs theme names134if (theme === "white") {135theme = "default";136} else if (theme === "black") {137theme = "dark";138}139// read theme140theme = formatResourcePath(141"revealjs",142join("themes", `${theme}.scss`),143);144return [themeLayer(theme)];145}146},147).flat();148if (!usedBrandLayers) {149themeLayers.unshift(...brandLayers);150}151// get any variables defined in yaml152const yamlLayer: SassLayer = {153uses: "",154defaults: pandocVariablesToThemeScss(format.metadata, true),155functions: "",156mixins: "",157rules: "",158};159160// Inject the highlighting theme, if not adaptive161const highlightingLayer = !hasAdaptiveTheme(format.pandoc)162? resolveTextHighlightingLayer(163input,164format,165"light",166)167: undefined;168const userLayers = [yamlLayer];169if (highlightingLayer) {170userLayers.push(highlightingLayer);171}172userLayers.push(...themeLayers);173174// Quarto layers175const quartoLayers = [176quartoBaseLayer(format, true, true, false, true),177quartoLayer(),178quartoRevealBrandLayer(),179];180const titleSlideLayer = titleSlideScss(format);181if (titleSlideLayer) {182userLayers.unshift(titleSlideLayer);183}184185// create sass bundle layers186const bundleLayers: SassBundleLayers = {187key: "reveal-theme",188user: userLayers,189quarto: mergeLayers(190...quartoLayers,191),192framework: revealFrameworkLayer(revealSrcDir),193loadPaths,194};195196// compile sass197const css = await compileSass([bundleLayers], project);198// Remove sourcemap information199cleanSourceMappingUrl(css);200// convert from string to bytes201const hash = await md5HashBytes(Deno.readFileSync(css));202const fileName = `quarto-${hash}`;203copyTo(204css,205join(revealDestDir, "dist", "theme", `${fileName}.css`),206);207metadata[kTheme] = fileName;208209const highlightingMode: "light" | "dark" =210cssHasDarkModeSentinel(Deno.readTextFileSync(css)) ? "dark" : "light";211212// return213return {214revealUrl,215revealDestDir,216metadata,217[kTextHighlightingMode]: highlightingMode,218};219}220221// Revealjs framework layer is supposed to be more files but:222// - Only mixins.scss and theme.scss are needed here223// - settings.scss is manually included in the quarto.scss file224// - exposer.scss is loaded in theme.scss and found through the loadPaths225function revealFrameworkLayer(revealDir: string): SassLayer {226const readTemplate = (template: string) => {227return Deno.readTextFileSync(228join(revealDir, "css", "theme", "template", template),229);230};231return {232uses: "",233defaults: "",234functions: "",235mixins: readTemplate("mixins.scss"),236rules: readTemplate("theme.scss"),237};238}239240export function pandocVariablesToThemeScss(241metadata: Metadata,242asDefaults = false,243) {244return pandocVariablesToRevealDefaults(metadata).map(245(variable) => {246return outputVariable(variable, asDefaults);247},248).join("\n");249}250251function pandocVariablesToRevealDefaults(252metadata: Metadata,253): SassVariable[] {254const explicitVars: SassVariable[] = [];255256// Helper for adding explicitly set variables257const add = (258defaults: SassVariable[],259name: string,260value?: unknown,261formatter?: (val: unknown) => unknown,262) => {263if (value) {264const sassVar = sassVariable(name, value, formatter);265defaults.push(sassVar);266}267};268269// Pass through to some theme variables variables270add(explicitVars, "font-family-sans-serif", metadata["mainfont"], asCssFont);271add(explicitVars, "font-family-monospace", metadata["monofont"], asCssFont);272add(explicitVars, "presentation-font-size-root", metadata["fontsize"]);273add(274explicitVars,275"presentation-line-height",276metadata["linestretch"],277asCssNumber,278);279add(explicitVars, "code-block-bg", metadata["monobackgroundcolor"]);280281// Non-pandoc options from front matter282add(explicitVars, "code-block-height", metadata[kCodeBlockHeight]);283return explicitVars;284}285286function quartoLayer(): SassLayer {287return sassLayerFile(formatResourcePath("revealjs", "quarto.scss"));288}289290function themeLayer(theme: string): SassLayer {291return sassLayerFile(theme);292}293294function quartoRevealBrandLayer(): SassLayer {295return sassLayerFile(296resourcePath(join("formats", "revealjs", "brand", "brand.scss")),297);298}299300301