Skip to content

Commit 106f5e0

Browse files
unified light and dark brand
fixes #12797 this introduces two brand schemas, "single" and "unified" brand.light and brand.dark can only take single brand takes unified and then splits it if there are no dark customizations then the unified brand has no dark mode
1 parent 6ca904a commit 106f5e0

24 files changed

+1047
-65
lines changed

news/changelog-1.8.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ All changes included in 1.8:
1919
- ([#12734](https://github.com/quarto-dev/quarto-cli/issues/12734)): `highlight-style` now correctly supports setting a different `light` and `dark`.
2020
- ([#12747](https://github.com/quarto-dev/quarto-cli/issues/12747)): Ensure `th` elements are properly restored when Quarto's HTML table processing is happening.
2121
- ([#12766](https://github.com/quarto-dev/quarto-cli/issues/12766)): Use consistent equation numbering display for `html-math-method` and `html-math-method.method` for MathJax and KaTeX (author: @mcanouil)
22+
- ([#12797](https://github.com/quarto-dev/quarto-cli/issues/12797)): Allow light and dark brands to be specified in one file, by specializing colors with `light:` and `dark:`.
2223

2324
### `revealjs`
2425

src/core/brand/brand.ts

Lines changed: 258 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@
77
*/
88

99
import {
10-
Brand as BrandJson,
10+
BrandColorLightDark,
1111
BrandFont,
1212
BrandLogoExplicitResource,
1313
BrandNamedThemeColor,
14-
BrandTypography,
14+
BrandSingle,
1515
BrandTypographyOptionsBase,
16-
BrandTypographyOptionsHeadings,
16+
BrandTypographyOptionsHeadingsSingle,
17+
BrandTypographySingle,
18+
BrandTypographyUnified,
19+
BrandUnified,
1720
Zod,
1821
} from "../../resources/types/zod/schema-types.ts";
1922
import { InternalError } from "../lib/error.ts";
@@ -53,7 +56,7 @@ type CanonicalLogoInfo = {
5356

5457
type ProcessedBrandData = {
5558
color: Record<string, string>;
56-
typography: BrandTypography;
59+
typography: BrandTypographySingle;
5760
logo: {
5861
small?: CanonicalLogoInfo;
5962
medium?: CanonicalLogoInfo;
@@ -63,7 +66,7 @@ type ProcessedBrandData = {
6366
};
6467

6568
export class Brand {
66-
data: BrandJson;
69+
data: BrandSingle;
6770
brandDir: string;
6871
projectDir: string;
6972
processedData: ProcessedBrandData;
@@ -73,13 +76,13 @@ export class Brand {
7376
brandDir: string,
7477
projectDir: string,
7578
) {
76-
this.data = Zod.Brand.parse(brand);
79+
this.data = Zod.BrandSingle.parse(brand);
7780
this.brandDir = brandDir;
7881
this.projectDir = projectDir;
7982
this.processedData = this.processData(this.data);
8083
}
8184

82-
processData(data: BrandJson): ProcessedBrandData {
85+
processData(data: BrandSingle): ProcessedBrandData {
8386
const color: Record<string, string> = {};
8487
for (const colorName of Object.keys(data.color?.palette ?? {})) {
8588
color[colorName] = this.getColor(colorName);
@@ -91,7 +94,7 @@ export class Brand {
9194
color[colorName] = this.getColor(colorName);
9295
}
9396

94-
const typography: BrandTypography = {};
97+
const typography: BrandTypographySingle = {};
9598
const base = this.getFont("base");
9699
if (base) {
97100
typography.base = base;
@@ -221,7 +224,10 @@ export class Brand {
221224

222225
getFont(
223226
name: string,
224-
): BrandTypographyOptionsBase | BrandTypographyOptionsHeadings | undefined {
227+
):
228+
| BrandTypographyOptionsBase
229+
| BrandTypographyOptionsHeadingsSingle
230+
| undefined {
225231
if (!this.data.typography) {
226232
return undefined;
227233
}
@@ -304,10 +310,253 @@ export type LightDarkBrand = {
304310
dark?: Brand;
305311
};
306312

313+
export type LightDarkColor = {
314+
light?: string;
315+
dark?: string;
316+
};
317+
307318
export const getFavicon = (brand: Brand): string | undefined => {
308319
const logoInfo = brand.getLogo("small");
309320
if (!logoInfo) {
310321
return undefined;
311322
}
312323
return logoInfo.light.path;
313324
};
325+
326+
function splitColorLightDark(
327+
bcld: BrandColorLightDark,
328+
): LightDarkColor {
329+
if (typeof bcld === "string") {
330+
return { light: bcld, dark: bcld };
331+
}
332+
return bcld;
333+
}
334+
function colorIsUnified(blcd: BrandColorLightDark) {
335+
return typeof blcd === "object" && "dark" in blcd;
336+
}
337+
export function brandIsUnified(brand: BrandUnified): boolean {
338+
if (brand.color) {
339+
for (const colorName of Zod.BrandNamedThemeColor.options) {
340+
if (!brand.color[colorName]) {
341+
continue;
342+
}
343+
if (colorIsUnified(brand.color![colorName])) {
344+
return true;
345+
}
346+
}
347+
}
348+
if (brand.typography) {
349+
for (const elementName of Zod.BrandNamedTypographyElements.options) {
350+
const element = brand.typography![elementName];
351+
if (!element || typeof element === "string") {
352+
continue;
353+
}
354+
if (
355+
"background-color" in element && element["background-color"] &&
356+
colorIsUnified(element["background-color"])
357+
) {
358+
return true;
359+
}
360+
if (
361+
"color" in element && element["color"] &&
362+
colorIsUnified(element["color"])
363+
) {
364+
return true;
365+
}
366+
}
367+
}
368+
return false;
369+
}
370+
function sharedTypography(
371+
unified: BrandTypographyUnified,
372+
): BrandTypographySingle {
373+
const ret: BrandTypographySingle = {
374+
fonts: unified.fonts,
375+
};
376+
for (const elementName of Zod.BrandNamedTypographyElements.options) {
377+
if (!unified[elementName]) {
378+
continue;
379+
}
380+
if (typeof unified[elementName] === "string") {
381+
ret[elementName] = unified[elementName];
382+
continue;
383+
}
384+
ret[elementName] = Object.fromEntries(
385+
Object.entries(unified[elementName]).filter(
386+
([key, _]) => !["color", "background-color"].includes(key),
387+
),
388+
);
389+
}
390+
return ret;
391+
}
392+
export function splitUnifiedBrand(
393+
unified: unknown,
394+
brandDir: string,
395+
projectDir: string,
396+
): LightDarkBrand {
397+
const unifiedBrand: BrandUnified = Zod.BrandUnified.parse(unified);
398+
let typography: BrandTypographySingle | undefined = undefined;
399+
let headingsColor: LightDarkColor | undefined = undefined;
400+
let monospaceColor: LightDarkColor | undefined = undefined;
401+
let monospaceBackgroundColor: LightDarkColor | undefined = undefined;
402+
let monospaceInlineColor: LightDarkColor | undefined = undefined;
403+
let monospaceInlineBackgroundColor: LightDarkColor | undefined = undefined;
404+
let monospaceBlockColor: LightDarkColor | undefined = undefined;
405+
let monospaceBlockBackgroundColor: LightDarkColor | undefined = undefined;
406+
let linkColor: LightDarkColor | undefined = undefined;
407+
let linkBackgroundColor: LightDarkColor | undefined = undefined;
408+
if (unifiedBrand.typography) {
409+
typography = sharedTypography(unifiedBrand.typography);
410+
if (
411+
unifiedBrand.typography.headings &&
412+
typeof unifiedBrand.typography.headings !== "string" &&
413+
unifiedBrand.typography.headings.color
414+
) {
415+
headingsColor = splitColorLightDark(
416+
unifiedBrand.typography.headings.color,
417+
);
418+
}
419+
if (
420+
unifiedBrand.typography.monospace &&
421+
typeof unifiedBrand.typography.monospace !== "string"
422+
) {
423+
if (unifiedBrand.typography.monospace.color) {
424+
monospaceColor = splitColorLightDark(
425+
unifiedBrand.typography.monospace.color,
426+
);
427+
}
428+
if (unifiedBrand.typography.monospace["background-color"]) {
429+
monospaceBackgroundColor = splitColorLightDark(
430+
unifiedBrand.typography.monospace["background-color"],
431+
);
432+
}
433+
}
434+
if (
435+
unifiedBrand.typography["monospace-inline"] &&
436+
typeof unifiedBrand.typography["monospace-inline"] !== "string"
437+
) {
438+
if (unifiedBrand.typography["monospace-inline"].color) {
439+
monospaceInlineColor = splitColorLightDark(
440+
unifiedBrand.typography["monospace-inline"].color,
441+
);
442+
}
443+
if (unifiedBrand.typography["monospace-inline"]["background-color"]) {
444+
monospaceInlineBackgroundColor = splitColorLightDark(
445+
unifiedBrand.typography["monospace-inline"]["background-color"],
446+
);
447+
}
448+
}
449+
if (
450+
unifiedBrand.typography["monospace-block"] &&
451+
typeof unifiedBrand.typography["monospace-block"] !== "string"
452+
) {
453+
if (unifiedBrand.typography["monospace-block"].color) {
454+
monospaceBlockColor = splitColorLightDark(
455+
unifiedBrand.typography["monospace-block"].color,
456+
);
457+
}
458+
if (unifiedBrand.typography["monospace-block"]["background-color"]) {
459+
monospaceBlockBackgroundColor = splitColorLightDark(
460+
unifiedBrand.typography["monospace-block"]["background-color"],
461+
);
462+
}
463+
}
464+
if (
465+
unifiedBrand.typography.link &&
466+
typeof unifiedBrand.typography.link !== "string"
467+
) {
468+
if (unifiedBrand.typography.link.color) {
469+
linkColor = splitColorLightDark(
470+
unifiedBrand.typography.link.color,
471+
);
472+
}
473+
if (unifiedBrand.typography.link["background-color"]) {
474+
linkBackgroundColor = splitColorLightDark(
475+
unifiedBrand.typography.link["background-color"],
476+
);
477+
}
478+
}
479+
}
480+
const specializeTypography = (
481+
typography: BrandTypographySingle,
482+
mode: "light" | "dark",
483+
) =>
484+
typography && {
485+
fonts: typography.fonts && [...typography.fonts],
486+
base: !typography.base || typeof typography.base === "string"
487+
? typography.base
488+
: { ...typography.base },
489+
headings: !typography.headings || typeof typography.headings === "string"
490+
? typography.headings
491+
: {
492+
...typography.headings,
493+
color: headingsColor && headingsColor[mode],
494+
},
495+
monospace:
496+
!typography.monospace || typeof typography.monospace === "string"
497+
? typography.monospace
498+
: {
499+
...typography.monospace,
500+
color: monospaceColor && monospaceColor[mode],
501+
"background-color": monospaceBackgroundColor &&
502+
monospaceBackgroundColor[mode],
503+
},
504+
"monospace-inline": !typography["monospace-inline"] ||
505+
typeof typography["monospace-inline"] === "string"
506+
? typography["monospace-inline"]
507+
: {
508+
...typography["monospace-inline"],
509+
color: monospaceInlineColor && monospaceInlineColor[mode],
510+
"background-color": monospaceInlineBackgroundColor &&
511+
monospaceInlineBackgroundColor[mode],
512+
},
513+
"monospace-block": !typography["monospace-block"] ||
514+
typeof typography["monospace-block"] === "string"
515+
? typography["monospace-block"]
516+
: {
517+
...typography["monospace-block"],
518+
color: monospaceBlockColor && monospaceBlockColor[mode],
519+
"background-color": monospaceBlockBackgroundColor &&
520+
monospaceBlockBackgroundColor[mode],
521+
},
522+
link: !typography.link || typeof typography.link === "string"
523+
? typography.link
524+
: {
525+
...typography.link,
526+
color: linkColor && linkColor[mode],
527+
"background-color": linkBackgroundColor &&
528+
linkBackgroundColor[mode],
529+
},
530+
};
531+
const lightBrand: BrandSingle = {
532+
meta: unifiedBrand.meta,
533+
color: { palette: unifiedBrand.color && { ...unifiedBrand.color.palette } },
534+
typography: typography && specializeTypography(typography, "light"),
535+
logo: unifiedBrand.logo,
536+
defaults: unifiedBrand.defaults,
537+
};
538+
const darkBrand: BrandSingle = {
539+
meta: unifiedBrand.meta,
540+
color: { palette: unifiedBrand.color && { ...unifiedBrand.color.palette } },
541+
typography: typography && specializeTypography(typography, "dark"),
542+
logo: unifiedBrand.logo,
543+
defaults: unifiedBrand.defaults,
544+
};
545+
if (unifiedBrand.color) {
546+
for (const colorName of Zod.BrandNamedThemeColor.options) {
547+
if (!unifiedBrand.color[colorName]) {
548+
continue;
549+
}
550+
({
551+
light: lightBrand.color![colorName],
552+
dark: darkBrand.color![colorName],
553+
} = splitColorLightDark(unifiedBrand.color![colorName]));
554+
}
555+
}
556+
return {
557+
light: new Brand(lightBrand, brandDir, projectDir),
558+
dark: brandIsUnified(unifiedBrand)
559+
? new Brand(darkBrand, brandDir, projectDir)
560+
: undefined,
561+
};
562+
}

src/core/sass/brand.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -664,7 +664,7 @@ export async function brandSassFormatExtras(
664664
dark: htmlSassBundleLayers.dark.length
665665
? {
666666
user: htmlSassBundleLayers.dark,
667-
default: darkModeDefault(format.metadata),
667+
default: darkModeDefault(format),
668668
}
669669
: undefined,
670670
},

src/format/html/format-html-bootstrap.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1072,7 +1072,7 @@ function bootstrapHtmlFinalizer(format: Format, flags: PandocFlags) {
10721072

10731073
// start body with light or dark class for proper display when JS is disabled
10741074
let initialLightDarkClass = "quarto-light";
1075-
if (darkModeDefault(format.metadata)) {
1075+
if (darkModeDefault(format)) {
10761076
initialLightDarkClass = "quarto-dark";
10771077
}
10781078
doc.body.classList.add(initialLightDarkClass);

0 commit comments

Comments
 (0)