mirror of
https://github.com/nellowtcs/Obsidian-IroView.git
synced 2026-07-22 06:56:57 +00:00
Using existing APIs better
This commit is contained in:
parent
e4c131c6f1
commit
b415f5c8d1
3 changed files with 215 additions and 257 deletions
|
|
@ -5,141 +5,141 @@ import {
|
|||
Decoration,
|
||||
DecorationSet,
|
||||
WidgetType,
|
||||
MatchDecorator,
|
||||
PluginValue,
|
||||
} from '@codemirror/view';
|
||||
import { RangeSetBuilder } from '@codemirror/state';
|
||||
import { findColorsInText, hasGoodContrast, ColorMatch } from '../utils/colorParser';
|
||||
import { RangeSet } from '@codemirror/state';
|
||||
import { hasGoodContrast } from '../utils/colorParser';
|
||||
import type { ColorPreviewSettings } from '../types';
|
||||
|
||||
// Combined color regex
|
||||
// MatchDecorator needs its own RegExp
|
||||
// instance (it mutates lastIndex internally).
|
||||
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('|');
|
||||
|
||||
|
||||
// Swatch widget
|
||||
class ColorSwatchWidget extends WidgetType {
|
||||
constructor(private readonly color: string) {
|
||||
super();
|
||||
}
|
||||
constructor(readonly color: string) { super(); }
|
||||
|
||||
toDOM(): HTMLElement {
|
||||
const swatch = document.createElement('span');
|
||||
swatch.className = 'cp-color-swatch';
|
||||
swatch.style.backgroundColor = this.color;
|
||||
swatch.setAttribute('data-color', this.color);
|
||||
swatch.setAttribute('aria-label', `Color preview: ${this.color}`);
|
||||
return swatch;
|
||||
const el = document.createElement('span');
|
||||
el.className = 'cp-color-swatch';
|
||||
el.style.backgroundColor = this.color;
|
||||
el.setAttribute('aria-label', `Color: ${this.color}`);
|
||||
return el;
|
||||
}
|
||||
|
||||
eq(other: ColorSwatchWidget): boolean {
|
||||
return this.color === other.color;
|
||||
}
|
||||
|
||||
ignoreEvent(): boolean {
|
||||
return true;
|
||||
}
|
||||
eq(other: ColorSwatchWidget): boolean { return this.color === other.color; }
|
||||
ignoreEvent(): boolean { return true; }
|
||||
}
|
||||
|
||||
// Decoration builder
|
||||
// collect all (from, to, decoration) triples, sort them once, then feed them to the builder
|
||||
function buildDecorations(
|
||||
matches: ColorMatch[],
|
||||
settings: ColorPreviewSettings
|
||||
): DecorationSet {
|
||||
type Entry = { from: number; to: number; decoration: Decoration };
|
||||
const entries: Entry[] = [];
|
||||
|
||||
for (const match of matches) {
|
||||
if (settings.showSwatchInEditor) {
|
||||
// Point decoration: from === to === match.from
|
||||
entries.push({
|
||||
from: match.from,
|
||||
to: match.from,
|
||||
decoration: Decoration.widget({
|
||||
widget: new ColorSwatchWidget(match.color),
|
||||
side: -1, // render before the character at `from`
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (settings.colorizeTextInEditor && hasGoodContrast(match.color)) {
|
||||
entries.push({
|
||||
from: match.from,
|
||||
to: match.to,
|
||||
decoration: Decoration.mark({
|
||||
class: 'cp-colored-text',
|
||||
attributes: {
|
||||
style: `color: ${match.color} !important;`,
|
||||
'data-color': match.color,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort: primary key = from ascending; secondary = to ascending so that
|
||||
// the zero-length point decoration (to === from) comes before the mark
|
||||
// (to > from) when they share the same from.
|
||||
entries.sort((a, b) => a.from - b.from || a.to - b.to);
|
||||
|
||||
const builder = new RangeSetBuilder<Decoration>();
|
||||
for (const { from, to, decoration } of entries) {
|
||||
builder.add(from, to, decoration);
|
||||
}
|
||||
return builder.finish();
|
||||
// MatchDecorator factories
|
||||
function makeSwatchDecorator(): MatchDecorator {
|
||||
return new MatchDecorator({
|
||||
regexp: new RegExp(COMBINED_SRC, 'gi'),
|
||||
decoration: (match) =>
|
||||
Decoration.widget({
|
||||
widget: new ColorSwatchWidget(match[0]),
|
||||
side: -1, // insert before the matched text
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function makeMarkDecorator(): MatchDecorator {
|
||||
return new MatchDecorator({
|
||||
regexp: new RegExp(COMBINED_SRC, 'gi'),
|
||||
// Return null (cast) to skip colors with poor contrast
|
||||
decoration: (match) => {
|
||||
const color = match[0];
|
||||
if (!hasGoodContrast(color)) return null as unknown as Decoration;
|
||||
return Decoration.mark({
|
||||
class: 'cp-colored-text',
|
||||
attributes: { style: `color: ${color} !important;`, 'data-color': color },
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ViewPlugin
|
||||
function collectVisibleMatches(view: EditorView): ColorMatch[] {
|
||||
const matches: ColorMatch[] = [];
|
||||
for (const { from, to } of view.visibleRanges) {
|
||||
const text = view.state.doc.sliceString(from, to);
|
||||
matches.push(...findColorsInText(text, from));
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
function settingsKey(s: ColorPreviewSettings): string {
|
||||
return `${s.showSwatchInEditor}|${s.colorizeTextInEditor}`;
|
||||
}
|
||||
|
||||
class ColorPreviewViewPlugin {
|
||||
class ColorPreviewPlugin implements PluginValue {
|
||||
decorations: DecorationSet;
|
||||
private lastSettingsKey: string;
|
||||
|
||||
private swatchDeco: MatchDecorator | null = null;
|
||||
private markDeco: MatchDecorator | null = null;
|
||||
private swatchSet: DecorationSet = Decoration.none;
|
||||
private markSet: DecorationSet = Decoration.none;
|
||||
private lastSettingsKey = '';
|
||||
|
||||
constructor(
|
||||
private readonly view: EditorView,
|
||||
private readonly getSettings: () => ColorPreviewSettings
|
||||
private readonly getSettings: () => ColorPreviewSettings,
|
||||
) {
|
||||
this.lastSettingsKey = settingsKey(this.getSettings());
|
||||
this.decorations = this.rebuild();
|
||||
this.initialize(view);
|
||||
this.decorations = this.merged();
|
||||
}
|
||||
|
||||
update(update: ViewUpdate): void {
|
||||
const currentKey = settingsKey(this.getSettings());
|
||||
const settingsChanged = currentKey !== this.lastSettingsKey;
|
||||
const s = this.getSettings();
|
||||
const key = `${s.showSwatchInEditor}|${s.colorizeTextInEditor}`;
|
||||
const settingsChanged = key !== this.lastSettingsKey;
|
||||
|
||||
if (update.docChanged || update.viewportChanged || settingsChanged || update.geometryChanged) {
|
||||
this.lastSettingsKey = currentKey;
|
||||
this.decorations = this.rebuild();
|
||||
if (settingsChanged) {
|
||||
// Settings changed — recreate decorators and rebuild from scratch.
|
||||
this.initialize(update.view);
|
||||
} else if (update.docChanged || update.viewportChanged) {
|
||||
// Incremental update — MatchDecorator.updateDeco() only rescans
|
||||
// changed/newly visible ranges, reusing everything else.
|
||||
if (this.swatchDeco) {
|
||||
this.swatchSet = this.swatchDeco.updateDeco(update, this.swatchSet);
|
||||
}
|
||||
if (this.markDeco) {
|
||||
this.markSet = this.markDeco.updateDeco(update, this.markSet);
|
||||
}
|
||||
} else {
|
||||
return; // nothing to do
|
||||
}
|
||||
|
||||
this.decorations = this.merged();
|
||||
}
|
||||
|
||||
private rebuild(): DecorationSet {
|
||||
try {
|
||||
const matches = collectVisibleMatches(this.view);
|
||||
return buildDecorations(matches, this.getSettings());
|
||||
} catch (err) {
|
||||
console.error('ColorPreview: Failed to build decorations', err);
|
||||
return Decoration.none;
|
||||
}
|
||||
private initialize(view: EditorView): void {
|
||||
const s = this.getSettings();
|
||||
this.lastSettingsKey = `${s.showSwatchInEditor}|${s.colorizeTextInEditor}`;
|
||||
|
||||
this.swatchDeco = s.showSwatchInEditor ? makeSwatchDecorator() : null;
|
||||
this.markDeco = s.colorizeTextInEditor ? makeMarkDecorator() : null;
|
||||
|
||||
this.swatchSet = this.swatchDeco
|
||||
? this.swatchDeco.createDeco(view)
|
||||
: Decoration.none;
|
||||
this.markSet = this.markDeco
|
||||
? this.markDeco.createDeco(view)
|
||||
: Decoration.none;
|
||||
}
|
||||
|
||||
// Merge the two DecorationSets into one.
|
||||
private merged(): DecorationSet {
|
||||
if (this.swatchSet === Decoration.none) return this.markSet;
|
||||
if (this.markSet === Decoration.none) return this.swatchSet;
|
||||
return RangeSet.join([this.swatchSet, this.markSet]) as DecorationSet;
|
||||
}
|
||||
|
||||
destroy(): void { /* nothing to clean up */ }
|
||||
}
|
||||
|
||||
|
||||
// Public factory
|
||||
export function createColorPreviewExtension(getSettings: () => ColorPreviewSettings) {
|
||||
return ViewPlugin.fromClass(
|
||||
class extends ColorPreviewViewPlugin {
|
||||
constructor(view: EditorView) {
|
||||
super(view, getSettings);
|
||||
}
|
||||
class extends ColorPreviewPlugin {
|
||||
constructor(view: EditorView) { super(view, getSettings); }
|
||||
},
|
||||
{
|
||||
decorations: (plugin) => plugin.decorations,
|
||||
}
|
||||
{ decorations: (plugin) => plugin.decorations }
|
||||
);
|
||||
}
|
||||
89
src/main.ts
89
src/main.ts
|
|
@ -1,110 +1,59 @@
|
|||
import { Plugin, MarkdownPostProcessorContext } from 'obsidian';
|
||||
import { ColorPreviewSettings, DEFAULT_SETTINGS } from './types';
|
||||
import { createColorPreviewExtension } from './editor/editorExtension';
|
||||
import { processReadingView, clearReadingView } from './reading/readingViewProcessor';
|
||||
import { processReadingView } from './reading/readingViewProcessor';
|
||||
import { ColorPreviewSettingTab } from './ui/settingsTab';
|
||||
|
||||
export default class ColorPreviewPlugin extends Plugin {
|
||||
settings: ColorPreviewSettings = { ...DEFAULT_SETTINGS };
|
||||
|
||||
/**
|
||||
* Plugin initialization
|
||||
*/
|
||||
async onload(): Promise<void> {
|
||||
console.log('Loading Color Preview plugin');
|
||||
|
||||
await this.loadSettings();
|
||||
|
||||
// Register editor extension for live preview
|
||||
// Pass the settings object directly - it will be read reactively
|
||||
// Live preview / source mode
|
||||
this.registerEditorExtension(
|
||||
createColorPreviewExtension(() => this.settings)
|
||||
);
|
||||
|
||||
// Register markdown post-processor for reading view
|
||||
// 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) => {
|
||||
(element: HTMLElement, context: MarkdownPostProcessorContext) => {
|
||||
if (this.settings.enableInReadingView) {
|
||||
processReadingView(element, this.settings);
|
||||
processReadingView(element, context, this.settings);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Add settings tab
|
||||
this.addSettingTab(new ColorPreviewSettingTab(this.app, this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin cleanup
|
||||
*/
|
||||
onunload(): void {
|
||||
console.log('Unloading Color Preview plugin');
|
||||
// all registered extensions and post-processors are
|
||||
// cleaned up automatically by the Plugin base class.
|
||||
}
|
||||
|
||||
/**
|
||||
* Load plugin settings
|
||||
*/
|
||||
async loadSettings(): Promise<void> {
|
||||
const data = await this.loadData();
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, data);
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
/**
|
||||
* Save plugin settings and refresh views
|
||||
*/
|
||||
async saveSettings(): Promise<void> {
|
||||
console.log('ColorPreview: Saving settings', this.settings);
|
||||
await this.saveData(this.settings);
|
||||
|
||||
// Force refresh of all editor views
|
||||
this.refreshEditorViews();
|
||||
|
||||
// Refresh reading views with a small delay
|
||||
setTimeout(() => {
|
||||
this.refreshReadingViews();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force refresh of all CodeMirror editor views
|
||||
*/
|
||||
private refreshEditorViews(): void {
|
||||
// Refresh editor views so the new settings take effect immediately.
|
||||
this.app.workspace.iterateAllLeaves((leaf) => {
|
||||
const view = leaf.view;
|
||||
|
||||
if (view.getViewType() === 'markdown') {
|
||||
const markdownView = view as any;
|
||||
|
||||
if (markdownView.editor?.cm) {
|
||||
const cm = markdownView.editor.cm;
|
||||
|
||||
// Force a full viewport update
|
||||
cm.requestMeasure();
|
||||
|
||||
// Dispatch an empty transaction to trigger decoration rebuild
|
||||
cm.dispatch({
|
||||
effects: [],
|
||||
});
|
||||
}
|
||||
const view = leaf.view as any;
|
||||
if (view?.getViewType?.() === 'markdown' && view?.editor?.cm) {
|
||||
view.editor.cm.dispatch({});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh all reading view elements
|
||||
*/
|
||||
private refreshReadingViews(): void {
|
||||
const readingViews = document.querySelectorAll('.markdown-preview-view');
|
||||
|
||||
readingViews.forEach((element) => {
|
||||
const htmlElement = element as HTMLElement;
|
||||
|
||||
// Clear existing previews
|
||||
clearReadingView(htmlElement);
|
||||
|
||||
// Re-process if enabled
|
||||
if (this.settings.enableInReadingView) {
|
||||
processReadingView(htmlElement, this.settings);
|
||||
// Reading view: re-opening the note re-runs the post-processor naturally.
|
||||
// For an immediate refresh without closing the note, re-render the preview.
|
||||
this.app.workspace.iterateAllLeaves((leaf) => {
|
||||
const view = leaf.view as any;
|
||||
if (view?.getViewType?.() === 'markdown' && view?.previewMode) {
|
||||
view.previewMode.rerender(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,99 +1,108 @@
|
|||
import { MarkdownRenderChild } from 'obsidian';
|
||||
import { findColorsInText, hasGoodContrast } from '../utils/colorParser';
|
||||
import type { ColorPreviewSettings } from '../types';
|
||||
import type { MarkdownPostProcessorContext } from 'obsidian';
|
||||
|
||||
// Helpers
|
||||
function shouldProcessNode(node: Node): boolean {
|
||||
if (!node.nodeValue?.trim()) return false;
|
||||
// Skip code/pre blocks
|
||||
if ((node as Text).parentElement?.closest('code, pre')) return false;
|
||||
// Skip already-processed wrappers
|
||||
if ((node as Text).parentElement?.classList.contains('cp-color-wrapper')) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function cleanupExistingPreviews(root: HTMLElement): void {
|
||||
// Replace each wrapper with its plain-text content, then normalize.
|
||||
root.querySelectorAll('.cp-color-wrapper').forEach((wrapper) => {
|
||||
const text = document.createTextNode(wrapper.textContent ?? '');
|
||||
wrapper.parentNode?.replaceChild(text, wrapper);
|
||||
});
|
||||
root.normalize();
|
||||
}
|
||||
|
||||
function createColorElement(colorStr: string, settings: ColorPreviewSettings): HTMLElement {
|
||||
const wrapper = document.createElement('span');
|
||||
wrapper.className = 'cp-color-wrapper';
|
||||
|
||||
if (settings.showSwatchInEditor) {
|
||||
const swatch = document.createElement('span');
|
||||
swatch.className = 'cp-color-swatch';
|
||||
swatch.style.backgroundColor = colorStr;
|
||||
swatch.setAttribute('aria-label', `Color: ${colorStr}`);
|
||||
wrapper.appendChild(swatch);
|
||||
// Wraps a single paragraph element (<p>, <li>, etc.) that contains color strings
|
||||
class ColorSpanChild extends MarkdownRenderChild {
|
||||
constructor(
|
||||
containerEl: HTMLElement,
|
||||
private readonly settings: ColorPreviewSettings,
|
||||
) {
|
||||
super(containerEl);
|
||||
}
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.textContent = colorStr;
|
||||
|
||||
if (settings.colorizeTextInEditor && hasGoodContrast(colorStr)) {
|
||||
label.className = 'cp-colored-text';
|
||||
label.style.color = colorStr;
|
||||
onload(): void {
|
||||
this.processElement(this.containerEl);
|
||||
}
|
||||
|
||||
wrapper.appendChild(label);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function processTextNode(node: Text, settings: ColorPreviewSettings): void {
|
||||
const text = node.nodeValue ?? '';
|
||||
const matches = findColorsInText(text);
|
||||
if (matches.length === 0) return;
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
let cursor = 0;
|
||||
|
||||
for (const match of matches) {
|
||||
if (match.from > cursor) {
|
||||
fragment.appendChild(document.createTextNode(text.slice(cursor, match.from)));
|
||||
}
|
||||
fragment.appendChild(createColorElement(match.color, settings));
|
||||
cursor = match.to;
|
||||
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();
|
||||
}
|
||||
|
||||
if (cursor < text.length) {
|
||||
fragment.appendChild(document.createTextNode(text.slice(cursor)));
|
||||
}
|
||||
|
||||
node.parentNode?.replaceChild(fragment, node);
|
||||
}
|
||||
|
||||
// Public API
|
||||
export function processReadingView(root: HTMLElement, settings: ColorPreviewSettings): void {
|
||||
cleanupExistingPreviews(root);
|
||||
|
||||
// Collect all text nodes before touching the DOM, mutating the tree
|
||||
// while a TreeWalker is active causes nodes to be skipped.
|
||||
const textNodes: Text[] = [];
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
||||
acceptNode: (node) =>
|
||||
shouldProcessNode(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT,
|
||||
});
|
||||
|
||||
let n: Node | null;
|
||||
while ((n = walker.nextNode())) {
|
||||
textNodes.push(n as Text);
|
||||
}
|
||||
|
||||
// Now mutate
|
||||
for (const node of textNodes) {
|
||||
try {
|
||||
processTextNode(node, settings);
|
||||
} catch (err) {
|
||||
console.error('ColorPreview: Error processing text node', err);
|
||||
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);
|
||||
for (const node of textNodes) {
|
||||
this.processTextNode(node);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
return NodeFilter.FILTER_ACCEPT;
|
||||
},
|
||||
});
|
||||
let n: Node | null;
|
||||
while ((n = walker.nextNode())) nodes.push(n as Text);
|
||||
return nodes;
|
||||
}
|
||||
|
||||
private processTextNode(node: Text): void {
|
||||
const text = node.nodeValue ?? '';
|
||||
const matches = findColorsInText(text);
|
||||
if (matches.length === 0) return;
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
let cursor = 0;
|
||||
|
||||
for (const match of matches) {
|
||||
if (match.from > cursor) {
|
||||
fragment.appendChild(document.createTextNode(text.slice(cursor, match.from)));
|
||||
}
|
||||
fragment.appendChild(this.createColorElement(match.color));
|
||||
cursor = match.to;
|
||||
}
|
||||
if (cursor < text.length) {
|
||||
fragment.appendChild(document.createTextNode(text.slice(cursor)));
|
||||
}
|
||||
|
||||
node.parentNode?.replaceChild(fragment, node);
|
||||
}
|
||||
|
||||
private createColorElement(color: string): HTMLElement {
|
||||
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}`);
|
||||
wrapper.appendChild(swatch);
|
||||
}
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.textContent = color;
|
||||
|
||||
if (this.settings.colorizeTextInEditor && hasGoodContrast(color)) {
|
||||
label.className = 'cp-colored-text';
|
||||
label.style.color = color;
|
||||
}
|
||||
|
||||
wrapper.appendChild(label);
|
||||
return wrapper;
|
||||
}
|
||||
}
|
||||
|
||||
export function clearReadingView(root: HTMLElement): void {
|
||||
cleanupExistingPreviews(root);
|
||||
// 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));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue