Parse and display inlays for palette colors

This commit is contained in:
Benji Grant 2025-10-24 18:49:34 +11:00
parent 863b0c1bdf
commit 8b446c94f0
No known key found for this signature in database
GPG key ID: D41929A51D291D4D
7 changed files with 110 additions and 27 deletions

View file

@ -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; }
}

View file

@ -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<Decoration>[] = []
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

View file

@ -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`)

View file

@ -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 }

View file

@ -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'),

View file

@ -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;

View file

@ -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"]
}