From 8b446c94f0b9bec240daba74da8a7af02f606b8b Mon Sep 17 00:00:00 2001 From: Benji Grant Date: Fri, 24 Oct 2025 18:49:34 +1100 Subject: [PATCH] Parse and display inlays for palette colors --- palettes.css | 4 ++++ src/live.ts | 21 +++++++++++++------ src/palettes.ts | 51 +++++++++++++++++++++++++++++++++++++++++++++++ src/parseColor.ts | 33 +++++++++++++++++++++--------- src/preview.ts | 22 +++++++++++--------- styles.css | 4 ++-- tsconfig.json | 2 +- 7 files changed, 110 insertions(+), 27 deletions(-) diff --git a/palettes.css b/palettes.css index 62a15ca..ae3ab9b 100644 --- a/palettes.css +++ b/palettes.css @@ -9,3 +9,7 @@ fs: Federal Standard (FS 595C, ANA) pantone: Pantone ral: RAL Colors (Classic, Design, Effect, Plastics) */ + +.css-color-inlay { + &.aci-0 { color: #000000; } +} diff --git a/src/live.ts b/src/live.ts index dee05bf..b5288e6 100644 --- a/src/live.ts +++ b/src/live.ts @@ -12,6 +12,7 @@ import type { NodeProp } from '@lezer/common' import { type Color, formatHex } from 'culori' import { editorLivePreviewField } from 'obsidian' import { formatColor } from './formatColor' +import { getPaletteClass, getPaletteClasses } from './palettes' import { parseColor } from './parseColor' import type { CssColorsPluginSettings } from './settings' @@ -44,7 +45,7 @@ export const inlayExtension = (settings: CssColorsPluginSettings) => { class CSSColorInlayWidget extends WidgetType { constructor( readonly text: string, - readonly color: Color, + readonly color: Color | 'palette', readonly colorPickerEnabled: boolean, readonly hideName: boolean, readonly view: EditorView, @@ -58,10 +59,14 @@ class CSSColorInlayWidget extends WidgetType { toDOM() { const inlay = document.createElement('label') - inlay.className = `css-color-inlay ${this.hideName ? 'css-color-name-hidden' : ''}` - inlay.style.setProperty('--css-color-inlay-color', this.text) + inlay.className = `css-color-inlay ${ + this.color === 'palette' + ? `css-color-palette ${getPaletteClass(this.text)}` + : '' + } ${this.hideName ? 'css-color-name-hidden' : ''}` + inlay.style.color = this.text - if (this.colorPickerEnabled) { + if (this.colorPickerEnabled && this.color !== 'palette') { const input = document.createElement('input') input.type = 'color' input.value = formatHex(this.color) @@ -78,7 +83,7 @@ class CSSColorInlayWidget extends WidgetType { to: pos + this.text.length, insert: formatColor( this.text, - this.color, + this.color as Color, (e.currentTarget as HTMLInputElement).value, ), }, @@ -103,6 +108,7 @@ const createColorWidgets = ( if (!view.state.field(editorLivePreviewField)) return Decoration.none const widgets: Range[] = [] + const paletteClasses = getPaletteClasses() for (const { from, to } of view.visibleRanges) { syntaxTree(view.state).iterate({ @@ -112,7 +118,10 @@ const createColorWidgets = ( if ( node.type.prop(tokenClassNodeProp)?.split(' ').includes('inline-code') ) { - const res = parseColor(view.state.sliceDoc(node.from, node.to)) + const res = parseColor( + view.state.sliceDoc(node.from, node.to), + paletteClasses, + ) if (!res) return const { text, color, isNameHidden } = res diff --git a/src/palettes.ts b/src/palettes.ts index 864504f..8d9bc3c 100644 --- a/src/palettes.ts +++ b/src/palettes.ts @@ -2,6 +2,57 @@ import { type App, Modal, Notice, normalizePath, Setting } from 'obsidian' export const SNIPPET_NAME = 'css-inlay-palettes' +/** Normalize a palette name into a class */ +export const getPaletteClass = (text: string) => + text.trim().toLocaleLowerCase().replace(/\s+/, '-') + +const parsePaletteClasses = (selectorText: string) => { + return selectorText.split(',').flatMap((selector) => { + const name = selector + .replaceAll('.css-color-inlay', '') + .replace(/[&.]/g, '') + .trim() + return name.toLocaleLowerCase() === name ? [name] : [] + }) +} + +/** Get a list of all palette classes available */ +export const getPaletteClasses = () => { + const classes = [] + + for (const sheet of document.styleSheets) { + for (const rule of sheet.cssRules) { + if ( + rule instanceof CSSStyleRule && + rule.selectorText.contains('.css-color-inlay') + ) { + // Nested rule + if (rule.cssRules.length > 0) { + for (const nestedRule of rule.cssRules) { + if ( + nestedRule instanceof CSSStyleRule && + nestedRule.selectorText.startsWith('&') && + nestedRule.styleMap.has('color') + ) { + classes.push(...parsePaletteClasses(nestedRule.selectorText)) + } + } + } + // Individual rule (.css-color-inlay.my-color) + if ( + rule.selectorText !== '.css-color-inlay' && + rule.cssRules.length === 0 && + rule.styleMap.has('color') + ) { + classes.push(...parsePaletteClasses(rule.selectorText)) + } + } + } + } + + return classes +} + const createPaletteFilePath = (app: App) => normalizePath(`${app.vault.configDir}/snippets/${SNIPPET_NAME}.css`) diff --git a/src/parseColor.ts b/src/parseColor.ts index 13c0934..efe9e75 100644 --- a/src/parseColor.ts +++ b/src/parseColor.ts @@ -1,6 +1,7 @@ import { type Color, parse, parseHex } from 'culori' +import { getPaletteClass } from 'src/palettes' -export const parseColor = (text: string) => { +export const parseColor = (text: string, paletteClasses: string[]) => { // If color is surrounded with square brackets, the name should be hidden let isNameHidden = false if (text.startsWith('[') && text.endsWith(']')) { @@ -9,14 +10,28 @@ export const parseColor = (text: string) => { } // Check if color is valid - let color: Color | undefined - try { - color = parse(text) - if (color === undefined) return - // Ignore hex colors that don't start with a hash - if (color.mode === 'rgb' && parseHex(text) && text.charAt(0) !== '#') return - } catch { - return + let color: Color | 'palette' | undefined + + // Is palette color + if ( + text.startsWith('(') && + text.endsWith(')') && + paletteClasses.includes(getPaletteClass(text.slice(1, -1))) + ) { + text = text.slice(1, -1) + color = 'palette' + } + + if (!color) { + try { + color = parse(text) + if (color === undefined) return + // Ignore hex colors that don't start with a hash + if (color.mode === 'rgb' && parseHex(text) && text.charAt(0) !== '#') + return + } catch { + return + } } return { text, color, isNameHidden } diff --git a/src/preview.ts b/src/preview.ts index a53c9d6..815ee16 100644 --- a/src/preview.ts +++ b/src/preview.ts @@ -1,23 +1,27 @@ import { Notice } from 'obsidian' +import { getPaletteClass, getPaletteClasses } from './palettes' import { parseColor } from './parseColor' import type { CssColorsPluginSettings } from './settings' export const inlayPostProcessor = (settings: CssColorsPluginSettings) => (el: HTMLElement) => { + const paletteClasses = getPaletteClasses() + for (const code of el.findAll('code')) { - const res = parseColor(code.innerText.trim()) + const res = parseColor(code.innerText.trim(), paletteClasses) if (!res) continue - const { text, isNameHidden } = res + const { text, color, isNameHidden } = res // Clear codeblock before adding inlay - if (settings.hideNames || isNameHidden) { - code.innerHTML = '' - } + if (settings.hideNames || isNameHidden) code.empty() + + const paletteClass = + color === 'palette' ? `css-color-palette ${getPaletteClass(text)}` : '' code.createSpan({ prepend: true, - cls: `css-color-inlay ${settings.hideNames || isNameHidden ? 'css-color-name-hidden' : ''}`, - attr: { style: `--css-color-inlay-color: ${text};` }, + cls: `css-color-inlay ${paletteClass} ${settings.hideNames || isNameHidden ? 'css-color-name-hidden' : ''}`, + attr: { style: `color: ${text};` }, }) if (settings.copyOnClick) { @@ -31,8 +35,8 @@ export const inlayPostProcessor = .then(() => { const toast = document.createDocumentFragment() const toastColor = document.createElement('span') - toastColor.className = 'css-color-inlay' - toastColor.style = `--css-color-inlay-color: ${text};` + toastColor.className = `css-color-inlay ${paletteClass}` + toastColor.style.color = text toast.append(toastColor) toast.append( document.createTextNode('Copied color to the clipboard'), diff --git a/styles.css b/styles.css index 74b1ea5..d89c988 100644 --- a/styles.css +++ b/styles.css @@ -1,13 +1,13 @@ .css-color-inlay { height: 1em; width: 1em; - border: 1px solid currentColor; + border: 1px solid var(--code-normal); display: inline-block; border-radius: 0.2em; vertical-align: middle; margin-right: 0.3em; margin-top: -0.1em; - background: var(--css-color-inlay-color); + background: currentColor; &.css-color-name-hidden { margin-right: 0; diff --git a/tsconfig.json b/tsconfig.json index cb16951..af872e2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,7 @@ "skipLibCheck": true, "strict": true, "noEmit": true, - "lib": ["DOM", "ES5", "ES6", "ES7", "ES2024"] + "lib": ["DOM", "DOM.Iterable", "ES5", "ES6", "ES7", "ES2024"] }, "include": ["**/*.ts"] }