Skip to content

_brand.yml - color field into HTML formats #10327

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jul 19, 2024
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
15 changes: 4 additions & 11 deletions src/command/render/pandoc-html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { cloneDeep, uniqBy } from "../../core/lodash.ts";
import {
Format,
FormatExtras,
FormatPandoc,
kDependencies,
kQuartoCssVariables,
kTextHighlightingMode,
Expand All @@ -36,6 +35,7 @@ import {
generateCssKeyValues,
} from "../../core/pandoc/css.ts";
import { kMinimal } from "../../format/html/format-html-shared.ts";
import { kSassBundles } from "../../config/types.ts";

// The output target for a sass bundle
// (controls the overall style tag that is emitted)
Expand All @@ -50,8 +50,6 @@ export async function resolveSassBundles(
extras: FormatExtras,
format: Format,
temp: TempContext,
formatBundles?: SassBundle[],
projectBundles?: SassBundle[],
project?: ProjectContext,
) {
extras = cloneDeep(extras);
Expand All @@ -71,14 +69,9 @@ export async function resolveSassBundles(
});
};

// group project provided bundles
if (projectBundles) {
group(projectBundles, mergedBundles);
}

// group format provided bundles
if (formatBundles) {
group(formatBundles, mergedBundles);
// group available sass bundles
if (extras?.["html"]?.[kSassBundles]) {
group(extras["html"][kSassBundles], mergedBundles);
}

// Go through and compile the cssPath for each dependency
Expand Down
34 changes: 20 additions & 14 deletions src/command/render/pandoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ import {
MarkdownPipelineHandler,
} from "../../core/markdown-pipeline.ts";
import { getEnv } from "../../../package/src/util/utils.ts";
import { brandSassFormatExtras } from "../../core/sass/brand.ts";

// in case we are running multiple pandoc processes
// we need to make sure we capture all of the trace files
Expand Down Expand Up @@ -404,9 +405,26 @@ export async function runPandoc(
))
: {};

