From 2d21d0f676582383aaa5766d7911711498eb9a34 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Mon, 14 Apr 2025 11:56:10 -0400 Subject: [PATCH 1/2] brand - warn on invalid color name usage --- src/core/brand/brand.ts | 10 ++- src/core/css/color-names.ts | 163 ++++++++++++++++++++++++++++++++++++ src/core/sass/brand.ts | 23 +++-- 3 files changed, 183 insertions(+), 13 deletions(-) create mode 100644 src/core/css/color-names.ts diff --git a/src/core/brand/brand.ts b/src/core/brand/brand.ts index 529ba982bee..1415705afad 100644 --- a/src/core/brand/brand.ts +++ b/src/core/brand/brand.ts @@ -18,6 +18,8 @@ import { import { InternalError } from "../lib/error.ts"; import { join, relative } from "../../deno_ral/path.ts"; +import { warnOnce } from "../log.ts"; +import { isCssColorName } from "../css/color-names.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 @@ -176,7 +178,7 @@ export class Brand { // - if the name is in the "palette" 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 { + getColor(name: string, quiet = false): string { const seenValues = new Set(); do { @@ -198,6 +200,12 @@ export class Brand { ) { name = this.data.color[name as BrandNamedThemeColor]!; } else { + // if the name is not a default color name, assume it's a color value + if (!isCssColorName(name) && !quiet) { + warnOnce( + `"${name}" is not a valid CSS color name.\nThis might cause SCSS compilation to fail, or the color to have no effect.`, + ); + } return name; } } while (seenValues.size < 100); // 100 ought to be enough for anyone, with apologies to Bill Gates diff --git a/src/core/css/color-names.ts b/src/core/css/color-names.ts new file mode 100644 index 00000000000..c2cd0cce009 --- /dev/null +++ b/src/core/css/color-names.ts @@ -0,0 +1,163 @@ +/* + * color-names.ts + * + * Copyright (C) 2025 Posit Software, PBC + */ + +// https://www.w3.org/TR/css-color-3/#svg-color +export const isCssColorName = (name: string): boolean => { + if (name.startsWith("#")) return true; + if (name.startsWith("rgb")) return true; + if (name.startsWith("hsl")) return true; + if (name.startsWith("hwb")) return true; + + return [ + "aliceblue", + "antiquewhite", + "aqua", + "aquamarine", + "azure", + "beige", + "bisque", + "black", + "blanchedalmond", + "blue", + "blueviolet", + "brown", + "burlywood", + "cadetblue", + "chartreuse", + "chocolate", + "coral", + "cornflowerblue", + "cornsilk", + "crimson", + "cyan", + "darkblue", + "darkcyan", + "darkgoldenrod", + "darkgray", + "darkgreen", + "darkgrey", + "darkkhaki", + "darkmagenta", + "darkolivegreen", + "darkorange", + "darkorchid", + "darkred", + "darksalmon", + "darkseagreen", + "darkslateblue", + "darkslategray", + "darkslategrey", + "darkturquoise", + "darkviolet", + "deeppink", + "deepskyblue", + "dimgray", + "dimgrey", + "dodgerblue", + "firebrick", + "floralwhite", + "forestgreen", + "fuchsia", + "gainsboro", + "ghostwhite", + "gold", + "goldenrod", + "gray", + "green", + "greenyellow", + "grey", + "honeydew", + "hotpink", + "indianred", + "indigo", + "ivory", + "khaki", + "lavender", + "lavenderblush", + "lawngreen", + "lemonchiffon", + "lightblue", + "lightcoral", + "lightcyan", + "lightgoldenrodyellow", + "lightgray", + "lightgreen", + "lightgrey", + "lightpink", + "lightsalmon", + "lightseagreen", + "lightskyblue", + "lightslategray", + "lightslategrey", + "lightsteelblue", + "lightyellow", + "lime", + "limegreen", + "linen", + "magenta", + "maroon", + "mediumaquamarine", + "mediumblue", + "mediumorchid", + "mediumpurple", + "mediumseagreen", + "mediumslateblue", + "mediumspringgreen", + "mediumturquoise", + "mediumvioletred", + "midnightblue", + "mintcream", + "mistyrose", + "moccasin", + "navajowhite", + "navy", + "oldlace", + "olive", + "olivedrab", + "orange", + "orangered", + "orchid", + "palegoldenrod", + "palegreen", + "paleturquoise", + "palevioletred", + "papayawhip", + "peachpuff", + "peru", + "pink", + "plum", + "powderblue", + "purple", + "red", + "rosybrown", + "royalblue", + "saddlebrown", + "salmon", + "sandybrown", + "seagreen", + "seashell", + "sienna", + "silver", + "skyblue", + "slateblue", + "slategray", + "slategrey", + "snow", + "springgreen", + "steelblue", + "tan", + "teal", + "thistle", + "tomato", + "turquoise", + "violet", + "wheat", + "white", + "whitesmoke", + "yellow", + "yellowgreen", + ].includes(name); +}; diff --git a/src/core/sass/brand.ts b/src/core/sass/brand.ts index 93c1a2effab..9455023a9b3 100644 --- a/src/core/sass/brand.ts +++ b/src/core/sass/brand.ts @@ -22,10 +22,7 @@ import { BrandFontWeight, } from "../../resources/types/schema-types.ts"; import { Brand } from "../brand/brand.ts"; -import { - darkModeDefault, -} from "../../format/html/format-html-info.ts"; - +import { darkModeDefault } from "../../format/html/format-html-info.ts"; const defaultColorNameMap: Record = { "link-color": "link", @@ -81,8 +78,8 @@ export async function brandBootstrapSassBundles( dependency: "bootstrap", user: layers.light, dark: { - user: layers.dark - } + user: layers.dark, + }, }]; } @@ -187,7 +184,7 @@ const brandColorLayer = ( // format-specific name mapping for (const [key, value] of Object.entries(nameMap)) { - const resolvedValue = brand.getColor(value); + const resolvedValue = brand.getColor(value, true); if (resolvedValue !== value) { colorVariables.push( `$${key}: ${resolvedValue} !default;`, @@ -579,7 +576,7 @@ export async function brandSassLayers( const brand = await project.resolveBrand(fileName); const sassLayers: LightDarkSassLayers = { light: [], - dark: [] + dark: [], }; for (const mode of ["light", "dark"] as Array<"dark" | "light">) { @@ -660,10 +657,12 @@ export async function brandSassFormatExtras( key: "brand", dependency: "bootstrap", user: htmlSassBundleLayers.light, - dark: htmlSassBundleLayers.dark.length ? { - user: htmlSassBundleLayers.dark, - default: darkModeDefault(format.metadata) - } : undefined + dark: htmlSassBundleLayers.dark.length + ? { + user: htmlSassBundleLayers.dark, + default: darkModeDefault(format.metadata), + } + : undefined, }, ], }, From 280e7eedfb6d7370e190485b5bcc775cf34ea260 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Mon, 14 Apr 2025 19:02:41 +0200 Subject: [PATCH 2/2] test - add a simple check for warning only for now --- tests/docs/smoke-all/2025/04/14/11665.qmd | 27 +++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tests/docs/smoke-all/2025/04/14/11665.qmd diff --git a/tests/docs/smoke-all/2025/04/14/11665.qmd b/tests/docs/smoke-all/2025/04/14/11665.qmd new file mode 100644 index 00000000000..1dfc5964c27 --- /dev/null +++ b/tests/docs/smoke-all/2025/04/14/11665.qmd @@ -0,0 +1,27 @@ +--- +title: Hello, `_brand.yml`! +brand: + typography: + monospace: + color: foreground +format: html +_quarto: + tests: + html: + noErrors: default + printsMessage: + level: WARN + regex: 'foreground.*valid CSS color name' +--- + +## Links + +Download the Positron Beta here: + +Docs for `_brand.yml`: , + +## Placeholder text + +This is an example document with a [number](https://quarto.org) of `features` to help you come with a nicely branded document. + +Duis ornare ex ac iaculis pretium. Maecenas sagittis odio id erat pharetra, sit amet consectetur quam sollicitudin. Vivamus pharetra quam purus, nec sagittis risus pretium at. Nullam feugiat, turpis ac accumsan interdum, sem tellus blandit neque, id vulputate diam quam semper nisl. Donec sit amet enim at neque porttitor aliquet. Phasellus facilisis nulla eget placerat eleifend. Vestibulum non egestas eros, eget lobortis ipsum. Nulla rutrum massa eget enim aliquam, id porttitor erat luctus. Nunc sagittis quis eros eu sagittis. Pellentesque dictum, erat at pellentesque sollicitudin, justo augue pulvinar metus, quis rutrum est mi nec felis. Vestibulum efficitur mi lorem, at elementum purus tincidunt a. Aliquam finibus enim magna, vitae pellentesque erat faucibus at. Nulla mauris tellus, imperdiet id lobortis et, dignissim condimentum ipsum. Morbi nulla orci, varius at aliquet sed, facilisis id tortor. Donec ut urna nisi. \ No newline at end of file