import { Plugin, MarkdownPostProcessorContext, MarkdownRenderChild, MarkdownRenderer, TFile, App, stringifyYaml, Notice, SuggestModal, loadMathJax, PluginSettingTab, Setting } from 'obsidian'; import { createRoot } from 'react-dom/client'; import { transform } from '@babel/standalone'; import * as React from 'react'; import postcss from 'postcss'; import autoprefixer from 'autoprefixer'; import { StorageManager } from 'src/core/storage'; import { useStorage } from 'src/hooks/useStorage'; import { ComponentRegistry } from 'src/components/componentRegistry'; import { ErrorBoundary } from 'src/components/ErrorBoundary'; import {useMathJax} from 'src/hooks/mathJax'; import { read } from 'fs'; import { error } from 'console'; declare global { interface Window { MathJax: any; } } class ReactComponentChild extends MarkdownRenderChild { private root: ReturnType; private storage: StorageManager; private static styleSheet: HTMLStyleElement | null = null; private static componentStyles = new Set(); private noteFile: TFile; private ctx: MarkdownPostProcessorContext; private app: App; private settings: ReactiveNotesSettings; private processedPaths = new Set(); constructor(containerEl: HTMLElement, plugin: ReactNotesPlugin, ctx: MarkdownPostProcessorContext) { super(containerEl); this.root = createRoot(containerEl); this.storage = new StorageManager(plugin); this.ctx = ctx; this.app = plugin.app; // Get app from plugin this.settings=plugin.settings; // Get the source file from context if (ctx.sourcePath) { const abstractFile = plugin.app.vault.getAbstractFileByPath(ctx.sourcePath); if (abstractFile instanceof TFile) { this.noteFile = abstractFile; } else { // Handle the case where it's not a TFile console.warn(`Source path ${ctx.sourcePath} is not a file`); throw new Error(`Cannot initialize component: Source path "${ctx.sourcePath}" is not a file.`); } } // Add these classes to the container containerEl.classList.add('react-component-container'); if (document.body.hasClass('theme-dark')) { // containerEl.classList.add('theme-dark'); containerEl.classList.add('dark'); }else { // containerEl.classList.add('theme-light'); containerEl.classList.remove('dark'); } } private async getFrontmatter( key: string | null = null, defaultValue: T | null = null, notePath: string | null = null, extProp:boolean=false ): Promise | null> { try { // Get the file - either current note or specified path const file = notePath ? this.app.vault.getAbstractFileByPath(notePath) : this.noteFile; if (!file || !(file instanceof TFile)) { const errorMsg=`Invalid file: ${notePath}`; console.error(errorMsg); new Notice(errorMsg); return defaultValue; } const cache = this.app.metadataCache.getFileCache(file); const frontmatter = cache?.frontmatter; if (!frontmatter) { const errorMsg=`File cache not found for ${file.path}`; console.error(errorMsg); new Notice(errorMsg); return defaultValue; } //console.log('Frontmatter:', frontmatter); // Return specific key if requested if (key) { if(extProp&&!frontmatter[key]&&frontmatter.react_data[key]) new Notice(`Key "${key}" not found in outer frontmatter, But found in react_data. Set extProp to false to access it.`); if (!extProp&&frontmatter[key] && !frontmatter.react_data?.[key]) new Notice(`Key "${key}" not found in react_data, but found in frontmatter. Set extProp to true to access it.`); if (extProp) return frontmatter[key] ?? defaultValue; else return frontmatter.react_data?.[key] ?? defaultValue; } // Otherwise return entire frontmatter return extProp?frontmatter:frontmatter.react_data??defaultValue; } catch (error) { console.error('Error getting frontmatter:', error); return defaultValue; } } private async updateFrontmatter(key: string, value: T,notePath:string|null=null,extProp:boolean=false): Promise { // Get the file - either current note or specified path const file = notePath ? this.app.vault.getAbstractFileByPath(notePath) : this.noteFile; if (!file || !(file instanceof TFile)) {console.error(`Invalid file: ${notePath}`); const errorMsg = `Cannot save to non-existent file: ${notePath}`; console.error(errorMsg); new Notice(errorMsg); throw new Error(errorMsg); // Throw an error to notify callers } try { await this.app.fileManager.processFrontMatter(file, (frontmatter) => { if (extProp) { // Update the specific key at the root level frontmatter[key] = value; } else{ // Initialize react_data section if it doesn't exist if (!frontmatter.react_data) { frontmatter.react_data = {}; } // Update the specific key frontmatter.react_data[key] = value; } }); } catch (error) { console.error(`Failed to update frontmatter for key "${key}", External Property: ${extProp}:`, error); new Notice(`Failed to save component state: ${error.message}`); throw error; } } private RenderWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => { const containerRef=this.settings.mathJaxEnabled?useMathJax([children]):null; return ( { // The ErrorBoundary will catch the error and display a fallback UI. // The console log for "Objects are not valid as a React child" should have been suppressed. if (error.message?.includes('Objects are not valid as a React child')) { const objectMatch = error.message.match(/found: object with keys {([^}]+)}/); const keys = objectMatch ? objectMatch[1].split(',').map(k => k.trim()) : ['unknown']; return (
🧩 Component Rendered an Object
A React component attempted to render a plain JavaScript object. Objects need to be mapped to renderable elements.
Object keys found:
    {keys.map(key =>
  • {key}
  • )}
); } // Default error display for other types of errors return (