const extras = await resolveExtras(
const brandExtras: FormatExtras = await brandSassFormatExtras(
options.format,
options.project,
);

// start with the merge
const inputExtras = mergeConfigs(
projectExtras,
formatExtras,
brandExtras,
// project documentclass always wins
{
metadata: {
[kDocumentClass]: projectExtras.metadata?.[kDocumentClass],
},
},
);

const extras = await resolveExtras(
inputExtras,
options.format,
cwd,
options.libDir,
Expand Down Expand Up @@ -1268,24 +1286,14 @@ function cleanupPandocMetadata(metadata: Metadata) {
}

async function resolveExtras(
projectExtras: FormatExtras,
formatExtras: FormatExtras,
extras: FormatExtras, // input format extras (project, format, brand)
format: Format,
inputDir: string,
libDir: string,
temp: TempContext,
dependenciesFile: string,
project?: ProjectContext,
) {
// start with the merge
let extras = mergeConfigs(projectExtras, formatExtras);

// project documentclass always wins
if (projectExtras.metadata?.[kDocumentClass]) {
extras.metadata = extras.metadata || {};
extras.metadata[kDocumentClass] = projectExtras.metadata?.[kDocumentClass];
}

// resolve format resources
await writeFormatResources(
inputDir,
Expand All @@ -1301,8 +1309,6 @@ async function resolveExtras(
extras,
format,
temp,
formatExtras.html?.[kSassBundles],
projectExtras.html?.[kSassBundles],
project,
);

Expand Down
4 changes: 2 additions & 2 deletions src/command/render/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,8 @@ export interface PandocOptions {
// optional execution engine
executionEngine?: string;

// optoinal project context
project?: ProjectContext;
// project context
project: ProjectContext;

// quiet quarto pandoc informational output
quiet?: boolean;
Expand Down
2 changes: 1 addition & 1 deletion src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ import {
import { HtmlPostProcessor, RenderServices } from "../command/render/types.ts";
import { QuartoFilterSpec } from "../command/render/types.ts";
import { ProjectContext } from "../project/types.ts";
import { Brand } from "../resources/types/schema-types.ts";
import { Brand } from "../core/brand/brand.ts";

export const kDependencies = "dependencies";
export const kSassBundles = "sass-bundles";
Expand Down
81 changes: 81 additions & 0 deletions src/core/brand/brand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* brand.ts
*
* Class that implements support for `_brand.yml` data in Quarto
*
* Copyright (C) 2024 Posit Software, PBC
*/

import {
Brand as BrandJson,
BrandNamedThemeColor,
} from "../../resources/types/schema-types.ts";

// we can't programmatically convert typescript types to string arrays,
// so we have to define this manually. They should match `BrandNamedThemeColor` in schema-types.ts

export const defaultColorNames: BrandNamedThemeColor[] = [
"foreground",
"background",
"primary",
"secondary",
"tertiary",
"success",
"info",
"warning",
"danger",
"light",
"dark",
"emphasis",
"link",
];

// const defaultFontNames: string[] = [
// "base",
// "emphasis",
// "heading",
// "link",
// "monospace",
// ];

export class Brand {
data: BrandJson;

constructor(readonly brand: BrandJson) {
this.data = brand;
}

// semantics of name resolution for colors, logo and fonts are:
// - if the name is in the "with" key, use that value as they key for a recursive call (so color names can be aliased or redefined away from scss defaults)
// - if the name is a default color name, call getColor recursively (so defaults can use named values)
// - otherwise, assume it's a color value and return it
getColor(name: string): string {
const seenValues = new Set<string>();

do {
if (seenValues.has(name)) {
throw new Error(
`Circular reference in _brand.yml color definitions: ${
Array.from(seenValues).join(
" -> ",
)
}`,
);
}
seenValues.add(name);
if (this.data.color?.with?.[name]) {
name = this.data.color.with[name];
} else if (
defaultColorNames.includes(name as BrandNamedThemeColor) &&
this.data.color?.[name as BrandNamedThemeColor]
) {
name = this.data.color[name as BrandNamedThemeColor]!;
} else {
return name;
}
} while (seenValues.size < 100); // 100 ought to be enough for anyone, with apologies to Bill Gates
throw new Error(
"Recursion depth exceeded 100 in _brand.yml color definitions",
);
}
}
62 changes: 62 additions & 0 deletions src/core/sass/brand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* brand.ts
*
* Generate SASS bundles from `_brand.yml`
*
* Copyright (C) 2024 Posit Software, PBC
*/

import {
Format,
FormatExtras,
kSassBundles,
SassBundle,
} from "../../config/types.ts";
import { ProjectContext } from "../../project/types.ts";

export async function brandSassFormatExtras(
_format: Format,
project: ProjectContext,
): Promise<FormatExtras> {
const brand = await project.resolveBrand();
if (!brand) {
return {};
}
const sassBundles: SassBundle[] = [];

if (brand?.data.color) {
const colorVariables: string[] = ["/* color variables from _brand.yml */"];
for (const colorKey of Object.keys(brand.data.color.with ?? {})) {
colorVariables.push(
`$${colorKey}: ${brand.getColor(colorKey)} !default;`,
);
}
for (const colorKey of Object.keys(brand.data.color)) {
if (colorKey === "with") {
continue;
}
colorVariables.push(
`$${colorKey}: ${brand.getColor(colorKey)} !default;`,
);
}
// const colorEntries = Object.keys(brand.color);
const colorBundle: SassBundle = {
key: "brand-color",
dependency: "bootstrap",
quarto: {
defaults: colorVariables.join("\n"),
uses: "",
functions: "",
mixins: "",
rules: "",
},
};
sassBundles.push(colorBundle);
}

return {
html: {
[kSassBundles]: sassBundles,
},
};
}
7 changes: 4 additions & 3 deletions src/project/project-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ import { normalizeNewlines } from "../core/lib/text.ts";
import { DirectiveCell } from "../core/lib/break-quarto-md-types.ts";
import { QuartoJSONSchema } from "../core/yaml.ts";
import { refSchema } from "../core/lib/yaml-schema/common.ts";
import { Brand } from "../resources/types/schema-types.ts";
import { Brand as BrandJson } from "../resources/types/schema-types.ts";
import { Brand } from "../core/brand/brand.ts";

export function projectExcludeDirs(context: ProjectContext): string[] {
const outputDir = projectOutputDir(context);
Expand Down Expand Up @@ -501,8 +502,8 @@ export async function projectResolveBrand(project: ProjectContext) {
brandPath,
refSchema("brand", "Format-independent brand configuration."),
"Brand validation failed for " + brandPath + ".",
) as Brand;
project.brandCache.brand = brand;
) as BrandJson;
project.brandCache.brand = new Brand(brand);
}
return project.brandCache.brand;
}
3 changes: 2 additions & 1 deletion src/project/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
import { RenderServices } from "../command/render/types.ts";
import { Metadata, PandocFlags } from "../config/types.ts";
import { Format, FormatExtras } from "../config/types.ts";
import { Brand } from "../core/brand/brand.ts";
import { MappedString } from "../core/mapped-text.ts";
import { PartitionedMarkdown } from "../core/pandoc/types.ts";
import { ExecutionEngine, ExecutionTarget } from "../execute/types.ts";
import { InspectedMdCell } from "../quarto-core/inspect-types.ts";
import { NotebookContext } from "../render/notebook/notebook-types.ts";
import {
Brand,
Brand as BrandJson,
NavigationItem as NavItem,
NavigationItemObject,
NavigationItemObject as SidebarTool,
Expand Down
21 changes: 16 additions & 5 deletions src/resources/editor/tools/vs-code.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12010,6 +12010,18 @@ var require_yaml_intelligence_resources = __commonJS({
schema: {
ref: "brand-color-value"
}
},
emphasis: {
description: "A color used to emphasize or highlight text or elements.\n",
schema: {
ref: "brand-color-value"
}
},
link: {
description: "The color used for hyperlinks. If not defined, the `primary` color is used.\n",
schema: {
ref: "brand-color-value"
}
}
}
}
Expand Down Expand Up @@ -21345,13 +21357,12 @@ var require_yaml_intelligence_resources = __commonJS({
"The brand\u2019s Facebook URL.",
"A link or path to the brand\u2019s light-colored logo or icon.",
"A link or path to the brand\u2019s dark-colored logo or icon.",
"Provide links to the brand\u2019s logo in various formats and sizes.",
"Provide definitions and defaults for brand\u2019s logo in various formats\nand sizes.",
"A link or path to the brand\u2019s small-sized logo or icon, or a link or\npath to both the light and dark versions.",
"A link or path to the brand\u2019s medium-sized logo, or a link or path to\nboth the light and dark versions.",
"A link or path to the brand\u2019s large- or full-sized logo, or a link or\npath to both the light and dark versions.",
"The brand\u2019s custom color palette and theme.",
"The brand\u2019s custom color palette. Any number of colors can be\ndefined, each color having a custom name.",
"The brand\u2019s theme colors. These are semantic or theme-oriented\ncolors.",
"The foreground color, used for text.",
"The background color, used for the page background.",
"The primary accent color, i.e.&nbsp;the main theme color. Typically used\nfor hyperlinks, active states, primary action buttons, etc.",
Expand Down Expand Up @@ -23667,12 +23678,12 @@ var require_yaml_intelligence_resources = __commonJS({
mermaid: "%%"
},
"handlers/mermaid/schema.yml": {
_internalId: 187374,
_internalId: 187413,
type: "object",
description: "be an object",
properties: {
"mermaid-format": {
_internalId: 187366,
_internalId: 187405,
type: "enum",
enum: [
"png",
Expand All @@ -23688,7 +23699,7 @@ var require_yaml_intelligence_resources = __commonJS({
exhaustiveCompletions: true
},
theme: {
_internalId: 187373,
_internalId: 187412,
type: "anyOf",
anyOf: [
{
Expand Down
Loading
Loading