Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 26 additions & 9 deletions src/command/render/pandoc-html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,28 +88,45 @@ export async function resolveSassBundles(
// the brand bundle itself doesn't have any 'brand' entries;
// those are used to specify where the brand-specific layers should be inserted
// in the final bundle.
const brandLayersMaybeBrand = bundlesWithBrand.find((bundle) =>
const maybeBrandBundle = bundlesWithBrand.find((bundle) =>
bundle.key === "brand"
)?.user || [];
assert(!brandLayersMaybeBrand.find((v) => v === "brand"));
const brandLayers = brandLayersMaybeBrand as SassLayer[];
let foundBrand = false;
);
assert(!maybeBrandBundle ||
!maybeBrandBundle.user?.find((v) => v === "brand") &&
!maybeBrandBundle.dark?.user?.find((v) => v === "brand"));
let foundBrand = {light: false, dark: false};
const bundles: SassBundle[] = bundlesWithBrand.filter((bundle) =>
bundle.key !== "brand"
).map((bundle) => {
const userBrand = bundle.user?.findIndex((layer) => layer === "brand");
let cloned = false;
if (userBrand && userBrand !== -1) {
// console.log('light brand order specified', userBrand, cloned);
bundle = cloneDeep(bundle);
bundle.user!.splice(userBrand, 1, ...brandLayers);
foundBrand = true;
cloned = true;
bundle.user!.splice(userBrand, 1, ...(maybeBrandBundle?.user || []));
foundBrand.light = true;
}
const darkBrand = bundle.dark?.user?.findIndex((layer) => layer === "brand");
if (darkBrand && darkBrand !== -1) {
// console.log('dark brand order specified', darkBrand, cloned);
if (!cloned) {
bundle = cloneDeep(bundle);
}
bundle.dark!.user!.splice(darkBrand, 1, ...(maybeBrandBundle?.dark?.user || []))
foundBrand.dark = true;
}
return bundle as SassBundle;
});
if (!foundBrand) {
if (!foundBrand.light || !foundBrand.dark) {
bundles.unshift({
dependency,
key: "brand",
user: brandLayers,
user: !foundBrand.light && maybeBrandBundle?.user as SassLayer[] || [],
dark: !foundBrand.dark && maybeBrandBundle?.dark?.user && {
user: maybeBrandBundle.dark.user as SassLayer[],
default: maybeBrandBundle.dark.default
} || undefined
});
}

Expand Down
2 changes: 1 addition & 1 deletion src/command/render/pandoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1467,7 +1467,7 @@ async function resolveExtras(

// perform typst-specific merging
if (isTypstOutput(format.pandoc)) {
const brand = await project.resolveBrand(input);
const brand = (await project.resolveBrand(input))?.light;
const fontdirs: Set<string> = new Set();
const base_urls = {
google: "https://fonts.googleapis.com/css",
Expand Down
4 changes: 2 additions & 2 deletions src/command/render/render-contexts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -614,11 +614,11 @@ async function resolveFormats(

// resolve brand in project and forward it to format
const brand = await project.resolveBrand(target.source);
mergedFormats[format].render.brand = brand;
mergedFormats[format].render.brand = brand?.light;

// apply defaults from brand yaml under the metadata of the current format
const brandFormatDefaults: Metadata =
(brand?.data?.defaults?.quarto as unknown as Record<
(brand?.light?.data?.defaults?.quarto as unknown as Record<
string,
Record<string, Metadata>
>)?.format
Expand Down
5 changes: 5 additions & 0 deletions src/core/brand/brand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,11 @@ export class Brand {
}
}

export type LightDarkBrand = {
light?: Brand;
dark?: Brand;
};

export const getFavicon = (brand: Brand): string | undefined => {
const logoInfo = brand.getLogo("small");
if (!logoInfo) {
Expand Down
61 changes: 44 additions & 17 deletions src/core/sass/brand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ import {
BrandFontWeight,
} from "../../resources/types/schema-types.ts";
import { Brand } from "../brand/brand.ts";
import {
darkModeDefault,
} from "../../format/html/format-html-info.ts";


const defaultColorNameMap: Record<string, string> = {
"link-color": "link",
Expand Down Expand Up @@ -75,7 +79,10 @@ export async function brandBootstrapSassBundles(
return [{
key,
dependency: "bootstrap",
user: layers,
user: layers.light,
dark: {
user: layers.dark
}
}];
}

Expand Down Expand Up @@ -559,29 +566,42 @@ const brandTypographyLayer = (
};
};

export interface LightDarkSassLayers {
light: SassLayer[];
dark: SassLayer[];
}

export async function brandSassLayers(
fileName: string | undefined,
project: ProjectContext,
nameMap: Record<string, string> = {},
): Promise<SassLayer[]> {
): Promise<LightDarkSassLayers> {
const brand = await project.resolveBrand(fileName);
const sassLayers: SassLayer[] = [];
const sassLayers: LightDarkSassLayers = {
light: [],
dark: []
};

if (brand) {
sassLayers.push({
for (const layer of [sassLayers.light, sassLayers.dark]) {
layer.push({
defaults: '$theme: "brand" !default;',
uses: "",
functions: "",
mixins: "",
rules: "",
});
}
if (brand?.data.color) {
sassLayers.push(brandColorLayer(brand, nameMap));
if (brand?.light?.data.color) {
sassLayers.light.push(brandColorLayer(brand?.light, nameMap));
}

if (brand?.data.typography) {
sassLayers.push(brandTypographyLayer(brand));
if (brand?.dark?.data.color) {
sassLayers.dark.push(brandColorLayer(brand?.dark, nameMap));
}
if (brand?.light?.data.typography) {
sassLayers.light.push(brandTypographyLayer(brand?.light));
}
if (brand?.dark?.data.typography) {
sassLayers.dark.push(brandTypographyLayer(brand?.dark));
}

return sassLayers;
Expand All @@ -591,16 +611,19 @@ export async function brandBootstrapSassLayers(
fileName: string | undefined,
project: ProjectContext,
nameMap: Record<string, string> = {},
): Promise<SassLayer[]> {
): Promise<LightDarkSassLayers> {
const layers = await brandSassLayers(
fileName,
project,
nameMap,
);

const brand = await project.resolveBrand(fileName);
if (brand?.data?.defaults?.bootstrap) {
layers.unshift(brandDefaultsBootstrapLayer(brand));
if (brand?.light?.data?.defaults?.bootstrap) {
layers.light.unshift(brandDefaultsBootstrapLayer(brand.light));
}
if (brand?.dark?.data?.defaults?.bootstrap) {
layers.dark.unshift(brandDefaultsBootstrapLayer(brand.dark));
}

return layers;
Expand All @@ -611,16 +634,16 @@ export async function brandRevealSassLayers(
_format: Format,
project: ProjectContext,
): Promise<SassLayer[]> {
return brandSassLayers(
return (await brandSassLayers(
input,
project,
defaultColorNameMap,
);
)).light;
}

export async function brandSassFormatExtras(
input: string | undefined,
_format: Format,
format: Format,
project: ProjectContext,
): Promise<FormatExtras> {
const htmlSassBundleLayers = await brandBootstrapSassLayers(
Expand All @@ -634,7 +657,11 @@ export async function brandSassFormatExtras(
{
key: "brand",
dependency: "bootstrap",
user: htmlSassBundleLayers,
user: htmlSassBundleLayers.light,
dark: {
user: htmlSassBundleLayers.dark,
default: darkModeDefault(format.metadata)
}
},
],
},
Expand Down
13 changes: 6 additions & 7 deletions src/format/html/format-html-bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ import {
documentTitleScssLayer,
processDocumentTitle,
} from "./format-html-title.ts";
import {
darkModeDefault,
} from "./format-html-info.ts";

import { kTemplatePartials } from "../../command/render/template.ts";
import { isHtmlOutput } from "../../config/format.ts";
import { emplaceNotebookPreviews } from "./format-html-notebook.ts";
Expand Down Expand Up @@ -1062,13 +1066,8 @@ function bootstrapHtmlFinalizer(format: Format, flags: PandocFlags) {

// start body with light or dark class for proper display when JS is disabled
let initialLightDarkClass = "quarto-light";
// some logic duplicated from resolveThemeLayer
const theme = format.metadata.theme;
if (theme && !Array.isArray(theme) && typeof theme === "object") {
const keys = Object.keys(theme);
if(keys.length > 1 && keys[0] === "dark") {
initialLightDarkClass = "quarto-dark";
}
if (darkModeDefault(format.metadata)) {
initialLightDarkClass = "quarto-dark";
}
doc.body.classList.add(initialLightDarkClass);

Expand Down
19 changes: 10 additions & 9 deletions src/format/html/format-html-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Copyright (C) 2020-2022 Posit Software, PBC
*/

import { kTheme } from "../../config/constants.ts";
import { kTheme, kBrand } from "../../config/constants.ts";
import { isHtmlDashboardOutput, isHtmlOutput } from "../../config/format.ts";
import { Format, Metadata } from "../../config/types.ts";

Expand Down Expand Up @@ -40,14 +40,15 @@ export function formatDarkMode(format: Format): boolean | undefined {

export function darkModeDefault(metadata?: Metadata): boolean | undefined {
if (metadata !== undefined) {
const theme = metadata[kTheme];
if (theme && typeof (theme) === "object") {
const keys = Object.keys(theme);
if (keys.includes("dark")) {
if (keys[0] === "dark") {
return true;
} else {
return false;
for (const darkable of [metadata[kTheme], metadata[kBrand]]) {
if (darkable && typeof (darkable) === "object") {
const keys = Object.keys(darkable);
if (keys.includes("dark")) {
if (keys[0] === "dark") {
return true;
} else {
return false;
}
}
}
}
Expand Down
80 changes: 55 additions & 25 deletions src/project/project-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import { normalizeNewlines } from "../core/lib/text.ts";
import { DirectiveCell } from "../core/lib/break-quarto-md-types.ts";
import { QuartoJSONSchema, readYamlFromMarkdown } from "../core/yaml.ts";
import { refSchema } from "../core/lib/yaml-schema/common.ts";
import { Brand as BrandJson } from "../resources/types/schema-types.ts";
import { Brand as BrandJson, BrandPathBoolLightDark } from "../resources/types/schema-types.ts";
import { Brand } from "../core/brand/brand.ts";
import { warnOnce } from "../core/log.ts";
import { assert } from "testing/asserts";
Expand Down Expand Up @@ -512,7 +512,24 @@ export const ensureFileInformationCache = (
export async function projectResolveBrand(
project: ProjectContext,
fileName?: string,
) {
) : Promise<{light?: Brand, dark?: Brand} | undefined> {
async function loadBrand(brandPath: string) : Promise<Brand> {
const brand = await readAndValidateYamlFromFile(
brandPath,
refSchema("brand", "Format-independent brand configuration."),
"Brand validation failed for " + brandPath + ".",
) as BrandJson;
return new Brand(brand, dirname(brandPath), project.dir);
}
async function loadLocalBrand(brandPath: string) : Promise<Brand> {
let resolved: string = "";
if (brandPath.startsWith("/")) {
resolved = join(project.dir, brandPath);
} else {
resolved = join(dirname(fileName!), brandPath);
}
return await loadBrand(resolved);
}
if (fileName === undefined) {
if (project.brandCache) {
return project.brandCache.brand;
Expand All @@ -538,46 +555,59 @@ export async function projectResolveBrand(
refSchema("brand", "Format-independent brand configuration."),
"Brand validation failed for " + brandPath + ".",
) as BrandJson;
project.brandCache.brand = new Brand(
project.brandCache.brand = {light: new Brand(
brand,
dirname(brandPath),
project.dir,
);
)};
}
return project.brandCache.brand;
} else {
const metadata = await project.fileMetadata(fileName);
if (metadata.brand === false) {
const brand = metadata.brand as BrandPathBoolLightDark;
if (brand === false) {
return undefined;
}
if (metadata.brand === true || metadata.brand === undefined) {
if (brand === true || brand === undefined) {
return project.resolveBrand();
}
const fileInformation = ensureFileInformationCache(project, fileName);
if (fileInformation.brand) {
return fileInformation.brand;
}
if (typeof metadata.brand === "string") {
let brandPath: string = "";
if (brandPath.startsWith("/")) {
brandPath = join(project.dir, metadata.brand);
} else {
brandPath = join(dirname(fileName), metadata.brand);
}
const brand = await readAndValidateYamlFromFile(
brandPath,
refSchema("brand", "Format-independent brand configuration."),
"Brand validation failed for " + brandPath + ".",
) as BrandJson;
fileInformation.brand = new Brand(brand, dirname(brandPath), project.dir);
if (typeof brand === "string") {
fileInformation.brand = {light: await loadLocalBrand(brand)};
return fileInformation.brand;
} else {
assert(typeof metadata.brand === "object");
fileInformation.brand = new Brand(
metadata.brand as BrandJson,
dirname(fileName),
project.dir,
);
assert(typeof brand === "object");
if ("light" in brand || "dark" in brand) {
let light, dark;
if (typeof brand.light === "string") {
light = await loadLocalBrand(brand.light)
} else {
light = new Brand(
brand.light!,
dirname(fileName),
project.dir
);
}
if (typeof brand.dark === "string") {
dark = await loadLocalBrand(brand.dark)
} else {
dark = new Brand(
brand.dark!,
dirname(fileName),
project.dir
);
}
fileInformation.brand = {light, dark};
} else {
fileInformation.brand = {light: new Brand(
brand as BrandJson,
dirname(fileName),
project.dir,
)};
}
return fileInformation.brand;
}
}
Expand Down
Loading
Loading