mirror of
https://github.com/nellowtcs/Obsidian-IroView.git
synced 2026-07-22 06:56:57 +00:00
Le big cleanup starts
This commit is contained in:
parent
71f0e3eb2b
commit
726bcefe89
17 changed files with 4224 additions and 1363 deletions
|
|
@ -1,3 +0,0 @@
|
|||
node_modules/
|
||||
|
||||
main.js
|
||||
23
.eslintrc
23
.eslintrc
|
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"env": { "node": true },
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"@typescript-eslint/no-empty-function": "off"
|
||||
}
|
||||
}
|
||||
138
.github/workflows/build.yml
vendored
138
.github/workflows/build.yml
vendored
|
|
@ -1,86 +1,86 @@
|
|||
name: Build Obsidian plugin
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18.x"
|
||||
- name: Use Node.js 18
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18.x"
|
||||
|
||||
- name: Cache npm
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
- name: Cache npm
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm i
|
||||
- name: Install dependencies
|
||||
run: npm i
|
||||
|
||||
- name: Build plugin
|
||||
run: npm run build
|
||||
- name: Build plugin
|
||||
run: npm run build
|
||||
|
||||
- name: Collect dist/main.js and manifest.json and styles.css
|
||||
id: meta
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p build-output
|
||||
- name: Collect dist/main.js and manifest.json and styles.css
|
||||
id: meta
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p build-output
|
||||
|
||||
# copy dist/main.js
|
||||
if [ -f "dist/main.js" ]; then
|
||||
cp -v dist/main.js build-output/main.js
|
||||
else
|
||||
echo "Error: dist/main.js not found."
|
||||
ls -la dist || true
|
||||
exit 1
|
||||
fi
|
||||
# copy dist/main.js
|
||||
if [ -f "dist/main.js" ]; then
|
||||
cp -v dist/main.js build-output/main.js
|
||||
else
|
||||
echo "Error: dist/main.js not found."
|
||||
ls -la dist || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# copy dist/styles.css
|
||||
if [ -f "dist/styles.css" ]; then
|
||||
cp -v dist/styles.css build-output/styles.css
|
||||
else
|
||||
echo "Error: dist/styles.css not found."
|
||||
ls -la dist || true
|
||||
exit 1
|
||||
fi
|
||||
# copy dist/styles.css
|
||||
if [ -f "dist/styles.css" ]; then
|
||||
cp -v dist/styles.css build-output/styles.css
|
||||
else
|
||||
echo "Error: dist/styles.css not found."
|
||||
ls -la dist || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# copy manifest.json from repo root
|
||||
if [ -f "manifest.json" ]; then
|
||||
cp -v manifest.json build-output/manifest.json
|
||||
else
|
||||
echo "Error: manifest.json not found in repository root."
|
||||
exit 2
|
||||
fi
|
||||
# copy manifest.json from repo root
|
||||
if [ -f "manifest.json" ]; then
|
||||
cp -v manifest.json build-output/manifest.json
|
||||
else
|
||||
echo "Error: manifest.json not found in repository root."
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# set outputs for artifact naming (requires jq, already in GitHub runners)
|
||||
PACKAGE_NAME=$(jq -r .name manifest.json)
|
||||
PACKAGE_VERSION=$(jq -r .version manifest.json)
|
||||
echo "package_name=${PACKAGE_NAME}" >> "$GITHUB_OUTPUT"
|
||||
echo "package_version=${PACKAGE_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
# set outputs for artifact naming (requires jq, already in GitHub runners)
|
||||
PACKAGE_NAME=$(jq -r .name manifest.json)
|
||||
PACKAGE_VERSION=$(jq -r .version manifest.json)
|
||||
echo "package_name=${PACKAGE_NAME}" >> "$GITHUB_OUTPUT"
|
||||
echo "package_version=${PACKAGE_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "build-output contents:"
|
||||
ls -la build-output
|
||||
echo "build-output contents:"
|
||||
ls -la build-output
|
||||
|
||||
- name: Upload artifact (main.js + styles.css+ manifest.json)
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.meta.outputs.package_name }}-${{ steps.meta.outputs.package_version }}-${{ github.run_id }}
|
||||
path: |
|
||||
build-output/main.js
|
||||
build-output/styles.css
|
||||
build-output/manifest.json
|
||||
- name: Upload artifact (main.js + styles.css+ manifest.json)
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.meta.outputs.package_name }}-${{ steps.meta.outputs.package_version }}-${{ github.run_id }}
|
||||
path: |
|
||||
build-output/main.js
|
||||
build-output/styles.css
|
||||
build-output/manifest.json
|
||||
|
|
|
|||
64
.github/workflows/release.yml
vendored
64
.github/workflows/release.yml
vendored
|
|
@ -1,41 +1,41 @@
|
|||
name: Release Obsidian plugin
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18.x"
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18.x"
|
||||
|
||||
- name: Build plugin
|
||||
run: |
|
||||
npm install
|
||||
npm run build
|
||||
- name: Build plugin
|
||||
run: |
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
- name: Stage release files
|
||||
run: |
|
||||
mkdir -p build-output
|
||||
cp dist/main.js build-output/
|
||||
cp dist/styles.css build-output/
|
||||
cp manifest.json build-output/
|
||||
- name: Stage release files
|
||||
run: |
|
||||
mkdir -p build-output
|
||||
cp dist/main.js build-output/
|
||||
cp dist/styles.css build-output/
|
||||
cp manifest.json build-output/
|
||||
|
||||
- name: Create release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
tag="${GITHUB_REF#refs/tags/}"
|
||||
gh release create "$tag" \
|
||||
--title="$tag" \
|
||||
--draft \
|
||||
build-output/main.js build-output/manifest.json build-output/styles.css
|
||||
- name: Create release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
tag="${GITHUB_REF#refs/tags/}"
|
||||
gh release create "$tag" \
|
||||
--title="$tag" \
|
||||
--draft \
|
||||
build-output/main.js build-output/manifest.json build-output/styles.css
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
# Obsidian-ColorPreview
|
||||
|
||||
A color preview for HEX, HSL, RGB values in your notes, similar to VSCode's color preview.
|
||||
|
||||
> **WIP**
|
||||
|
|
|
|||
|
|
@ -4,14 +4,13 @@ import builtins from "builtin-modules";
|
|||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
const banner =
|
||||
`/*
|
||||
const banner = `/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
if you want to view the source, please visit the github repository of this plugin
|
||||
*/
|
||||
`;
|
||||
|
||||
const prod = (process.argv[2] === "production");
|
||||
const prod = process.argv[2] === "production";
|
||||
|
||||
// Helper function to copy files
|
||||
async function copyFile(src, destDir) {
|
||||
|
|
@ -42,7 +41,7 @@ const context = await esbuild.context({
|
|||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
...builtins
|
||||
...builtins,
|
||||
],
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
|
|
|
|||
30
eslint.config.mjs
Normal file
30
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import js from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
import obsidianmd from "eslint-plugin-obsidianmd";
|
||||
|
||||
export default tseslint.config(
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
...obsidianmd.configs.recommended,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"no-console": ["error", { "allow": ["warn", "error", "debug"] }],
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ["dist/**/*", "node_modules/**/*", "main.js"]
|
||||
}
|
||||
);
|
||||
4693
package-lock.json
generated
4693
package-lock.json
generated
File diff suppressed because it is too large
Load diff
26
package.json
26
package.json
|
|
@ -6,19 +6,29 @@
|
|||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"lint": "eslint src/",
|
||||
"make-pretty": "prettier --write .",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
"builtin-modules": "3.3.0",
|
||||
"esbuild": "0.17.3",
|
||||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/node": "^25.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.59.1",
|
||||
"@typescript-eslint/parser": "8.59.1",
|
||||
"builtin-modules": "5.1.0",
|
||||
"esbuild": "0.28.0",
|
||||
"eslint": "^10.2.1",
|
||||
"eslint-plugin-obsidianmd": "^0.2.9",
|
||||
"obsidian": "1.12.3",
|
||||
"prettier": "^3.8.3",
|
||||
"tslib": "2.8.1",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "^8.59.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/view": "^6.38.6"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,24 +7,29 @@ import {
|
|||
WidgetType,
|
||||
MatchDecorator,
|
||||
PluginValue,
|
||||
} from '@codemirror/view';
|
||||
import { hasGoodContrast } from '../utils/colorParser';
|
||||
import type { ColorPreviewSettings } from '../types';
|
||||
} from "@codemirror/view";
|
||||
import { hasGoodContrast } from "../utils/colorParser";
|
||||
import type { ColorPreviewSettings } from "../types";
|
||||
|
||||
// Regex source
|
||||
const HEX_SRC = /#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{4}|[0-9a-fA-F]{3})(?![0-9a-fA-F])/;
|
||||
const RGB_SRC = /rgba?\(\s*(?:25[0-5]|2[0-4]\d|1?\d{1,2})\s*,\s*(?:25[0-5]|2[0-4]\d|1?\d{1,2})\s*,\s*(?:25[0-5]|2[0-4]\d|1?\d{1,2})(?:\s*,\s*(?:0|1|0?\.\d+))?\s*\)/;
|
||||
const HSL_SRC = /hsla?\(\s*(?:36[0]|3[0-5]\d|[12]?\d{1,2})\s*,\s*(?:100|\d{1,2})%\s*,\s*(?:100|\d{1,2})%(?:\s*,\s*(?:0|1|0?\.\d+))?\s*\)/;
|
||||
const COMBINED_SRC = [HEX_SRC, RGB_SRC, HSL_SRC].map(r => r.source).join('|');
|
||||
// Regex
|
||||
const HEX_SRC =
|
||||
/#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{4}|[0-9a-fA-F]{3})(?![0-9a-fA-F])/;
|
||||
const RGB_SRC =
|
||||
/rgba?\(\s*(?:25[0-5]|2[0-4]\d|1?\d{1,2})\s*,\s*(?:25[0-5]|2[0-4]\d|1?\d{1,2})\s*,\s*(?:25[0-5]|2[0-4]\d|1?\d{1,2})(?:\s*,\s*(?:0|1|0?\.\d+))?\s*\)/;
|
||||
const HSL_SRC =
|
||||
/hsla?\(\s*(?:36[0]|3[0-5]\d|[12]?\d{1,2})\s*,\s*(?:100|\d{1,2})%\s*,\s*(?:100|\d{1,2})%(?:\s*,\s*(?:0|1|0?\.\d+))?\s*\)/;
|
||||
const COMBINED_SRC = [HEX_SRC, RGB_SRC, HSL_SRC].map((r) => r.source).join("|");
|
||||
|
||||
// ColorWidget
|
||||
// Widget
|
||||
class ColorWidget extends WidgetType {
|
||||
constructor(
|
||||
readonly color: string,
|
||||
readonly originalText: string,
|
||||
readonly showSwatch: boolean,
|
||||
readonly colorizeText: boolean,
|
||||
) { super(); }
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
eq(other: ColorWidget): boolean {
|
||||
return (
|
||||
|
|
@ -36,109 +41,87 @@ class ColorWidget extends WidgetType {
|
|||
}
|
||||
|
||||
toDOM(): HTMLElement {
|
||||
const wrapper = document.createElement('span');
|
||||
wrapper.className = 'cp-color-inline';
|
||||
// Let CM6 know this widget represents the replaced text, for a11y
|
||||
wrapper.setAttribute('aria-label', `Color: ${this.color}`);
|
||||
const wrapper = document.createElement("span");
|
||||
wrapper.className = "cp-color-inline";
|
||||
wrapper.setAttribute("aria-label", `Color: ${this.color}`);
|
||||
|
||||
if (this.showSwatch) {
|
||||
const swatch = document.createElement('span');
|
||||
swatch.className = 'cp-color-swatch';
|
||||
swatch.style.backgroundColor = this.color;
|
||||
const swatch = document.createElement("span");
|
||||
swatch.className = "cp-color-swatch";
|
||||
swatch.setCssProps({ "--cp-swatch-color": this.color });
|
||||
wrapper.appendChild(swatch);
|
||||
}
|
||||
|
||||
const label = document.createElement('span');
|
||||
const label = document.createElement("span");
|
||||
label.textContent = this.originalText;
|
||||
|
||||
const canColorize = this.colorizeText && hasGoodContrast(this.color);
|
||||
if (canColorize) {
|
||||
label.className = 'cp-colored-text';
|
||||
label.style.color = this.color;
|
||||
if (this.colorizeText && hasGoodContrast(this.color)) {
|
||||
label.className = "cp-colored-text";
|
||||
label.setCssProps({ "--cp-text-color": this.color });
|
||||
}
|
||||
|
||||
wrapper.appendChild(label);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
ignoreEvent(): boolean { return false; }
|
||||
ignoreEvent(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// MatchDecorator factory
|
||||
// MatchDecorator
|
||||
function makeDecorator(settings: ColorPreviewSettings): MatchDecorator {
|
||||
return new MatchDecorator({
|
||||
regexp: new RegExp(COMBINED_SRC, 'gi'),
|
||||
decoration: (match) => {
|
||||
const color = match[0];
|
||||
return Decoration.replace({
|
||||
regexp: new RegExp(COMBINED_SRC, "gi"),
|
||||
decoration: (match) =>
|
||||
Decoration.replace({
|
||||
widget: new ColorWidget(
|
||||
color,
|
||||
match[0],
|
||||
match[0],
|
||||
settings.showSwatchInEditor,
|
||||
settings.colorizeTextInEditor,
|
||||
),
|
||||
});
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// ViewPlugin
|
||||
class ColorPreviewPlugin implements PluginValue {
|
||||
decorations: DecorationSet;
|
||||
private decorator: MatchDecorator;
|
||||
private lastSettingsKey: string;
|
||||
|
||||
constructor(
|
||||
private readonly view: EditorView,
|
||||
private readonly getSettings: () => ColorPreviewSettings,
|
||||
) {
|
||||
const s = this.getSettings();
|
||||
this.lastSettingsKey = this.settingsKey(s);
|
||||
this.decorator = makeDecorator(s);
|
||||
this.decorations = this.decorator.createDeco(view);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate): void {
|
||||
const s = this.getSettings();
|
||||
const key = this.settingsKey(s);
|
||||
|
||||
if (key !== this.lastSettingsKey) {
|
||||
// Settings changed — new decorator needed (widget params changed)
|
||||
this.lastSettingsKey = key;
|
||||
this.decorator = makeDecorator(s);
|
||||
this.decorations = this.decorator.createDeco(update.view);
|
||||
} else if (update.docChanged || update.viewportChanged) {
|
||||
// Incremental update — reuse existing decorations where unchanged
|
||||
this.decorations = this.decorator.updateDeco(update, this.decorations);
|
||||
}
|
||||
}
|
||||
|
||||
private settingsKey(s: ColorPreviewSettings): string {
|
||||
return `${s.showSwatchInEditor}|${s.colorizeTextInEditor}`;
|
||||
}
|
||||
|
||||
destroy(): void { /* nothing to clean up */ }
|
||||
}
|
||||
|
||||
// Public factory
|
||||
export function createColorPreviewExtension(getSettings: () => ColorPreviewSettings) {
|
||||
export function createColorPreviewExtension(
|
||||
getSettings: () => ColorPreviewSettings,
|
||||
) {
|
||||
return ViewPlugin.fromClass(
|
||||
class implements PluginValue {
|
||||
decorations: DecorationSet;
|
||||
private inner: ColorPreviewPlugin;
|
||||
private decorator: MatchDecorator;
|
||||
private lastKey: string;
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.inner = new ColorPreviewPlugin(view, getSettings);
|
||||
this.decorations = this.inner.decorations;
|
||||
const s = getSettings();
|
||||
this.lastKey = `${s.showSwatchInEditor}|${s.colorizeTextInEditor}`;
|
||||
this.decorator = makeDecorator(s);
|
||||
this.decorations = this.decorator.createDeco(view);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
this.inner.update(update);
|
||||
this.decorations = this.inner.decorations;
|
||||
const s = getSettings();
|
||||
const key = `${s.showSwatchInEditor}|${s.colorizeTextInEditor}`;
|
||||
|
||||
if (key !== this.lastKey) {
|
||||
this.lastKey = key;
|
||||
this.decorator = makeDecorator(s);
|
||||
this.decorations = this.decorator.createDeco(update.view);
|
||||
} else if (update.docChanged || update.viewportChanged) {
|
||||
this.decorations = this.decorator.updateDeco(
|
||||
update,
|
||||
this.decorations,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() { this.inner.destroy(); }
|
||||
destroy() {
|
||||
/* nothing to clean up */
|
||||
}
|
||||
},
|
||||
{ decorations: v => v.decorations }
|
||||
{ decorations: (v) => v.decorations },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
54
src/main.ts
54
src/main.ts
|
|
@ -1,8 +1,9 @@
|
|||
import { Plugin, MarkdownPostProcessorContext } from 'obsidian';
|
||||
import { ColorPreviewSettings, DEFAULT_SETTINGS } from './types';
|
||||
import { createColorPreviewExtension } from './editor/editorExtension';
|
||||
import { processReadingView } from './reading/readingViewProcessor';
|
||||
import { ColorPreviewSettingTab } from './ui/settingsTab';
|
||||
import { Plugin, MarkdownView, MarkdownPostProcessorContext } from "obsidian";
|
||||
import { ColorPreviewSettings, DEFAULT_SETTINGS } from "./types";
|
||||
import { createColorPreviewExtension } from "./editor/editorExtension";
|
||||
import { processReadingView } from "./reading/readingViewProcessor";
|
||||
import { ColorPreviewSettingTab } from "./ui/settingsTab";
|
||||
import type { EditorView } from "@codemirror/view";
|
||||
|
||||
export default class ColorPreviewPlugin extends Plugin {
|
||||
settings: ColorPreviewSettings = { ...DEFAULT_SETTINGS };
|
||||
|
|
@ -10,50 +11,53 @@ export default class ColorPreviewPlugin extends Plugin {
|
|||
async onload(): Promise<void> {
|
||||
await this.loadSettings();
|
||||
|
||||
// Live preview / source mode
|
||||
this.registerEditorExtension(
|
||||
createColorPreviewExtension(() => this.settings)
|
||||
createColorPreviewExtension(() => this.settings),
|
||||
);
|
||||
|
||||
// Obsidian calls this once per block-level element. We hand each block to
|
||||
// a MarkdownRenderChild so Obsidian manages the mount/unmount lifecycle.
|
||||
this.registerMarkdownPostProcessor(
|
||||
(element: HTMLElement, context: MarkdownPostProcessorContext) => {
|
||||
if (this.settings.enableInReadingView) {
|
||||
processReadingView(element, context, this.settings);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
this.addSettingTab(new ColorPreviewSettingTab(this.app, this));
|
||||
}
|
||||
|
||||
onunload(): void {
|
||||
// all registered extensions and post-processors are
|
||||
// cleaned up automatically by the Plugin base class.
|
||||
}
|
||||
|
||||
async loadSettings(): Promise<void> {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
this.settings = Object.assign(
|
||||
{},
|
||||
DEFAULT_SETTINGS,
|
||||
await this.loadData(),
|
||||
);
|
||||
}
|
||||
|
||||
async saveSettings(): Promise<void> {
|
||||
await this.saveData(this.settings);
|
||||
|
||||
// Refresh editor views so the new settings take effect immediately.
|
||||
// Nudge all open editor views to re-evaluate their decorations.
|
||||
this.app.workspace.iterateAllLeaves((leaf) => {
|
||||
const view = leaf.view as any;
|
||||
if (view?.getViewType?.() === 'markdown' && view?.editor?.cm) {
|
||||
view.editor.cm.dispatch({});
|
||||
const view = leaf.view;
|
||||
if (view instanceof MarkdownView) {
|
||||
// editor.cm is not in the public API types but is stable
|
||||
const cm = (view.editor as unknown as { cm?: EditorView }).cm;
|
||||
if (cm) cm.dispatch({});
|
||||
}
|
||||
});
|
||||
|
||||
// Reading view: re-opening the note re-runs the post-processor naturally.
|
||||
// For an immediate refresh without closing the note, re-render the preview.
|
||||
// Re-render reading view panes so post-processors run again with new settings.
|
||||
this.app.workspace.iterateAllLeaves((leaf) => {
|
||||
const view = leaf.view as any;
|
||||
if (view?.getViewType?.() === 'markdown' && view?.previewMode) {
|
||||
view.previewMode.rerender(true);
|
||||
const view = leaf.view;
|
||||
if (view instanceof MarkdownView) {
|
||||
// previewMode.rerender is not in public types
|
||||
const preview = (
|
||||
view as unknown as {
|
||||
previewMode?: { rerender: (full: boolean) => void };
|
||||
}
|
||||
).previewMode;
|
||||
if (preview) preview.rerender(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { MarkdownRenderChild } from 'obsidian';
|
||||
import { findColorsInText, hasGoodContrast } from '../utils/colorParser';
|
||||
import type { ColorPreviewSettings } from '../types';
|
||||
import type { MarkdownPostProcessorContext } from 'obsidian';
|
||||
import { MarkdownRenderChild } from "obsidian";
|
||||
import { findColorsInText, hasGoodContrast } from "../utils/colorParser";
|
||||
import type { ColorPreviewSettings } from "../types";
|
||||
import type { MarkdownPostProcessorContext } from "obsidian";
|
||||
|
||||
// Wraps a single paragraph element (<p>, <li>, etc.) that contains color strings
|
||||
class ColorSpanChild extends MarkdownRenderChild {
|
||||
constructor(
|
||||
containerEl: HTMLElement,
|
||||
|
|
@ -13,32 +12,36 @@ class ColorSpanChild extends MarkdownRenderChild {
|
|||
}
|
||||
|
||||
onload(): void {
|
||||
this.processElement(this.containerEl);
|
||||
}
|
||||
|
||||
onunload(): void {
|
||||
// replace every .cp-color-wrapper with its text content to restore
|
||||
this.containerEl.querySelectorAll('.cp-color-wrapper').forEach((wrapper) => {
|
||||
wrapper.replaceWith(document.createTextNode(wrapper.textContent ?? ''));
|
||||
});
|
||||
this.containerEl.normalize();
|
||||
}
|
||||
|
||||
private processElement(root: HTMLElement): void {
|
||||
// Mutating the tree while a TreeWalker is active causes nodes to be skipped or revisited.
|
||||
const textNodes = this.collectTextNodes(root);
|
||||
const textNodes = this.collectTextNodes(this.containerEl);
|
||||
for (const node of textNodes) {
|
||||
this.processTextNode(node);
|
||||
}
|
||||
}
|
||||
|
||||
onunload(): void {
|
||||
this.containerEl
|
||||
.querySelectorAll(".cp-color-wrapper")
|
||||
.forEach((wrapper) => {
|
||||
wrapper.replaceWith(
|
||||
document.createTextNode(wrapper.textContent ?? ""),
|
||||
);
|
||||
});
|
||||
this.containerEl.normalize();
|
||||
}
|
||||
|
||||
private collectTextNodes(root: HTMLElement): Text[] {
|
||||
const nodes: Text[] = [];
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
||||
acceptNode: (node) => {
|
||||
if (!node.nodeValue?.trim()) return NodeFilter.FILTER_REJECT;
|
||||
if ((node as Text).parentElement?.closest('code, pre')) return NodeFilter.FILTER_REJECT;
|
||||
if ((node as Text).parentElement?.classList.contains('cp-color-wrapper')) return NodeFilter.FILTER_REJECT;
|
||||
if ((node as Text).parentElement?.closest("code, pre"))
|
||||
return NodeFilter.FILTER_REJECT;
|
||||
if (
|
||||
(node as Text).parentElement?.classList.contains(
|
||||
"cp-color-wrapper",
|
||||
)
|
||||
)
|
||||
return NodeFilter.FILTER_REJECT;
|
||||
return NodeFilter.FILTER_ACCEPT;
|
||||
},
|
||||
});
|
||||
|
|
@ -48,7 +51,7 @@ class ColorSpanChild extends MarkdownRenderChild {
|
|||
}
|
||||
|
||||
private processTextNode(node: Text): void {
|
||||
const text = node.nodeValue ?? '';
|
||||
const text = node.nodeValue ?? "";
|
||||
const matches = findColorsInText(text);
|
||||
if (matches.length === 0) return;
|
||||
|
||||
|
|
@ -57,7 +60,9 @@ class ColorSpanChild extends MarkdownRenderChild {
|
|||
|
||||
for (const match of matches) {
|
||||
if (match.from > cursor) {
|
||||
fragment.appendChild(document.createTextNode(text.slice(cursor, match.from)));
|
||||
fragment.appendChild(
|
||||
document.createTextNode(text.slice(cursor, match.from)),
|
||||
);
|
||||
}
|
||||
fragment.appendChild(this.createColorElement(match.color));
|
||||
cursor = match.to;
|
||||
|
|
@ -70,23 +75,23 @@ class ColorSpanChild extends MarkdownRenderChild {
|
|||
}
|
||||
|
||||
private createColorElement(color: string): HTMLElement {
|
||||
const wrapper = document.createElement('span');
|
||||
wrapper.className = 'cp-color-wrapper';
|
||||
const wrapper = document.createElement("span");
|
||||
wrapper.className = "cp-color-wrapper";
|
||||
|
||||
if (this.settings.showSwatchInEditor) {
|
||||
const swatch = document.createElement('span');
|
||||
swatch.className = 'cp-color-swatch';
|
||||
swatch.style.backgroundColor = color;
|
||||
swatch.setAttribute('aria-label', `Color: ${color}`);
|
||||
const swatch = document.createElement("span");
|
||||
swatch.className = "cp-color-swatch";
|
||||
swatch.setAttribute("aria-label", `Color: ${color}`);
|
||||
swatch.setCssProps({ "--cp-swatch-color": color });
|
||||
wrapper.appendChild(swatch);
|
||||
}
|
||||
|
||||
const label = document.createElement('span');
|
||||
const label = document.createElement("span");
|
||||
label.textContent = color;
|
||||
|
||||
if (this.settings.colorizeTextInEditor && hasGoodContrast(color)) {
|
||||
label.className = 'cp-colored-text';
|
||||
label.style.color = color;
|
||||
label.className = "cp-colored-text";
|
||||
label.setCssProps({ "--cp-text-color": color });
|
||||
}
|
||||
|
||||
wrapper.appendChild(label);
|
||||
|
|
@ -94,15 +99,10 @@ class ColorSpanChild extends MarkdownRenderChild {
|
|||
}
|
||||
}
|
||||
|
||||
// Call processReadingView from registerMarkdownPostProcessor.
|
||||
// Pass the context so Obsidian can manage the child's lifecycle automatically.
|
||||
export function processReadingView(
|
||||
element: HTMLElement,
|
||||
context: MarkdownPostProcessorContext,
|
||||
settings: ColorPreviewSettings,
|
||||
): void {
|
||||
// registerMarkdownPostProcessor receives one block-level element at a time
|
||||
// (a <p>, <ul>, <h1>, etc.). We register one ColorSpanChild per block.
|
||||
// Obsidian calls onload() immediately and onunload() when it's removed.
|
||||
context.addChild(new ColorSpanChild(element, settings));
|
||||
}
|
||||
|
|
|
|||
144
src/styles.css
144
src/styles.css
|
|
@ -1,116 +1,108 @@
|
|||
.cp-color-swatch {
|
||||
display: inline-block;
|
||||
width: 0.9em;
|
||||
height: 0.9em;
|
||||
border-radius: 3px;
|
||||
margin-right: 0.35em;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
vertical-align: -0.1em;
|
||||
flex-shrink: 0;
|
||||
cursor: help;
|
||||
transition: transform 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-color 0.15s ease-in-out;
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(0, 0, 0, 0.08),
|
||||
0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
display: inline-block;
|
||||
width: 0.9em;
|
||||
height: 0.9em;
|
||||
border-radius: 3px;
|
||||
margin-right: 0.35em;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
vertical-align: -0.1em;
|
||||
flex-shrink: 0;
|
||||
cursor: help;
|
||||
background-color: var(--cp-swatch-color, transparent);
|
||||
transition:
|
||||
transform 0.15s ease-in-out,
|
||||
box-shadow 0.15s ease-in-out,
|
||||
border-color 0.15s ease-in-out;
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(0, 0, 0, 0.08),
|
||||
0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.cp-color-swatch:hover {
|
||||
transform: scale(1.15);
|
||||
border-color: var(--interactive-accent);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transform: scale(1.15);
|
||||
border-color: var(--interactive-accent);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.cp-color-swatch:focus-visible {
|
||||
outline: 2px solid var(--interactive-accent);
|
||||
outline-offset: 2px;
|
||||
outline: 2px solid var(--interactive-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.cp-colored-text {
|
||||
font-weight: 500;
|
||||
color: var(--cp-text-color, inherit);
|
||||
}
|
||||
|
||||
.cp-color-inline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
cursor: text;
|
||||
white-space: nowrap;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
cursor: text;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Keep swatch vertically aligned inside the inline widget */
|
||||
.cp-color-inline .cp-color-swatch {
|
||||
vertical-align: -0.1em;
|
||||
vertical-align: -0.1em;
|
||||
}
|
||||
|
||||
.cp-color-wrapper {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
white-space: nowrap;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cp-color-wrapper .cp-colored-text,
|
||||
.cp-color-wrapper span:not(.cp-color-swatch) {
|
||||
white-space: nowrap;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cp-colored-text {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Ensure extreme values don't vanish against the background */
|
||||
.cp-colored-text[data-color="#ffffff"],
|
||||
.cp-colored-text[data-color="#fff"],
|
||||
.cp-colored-text[data-color="white"],
|
||||
.cp-colored-text[data-color="#000000"],
|
||||
.cp-colored-text[data-color="#000"],
|
||||
.cp-colored-text[data-color="black"] {
|
||||
color: var(--text-normal) !important;
|
||||
text-decoration: underline dotted var(--text-muted);
|
||||
}
|
||||
|
||||
/* Dark mode overrides */
|
||||
.theme-dark .cp-color-swatch {
|
||||
border-color: var(--background-modifier-border-hover);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.12),
|
||||
0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
border-color: var(--background-modifier-border-hover);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.12),
|
||||
0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.theme-dark .cp-color-swatch:hover {
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.15),
|
||||
0 2px 4px rgba(0, 0, 0, 0.4);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.15),
|
||||
0 2px 4px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* Light mode overrides */
|
||||
.theme-light .cp-color-swatch {
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(0, 0, 0, 0.08),
|
||||
0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(0, 0, 0, 0.08),
|
||||
0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.theme-light .cp-color-swatch:hover {
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(0, 0, 0, 0.12),
|
||||
0 2px 4px rgba(0, 0, 0, 0.08);
|
||||
transform: scale(1.15);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(0, 0, 0, 0.12),
|
||||
0 2px 4px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.cp-color-swatch {
|
||||
transition: none;
|
||||
}
|
||||
.cp-color-swatch {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.cp-color-swatch:hover {
|
||||
transform: none;
|
||||
}
|
||||
.cp-color-swatch:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.cp-color-swatch {
|
||||
border: 1px solid #000;
|
||||
box-shadow: none;
|
||||
}
|
||||
.cp-color-swatch {
|
||||
border: 1px solid #000;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.cp-colored-text {
|
||||
color: inherit !important;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
.cp-colored-text {
|
||||
color: inherit !important;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
src/types.ts
16
src/types.ts
|
|
@ -6,10 +6,10 @@
|
|||
export interface ColorPreviewSettings {
|
||||
/** Show color swatch in editor */
|
||||
showSwatchInEditor: boolean;
|
||||
|
||||
|
||||
/** Colorize the text itself in editor */
|
||||
colorizeTextInEditor: boolean;
|
||||
|
||||
|
||||
/** Enable color previews in reading view */
|
||||
enableInReadingView: boolean;
|
||||
}
|
||||
|
|
@ -26,14 +26,14 @@ export const DEFAULT_SETTINGS: ColorPreviewSettings = {
|
|||
/**
|
||||
* Constants
|
||||
*/
|
||||
export const PLUGIN_NAME = 'Color Preview';
|
||||
export const PLUGIN_ID = 'color-preview';
|
||||
export const PLUGIN_NAME = "Color Preview";
|
||||
export const PLUGIN_ID = "color-preview";
|
||||
|
||||
/**
|
||||
* CSS class names
|
||||
*/
|
||||
export const CSS_CLASSES = {
|
||||
swatch: 'cp-color-swatch',
|
||||
coloredText: 'cp-colored-text',
|
||||
wrapper: 'cp-color-wrapper',
|
||||
} as const;
|
||||
swatch: "cp-color-swatch",
|
||||
coloredText: "cp-colored-text",
|
||||
wrapper: "cp-color-wrapper",
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,10 @@
|
|||
// src/ui/settingsTab.ts
|
||||
import { App, PluginSettingTab, Setting } from "obsidian";
|
||||
import type ColorPreviewPlugin from "../main";
|
||||
|
||||
import { App, PluginSettingTab, Setting } from 'obsidian';
|
||||
import type ColorPreviewPlugin from '../main';
|
||||
|
||||
/**
|
||||
* Settings tab for the Color Preview plugin
|
||||
*/
|
||||
export class ColorPreviewSettingTab extends PluginSettingTab {
|
||||
constructor(
|
||||
app: App,
|
||||
private readonly plugin: ColorPreviewPlugin
|
||||
private readonly plugin: ColorPreviewPlugin,
|
||||
) {
|
||||
super(app, plugin);
|
||||
}
|
||||
|
|
@ -18,34 +13,25 @@ export class ColorPreviewSettingTab extends PluginSettingTab {
|
|||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
// Header
|
||||
containerEl.createEl('h2', { text: 'Color Preview Settings' });
|
||||
new Setting(containerEl).setName("Configuration").setHeading();
|
||||
|
||||
// Description
|
||||
containerEl.createEl('p', {
|
||||
text: 'Configure how color values are displayed in your notes.',
|
||||
cls: 'setting-item-description',
|
||||
});
|
||||
|
||||
// Show color swatch setting
|
||||
new Setting(containerEl)
|
||||
.setName('Show color swatch')
|
||||
.setDesc('Display a small colored square before color values')
|
||||
.setName("Show color swatch")
|
||||
.setDesc("Display a small colored square before color values")
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.showSwatchInEditor)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.showSwatchInEditor = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// Colorize text setting
|
||||
new Setting(containerEl)
|
||||
.setName('Colorize text')
|
||||
.setName("Colorize text")
|
||||
.setDesc(
|
||||
'Apply the color to the text itself (e.g., #ff0000 appears in red). ' +
|
||||
'Very light or dark colors will not be colorized for readability.'
|
||||
"Apply the color to the text itself (e.g., #ff0000 appears in red). " +
|
||||
"Colors with low contrast against the current theme background will not be colorized.",
|
||||
)
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
|
|
@ -53,32 +39,27 @@ export class ColorPreviewSettingTab extends PluginSettingTab {
|
|||
.onChange(async (value) => {
|
||||
this.plugin.settings.colorizeTextInEditor = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// Enable in reading view setting
|
||||
new Setting(containerEl)
|
||||
.setName('Enable in reading view')
|
||||
.setDesc('Show color previews when viewing rendered markdown')
|
||||
.setName("Enable in reading view")
|
||||
.setDesc("Show color previews when viewing rendered markdown")
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.enableInReadingView)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.enableInReadingView = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// Supported formats info
|
||||
containerEl.createDiv({ cls: 'setting-item-description' }, (div) => {
|
||||
div.createEl('h3', { text: 'Supported Color Formats', cls: 'u-font-weight-bold' });
|
||||
div.createEl('ul', {}, (ul) => {
|
||||
ul.createEl('li', { text: 'HEX: #RGB, #RRGGBB, #RGBA, #RRGGBBAA' });
|
||||
ul.createEl('li', { text: 'RGB: rgb(255, 0, 0)' });
|
||||
ul.createEl('li', { text: 'RGBA: rgba(255, 0, 0, 0.5)' });
|
||||
ul.createEl('li', { text: 'HSL: hsl(120, 100%, 50%)' });
|
||||
ul.createEl('li', { text: 'HSLA: hsla(120, 100%, 50%, 0.5)' });
|
||||
});
|
||||
});
|
||||
new Setting(containerEl)
|
||||
.setName("Supported color formats")
|
||||
.setHeading();
|
||||
|
||||
new Setting(containerEl).setDesc(
|
||||
"HEX: #RGB, #RRGGBB, #RGBA, #RRGGBBAA · RGB/RGBA: rgb(255, 0, 0) · HSL/HSLA: hsl(120, 100%, 50%)",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,12 +17,15 @@ const HSL_PATTERN =
|
|||
|
||||
const COMBINED_SOURCE = [HEX_PATTERN, RGB_PATTERN, HSL_PATTERN]
|
||||
.map((r) => r.source)
|
||||
.join('|');
|
||||
.join("|");
|
||||
|
||||
// Parsing
|
||||
function parseHex(hex: string): [number, number, number, number] | null {
|
||||
const h = hex.slice(1);
|
||||
let r: number, g: number, b: number, a = 255;
|
||||
let r: number,
|
||||
g: number,
|
||||
b: number,
|
||||
a = 255;
|
||||
if (h.length === 3) {
|
||||
r = parseInt(h[0] + h[0], 16);
|
||||
g = parseInt(h[1] + h[1], 16);
|
||||
|
|
@ -54,21 +57,28 @@ function hslToRgb(h: number, s: number, l: number): [number, number, number] {
|
|||
const a = s * Math.min(l, 1 - l);
|
||||
const f = (n: number) =>
|
||||
l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
||||
return [Math.round(f(0) * 255), Math.round(f(8) * 255), Math.round(f(4) * 255)];
|
||||
return [
|
||||
Math.round(f(0) * 255),
|
||||
Math.round(f(8) * 255),
|
||||
Math.round(f(4) * 255),
|
||||
];
|
||||
}
|
||||
|
||||
function colorToRgba(colorStr: string): [number, number, number, number] | null {
|
||||
function colorToRgba(
|
||||
colorStr: string,
|
||||
): [number, number, number, number] | null {
|
||||
const s = colorStr.trim();
|
||||
if (!s) return null;
|
||||
|
||||
if (s.startsWith('#')) return parseHex(s);
|
||||
if (s.startsWith("#")) return parseHex(s);
|
||||
|
||||
const funcMatch = s.match(/^(rgba?|hsla?)\(\s*([^)]+)\)/i);
|
||||
if (!funcMatch) return null;
|
||||
|
||||
const fn = funcMatch[1].toLowerCase();
|
||||
const parts = funcMatch[2].split(',').map((p) => p.trim());
|
||||
const parts = funcMatch[2].split(",").map((p) => p.trim());
|
||||
|
||||
if (fn === 'rgb' || fn === 'rgba') {
|
||||
if (fn === "rgb" || fn === "rgba") {
|
||||
const r = parseInt(parts[0]);
|
||||
const g = parseInt(parts[1]);
|
||||
const b = parseInt(parts[2]);
|
||||
|
|
@ -77,7 +87,7 @@ function colorToRgba(colorStr: string): [number, number, number, number] | null
|
|||
return [r, g, b, a];
|
||||
}
|
||||
|
||||
if (fn === 'hsl' || fn === 'hsla') {
|
||||
if (fn === "hsl" || fn === "hsla") {
|
||||
const h = parseFloat(parts[0]);
|
||||
const s2 = parseFloat(parts[1]);
|
||||
const l2 = parseFloat(parts[2]);
|
||||
|
|
@ -91,16 +101,43 @@ function colorToRgba(colorStr: string): [number, number, number, number] | null
|
|||
return null;
|
||||
}
|
||||
|
||||
// Reads --background-primary off document.body, which is set by whatever
|
||||
// Obsidian theme is active. Falls back to white if unreadable.
|
||||
function getThemeBackground(): [number, number, number] {
|
||||
const raw = getComputedStyle(document.body)
|
||||
.getPropertyValue('--background-primary')
|
||||
.trim();
|
||||
const rgba = colorToRgba(raw);
|
||||
return rgba ? [rgba[0], rgba[1], rgba[2]] : [255, 255, 255];
|
||||
// Theme background
|
||||
let cachedBg: [number, number, number] | null = null;
|
||||
let cacheScheduled = false;
|
||||
|
||||
function invalidateBgCache() {
|
||||
cachedBg = null;
|
||||
cacheScheduled = false;
|
||||
}
|
||||
|
||||
function getThemeBackground(): [number, number, number] {
|
||||
if (cachedBg) return cachedBg;
|
||||
|
||||
const raw = getComputedStyle(document.body)
|
||||
.getPropertyValue("--background-primary")
|
||||
.trim();
|
||||
|
||||
const parsed = colorToRgba(raw);
|
||||
|
||||
if (parsed) {
|
||||
cachedBg = [parsed[0], parsed[1], parsed[2]];
|
||||
} else {
|
||||
// --background-primary uses a format we can't parse (oklch, color-mix, etc.)
|
||||
// Fall back to theme class detection.
|
||||
const isDark = document.body.classList.contains("theme-dark");
|
||||
cachedBg = isDark ? [30, 30, 30] : [255, 255, 255];
|
||||
}
|
||||
|
||||
// Invalidate once per frame so theme switches are picked up promptly
|
||||
if (!cacheScheduled) {
|
||||
cacheScheduled = true;
|
||||
requestAnimationFrame(invalidateBgCache);
|
||||
}
|
||||
|
||||
return cachedBg;
|
||||
}
|
||||
|
||||
// WCAG contrast
|
||||
function toLinear(c: number): number {
|
||||
const s = c / 255;
|
||||
return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
||||
|
|
@ -112,7 +149,7 @@ function relativeLuminance(r: number, g: number, b: number): number {
|
|||
|
||||
function contrastRatio(
|
||||
[r1, g1, b1]: [number, number, number],
|
||||
[r2, g2, b2]: [number, number, number]
|
||||
[r2, g2, b2]: [number, number, number],
|
||||
): number {
|
||||
const l1 = relativeLuminance(r1, g1, b1);
|
||||
const l2 = relativeLuminance(r2, g2, b2);
|
||||
|
|
@ -134,10 +171,9 @@ export function isValidColor(colorStr: string): boolean {
|
|||
return colorToRgba(colorStr) !== null;
|
||||
}
|
||||
|
||||
// Match finder
|
||||
export function findColorsInText(text: string, startOffset = 0): ColorMatch[] {
|
||||
const matches: ColorMatch[] = [];
|
||||
const regex = new RegExp(COMBINED_SOURCE, 'gi');
|
||||
const regex = new RegExp(COMBINED_SOURCE, "gi");
|
||||
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
|
|
|
|||
|
|
@ -1,25 +1,17 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES6",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ES5",
|
||||
"ES6",
|
||||
"ES7"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
"compilerOptions": {
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES6",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "bundler",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"lib": ["DOM", "ES5", "ES6", "ES7"]
|
||||
},
|
||||
"include": ["**/*.ts"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue