From 07d5690135da5dec2d9b63b922df1134e73e9775 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Mon, 26 Aug 2024 13:51:23 -0700 Subject: [PATCH 1/8] use content hash as cache key; use hash in output file name --- src/core/sass.ts | 8 ++++++-- src/format/reveal/format-reveal-theme.ts | 9 +++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/core/sass.ts b/src/core/sass.ts index eea2aa50f2..d50f20f0d5 100644 --- a/src/core/sass.ts +++ b/src/core/sass.ts @@ -16,6 +16,7 @@ import { dartCompile } from "./dart-sass.ts"; import * as ld from "./lodash.ts"; import { lines } from "./text.ts"; import { sassCache } from "./sass/cache.ts"; +import { md5HashBytes } from "./hash.ts"; export interface SassVariable { name: string; @@ -119,10 +120,13 @@ export async function compileSass( ...userRules, ].join("\n\n"); + const hash = md5HashBytes(new TextEncoder().encode(scssInput)); + // Compile the scss // Note that you can set this to undefined to bypass the cache entirely - const cacheKey = bundles.map((bundle) => bundle.key).join("|") + "-" + - (minified ? "min" : "nomin"); + const cacheKey = hash; + // bundles.map((bundle) => bundle.key).join("|") + "-" + + // (minified ? "min" : "nomin"); return await compileWithCache( scssInput, diff --git a/src/format/reveal/format-reveal-theme.ts b/src/format/reveal/format-reveal-theme.ts index 8ee0750d7c..dcc7fa8f75 100644 --- a/src/format/reveal/format-reveal-theme.ts +++ b/src/format/reveal/format-reveal-theme.ts @@ -40,6 +40,7 @@ import { cssHasDarkModeSentinel } from "../../core/pandoc/css.ts"; import { pandocNativeStr } from "../../core/pandoc/codegen.ts"; import { ProjectContext } from "../../project/types.ts"; import { brandRevealSassBundleLayers } from "../../core/sass/brand.ts"; +import { md5Hash, md5HashBytes } from "../../core/hash.ts"; export const kRevealLightThemes = [ "white", @@ -185,11 +186,15 @@ export async function revealTheme( // compile sass const css = await compileSass([bundleLayers, ...brandLayers], temp); + // convert from string to bytes + const hash = md5Hash(Deno.readTextFileSync(css)); + const fileName = `quarto-${hash}`; copyTo( css, - join(revealDestDir, "dist", "theme", "quarto.css"), + join(revealDestDir, "dist", "theme", `${fileName}.css`), ); - metadata[kTheme] = "quarto"; + metadata[kTheme] = fileName; + console.log({ fileName }); const highlightingMode: "light" | "dark" = cssHasDarkModeSentinel(Deno.readTextFileSync(css)) ? "dark" : "light"; From d467f65f9621d60cc92e49ef645366c2f4982e0a Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Mon, 26 Aug 2024 13:53:40 -0700 Subject: [PATCH 2/8] remove console.log --- src/format/reveal/format-reveal-theme.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/format/reveal/format-reveal-theme.ts b/src/format/reveal/format-reveal-theme.ts index dcc7fa8f75..fc06376150 100644 --- a/src/format/reveal/format-reveal-theme.ts +++ b/src/format/reveal/format-reveal-theme.ts @@ -194,7 +194,6 @@ export async function revealTheme( join(revealDestDir, "dist", "theme", `${fileName}.css`), ); metadata[kTheme] = fileName; - console.log({ fileName }); const highlightingMode: "light" | "dark" = cssHasDarkModeSentinel(Deno.readTextFileSync(css)) ? "dark" : "light"; From 0e877c36e9ba6352dc248427a93938fcce9e2a05 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Mon, 26 Aug 2024 13:54:29 -0700 Subject: [PATCH 3/8] use stdlib md5 hasher --- src/format/reveal/format-reveal-theme.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/format/reveal/format-reveal-theme.ts b/src/format/reveal/format-reveal-theme.ts index fc06376150..1dc1518580 100644 --- a/src/format/reveal/format-reveal-theme.ts +++ b/src/format/reveal/format-reveal-theme.ts @@ -40,7 +40,7 @@ import { cssHasDarkModeSentinel } from "../../core/pandoc/css.ts"; import { pandocNativeStr } from "../../core/pandoc/codegen.ts"; import { ProjectContext } from "../../project/types.ts"; import { brandRevealSassBundleLayers } from "../../core/sass/brand.ts"; -import { md5Hash, md5HashBytes } from "../../core/hash.ts"; +import { md5HashBytes } from "../../core/hash.ts"; export const kRevealLightThemes = [ "white", @@ -187,7 +187,7 @@ export async function revealTheme( // compile sass const css = await compileSass([bundleLayers, ...brandLayers], temp); // convert from string to bytes - const hash = md5Hash(Deno.readTextFileSync(css)); + const hash = md5HashBytes(Deno.readFileSync(css)); const fileName = `quarto-${hash}`; copyTo( css, From 72aae376549a98774c57a2ce8317aa5f438a5c62 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Mon, 26 Aug 2024 16:10:57 -0700 Subject: [PATCH 4/8] update test --- tests/docs/smoke-all/2024/05/03/9548.qmd | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/docs/smoke-all/2024/05/03/9548.qmd b/tests/docs/smoke-all/2024/05/03/9548.qmd index 67c14a4bb4..d0cf377c3e 100644 --- a/tests/docs/smoke-all/2024/05/03/9548.qmd +++ b/tests/docs/smoke-all/2024/05/03/9548.qmd @@ -6,10 +6,12 @@ _quarto: tests: revealjs: ensureHtmlElements: - - ['head > link[rel="stylesheet"][href$="quarto.css"]'] + - ['head > link[rel="stylesheet"][href$="quarto-a0be57805c9fbfb2425d641eb5c8e4cc.css"]'] - ['head > link[rel="stylesheet"][href$="beige.css"]'] --- # Revealjs theme handling -User provided theme should be used to build a `quarto.css` using SASS and the `theme: beige` should internally by overridden to `theme: quarto` so that the later is added in Pandoc's template +User provided theme should be used to build a `quarto-a0be57805c9fbfb2425d641eb5c8e4cc.css` using SASS and the `theme: beige` should internally by overridden to `theme: quarto` so that the later is added in Pandoc's template + +2024-08-26: `quarto.css` now carries the MD5 hash of the content to allow different revealjs themes to work on the same website. From 706b26237af2c2775eb096e3ead4802b1409bb39 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Mon, 26 Aug 2024 17:43:10 -0700 Subject: [PATCH 5/8] use hash for bootstrap css in html --- src/command/render/pandoc-html.ts | 39 +++++++++++++++++---- src/command/render/render-contexts.ts | 50 +++++++++++++-------------- 2 files changed, 57 insertions(+), 32 deletions(-) diff --git a/src/command/render/pandoc-html.ts b/src/command/render/pandoc-html.ts index 9153976ebd..e3f3ff5b69 100644 --- a/src/command/render/pandoc-html.ts +++ b/src/command/render/pandoc-html.ts @@ -36,6 +36,9 @@ import { } from "../../core/pandoc/css.ts"; import { kMinimal } from "../../format/html/format-html-shared.ts"; import { kSassBundles } from "../../config/types.ts"; +import { md5HashBytes } from "../../core/hash.ts"; +import { getStack } from "../../core/deno/debug.ts"; +import { InternalError } from "../../core/lib/error.ts"; // The output target for a sass bundle // (controls the overall style tag that is emitted) @@ -92,7 +95,9 @@ export async function resolveSassBundles( const targets: SassTarget[] = [{ name: `${dependency}.min.css`, bundles, - attribs: {}, + attribs: { + "append-hash": "true", + }, }]; if (hasDark) { // Note that the other bundle provides light @@ -162,12 +167,30 @@ export async function resolveSassBundles( extraDep.name === dependency ); + let targetName = target.name; + if (target.attribs["append-hash"] === "true") { + const hashFragment = `-${md5HashBytes(Deno.readFileSync(cssPath))}`; + let extension = ""; + if (target.name.endsWith(".min.css")) { + extension = ".min.css"; + } else if (target.name.endsWith(".css")) { + extension = ".css"; + } else { + throw new InternalError("Unexpected target name: " + target.name); + } + targetName = + targetName.slice(0, target.name.length - extension.length) + + hashFragment + extension; + } else { + targetName = target.name; + } + if (existingDependency) { if (!existingDependency.stylesheets) { existingDependency.stylesheets = []; } existingDependency.stylesheets.push({ - name: target.name, + name: targetName, path: cssPath, attribs: target.attribs, }); @@ -179,7 +202,7 @@ export async function resolveSassBundles( extraDeps.push({ name: dependency, stylesheets: [{ - name: target.name, + name: targetName, path: cssPath, attribs: target.attribs, }, ...imports], @@ -249,7 +272,7 @@ async function resolveQuartoSyntaxHighlighting( // Generate and inject the text highlighting css const cssFileName = `quarto-syntax-highlighting${ style === "dark" ? "-dark" : "" - }.css`; + }`; // Read the highlight style (theme name) const themeDescriptor = readHighlightingTheme(inputDir, format.pandoc, style); @@ -283,7 +306,7 @@ async function resolveQuartoSyntaxHighlighting( // Compile the scss const highlightCssPath = await compileSass( [{ - key: cssFileName, + key: cssFileName + ".css", quarto: { uses: "", defaults: "", @@ -312,8 +335,9 @@ async function resolveQuartoSyntaxHighlighting( existingDependency.stylesheets = existingDependency.stylesheets || []; + const hash = md5HashBytes(Deno.readFileSync(highlightCssPath)); existingDependency.stylesheets.push({ - name: cssFileName, + name: cssFileName + `-${hash}.css`, path: highlightCssPath, attribs: mediaAttr, }); @@ -448,7 +472,8 @@ function processCssIntoExtras( if (dirty) { const cleanedCss = css.replaceAll(kVariablesRegex, ""); - const newCssPath = temp.createFile({ suffix: ".css" }); + const hash = md5HashBytes(new TextEncoder().encode(cleanedCss)); + const newCssPath = temp.createFile({ suffix: `-${hash}.css` }); // Preserve the existing permissions if possible // See https://github.com/quarto-dev/quarto-cli/issues/660 diff --git a/src/command/render/render-contexts.ts b/src/command/render/render-contexts.ts index 14ad0fa0d2..9df6139c95 100644 --- a/src/command/render/render-contexts.ts +++ b/src/command/render/render-contexts.ts @@ -507,31 +507,31 @@ async function resolveFormats( (isHtmlOutput(format, true) || isHtmlDashboardOutput(format)) && formatHasBootstrap(projFormat) && projectTypeIsWebsite(projType) ) { - if (formatHasBootstrap(inputFormat)) { - if ( - inputFormat.metadata[kTheme] !== undefined && - !ld.isEqual(inputFormat.metadata[kTheme], projFormat.metadata[kTheme]) - ) { - warnOnce( - `The file ${file.path} contains a theme property which is being ignored. Website projects do not support per document themes since all pages within a website share the website's theme.`, - ); - } - delete inputFormat.metadata[kTheme]; - } - if (formatHasBootstrap(directoryFormat)) { - if ( - directoryFormat.metadata[kTheme] !== undefined && - !ld.isEqual( - directoryFormat.metadata[kTheme], - projFormat.metadata[kTheme], - ) - ) { - warnOnce( - `The file ${file.path} contains a theme provided by a metadata file. This theme metadata is being ignored. Website projects do not support per directory themes since all pages within a website share the website's theme.`, - ); - } - delete directoryFormat.metadata[kTheme]; - } + // if (formatHasBootstrap(inputFormat)) { + // if ( + // inputFormat.metadata[kTheme] !== undefined && + // !ld.isEqual(inputFormat.metadata[kTheme], projFormat.metadata[kTheme]) + // ) { + // warnOnce( + // `The file ${file.path} contains a theme property which is being ignored. Website projects do not support per document themes since all pages within a website share the website's theme.`, + // ); + // } + // delete inputFormat.metadata[kTheme]; + // } + // if (formatHasBootstrap(directoryFormat)) { + // if ( + // directoryFormat.metadata[kTheme] !== undefined && + // !ld.isEqual( + // directoryFormat.metadata[kTheme], + // projFormat.metadata[kTheme], + // ) + // ) { + // warnOnce( + // `The file ${file.path} contains a theme provided by a metadata file. This theme metadata is being ignored. Website projects do not support per directory themes since all pages within a website share the website's theme.`, + // ); + // } + // delete directoryFormat.metadata[kTheme]; + // } } // combine user formats From 034154900243298b8cd9c65d388e1d6ddb735d82 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Mon, 26 Aug 2024 17:52:27 -0700 Subject: [PATCH 6/8] remove unneeded import --- src/command/render/pandoc-html.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/command/render/pandoc-html.ts b/src/command/render/pandoc-html.ts index e3f3ff5b69..2f21943d1a 100644 --- a/src/command/render/pandoc-html.ts +++ b/src/command/render/pandoc-html.ts @@ -37,7 +37,6 @@ import { import { kMinimal } from "../../format/html/format-html-shared.ts"; import { kSassBundles } from "../../config/types.ts"; import { md5HashBytes } from "../../core/hash.ts"; -import { getStack } from "../../core/deno/debug.ts"; import { InternalError } from "../../core/lib/error.ts"; // The output target for a sass bundle From a6a0782d35a3f10bbb363fa24a1da602c7f2ddfe Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Mon, 26 Aug 2024 17:52:41 -0700 Subject: [PATCH 7/8] get dashboard theme from dashboard format --- src/format/dashboard/format-dashboard.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/format/dashboard/format-dashboard.ts b/src/format/dashboard/format-dashboard.ts index 5f96a2df1e..c66236bb99 100644 --- a/src/format/dashboard/format-dashboard.ts +++ b/src/format/dashboard/format-dashboard.ts @@ -110,7 +110,10 @@ export function dashboardFormat() { const formats: Record = format.metadata .format as Record; const htmlFormat = formats["html"]; - if (htmlFormat && htmlFormat[kTheme]) { + const dashboardFormat = formats["dashboard"]; + if (dashboardFormat && dashboardFormat[kTheme]) { + format.metadata[kTheme] = dashboardFormat[kTheme]; + } else if (htmlFormat && htmlFormat[kTheme]) { format.metadata[kTheme] = htmlFormat[kTheme]; } } From c66d9f38591f0444f14bc8e8727a0292bfa198c3 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Tue, 27 Aug 2024 09:12:47 -0700 Subject: [PATCH 8/8] changelog --- news/changelog-1.6.md | 1 + 1 file changed, 1 insertion(+) diff --git a/news/changelog-1.6.md b/news/changelog-1.6.md index db56f3d8ea..219f083ddb 100644 --- a/news/changelog-1.6.md +++ b/news/changelog-1.6.md @@ -37,3 +37,4 @@ All changes included in 1.6: - ([#10332](https://github.com/quarto-dev/quarto-cli/issues/10332)): Use `exitWithCleanup` whenever possible instead of `Deno.exit` to clean up temporary resources. - ([#10334](https://github.com/quarto-dev/quarto-cli/issues/10334)): Fix `author` field rendered incorrectly in dashboards when multiple authors are present. - ([#10552](https://github.com/quarto-dev/quarto-cli/issues/10552)): Add `contents` shortcode. +- ([#8383](https://github.com/quarto-dev/quarto-cli/issues/8383)), ([#10087](https://github.com/quarto-dev/quarto-cli/issues/10087)), ([#10369](https://github.com/quarto-dev/quarto-cli/issues/10369)): Track theme generation and file naming through content hashing to allow different themes to coexist in the same project.