Error in component:

{error.message}
{error.stack &&
Stack Trace
{error.stack}
}
); }} >
{children}
); }; private needsThree(code: string): boolean { // Check if the code contains any Three.js specific imports or usage // More comprehensive detection of THREE usage const hasThreeImports = /import\s+.*?three['"];?\s*$/gm.test(code) || /import\s*{\s*[^}]*}\s*from\s*['"]three['"];?\s*$/gm.test(code) || /import\s+.*?\s+as\s+(\w+)\s+from\s+['"]three['"];?\s*$/gm.test(code); // Save any custom import name from "import * as CustomName from 'three'" const threeAliasMatch = code.match(/import\s+\*\s+as\s+(\w+)\s+from\s+['"]three['"];?\s*$/m); const threeAlias = threeAliasMatch ? threeAliasMatch[1] : null; // Check for various THREE usage patterns let hasThreeUsage = /\bTHREE\.|\bnew\s+THREE\.|\bextends\s+THREE\./.test(code) || // Direct THREE usage /\bUtilities\.THREE\.|\bnew\s+Utilities\.THREE\./.test(code) || // Utilities.THREE usage /\bComponentRegistry\.Utilities\.THREE\./.test(code); // Full path usage // If there's a custom alias, check for that too if (threeAlias) { hasThreeUsage = hasThreeUsage || new RegExp(`\\b${threeAlias}\\.`).test(code); } // Also check for common THREE classes even without the THREE prefix const commonThreeClasses = /\b(Scene|PerspectiveCamera|WebGLRenderer|Vector3|BoxGeometry|MeshBasicMaterial|Mesh|Object3D|Group|AmbientLight|DirectionalLight)\b/.test(code); // Combined check return hasThreeImports || hasThreeUsage || commonThreeClasses; } private async preprocessCode(code: string): Promise { // Replace CDN imports with script loading code = code.replace( /import\s+(\w+)\s+from\s+['"]https:\/\/cdnjs\.cloudflare\.com\/([^'"]+)['"]/g, (match, importName, cdnPath) => ` const script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/${cdnPath}'; document.body.appendChild(script);`); // Remove imports carefully code = code.replace(/import\s+.*?['"].*$/gm, ''); code = code.replace(/import\s*{[^}]*}\s*from\s*['"][^'"]*['"].*$/gm, ''); code = code.replace(/import\s*\([^)]*\).*$/gm, ''); // Handle both JS/TS exports code = code.replace(/export\s+default\s+/gm, ''); code = code.replace(/export\s+const\s+/gm, 'const '); code = code.replace(/export\s+function\s+/gm, 'function '); code = code.replace(/export\s+class\s+/gm, 'class '); // Remove type annotations //code = code.replace(/:\s*[A-Za-z<>[\]]+/g, ''); //code = code.replace(/<[A-Za-z,\s]+>/g, ''); // Find the component name const componentMatch = code.match(/^(?![\s]*\/\/).*?(?:function\s+(\w+)|const\s+(\w+)\s*=\s*(?:(?:\([^)]*\)|)\s*=>|function\s*\(|React\.memo\(|React\.forwardRef\(|class\s+extends\s+React\.Component))/m); const storeMatch = !componentMatch ? code.match(/(?:const\s+(\w+)\s*=\s*{|class\s+(\w+)\s*{)/) : null; if (!componentMatch) { // If no component found, look for object literals or other exports if (!storeMatch) { throw new Error('No React component found'); } } // Set safeName using either match const componentName = componentMatch ? (componentMatch[1] || componentMatch[2]) : (storeMatch ? (storeMatch[1]|| storeMatch[2]) : 'EmptyComponent'); console.log('Found component:', componentName); // Find component name and rename if it's 'WrappedComponent' const safeName = componentName === 'WrappedComponent' ? 'UserComponent' : componentName; if (componentName === 'WrappedComponent') { code = code.replace(/\bWrappedComponent\b/, safeName); } // Create a HOC wrapper for chart components const chartWrapper = ` const withChartContainer = (WrappedComponent) => { return function ChartContainer(props) { const [mounted, setMounted] = React.useState(false); React.useEffect(() => { const timer = setTimeout(() => setMounted(true), 100); return () => clearTimeout(timer); }, []); return React.createElement( 'div', { style: { width: '100%', minHeight: '400px' } }, mounted ? React.createElement(WrappedComponent, props) : null ); }; }; `; // Combine everything // added Component assignment code = ` ${chartWrapper} ${code} const WrappedComponent = (() => { // Add error handling for component existence if (typeof ${safeName} === 'undefined') { throw new Error(\`Component "${safeName}" was matched but is undefined. Code context: ${code.slice(0, 100).replace(/`/g, '\\`')}...\`); } // Simple check - is it a valid React component? const isValidComponent = (() => { // Class component if (${safeName}.prototype?.isReactComponent) return true; // Function that returns React elements if (typeof ${safeName} === 'function') { try { const result = ${safeName}({}); return React.isValidElement(result); } catch { // If it throws, check source code return ${safeName}.toString().includes('createElement') || ${safeName}.toString().includes('<'); } } // Direct objects are never valid components return false; })(); // If not a valid component, treat as a storage block if (!isValidComponent) { return function StorageBlockWrapper() { // Get the definitions - handle all cases let definitions; if (typeof ${safeName} === 'function') { try { // Try as function definitions = ${safeName}({}); } catch (e) { try { // Try as class definitions = new ${safeName}(); } catch (e2) { // Last resort definitions = ${safeName}; } } } else { // Already an object (like IIFE or object literal) definitions = ${safeName}; } // Ensure it's an object if (!definitions || typeof definitions !== 'object') { definitions = { error: "Couldn't extract definitions" }; } const keys = Object.keys(definitions); return React.createElement('div', { style: { padding: '12px', backgroundColor: 'var(--background-modifier-form-field)', borderRadius: '6px', border: '1px solid var(--background-modifier-border)', fontFamily: 'var(--font-monospace)' } }, [ React.createElement('div', { style: { fontWeight: 'bold', marginBottom: '8px', display: 'flex', alignItems: 'center', gap: '8px' } }, [ React.createElement('span', null, '📦'), React.createElement('span', null, 'Definitions Storage Container:') ]), React.createElement('div', { style: { padding: '8px', marginBottom: '12px', color: 'var(--text-on-accent)', borderRadius: '4px', display: 'flex', alignItems: 'center', gap: '8px' } }, [ React.createElement('code', { style: { padding: '2px 6px', backgroundColor: 'rgba(255,255,255,0.2)', borderRadius: '3px', fontWeight: 'bold' } }, '${safeName}') ]), React.createElement('div', { style: { fontSize: '0.9em', opacity: 0.8 } }, 'Exported definitions:'), React.createElement('ul', { style: { margin: '8px 0', paddingLeft: '20px' } }, keys.map(key => React.createElement('li', { key }, \`\${key}: \${typeof definitions[key]}\` ) )) ]); }; } const isChartComponent = ${safeName}.toString().includes('ResponsiveContainer') || ${safeName}.toString().includes('svg'); return isChartComponent ? withChartContainer(${safeName}) : ${safeName}; })(); `; code = await this.processVaultImports(code); // Wrap code in async IIFE to allow for await return ` (async () => { ${code} return WrappedComponent; })() `; } // Helper to get THREE.js directly private getThreeJs() { // Check if THREE exists in window if (typeof window !== 'undefined' && 'THREE' in window) { return window.THREE; } // Fall back to require - only execute if needed try { return require('three'); } catch (error) { console.warn('Failed to load THREE.js:', error); return undefined; } } private async processVaultImports(code: string, depth = 0): Promise { if (depth > 10) { console.warn('Maximum import depth reached (10)'); return code; } const vaultImportRegex = /vaultImport\s*\(\s*['"]([^'"]+)['"]\s*(?:,\s*(\d+))?\s*\)/g; // Create a map to track processed imports (avoid duplicates) const processedImports = new Map(); // Find all vaultImport calls const matches = [...code.matchAll(vaultImportRegex)]; for (const match of matches) { const [fullMatch, path, index = '0'] = match; const importKey = `${path}#${index}`; // Skip if already processed if (processedImports.has(importKey)) continue; try { const file = this.app.vault.getAbstractFileByPath(path); if (!file || !(file instanceof TFile)) continue; const content = await this.app.vault.read(file); const blocks = content.match(/```react\n([\s\S]*?)```/g) || []; if (blocks[parseInt(index)]) { let blockCode = blocks[parseInt(index)] .replace(/```react\n/, '') .replace(/```$/, ''); // Check for nested imports const importKey = `${path}#${index}`; if (!this.processedPaths.has(importKey)) { this.processedPaths.add(importKey); // Recursively process nested imports blockCode = await this.processVaultImports(blockCode, depth + 1); } blockCode = blockCode.replace( /import\s+(\w+)\s+from\s+['"]https:\/\/cdnjs\.cloudflare\.com\/([^'"]+)['"]/g, (match, importName, cdnPath) => ` const script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/${cdnPath}'; document.body.appendChild(script);`); blockCode = blockCode.replace(/import\s+.*?['"]\s*;?\s*$/gm, '') .replace(/import\s*{[^}]*}\s*from\s*['"][^'"]*['"];?\s*$/gm, '') .replace(/import\s*\([^)]*\);?\s*$/gm, ''); // Strip exports blockCode = blockCode .replace(/export\s+default\s+/gm, '') .replace(/export\s+const\s+/gm, 'const ') .replace(/export\s+function\s+/gm, 'function ') .replace(/export\s+class\s+/gm, 'class ') .replace(/export\s+{[^}]*}\s*;?/gm, '') const importedCode = `\n// Imported from ${path}#${index}\n${blockCode}\n`; processedImports.set(importKey, importedCode); } } catch (error) { console.error(`Error importing from ${path}:`, error); } } // Append all unique imports const appendedCode = Array.from(processedImports.values()).join('\n'); // Remove ALL vaultImport calls in one pass const cleanedCode = code.replace(vaultImportRegex, ''); return appendedCode+cleanedCode; } async render(code: string) { console.time('component-render'); try { // Add container classes for styling isolation this.containerEl.classList.add('react-component-container'); // Apply theme class for dark mode if (document.body.classList.contains('theme-dark')) { this.containerEl.classList.add('dark'); } else { this.containerEl.classList.remove('dark'); } //console.log('Original code:', code); console.time('preprocess'); // Preprocess code const needsThree=this.needsThree(code); const processedCode = await this.preprocessCode(code); console.timeEnd('preprocess'); console.time('tailwind'); // Process Tailwind //Could potentially generate tailwind css based on specific user code using build:css script defined in config // This would let it work with any user defined tailwind classes //await this.processTailwind(processedCode); //console.timeEnd('tailwind'); console.time('babel'); const isTypeScript = code.includes(':') || code.includes('interface') || code.includes('<'); const presets: (string | [string, object])[] = isTypeScript ? ['react', ['typescript', { isTSX: true, allExtensions: true }]] : ['react']; // Transform with Babel const transformedCode = transform(processedCode, { presets, sourceType: 'module', filename: isTypeScript ? 'dynamic-component.tsx' : 'dynamic-component.jsx', configFile: false, babelrc: false, }).code; console.timeEnd('babel'); //console.log('Transformed code:', transformedCode); // Create bound storage hook with frontmatter support const boundUseStorage = (key: string, defaultValue: T, notePath: string | null = null, // Optional: path to another note extProp: boolean = false // Optional: true to store at root, false for react_data) ) => { const [value, setValue] = React.useState(defaultValue); const [error, setError] = React.useState(null); React.useEffect(() => { let mounted = true; const loadValue = async () => { try { const result = await this.getFrontmatter(key, defaultValue, notePath, extProp); if (mounted) { if (result !== null) { setValue(result as T); } else { setValue(defaultValue); } } } catch (error) { console.error(`Error loading from frontmatter: ${key}, error`); if (mounted) { // Fall back to default value on error setValue(defaultValue); } // Fall back to default value } }; loadValue(); return () => { mounted = false; }; }, [key, notePath, extProp]); const updateValue = React.useCallback( async (newValue: T | ((prev: T) => T)) => { try { const actualNewValue = newValue instanceof Function ? newValue(value) : newValue; setValue(actualNewValue); this.updateFrontmatter(key, actualNewValue,notePath, extProp).catch(err => { console.error("Failed to update frontmatter:", err); setError(err.message); }); setError(null); } catch (err) { console.error("Failed to update frontmatter:", err); setError(err.message); } }, [key, value, notePath, extProp] ); return [value, updateValue, error] as const; }; // Create scope with all required dependencies const scope = { ...ComponentRegistry, useStorage: boundUseStorage, //Not included MarketAnalysis functions getTheme: () => document.body.hasClass('theme-dark') ? 'dark' : 'light', // Add note context noteContext: { path: this.noteFile?.path, basename: this.noteFile?.basename, frontmatter: this.app.metadataCache.getFileCache(this.noteFile)?.frontmatter }, // Add useful chart utilities getChartTheme: () => document.body.hasClass('theme-dark') ? 'dark' : 'light', getChartDefaults: () => ({ margin: { top: 10, right: 30, left: 0, bottom: 0 }, style: { backgroundColor: document.body.hasClass('theme-dark') ? '#1a1b1e' : '#ffffff', color: document.body.hasClass('theme-dark') ? '#ffffff' : '#000000' } }), //lazy load THREE if needed ...needsThree ? this.getThreeJs() : undefined, readFile: async (path = null, extensions = ['txt', 'md', 'json', 'csv']) => { return await this.readFile(this.app, path, extensions); }, // Frontmatter access utility //Default extProp to true to access outer frontmatter, maybe more intuitive for users getFrontmatter: async ( key = null, defaultValue = null,notePath = null,extProp=true) => { // Self-protecting implementation return new Promise((resolve) => { // Use setTimeout to prevent UI blocking setTimeout(async () => { try { const result = await this.getFrontmatter(key, defaultValue, notePath,extProp); resolve(result); } catch (error) { console.error("Error in getFrontmatter:", error); resolve(defaultValue); } }, 0); }); }, // Defualt extProp to false to access react_data frontmatter, as a precaution to avoid propery conflicts updateFrontmatter: async (key:string, value:T, notePath = null, extProp = false) => { return new Promise((resolve) => { setTimeout(async () => { return await this.updateFrontmatter(key, value, notePath, extProp); },0); }); }, Notice: (message:string, timeout = 4000) => new Notice(message, timeout), }; //console.log("MathJax version/config:", window.MathJax?.version, window.MathJax?.config); // Execute the code and get the component const Component = await new Function( ...Object.keys(scope), `return ${transformedCode}` )(...Object.values(scope)); // Wrap the rendered component in ErrorBoundary this.root.render( ); } catch (error) { console.error('Rendering error:', error); this.renderError(error instanceof Error ? error : new Error('Unknown error')); } console.timeEnd('component-render'); } async readFile( app: App, path: string | null = null, extensions: string[] = ['txt', 'md', 'json', 'csv'] ): Promise<{ path: string; name: string; extension?: string; content: string } | null> { return new Promise((resolve) => { let isResolved = false; let cancelTimeoutHandle: number | undefined = undefined; // To store timeout handle for cancellation // Create a SuggestModal subclass that lists vault files // If a specific path is provided, read that file directly without showing modal if (path) { const file = this.app.vault.getAbstractFileByPath(path); if (file && file instanceof TFile) { this.app.vault.cachedRead(file).then(content => { isResolved = true; resolve({ path: file.path, name: file.name, content: content }); }).catch(err => { console.error("Failed to read file:", err); isResolved = true; resolve(null); }); } else { console.error("File not found:", path); isResolved = true; resolve(null); } return; } class FileSuggestModal extends SuggestModal { getSuggestions(query:string) { // Get all files with the right extension const files = this.app.vault.getFiles() .filter(file => extensions.includes(file.extension)); // Filter by query if provided if (query) { return files.filter(file => file.path.toLowerCase().includes(query.toLowerCase()) ); } return files; } renderSuggestion(file:TFile, el: HTMLElement) { el.createEl("div", { text: file.name }); el.createEl("small", { text: file.path }); } onChooseSuggestion(file:TFile, evt: MouseEvent|KeyboardEvent) { //console.log("File content:"); if (cancelTimeoutHandle !== undefined) { clearTimeout(cancelTimeoutHandle); cancelTimeoutHandle = undefined; } this.app.vault.cachedRead(file).then(content => {isResolved = true; resolve({ path: file.path, name: file.basename, extension: file.extension, content: content }); }).catch(err => { console.error("Failed to read file:", err); resolve(null);});} } // Show the modal const modal = new FileSuggestModal(this.app); modal.setPlaceholder("Select a file"); modal.onClose = () => { // We use a setTimeout to yield to the event loop, giving onChooseSuggestion // a chance to run if it was triggered by the same user action that closed the modal. if (cancelTimeoutHandle !== undefined) { // Clear previous timeout if any (e.g. rapid open/close) clearTimeout(cancelTimeoutHandle); } cancelTimeoutHandle = window.setTimeout(() => { // window.setTimeout for NodeJS.Timeout type // After yielding, if the promise hasn't been resolved by onChooseSuggestion // and a suggestion wasn't flagged as chosen, then resolve as null (cancellation). if (!isResolved) { resolve(null); } // If suggestionChosen IS true here, it means onChooseSuggestion ran, // set the flag, and it's responsible for resolving (or has already). // If promiseResolved IS true, it's already handled. }, 0); // Yield to the event loop. }; modal.open(); }); } onunload() { this.root.unmount(); } renderError(error: Error) { this.root.render( React.createElement('div', { style: { color: 'red', padding: '1rem', border: '1px solid red', borderRadius: '4px', margin: '1rem 0' } }, `Error: ${error.message}`) ); } getStorage() { return this.storage; } private nonBlockingUpdate( operation: () => Promise, onSuccess?: (result: T) => void, onError?: (error: Error) => void, onFinally?: () => void ): void { // Don't return anything, so it can't be awaited operation().then(result => { if (onSuccess) onSuccess(result); }).catch(error => { if (onError) onError(error); else console.error('Non-blocking operation error:', error); }).finally(() => { if (onFinally) onFinally(); }); } } interface ReactiveNotesSettings { mathJaxEnabled: boolean; mathJaxMode: 'auto' | 'manual' | 'off'; forceTheme: 'dark' | 'light' | 'auto'; } const DEFAULT_SETTINGS: ReactiveNotesSettings = { mathJaxEnabled: true, mathJaxMode: 'auto', forceTheme: 'auto', }; export default class ReactNotesPlugin extends Plugin { private tailwindStyles: HTMLStyleElement | null = null; // Tailwind styles reference settings: ReactiveNotesSettings async onload() { try { await this.loadSettings(); await loadMathJax(); // Read the CSS file using Obsidian's API // - only left here for development purposes, // Users wont have this in production, we may use the build:css script to generate and add the css file in future const css = await this.app.vault.adapter.read(`${this.manifest.dir}/outStyles.css`); // Inject the CSS const style = document.createElement('style'); style.id = 'tailwind-styles'; style.textContent = css; document.head.appendChild(style); this.tailwindStyles = style; } catch (error) { console.error("Failed to load CSS file:", error); } // Listen for theme changes this.registerEvent( this.app.workspace.on('css-change', () => { this.updateTheme(); }) ); // Register markdown processor this.registerMarkdownCodeBlockProcessor( 'react', (source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) => { const child = new ReactComponentChild(el, this, ctx); ctx.addChild(child); // Set a timeout to detect long-running operations const timeoutId = setTimeout(() => { new Notice('React component taking too long to render. Check for expensive operations.'); }, 3000); child.render(source); clearTimeout(timeoutId); } ); // Register a global Markdown postprocessor for HTML this.registerMarkdownPostProcessor((element, context) => { // Only process elements that are within ReactiveNotes components if (element.closest('.react-component-container')) { this.parseMarkdownInHtml(element); } }); // Initial theme setup this.updateTheme(); this.addSettingTab(new ReactiveNotesSettingTab(this.app, this)); } async saveSettings() { await this.saveData(this.settings); } async loadSettings() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); } onunload() { // Clean up styles if (this.tailwindStyles) { this.tailwindStyles.remove(); } // Reset MathJax state if (window.MathJax) { try { window.MathJax.typesetClear(); } catch (e) { console.warn("MathJax cleanup error:", e); } } } public updateTheme = () => { // Update theme class on all react component containers document.querySelectorAll('.react-component-container').forEach(el => { if (document.body.hasClass('theme-dark')) { el.classList.add('theme-dark'); el.classList.add('dark'); el.classList.remove('theme-light'); } else { el.classList.add('theme-light'); el.classList.remove('dark'); el.classList.remove('theme-dark'); } if (this.settings.forceTheme === 'dark') { el.classList.add('theme-dark'); el.classList.add('dark'); el.classList.remove('theme-light'); } else if (this.settings.forceTheme === 'light') { el.classList.add('theme-light'); el.classList.remove('dark'); el.classList.remove('theme-dark'); } }); }; private async parseMarkdownInHtml(container: HTMLElement) { // Query all divs or specific HTML blocks you want to process const htmlBlocks = Array.from(container.querySelectorAll('div:not(.markdown-rendered)')) as HTMLDivElement[]; //console.log('Processing HTML container:', container); // Iterate over each div block for (const block of htmlBlocks) { // Check if it already contains rendered Markdown (to avoid double processing) if (block.querySelector('.markdown-rendered')) continue; // Render Markdown for the inner content of each block await MarkdownRenderer.render( this.app, block.textContent || "", // // Get text content block, // Target container for rendered Markdown '', // Path (optional, current file path if needed) this // Plugin context ); /* // Alternative if more control is needed: // Create a temporary div to safely extract text const tempDiv = document.createElement('div'); tempDiv.appendChild(block.cloneNode(true)); const safeContent = tempDiv.textContent || ""; await MarkdownRenderer.renderMarkdown( this.app, safeContent, block, '', this ); */ // After rendering, mark the block to avoid double processing block.classList.add('markdown-rendered'); } } } class ReactiveNotesSettingTab extends PluginSettingTab { plugin: ReactNotesPlugin; constructor(app: App, plugin: ReactNotesPlugin) { super(app, plugin); this.plugin = plugin; } display(): void { const {containerEl} = this; containerEl.empty(); new Setting(containerEl) .setName('Force Theme') .setDesc('Force a specific theme for all components') .addDropdown(dropdown => dropdown .addOption('auto', 'Auto') .addOption('dark', 'Dark') .addOption('light', 'Light') .setValue(this.plugin.settings.forceTheme) .onChange(async (value) => { this.plugin.settings.forceTheme = value as 'dark' | 'light' | 'auto'; await this.plugin.saveSettings(); this.plugin.updateTheme(); })); new Setting(containerEl) .setName('Enable MathJax') .setDesc('Process MathJax in React components') .addToggle(toggle => toggle .setValue(this.plugin.settings.mathJaxEnabled) .onChange(async (value) => { this.plugin.settings.mathJaxEnabled = value; await this.plugin.saveSettings(); })); } }