Compare commits

...

2 commits
1.0.4 ... main

12 changed files with 1205 additions and 806 deletions

4
.gitignore vendored
View file

@ -49,4 +49,6 @@ src/core/tailwind.ts
# Build artifacts (commonly ignored) # Build artifacts (commonly ignored)
main.js.map main.js.map
*.tsbuildinfo *.tsbuildinfo
.DS_Store .DS_Store
forgottenStuff.txt

View file

@ -9,11 +9,14 @@ First off, thank you for considering contributing to ReactiveNotes! It's people
Before diving in, check our current focus areas: Before diving in, check our current focus areas:
- 📱 Mobile support optimization - 📱 Mobile support optimization
- 🎨 Enhanced visualization capabilities - ~~🎨 Enhanced visualization capabilities~~
- 🔄 State management improvements - ~~🔄 State management improvements~~
* 🧩 Component Templates *(New Focus)*
* 🖱️ GUI for Component Snippets *(New Focus)*
- 🧪 Testing infrastructure - 🧪 Testing infrastructure
- 📚 Documentation enhancements - ~~📚 Documentation enhancements~~
> **Extra note:** If utilising or implementing pre-existing functionality, the general idea is to mirror standard use cases to minimise learning curves for contributors and users alike.
## 🔧 Development Setup ## 🔧 Development Setup
### Prerequisites ### Prerequisites
@ -68,11 +71,10 @@ ReactiveNotes/
│ │ ├── core/ │ │ ├── core/
│ │ └── ui/ │ │ └── ui/
│ ├── services/ # Core services │ ├── services/ # Core services
│ │ ├── storage.ts │ │ └── storage.ts
│ │ └── marketData.ts
│ ├── hooks/ # Custom React hooks │ ├── hooks/ # Custom React hooks
│ ├── utils/ # Utility functions │ ├── utils/ # Utility functions
│ └── main.tsx # Plugin entry point ── main.tsx # Plugin entry point
├── styles/ # CSS and Tailwind configs ├── styles/ # CSS and Tailwind configs
├── tests/ # Test files ├── tests/ # Test files
└── types/ # TypeScript definitions └── types/ # TypeScript definitions
@ -315,7 +317,6 @@ We follow [Semantic Versioning](https://semver.org/):
## 🤝 Community Guidelines ## 🤝 Community Guidelines
### Communication ### Communication
- Be respectful and inclusive
- Keep discussions technical - Keep discussions technical
- Provide context with questions - Provide context with questions
- Use appropriate channels - Use appropriate channels
@ -328,7 +329,7 @@ We follow [Semantic Versioning](https://semver.org/):
### Recognition ### Recognition
Contributors are recognized in: Contributors are recognized in:
- CONTRIBUTORS.md - CONTRIBUTORS.md (coming soon)
- Release notes - Release notes
- Community spotlights - Community spotlights

562
README.md
View file

@ -17,11 +17,17 @@ Transform your Obsidian vault into a reactive computational environment. Reactiv
- **Dynamic & Interactive**: Turn static notes into living documents - **Dynamic & Interactive**: Turn static notes into living documents
- **Extensible Foundation**: Build your own tools and visualizations - **Extensible Foundation**: Build your own tools and visualizations
## Compatibility ## Compatibility
- Obsidian v1.4.0 or higher - Obsidian v1.4.0 or higher
- React v18.2.0 - React v18.2.0
---
**New to ReactiveNotes? Start here!**
**[🎨 See it in Action! (Examples)](./docs/EXAMPLES/README.md)**: Explore a gallery of what you can build.
---
## 🚀 Quick Start ## 🚀 Quick Start
### Installation ### Installation
@ -50,56 +56,114 @@ export default Greeting;
```` ````
**Component Requirements:** **Component Requirements:**
- Must include a default export
- Must be self-contained in one code block - Must be self-contained in one code block
- Component ro Render must be the first component within codeblock - Component to Render must be the first component within codeblock
### Component Structure ## 💻 Core Features
A typical component structure follows this basic structure: ### 1. React Components in Notes
```` Build dynamic UIs directly in your notes with React 18 and TypeScript. Features robust error handling, lifecycle management, and real-time updates.
```react
// Optional: CDN imports at the top
import ExternalLib from 'https://cdnjs.cloudflare.com/...';
// Component definition ### 2. Canvas Manipulation
const MyComponent = () => { - HTML Canvas API support for custom drawing
// 1. Hooks - THREE.js integration for 3D graphics
const [state, setState] = useState(null); - Lightweight Charts for financial visualization
const componentRef = useRef(null); - D3 force layout integration for network graphs
- Support for both 2D and 3D rendering contexts
// 2. Effects & Lifecycle
useEffect(() => {
// Setup code
return () => {
// Cleanup code
};
}, []);
// 3. Helper functions
const handleEvent = () => {
// Event handling logic
};
// 4. Render
return (
<div>
{/* Your JSX here */}
</div>
);
};
// Required: Default export ### 3. State Management
export default MyComponent; Effortlessly manage component state with multiple options:
- **Persistent Note State**: Use the `useStorage` hook to save and load component data directly within your note's frontmatter. State travels with your note!
- **Cross-Note Persistence**: Extend `useStorage` to interact with the frontmatter of *other* notes in your vault.
- **Root Level Frontmatter**: Opt to store data at the root of the frontmatter, outside the default `react_data` section, for broader compatibility.
- **Standard React State**: Utilize `useState` and other React hooks for local, non-persistent component state.
- **Browser `localStorage`**: Access global, non-note-specific storage for preferences or shared data across all notes.\
For greater detail and examples.
➡️ **[Deep Dive into State Management](./docs/03_STATE_MANAGEMENT.md)**
### 4. File Reading Utility
The plugin provides a flexible file reading utility through the `readFile` function that helps you interactively select and read files from your vault.
```javascript
const fileData = await readFile( path,extensions);
``` ```
```` ### Parameters
**Key Structure Points**: - `path` (optional): Direct path to a specific file. If provided, skips the file selector. Default: `null`
1. Imports always at the top - `extensions` (optional): Array of file extensions to filter by. Default: `['txt', 'md', 'json', 'csv']`
2. Component name matches default export
3. Hooks before effects
4. Helper functions before render
5. Single default export at bottom
This utility is particularly useful for components that need to process files from your vault, such as data visualizations, document analysis, and custom importers.\
➡️ **[Read Files Usage and Examples](./docs/04_FILE_SYSTEM_ACCESS.md)**
### 5. CDN Library Support
Import additional libraries from cdnjs:
```javascript
import Chart from 'https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.min.js';
```
Requirements:
- HTTPS URLs from cdnjs.cloudflare.com only
- Imports must exist alone in their own line
- Browser-compatible libraries only
### 6. Theme Integration
```javascript
const theme = getTheme(); // Returns 'dark' or 'light' correspodning to obsidian theme
const styles = {
background: theme === 'dark' ? 'var(--background-primary)' : 'white',
color: theme === 'dark' ? 'var(--text-normal)' : 'black'
};
// Or use 'dark:' within your components
```
### 7. Vault Code Imports
Import React code from other notes in your vault:
```javascript
// Import code from another note
vaultImport('Components/SharedComponents.md', 0); // 0 = first code block
vaultImport('Hooks/CustomHooks.md', 1); // 1 = second code block
const MyComponent = () => {
// Use imported components and hooks from these files
const data = useImportedHook();
return <ImportedComponent data={data} />;
};
```
### 8. Definition Storage Blocks
Not all react code blocks need to be valid React you can use React code blocks to store and organize utility functions, hooks, and other definitions:
```javascript
// This displays as a definition storage block instead of rendering
const utilities = () => ({
formatDate: (date) => date.toLocaleDateString(),
calculateSum: (arr) => arr.reduce((a, b) => a + b, 0),
parseData: (raw) => JSON.parse(raw)
});
export default utilities;
```
![Object Definitions Codeblock](assets/codeStorageBox.gif)
When a code block returns an object instead of JSX, it automatically displays as a storage block showing all exported definitions. If you have multiple objects containing definitions only the first object will be previewed.
Perfect for:
Utility function collections
Custom hook libraries
Configuration objects
Shared constants
Import these definitions into other components using vaultImport().
### 9. LaTeX Math Rendering with MathJax
- Display complex mathematical and scientific notations beautifully.
- Supports standard LaTeX syntax within your React components.
- Automatic post rendering of inline ($...$) and display ($$...$$) math according to standard latex rules.
- To display a dollar ($) character either first escape this character /$ or turn off MathJax post-rendering
## 📚 Available Libraries & Components ## 📚 Available Libraries & Components
@ -125,11 +189,12 @@ import { Card, Tabs, Switch } from '@/components/ui';
import { TrendingUp, Activity, Settings } from 'lucide-react'; // 70+ icons import { TrendingUp, Activity, Settings } from 'lucide-react'; // 70+ icons
import * as LucideIcons from 'lucide-react'; // For complete access just use LucideIcons.IconName import * as LucideIcons from 'lucide-react'; // For complete access just use LucideIcons.IconName
``` ```
See the full [component reference](./COMPONENTS.md) for a complete list of available libraries, components and utilities. Checkout the full [component reference](./COMPONENTS.md) for a complete list of available libraries, components and utilities.
Even if needed libraries are not provided within this list CDN imports are always an option.
If needed, libraries not provided within this list can be imported via CDN imports.
### Available in Component Scope ### Available in Component Scope
<details>
All components have access to these objects and functions: All components have access to these objects and functions:
```javascript ```javascript
@ -150,241 +215,9 @@ getFrontmatter(key?,defaultValue?,notePath?, extProp?)
vaultImport(path, index) // Preprocessor directive to include code from other notes (not a runtime function) vaultImport(path, index) // Preprocessor directive to include code from other notes (not a runtime function)
Notice(message, timeout?) // Display non-blocking notifications (use instead of alert() which blocks the UI thread) Notice(message, timeout?) // Display non-blocking notifications (use instead of alert() which blocks the UI thread)
``` ```
</details>
## 💻 Core Features
### 1. Component Systems
- Write React components directly in notes
- Full TypeScript and React 18 support
- Hot reloading during development
- Error Boundaries and Suspense
- Resource cleanup and lifecycle management
- Custom component registration
- Real-time component updates
- Error reporting
### 2. Canvas Manipulation
- HTML Canvas API support for custom drawing
- THREE.js integration for 3D graphics
- Lightweight Charts for financial visualization
- D3 force layout integration for network graphs
- Support for both 2D and 3D rendering contexts
### 3. Performance
- Efficient re-rendering
- Proper cleanup of resources in useEffect hooks
- Lazy loading support
### 4. State Management
```javascript
// Local state with React
const [local, setLocal] = useState(0);
useStorage(
key, // Property name to store in frontmatter
defaultValue, // Default value if property doesn't exist
notePath = null, // Optional: Path to another note (null = current note)
extProp = false // Optional: true = store at root level, false = store in react_data
)
// Persistent state in current note's frontmatter
const [stored, setStored] = useStorage('key', defaultValue);
// Store at root level of frontmatter
const [status, setStatus] = useStorage('status', 'draft', null, true);
// Cross-note + root level: Store at root level of another note
const [tags, setTags] = useStorage('tags', ['react'], 'Template.md', true);
// Set value to undefined to delete keys
// Direct frontmatter manipulation (alternative to useStorage hook)
await updateFrontmatter('react_key', newValue); // Updates react_data.react_key
await updateFrontmatter('custom_key', newValue, null, true); // Updates root level custom_key
await updateFrontmatter('tags', ['react', 'component'], 'path/to/other.md', true); // Updates another note's tags
```
- Persistent storage between sessions
- State synchronization across components
- Frontmatter integration
- Cross-note state management
- Support for both structured data in react_data and direct frontmatter properties
### Note-Specific Data Persistence
- Each note maintains independent state through frontmatter storage under a key called react_data
- Component states persist across sessions
- Data automatically travels with notes when shared
- Multiple instances of same component can have different states
- Perfect for creating reusable interactive templates
Example:
```javascript
// In Note1.md - counter starts at 0
const Counter = () => {
const [count1, setCount1] = useStorage('counter', 0);
return <button onClick={() => setCount1(count1 + 1)}>Count: {count1}</button>;
};
// In Note2.md - same component, independent state
const Counter = () => {
const [count2, setCount2] = useStorage('counter', 0);
return <button onClick={() => setCount2(count2 + 1)}>Count: {count2}</button>;
};
```
#### Browser LocalStorage
For components that need to share state across notes or maintain state independent of specific notes, browser localStorage is available.
```javascript
// Create a localStorage hook
const useLocalStorage = (key, defaultValue) => {
const [state, setState] = useState(() => {
try {
const storedValue = localStorage.getItem(key);
return storedValue ? JSON.parse(storedValue) : defaultValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return defaultValue;
}
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(state));
}, [key, state]);
return [state, setState];
};
// Use it in components
const [value, setValue] = useLocalStorage('shared-key', defaultValue);
//This key is shared across all notes
```
Key features:
- Data persists across browser sessions
- Shared state across all notes
- Independent of note content
- Approximately 5-10MB storage per domain
- Persists until browser data is cleared
### 5. File Reading Utility
The plugin provides a flexible file reading utility through the `readFile` function that helps you interactively select and read files from your vault.
```javascript
const fileData = await readFile( path,extensions);
```
### Parameters
- `path` (optional): Direct path to a specific file. If provided, skips the file selector. Default: `null`
- `extensions` (optional): Array of file extensions to filter by. Default: `['txt', 'md', 'json', 'csv']`
### Return Value
Returns a Promise that resolves to an object containing:
- `path`: Full file path
- `name`: File name without extension
- `extension`: File extension
- `content`: String content of the file
Returns `null` if the operation is canceled or fails, allowing you to handle this case gracefully.
```javascript
// Interactive file selection with file type filtering
const textFile = await readFile(null,['txt', 'md']);
if (textFile) {
console.log(`Selected: ${textFile.name}`);
console.log(`Content: ${textFile.content}`);
}
// Direct file access by path (no modal)
const jsonFile = await readFile('path/to/config.json');
if (jsonFile) {
const config = JSON.parse(jsonFile.content);
}
// Read CSV files
const csvFile = await readFile(null,['csv']);
if (csvFile) {
// Process CSV content
const lines = csvFile.content.split('\n');
const headers = lines[0].split(',');
// ...
}
// Read binary files like Excel spreadsheets
const excelFile = await readFile(null,['xlsx', 'xls']);
if (excelFile) {
// excelFile.content contains the file data
// Process with SheetJS or other libraries
}
```
This utility is particularly useful for components that need to process files from your vault, such as data visualizations, document analysis, and custom importers.
### 6. CDN Library Support
Import additional libraries from cdnjs:
```javascript
import Chart from 'https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.min.js';
```
Requirements:
- HTTPS URLs from cdnjs.cloudflare.com only
- Imports at component top
- Browser-compatible libraries only
### 7. Theme Integration
```javascript
const theme = getTheme(); // Returns 'dark' or 'light' correspodning to obsidian theme
const styles = {
background: theme === 'dark' ? 'var(--background-primary)' : 'white',
color: theme === 'dark' ? 'var(--text-normal)' : 'black'
};
// Or use 'dark:' within your components
```
### 8. Vault Code Imports
Import React code from other notes in your vault:
```javascript
// Import code from another note
vaultImport('Components/SharedComponents.md', 0); // 0 = first code block
vaultImport('Hooks/CustomHooks.md', 1); // 1 = second code block
const MyComponent = () => {
// Use imported components and hooks from these files
const data = useImportedHook();
return <ImportedComponent data={data} />;
};
```
### 9. Definition Storage Blocks
Not all react code blocks need to be valid React you can use React code blocks to store and organize utility functions, hooks, and other definitions:
```javascript
// This displays as a definition storage block instead of rendering
const utilities = () => ({
formatDate: (date) => date.toLocaleDateString(),
calculateSum: (arr) => arr.reduce((a, b) => a + b, 0),
parseData: (raw) => JSON.parse(raw)
});
export default utilities;
```
![Object Definitions Codeblock](assets/codeStorageBox.gif)
When a code block returns an object instead of JSX, it automatically displays as a storage block showing all exported definitions. If you have multiple objects containing definitions only the first object will be previewed.
Perfect for:
Utility function collections
Custom hook libraries
Configuration objects
Shared constants
Import these definitions into other components using vaultImport().
## 🎨 Component Examples ## 🎨 Component Examples
### Interactive Data Visualization ### Interactive Data Visualization
@ -514,108 +347,12 @@ Knowledge progression dashboard using persistant storage and ability to make new
**Games utilising event listeners for key action mapping** **Games utilising event listeners for key action mapping**
![alt text](assets/gamingObsidian.gif) ![alt text](assets/gamingObsidian.gif)
## 🛠Development Guide
### Component Structure
```react
// Basic component template
const MyComponent = () => {
// 1. Hooks and state
const [data, setData] = useState(null);
const componentRef = useRef(null);
// 2. Effects and lifecycle
useEffect(() => {
// Setup code
return () => {
// Cleanup code
};
}, []);
// 3. Event handlers
const handleUpdate = useCallback(() => {
// Handler code
}, []);
// 4. Render
return (
<div>
{/* Component JSX */}
</div>
);
};
export default MyComponent;
```
### Best Practices
1. **Component Design**
```react
const ResponsiveComponent = () => {
return (
<div className="w-full">
<ResponsiveContainer width="100%" height={400}>
{/* Content adapts to container */}
</ResponsiveContainer>
</div>
);
};
```
2. **State Management**
```react
const DataComponent = () => {
// Persistent data
const [savedData, setSavedData] = useStorage('data-key', []);
// Temporary UI state
const [isLoading, setIsLoading] = useState(false);
// Computed values
const processedData = useMemo(() => {
return savedData.map(item => /* process */);
}, [savedData]);
};
```
3. **Error Handling**
```react
const SafeComponent = () => {
return (
<ErrorBoundary
fallback={({ error }) => (
<div className="text-red-500">
Error: {error.message}
</div>
)}
>
{/* Protected content */}
</ErrorBoundary>
);
};
```
1. **Error Checking**:
- Check browser console for errors
- Verify imports are working
- Ensure all required exports are present
2. **Performance**:
- Keep components focused and minimal
- Use CDN imports sparingly
- Clean up resources in useEffect
3. **Testing**:
- Test in both light and dark themes
- Verify mobile responsiveness (Hasn't been tested for mobile yet)
- Check CDN imports load correctly
## 🔧 Troubleshooting ## 🔧 Troubleshooting
### Common Issues ### Common Issues
1. **Component not rendering** 1. **Component not rendering**
- Verify default export is present
- Check console for React errors - Check console for React errors
- Ensure all imports are available and supported
- Validate JSX syntax - Validate JSX syntax
2. **CDN imports not working** 2. **CDN imports not working**
@ -624,7 +361,6 @@ export default MyComponent;
- Confirm import syntax - Confirm import syntax
3. **State persistence issues** 3. **State persistence issues**
- Check frontmatter permissions
- Verify storage key uniqueness - Verify storage key uniqueness
- Validate data serialization - Validate data serialization
- Monitor storage size limits - Monitor storage size limits
@ -634,22 +370,8 @@ export default MyComponent;
- Implement proper cleanup in useEffect - Implement proper cleanup in useEffect
- Optimize re-renders with useMemo/useCallback - Optimize re-renders with useMemo/useCallback
- Monitor memory usage with large datasets - Monitor memory usage with large datasets
Having issues or have questions?
## ⚠️ Important Notes Bring them to my attention by creating an issue.
### Mobile Compatibility
- Currently optimized for desktop use
- Mobile support is under development
- Test thoroughly on mobile devices
### Performance and Resource Management
Best practices for optimal performance:
- Clean up subscriptions and intervals
- Dispose of chart instances
- Release canvas resources
- Free up WebGL contexts
- Use persistent storage thoughtfully
### Security Considerations ### Security Considerations
- CDN imports are restricted to cdnjs.cloudflare.com - CDN imports are restricted to cdnjs.cloudflare.com
@ -657,37 +379,10 @@ Best practices for optimal performance:
- Component code runs in sandbox - Component code runs in sandbox
- No external network requests - No external network requests
## 📋 Technical Reference
### Available APIs
1. **File System**
```javascript
// File operations
let file = await readFile(path, extensions);
let content=file.content;
```
2. **Theme Management**
```javascript
// Theme utilities
const theme = getTheme();
const isDark = theme === 'dark';
```
3. **Storage**
```javascript
// Storage hooks
const [value, setValue] = useStorage(key, defaultValue);
const stored = useStorage(key, null, storage);
```
## Limitations ## Limitations
1. **Environment Constraints** 1. **Environment Constraints**
- CDN imports limited to cdnjs.cloudflare.com - CDN imports limited to cdnjs.cloudflare.com
- Components must be self-contained in one code block
- Network requests follow Obsidian's security policies
2. **Performance Considerations** 2. **Performance Considerations**
- Large datasets may impact performance - Large datasets may impact performance
@ -696,30 +391,15 @@ Best practices for optimal performance:
- Mobile performance varies - Mobile performance varies
3. **Work in Progress** 3. **Work in Progress**
- Mobile support still under development - Mobile environment not yet tested
- Some touch interactions need optimization
- Advanced debugging tools coming soon
## 🔮 Future Plans ## 🔮 Future Plans & Upcoming Features
### Upcoming Features * **Preset Component Templates**: Kickstart your development with a library of ready-to-use component templates for common use cases.
1. Advanced Visualization * **AI-Powered Component Generation**: Leverage AI (e.g., via MCP integration) to assist in generating boilerplate or even complete React components based on your descriptions.
- More chart types * **GUI for Component Snippets**: An intuitive graphical user interface to easily save your favorite or frequently used component code snippets and load them into your notes.
- Enhanced animations
- 3D visualization support
- Custom chart templates
2. Developer Experience * **Mobile-Specific Components & UI**: Introducing components and UI patterns specifically designed for the mobile experience.
- Component templates
- Debug tools
- Performance monitoring
- Testing utilities
3. Mobile Support
- Touch optimization
- Responsive layouts
- Performance improvements
- Mobile-specific components
## 🤝 Contributing ## 🤝 Contributing

219
docs/03_STATE_MANAGEMENT.md Normal file
View file

@ -0,0 +1,219 @@
# Feature: State Management in ReactiveNotes
ReactiveNotes provides robust mechanisms for managing state within your components, allowing for both transient UI state and persistent data storage that travels with your notes.
## 1. Local Component State (React `useState`)
For temporary UI state that doesn't need to persist across sessions or be saved to the note, you can use React's standard `useState` hook.
```javascript
// Example: A simple counter
const MyCounter = () => {
const [count, setCount] = React.useState(0);
return (
<button onClick={() => setCount(prev => prev + 1)}>
Clicked {count} times
</button>
);
};
export default MyCounter;
```
This state is reset every time the component is unmounted or the note is reloaded.
## 2. Persistent State with `useStorage` (Frontmatter Integration)
For state that you want to save with the note and persist across Obsidian sessions, ReactiveNotes provides the powerful `useStorage` hook. This hook directly interacts with the current note's frontmatter.
### Basic Usage
The `useStorage` hook is the primary way to create persistent state within your components.
```tsx
const [storedValue, setStoredValue, error] = useStorage(
key, // String: Property name to store in frontmatter (under `react_data` by default)
defaultValue, // Any: Default value if the key doesn't exist in frontmatter
notePath = null, // Optional String: Path to another note (null = current note)
extProp = false // Optional Boolean:
// false (default): Store under `react_data.<key>` in frontmatter
// true: Store directly as `<key>` at the root level of frontmatter
);
```
- **`storedValue`**: The current value from frontmatter, or `defaultValue` if not found.
- **`setStoredValue`**: A function to update the value. It automatically saves the new value to the note's frontmatter.
- **`error`**: Contains an error message string if loading or saving failed, otherwise `null`.
**Example: A Persistent To-Do List Item**
```jsx
const PersistentTask = () => {
// Uses 'myTask' as the key in frontmatter, defaults to an object if not found.
const [task, setTask, storageError] = useStorage('myTask', { text: 'Default Task', completed: false });
const [componentError, setComponentError] = React.useState(null); // Local error for this component example
const toggleComplete = () => {
try {
setTask(prevTask => ({ ...prevTask, completed: !prevTask.completed }));
setComponentError(null); // Clear local error on success
} catch (e) {
// This catch block might not be strictly necessary for setTask errors,
// as useStorage aims to handle errors internally and expose them via storageError.
// However, it can be useful for other logic within this function.
setComponentError("Failed to update task: " + e.message);
}
};
// Display any error from useStorage or local component logic
if (storageError || componentError) {
return <div style={{color: 'red'}}>{storageError || componentError}</div>;
}
return (
<div>
<input
type="checkbox"
checked={task.completed}
onChange={toggleComplete}
id="taskCheckbox"
/>
<label htmlFor="taskCheckbox" style={{ textDecoration: task.completed ? 'line-through' : 'none', marginLeft: '8px' }}>
{task.text}
</label>
{/* You could add an input here to change task.text and call setTask with the new object */}
</div>
);
};
export default PersistentTask;
```
### Storing at Root Level of Frontmatter
If you want to store data directly at the root of the frontmatter (outside the default `react_data` object), set the `extProp` argument to `true`.
```jsx
// Stores 'draft' under a top-level 'status' key in the current note's frontmatter
const [status, setStatus] = useStorage('status', 'draft', null, true);
```
This can be useful for interacting with frontmatter keys used by other plugins or for general note metadata.
### Cross-Note State Management
You can read and write state to _another_ note's frontmatter by providing the `notePath` argument.
```jsx
// Reads/writes 'tags' array from/to 'Templates/MyProjectTemplate.md' frontmatter (at the root level)
const [tags, setTags] = useStorage('tags', ['react'], 'Templates/MyProjectTemplate.md', true);
```
**Important:** Ensure the target note exists. Writing to a non-existent note path may result in errors or unexpected behavior.
### Deleting Keys from Frontmatter
To remove a key managed by `useStorage` from the frontmatter, set its value to `undefined`.
```jsx
setMyValue(undefined); // This will remove 'myValue' (or react_data.myValue) from the frontmatter
```
## 3. Direct Frontmatter Manipulation (Advanced)
While `useStorage` is the recommended and most convenient way to interact with frontmatter from within React components, ReactiveNotes also provides global helper functions for more direct manipulation. These might be useful in utility scripts or more complex, non-component-based scenarios.
- **`getFrontmatter(key?, defaultValue?, notePath?, extProp?)`**: Asynchronously retrieves values from frontmatter.
- Returns the entire frontmatter object if `key` is null or undefined.
- **`updateFrontmatter(key, value, notePath?, extProp?)`**: Asynchronously updates or sets values in frontmatter.
```jsx
// Example of using global helpers (less common within typical components)
// This function is NOT a React component.
async function updateGlobalProjectStatus(newStatus) {
try {
// Updates the 'projectStatus' key at the root of 'ProjectPlan.md'
await updateFrontmatter('projectStatus', newStatus, 'ProjectPlan.md', true);
new Notice('Project status updated in ProjectPlan.md!');
} catch (e) {
new Notice('Failed to update project status: ' + e.message, 5000);
console.error("Error updating frontmatter:", e);
}
}
// To read the status:
async function readGlobalProjectStatus() {
try {
const status = await getFrontmatter('projectStatus', 'unknown', 'ProjectPlan.md', true);
console.log('Current project status:', status);
return status;
} catch (e) {
console.error("Error reading frontmatter:", e);
return 'unknown';
}
}
```
**Note:** These functions are asynchronous. For reactive updates that reflect in your UI, always prefer the `useStorage` hook within your React components.
## 4. Browser `localStorage` (Global, Non-Note-Specific State)
For state that needs to be shared across _all_ notes or persist independently of any specific note (e.g., global UI preferences for your plugin's components that aren't tied to a single file), you can use the browser's standard `localStorage` API.
ReactiveNotes does not provide a specific hook for `localStorage` out-of-the-box (as it's a standard browser feature), but you can easily create a custom hook or use `localStorage` directly.
Example: useLocalStorage Custom Hook
(This is a common pattern for creating a React-friendly localStorage wrapper)
```jsx
const useLocalStorage = (key, defaultValue) => {
const [state, setState] = React.useState(() => {
let storedValue;
try {
storedValue = localStorage.getItem(key);
return storedValue ? JSON.parse(storedValue) : defaultValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return defaultValue;
}
});
React.useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(state));
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
}, [key, state]);
return [state, setState];
};
// Usage in a component:
const GlobalSettingsComponent = () => {
const [userThemePreference, setUserThemePreference] = useLocalStorage('reactiveNotesUserTheme', 'system');
// ... component logic using userThemePreference ...
return (
<select value={userThemePreference} onChange={e => setUserThemePreference(e.target.value)}>
<option value="system">System Default</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
);
};
export default GlobalSettingsComponent;
```
**Key Characteristics of `localStorage`:**
- Data persists across browser/Obsidian sessions (Is not Vault specific).
- Shared globally across all notes (not tied to a specific note's frontmatter).
- Subject to browser storage limits (typically 5-10MB per domain).
- Persists until browser data is explicitly cleared by the user (e.g., through browser settings like "Clear browsing data") or programmatically via JavaScript (localStorage.removeItem('key') or `localStorage.clear

View file

@ -0,0 +1,198 @@
# Feature: File Reading Utility
ReactiveNotes provides a convenient `readFile` utility that allows your components to interactively select and read files from your Obsidian vault, or to read a specific file by its path.
This utility is essential for components that need to process data from various file types, such as text files, Markdown notes, CSV data, JSON configurations, and more.
## How to Use
The `readFile` function is available in the scope of your React components. You can call it using `await` as it returns a Promise.
**Basic Syntax:**
```javascript
async function loadAndProcessFile() {
const fileData = await readFile(path, extensions);
if (fileData) {
// Process fileData.content, fileData.name, etc.
console.log(`Successfully read: ${fileData.name}`);
console.log(`Content snippet: ${fileData.content.substring(0, 100)}`);
} else {
// Handle cancellation or file not found/read error
console.log('File selection was cancelled or file could not be read.');
}
}
```
## Parameters
The `readFile` function accepts two optional parameters:
1. **`path`** (optional):
- Type: `string | null`
- Default: `null`
- Description: If you provide a string representing the full path to a specific file within your vault (e.g., `'data/mydata.csv'` or `'Templates/My Template.md'`), the utility will attempt to read that file directly without showing the interactive file selection modal.
- If `null` or not provided, the utility will open a suggestion modal allowing the user to search and select a file from the vault.
2. **`extensions`** (optional):
- Type: `string[]`
- Default: `['txt',` 'md', 'json', 'csv']
- Description: An array of file extensions (without the leading dot) to filter the files shown in the suggestion modal. This helps users quickly find relevant files. This parameter is ignored if a direct `path` is provided.
## Return Value
The `readFile` function returns a `Promise` that resolves to either:
- **An object** containing file details if a file is successfully selected and read:
- `path`: (string) The full path to the file in the vault (e.g., `folder/My File.md`).
- `name`: (string) The base name of the file without the extension (e.g., `My File`).
- `extension`: (string) The file extension without the leading dot (e.g., `md`).
- `content`: (string) The raw text content of the file.
- **`null`**: If:
- The user cancels the file selection modal (e.g., by pressing Escape or closing it).
- A direct `path` is provided but the file is not found or cannot be read.
- An unexpected error occurs during the file reading process.
Your component should always check if the return value is `null` to handle these cases gracefully.
## Use Cases
The `readFile` utility is versatile and can be used for:
- Loading datasets for charts and visualizations (CSV, JSON).
- Importing configurations or settings for components.
- Displaying content from other notes or text files.
- Building simple knowledge base explorers.
- Creating components that process or analyze text from files.
## Examples
### 1. Interactive File Selection (Default Behavior)
This will open a modal for the user to choose a file. By default, it filters for `.txt`, `.md`, `.json`, and `.csv` files.
```jsx
const MyDataProcessor = () => {
const [fileContent, setFileContent] = React.useState('');
const [fileName, setFileName] = React.useState('');
const handleLoadFile = async () => {
const fileData = await readFile(); // Uses default extensions
if (fileData) {
setFileName(fileData.name);
setFileContent(fileData.content);
// For CSV, you might parse it here:
// if (fileData.extension === 'csv') {
// const parsedData = Papa.parse(fileData.content, { header: true });
// console.log(parsedData.data);
// }
} else {
new Notice('File selection cancelled.');
}
};
return (
<div>
<button onClick={handleLoadFile}>Load File</button>
{fileName && <h4>Displaying: {fileName}</h4>}
{fileContent && <pre style={{maxHeight: '200px', overflowY: 'auto'}}>{fileContent}</pre>}
</div>
);
};
export default MyDataProcessor;
```
### 2. Interactive Selection with Custom Extension Filters
This will open a modal filtering for only Markdown (`.md`) and text (`.txt`) files.
```
const loadMarkdownOrTextFile = async () => {
const fileData = await readFile(null, ['md', 'txt']);
if (fileData) {
console.log(`Content of ${fileData.path}:`, fileData.content);
}
};
```
### 3. Direct File Access by Path
This will attempt to read `path/to/my/config.json` directly without showing a modal.
```
const loadSpecificConfig = async () => {
const filePath = 'configs/settings.json'; // Relative to vault root
const fileData = await readFile(filePath);
if (fileData && fileData.extension === 'json') {
try {
const config = JSON.parse(fileData.content);
console.log('Configuration loaded:', config);
// Use the config object
} catch (e) {
new Notice(`Error parsing JSON from ${filePath}: ${e.message}`);
console.error("JSON parsing error:", e);
}
} else if (fileData) {
new Notice(`Expected a JSON file but got .${fileData.extension}`);
} else {
new Notice(`Could not read file: ${filePath}. Ensure it exists.`);
}
};
```
### 4. Reading CSV Data for Charts
```
// (Assuming you have a chart component that takes data)
const CSVChartLoader = () => {
const [chartData, setChartData] = React.useState(null);
const loadCSV = async () => {
const fileData = await readFile(null, ['csv']);
if (fileData && fileData.extension === 'csv') {
// Assuming PapaParse is available in scope
const parsed = Papa.parse(fileData.content, { header: true, dynamicTyping: true });
if (parsed.errors.length > 0) {
new Notice("Errors found while parsing CSV: " + parsed.errors[0].message);
return;
}
setChartData(parsed.data);
new Notice(`Loaded ${parsed.data.length} rows from ${fileData.name}`);
} else if (fileData) {
new Notice(`Selected file was not a CSV: ${fileData.name}`);
}
};
return (
<div>
<button onClick={loadCSV}>Load CSV for Chart</button>
{/* {chartData && <MyChartComponent data={chartData} />} */}
{chartData && <pre>Data Loaded: {JSON.stringify(chartData.slice(0,2))}...</pre>}
</div>
);
};
export default CSVChartLoader;
```
By providing both interactive selection and direct path access, `readFile` offers flexibility for various component needs within ReactiveNotes.

1
docs/EXAMPLES/README.md Normal file
View file

@ -0,0 +1 @@
**(To be Populated)**

786
main.tsx
View file

@ -1,4 +1,4 @@
import { Plugin, MarkdownPostProcessorContext, MarkdownRenderChild, MarkdownRenderer, TFile, App, stringifyYaml, Notice, SuggestModal } from 'obsidian'; import { Plugin, MarkdownPostProcessorContext, MarkdownRenderChild, MarkdownRenderer, TFile, App, stringifyYaml, Notice, SuggestModal, loadMathJax, PluginSettingTab, Setting } from 'obsidian';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { transform } from '@babel/standalone'; import { transform } from '@babel/standalone';
import * as React from 'react'; import * as React from 'react';
@ -8,12 +8,16 @@ import { StorageManager } from 'src/core/storage';
import { useStorage } from 'src/hooks/useStorage'; import { useStorage } from 'src/hooks/useStorage';
import { ComponentRegistry } from 'src/components/componentRegistry'; import { ComponentRegistry } from 'src/components/componentRegistry';
import { ErrorBoundary } from 'src/components/ErrorBoundary'; import { ErrorBoundary } from 'src/components/ErrorBoundary';
import {useMathJax} from 'src/hooks/mathJax';
import { read } from 'fs'; import { read } from 'fs';
import { error } from 'console'; import { error } from 'console';
declare global {
interface Window {
MathJax: any;
}
}
class ReactComponentChild extends MarkdownRenderChild { class ReactComponentChild extends MarkdownRenderChild {
private root: ReturnType<typeof createRoot>; private root: ReturnType<typeof createRoot>;
private storage: StorageManager; private storage: StorageManager;
@ -22,14 +26,16 @@ class ReactComponentChild extends MarkdownRenderChild {
private noteFile: TFile; private noteFile: TFile;
private ctx: MarkdownPostProcessorContext; private ctx: MarkdownPostProcessorContext;
private app: App; private app: App;
private settings: ReactiveNotesSettings;
private processedPaths = new Set<string>(); private processedPaths = new Set<string>();
constructor(containerEl: HTMLElement, plugin: Plugin, ctx: MarkdownPostProcessorContext) { constructor(containerEl: HTMLElement, plugin: ReactNotesPlugin, ctx: MarkdownPostProcessorContext) {
super(containerEl); super(containerEl);
this.root = createRoot(containerEl); this.root = createRoot(containerEl);
this.storage = new StorageManager(plugin); this.storage = new StorageManager(plugin);
this.ctx = ctx; this.ctx = ctx;
this.app = plugin.app; // Get app from plugin this.app = plugin.app; // Get app from plugin
this.settings=plugin.settings;
// Get the source file from context // Get the source file from context
if (ctx.sourcePath) { if (ctx.sourcePath) {
const abstractFile = plugin.app.vault.getAbstractFileByPath(ctx.sourcePath); const abstractFile = plugin.app.vault.getAbstractFileByPath(ctx.sourcePath);
@ -81,7 +87,7 @@ class ReactComponentChild extends MarkdownRenderChild {
new Notice(errorMsg); new Notice(errorMsg);
return defaultValue; return defaultValue;
} }
console.log('Frontmatter:', frontmatter); //console.log('Frontmatter:', frontmatter);
// Return specific key if requested // Return specific key if requested
if (key) { 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 outer frontmatter, But found in react_data. Set extProp to false to access it.`);
@ -132,260 +138,262 @@ class ReactComponentChild extends MarkdownRenderChild {
} }
} }
private RenderWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => { private RenderWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const containerRef=this.settings.mathJaxEnabled?useMathJax([children]):null;
return ( return (
<ErrorBoundary <ErrorBoundary
fallback={({ error }) => { fallback={({ error }) => {
// The ErrorBoundary will catch the error and display a fallback UI. // 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. // 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')) { if (error.message?.includes('Objects are not valid as a React child')) {
const objectMatch = error.message.match(/found: object with keys {([^}]+)}/); const objectMatch = error.message.match(/found: object with keys {([^}]+)}/);
const keys = objectMatch ? objectMatch[1].split(',').map(k => k.trim()) : ['unknown']; const keys = objectMatch ? objectMatch[1].split(',').map(k => k.trim()) : ['unknown'];
return (
<div style={{ padding: '12px', backgroundColor: 'var(--background-modifier-form-field)', borderRadius: '6px', border: '1px solid var(--background-modifier-border)', fontFamily: 'var(--font-monospace)', margin: '1rem 0' }}>
<div style={{ fontWeight: 'bold', marginBottom: '8px', display: 'flex', alignItems: 'center', gap: '8px' }}>
<span>🧩</span> <span>Component Rendered an Object</span>
</div>
<div style={{ fontSize: '0.9em', opacity: 0.8, marginBottom: '4px' }}>A React component attempted to render a plain JavaScript object. Objects need to be mapped to renderable elements.</div>
<div style={{ fontSize: '0.9em', opacity: 0.8 }}>Object keys found:</div>
<ul style={{ margin: '8px 0 0 20px', paddingLeft: '0', listStyleType: 'disc' }}>{keys.map(key => <li key={key} style={{fontSize: '0.85em'}}>{key}</li>)}</ul>
</div>
);
}
// Default error display for other types of errors
return ( return (
<div style={{ padding: '12px', backgroundColor: 'var(--background-modifier-form-field)', borderRadius: '6px', border: '1px solid var(--background-modifier-border)', fontFamily: 'var(--font-monospace)', margin: '1rem 0' }}> <div className="react-component-error" style={{ margin: '1rem 0' }}>
<div style={{ fontWeight: 'bold', marginBottom: '8px', display: 'flex', alignItems: 'center', gap: '8px' }}> <p style={{color: 'var(--text-error)', fontWeight: 'bold'}}>Error in component:</p>
<span>🧩</span> <span>Component Rendered an Object</span> <pre className="error-message" style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all', backgroundColor: 'var(--background-secondary-alt)', padding: '8px', borderRadius: '4px', color: 'var(--text-error)'}}>{error.message}</pre>
</div> {error.stack && <details style={{marginTop: '8px'}}><summary style={{cursor: 'pointer', fontSize: '0.9em'}}>Stack Trace</summary><pre style={{fontSize: '0.8em', maxHeight: '150px', overflowY: 'auto', backgroundColor: 'var(--background-secondary)', padding: '8px', borderRadius: '4px', marginTop: '4px'}}>{error.stack}</pre></details>}
<div style={{ fontSize: '0.9em', opacity: 0.8, marginBottom: '4px' }}>A React component attempted to render a plain JavaScript object. Objects need to be mapped to renderable elements.</div>
<div style={{ fontSize: '0.9em', opacity: 0.8 }}>Object keys found:</div>
<ul style={{ margin: '8px 0 0 20px', paddingLeft: '0', listStyleType: 'disc' }}>{keys.map(key => <li key={key} style={{fontSize: '0.85em'}}>{key}</li>)}</ul>
</div> </div>
); );
} }}
// Default error display for other types of errors >
return ( <div ref={containerRef}>
<div className="react-component-error" style={{ margin: '1rem 0' }}> {children}
<p style={{color: 'var(--text-error)', fontWeight: 'bold'}}>Error in component:</p> </div>
<pre className="error-message" style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all', backgroundColor: 'var(--background-secondary-alt)', padding: '8px', borderRadius: '4px', color: 'var(--text-error)'}}>{error.message}</pre> </ErrorBoundary>
{error.stack && <details style={{marginTop: '8px'}}><summary style={{cursor: 'pointer', fontSize: '0.9em'}}>Stack Trace</summary><pre style={{fontSize: '0.8em', maxHeight: '150px', overflowY: 'auto', backgroundColor: 'var(--background-secondary)', padding: '8px', borderRadius: '4px', marginTop: '4px'}}>{error.stack}</pre></details>} );
</div> };
);
}}
>
{children}
</ErrorBoundary>
);
};
private needsThree(code: string): boolean { private needsThree(code: string): boolean {
// Check if the code contains any Three.js specific imports or usage // Check if the code contains any Three.js specific imports or usage
// More comprehensive detection of THREE usage // More comprehensive detection of THREE usage
const hasThreeImports = /import\s+.*?three['"];?\s*$/gm.test(code) || const hasThreeImports = /import\s+.*?three['"];?\s*$/gm.test(code) ||
/import\s*{\s*[^}]*}\s*from\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); /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'" // 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 threeAliasMatch = code.match(/import\s+\*\s+as\s+(\w+)\s+from\s+['"]three['"];?\s*$/m);
const threeAlias = threeAliasMatch ? threeAliasMatch[1] : null; const threeAlias = threeAliasMatch ? threeAliasMatch[1] : null;
// Check for various THREE usage patterns // Check for various THREE usage patterns
let hasThreeUsage = /\bTHREE\.|\bnew\s+THREE\.|\bextends\s+THREE\./.test(code) || // Direct THREE usage 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 /\bUtilities\.THREE\.|\bnew\s+Utilities\.THREE\./.test(code) || // Utilities.THREE usage
/\bComponentRegistry\.Utilities\.THREE\./.test(code); // Full path usage /\bComponentRegistry\.Utilities\.THREE\./.test(code); // Full path usage
// If there's a custom alias, check for that too // If there's a custom alias, check for that too
if (threeAlias) { if (threeAlias) {
hasThreeUsage = hasThreeUsage || new RegExp(`\\b${threeAlias}\\.`).test(code); 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;
} }
// 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<string> { private async preprocessCode(code: string): Promise<string> {
// 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 // Replace CDN imports with script loading
code = code.replace(/export\s+default\s+/gm, ''); code = code.replace(
code = code.replace(/export\s+const\s+/gm, 'const '); /import\s+(\w+)\s+from\s+['"]https:\/\/cdnjs\.cloudflare\.com\/([^'"]+)['"]/g,
code = code.replace(/export\s+function\s+/gm, 'function '); (match, importName, cdnPath) => `
code = code.replace(/export\s+class\s+/gm, 'class '); const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/${cdnPath}';
document.body.appendChild(script);`);
// Remove type annotations // Remove imports carefully
//code = code.replace(/:\s*[A-Za-z<>[\]]+/g, ''); code = code.replace(/import\s+.*?['"].*$/gm, '');
//code = code.replace(/<[A-Za-z,\s]+>/g, ''); code = code.replace(/import\s*{[^}]*}\s*from\s*['"][^'"]*['"].*$/gm, '');
code = code.replace(/import\s*\([^)]*\).*$/gm, '');
// Find the component name // Handle both JS/TS exports
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); code = code.replace(/export\s+default\s+/gm, '');
const storeMatch = !componentMatch ? code.match(/(?:const\s+(\w+)\s*=\s*{|class\s+(\w+)\s*{)/) : null; code = code.replace(/export\s+const\s+/gm, 'const ');
code = code.replace(/export\s+function\s+/gm, 'function ');
if (!componentMatch) { code = code.replace(/export\s+class\s+/gm, 'class ');
// If no component found, look for object literals or other exports
if (!storeMatch) {
throw new Error('No React component found'); // 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
// Set safeName using either match const componentName = componentMatch
const componentName = componentMatch ? (componentMatch[1] || componentMatch[2])
? (componentMatch[1] || componentMatch[2]) : (storeMatch ? (storeMatch[1]|| storeMatch[2]) : 'EmptyComponent');
: (storeMatch ? (storeMatch[1]|| storeMatch[2]) : 'EmptyComponent'); console.log('Found component:', componentName);
console.log('Found component:', componentName);
// Find component name and rename if it's 'WrappedComponent'
// Find component name and rename if it's 'WrappedComponent' const safeName = componentName === 'WrappedComponent' ? 'UserComponent' : componentName;
const safeName = componentName === 'WrappedComponent' ? 'UserComponent' : componentName;
if (componentName === 'WrappedComponent') {
if (componentName === 'WrappedComponent') { code = code.replace(/\bWrappedComponent\b/, safeName);
code = code.replace(/\bWrappedComponent\b/, safeName); }
} // Create a HOC wrapper for chart components
// Create a HOC wrapper for chart components const chartWrapper = `
const chartWrapper = ` const withChartContainer = (WrappedComponent) => {
const withChartContainer = (WrappedComponent) => { return function ChartContainer(props) {
return function ChartContainer(props) { const [mounted, setMounted] = React.useState(false);
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
React.useEffect(() => { const timer = setTimeout(() => setMounted(true), 100);
const timer = setTimeout(() => setMounted(true), 100); return () => clearTimeout(timer);
return () => clearTimeout(timer); }, []);
}, []);
return React.createElement(
return React.createElement( 'div',
'div', { style: { width: '100%', minHeight: '400px' } },
{ style: { width: '100%', minHeight: '400px' } }, mounted ? React.createElement(WrappedComponent, props) : null
mounted ? React.createElement(WrappedComponent, props) : null );
); };
}; };
}; `;
`; // Combine everything
// Combine everything // added Component assignment
// added Component assignment code = `
code = ` ${chartWrapper}
${chartWrapper} ${code}
${code} const WrappedComponent = (() => {
const WrappedComponent = (() => { // Add error handling for component existence
// Add error handling for component existence if (typeof ${safeName} === 'undefined') {
if (typeof ${safeName} === 'undefined') { throw new Error(\`Component "${safeName}" was matched but is undefined. Code context: ${code.slice(0, 100).replace(/`/g, '\\`')}...\`);
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?
// Simple check - is it a valid React component? const isValidComponent = (() => {
const isValidComponent = (() => { // Class component
// Class component if (${safeName}.prototype?.isReactComponent) return true;
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;
// Function that returns React elements
if (typeof ${safeName} === 'function') { if (typeof ${safeName} === 'function') {
try { try {
// Try as function const result = ${safeName}({});
definitions = ${safeName}({}); return React.isValidElement(result);
} catch (e) { } 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 {
// Try as class // Try as function
definitions = new ${safeName}(); definitions = ${safeName}({});
} catch (e2) { } catch (e) {
// Last resort try {
definitions = ${safeName}; // Try as class
definitions = new ${safeName}();
} catch (e2) {
// Last resort
definitions = ${safeName};
}
} }
} else {
// Already an object (like IIFE or object literal)
definitions = ${safeName};
} }
} else {
// Already an object (like IIFE or object literal) // Ensure it's an object
definitions = ${safeName}; if (!definitions || typeof definitions !== 'object') {
} definitions = { error: "Couldn't extract definitions" };
// 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', { const keys = Object.keys(definitions);
style: { return React.createElement('div', {
fontWeight: 'bold', style: {
marginBottom: '8px', 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', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '8px' gap: '8px'
} }
}, [ }, [
React.createElement('span', null, '📦'), React.createElement('code', {
React.createElement('span', null, 'Definitions Storage Container:') style: {
padding: '2px 6px',
backgroundColor: 'rgba(255,255,255,0.2)',
borderRadius: '3px',
fontWeight: 'bold'
}
}, '${safeName}')
]), ]),
React.createElement('div', { React.createElement('div', {
style: { style: { fontSize: '0.9em', opacity: 0.8 }
padding: '8px', }, 'Exported definitions:'),
marginBottom: '12px', React.createElement('ul', {
color: 'var(--text-on-accent)', style: {
borderRadius: '4px', margin: '8px 0',
display: 'flex', paddingLeft: '20px'
alignItems: 'center', }
gap: '8px' }, keys.map(key =>
} React.createElement('li', { key },
}, [ \`\${key}: \${typeof definitions[key]}\`
React.createElement('code', { )
style: { ))
padding: '2px 6px', ]);
backgroundColor: 'rgba(255,255,255,0.2)', };
borderRadius: '3px', }
fontWeight: 'bold' const isChartComponent = ${safeName}.toString().includes('ResponsiveContainer') ||
} ${safeName}.toString().includes('svg');
}, '${safeName}') return isChartComponent ? withChartContainer(${safeName}) : ${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); code = await this.processVaultImports(code);
// Wrap code in async IIFE to allow for await // Wrap code in async IIFE to allow for await
return ` return `
@ -585,12 +593,11 @@ private async preprocessCode(code: string): Promise<string> {
); );
return [value, updateValue, error] as const; return [value, updateValue, error] as const;
}; };
// Create scope with all required dependencies // Create scope with all required dependencies
const scope = { const scope = {
...ComponentRegistry, ...ComponentRegistry,
useStorage: boundUseStorage, useStorage: boundUseStorage,
//Not included MarketAnalysis functions
getTheme: () => document.body.hasClass('theme-dark') ? 'dark' : 'light', getTheme: () => document.body.hasClass('theme-dark') ? 'dark' : 'light',
// Add note context // Add note context
noteContext: { noteContext: {
@ -639,7 +646,7 @@ private async preprocessCode(code: string): Promise<string> {
}, },
Notice: (message:string, timeout = 4000) => new Notice(message, timeout), 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 // Execute the code and get the component
const Component = await new Function( const Component = await new Function(
...Object.keys(scope), ...Object.keys(scope),
@ -713,7 +720,7 @@ private async preprocessCode(code: string): Promise<string> {
} }
onChooseSuggestion(file:TFile, evt: MouseEvent|KeyboardEvent) { onChooseSuggestion(file:TFile, evt: MouseEvent|KeyboardEvent) {
console.log("File content:"); //console.log("File content:");
if (cancelTimeoutHandle !== undefined) { if (cancelTimeoutHandle !== undefined) {
clearTimeout(cancelTimeoutHandle); clearTimeout(cancelTimeoutHandle);
cancelTimeoutHandle = undefined; cancelTimeoutHandle = undefined;
@ -793,114 +800,191 @@ private async preprocessCode(code: string): Promise<string> {
if (onFinally) onFinally(); 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`);
export default class ReactNotesPlugin extends Plugin { // Inject the CSS
private tailwindStyles: HTMLStyleElement | null = null; // Tailwind styles reference const style = document.createElement('style');
async onload() { style.id = 'tailwind-styles';
try { style.textContent = css;
// Read the CSS file using Obsidian's API document.head.appendChild(style);
// - only left here for development purposes, this.tailwindStyles = style;
// Users wont have this in production, we may use the build:css script to generate and add the css file in future } catch (error) {
const css = await this.app.vault.adapter.read(`${this.manifest.dir}/outStyles.css`); console.error("Failed to load CSS file:", error);
}
// Inject the CSS // Listen for theme changes
const style = document.createElement('style'); this.registerEvent(
style.id = 'tailwind-styles'; this.app.workspace.on('css-change', () => {
style.textContent = css; this.updateTheme();
document.head.appendChild(style); })
this.tailwindStyles = style; );
} catch (error) { // Register markdown processor
console.error("Failed to load CSS file:", error); this.registerMarkdownCodeBlockProcessor(
} 'react',
// Listen for theme changes (source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) => {
this.registerEvent( const child = new ReactComponentChild(el, this, ctx);
this.app.workspace.on('css-change', () => { ctx.addChild(child);
this.updateTheme(); // Set a timeout to detect long-running operations
}) const timeoutId = setTimeout(() => {
); new Notice('React component taking too long to render. Check for expensive operations.');
// Register markdown processor }, 3000);
this.registerMarkdownCodeBlockProcessor( child.render(source);
'react', clearTimeout(timeoutId);
(source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) => { }
const child = new ReactComponentChild(el, this, ctx); );
ctx.addChild(child); // Register a global Markdown postprocessor for HTML
// Set a timeout to detect long-running operations this.registerMarkdownPostProcessor((element, context) => {
const timeoutId = setTimeout(() => { // Only process elements that are within ReactiveNotes components
new Notice('React component taking too long to render. Check for expensive operations.'); if (element.closest('.react-component-container')) {
}, 3000); this.parseMarkdownInHtml(element);
child.render(source); }
clearTimeout(timeoutId); });
} // Initial theme setup
); this.updateTheme();
// Register a global Markdown postprocessor for HTML this.addSettingTab(new ReactiveNotesSettingTab(this.app, this));
this.registerMarkdownPostProcessor((element, context) => { }
// Only process elements that are within ReactiveNotes components async saveSettings() {
if (element.closest('.react-component-container')) { await this.saveData(this.settings);
this.parseMarkdownInHtml(element); }
} async loadSettings() {
}); this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
// Initial theme setup }
this.updateTheme(); onunload() {
} // Clean up styles
if (this.tailwindStyles) {
onunload() { this.tailwindStyles.remove();
// Clean up styles }
if (this.tailwindStyles) { // Reset MathJax state
this.tailwindStyles.remove(); if (window.MathJax) {
} try {
} window.MathJax.typesetClear();
private updateTheme = () => { } catch (e) {
// Update theme class on all react component containers console.warn("MathJax cleanup error:", e);
document.querySelectorAll('.react-component-container').forEach(el => { }
if (document.body.hasClass('theme-dark')) { }
el.classList.add('theme-dark'); }
el.classList.add('dark'); public updateTheme = () => {
el.classList.remove('theme-light'); // Update theme class on all react component containers
} else { document.querySelectorAll('.react-component-container').forEach(el => {
el.classList.add('theme-light'); if (document.body.hasClass('theme-dark')) {
el.classList.remove('dark'); el.classList.add('theme-dark');
el.classList.remove('theme-dark'); el.classList.add('dark');
} el.classList.remove('theme-light');
}); } else {
}; el.classList.add('theme-light');
private async parseMarkdownInHtml(container: HTMLElement) { el.classList.remove('dark');
// Query all divs or specific HTML blocks you want to process el.classList.remove('theme-dark');
const htmlBlocks = Array.from(container.querySelectorAll('div:not(.markdown-rendered)')) as HTMLDivElement[]; }
//console.log('Processing HTML container:', container); if (this.settings.forceTheme === 'dark') {
// Iterate over each div block el.classList.add('theme-dark');
for (const block of htmlBlocks) { el.classList.add('dark');
// Check if it already contains rendered Markdown (to avoid double processing) el.classList.remove('theme-light');
if (block.querySelector('.markdown-rendered')) continue; }
else if (this.settings.forceTheme === 'light') {
// Render Markdown for the inner content of each block el.classList.add('theme-light');
await MarkdownRenderer.render( el.classList.remove('dark');
this.app, el.classList.remove('theme-dark');
block.textContent || "", // // Get text content }
block, // Target container for rendered Markdown });
'', // Path (optional, current file path if needed) };
this // Plugin context
);
/* private async parseMarkdownInHtml(container: HTMLElement) {
// Alternative if more control is needed: // Query all divs or specific HTML blocks you want to process
// Create a temporary div to safely extract text const htmlBlocks = Array.from(container.querySelectorAll('div:not(.markdown-rendered)')) as HTMLDivElement[];
const tempDiv = document.createElement('div'); //console.log('Processing HTML container:', container);
tempDiv.appendChild(block.cloneNode(true)); // Iterate over each div block
const safeContent = tempDiv.textContent || ""; for (const block of htmlBlocks) {
// Check if it already contains rendered Markdown (to avoid double processing)
await MarkdownRenderer.renderMarkdown( if (block.querySelector('.markdown-rendered')) continue;
this.app,
safeContent, // Render Markdown for the inner content of each block
block, await MarkdownRenderer.render(
'', this.app,
this block.textContent || "", // // Get text content
); block, // Target container for rendered Markdown
*/ '', // Path (optional, current file path if needed)
this // Plugin context
// After rendering, mark the block to avoid double processing );
block.classList.add('markdown-rendered'); /*
} // 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();
}));
}
}

View file

@ -1,7 +1,7 @@
{ {
"id": "reactive-notes", "id": "reactive-notes",
"name": "Reactive Notes", "name": "Reactive Notes",
"version": "1.0.4", "version": "1.0.5",
"minAppVersion": "1.4.0", "minAppVersion": "1.4.0",
"description": "Transform your vault into a reactive computational environment. Create dynamic React components directly in your notes.", "description": "Transform your vault into a reactive computational environment. Create dynamic React components directly in your notes.",
"author": "Elias Noesis", "author": "Elias Noesis",

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "reactive-notes", "name": "reactive-notes",
"version": "1.0.4", "version": "1.0.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "reactive-notes", "name": "reactive-notes",
"version": "1.0.4", "version": "1.0.5",
"dependencies": { "dependencies": {
"@codemirror/language": "^6.10.6", "@codemirror/language": "^6.10.6",
"@emotion/css": "^11.13.5", "@emotion/css": "^11.13.5",

View file

@ -1,6 +1,6 @@
{ {
"name": "reactive-notes", "name": "reactive-notes",
"version": "1.0.4", "version": "1.0.5",
"description": "Transform your vault into a reactive computational environment with React components.", "description": "Transform your vault into a reactive computational environment with React components.",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {

213
src/hooks/mathJax.tsx Normal file
View file

@ -0,0 +1,213 @@
import { e } from "mathjs";
import React from "react";
export function useMathJax(children: React.ReactNode) {
// const containerRef = React.useRef(null);
const containerRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (!containerRef.current || !window.MathJax) {
if (!window.MathJax) {
console.warn("MathJax not available when RenderWrapper mounted.");
}
return;
}
const container = containerRef.current;
let mathJaxProcessed = false; // Flag to ensure typesetPromise runs only after processing
interface MathMatch {
type: 'math';
matched: string;
displayMath?: string;
inlineMath?: string;
escapedMath?: string;
startIndex: number;
endIndex: number;
}
interface CodeMatch {
type: 'code';
matched: string;
delimiter: string;
content: string;
startIndex: number;
endIndex: number;
}
type Match = MathMatch | CodeMatch;
const processSingleTextNode = (textNode: Text): DocumentFragment | null => {
let currentText = textNode.nodeValue || '';
if (!currentText.includes('$')) { // No dollar signs at all, nothing to do
return null;
}
// Find all matches of both patterns
const allMatches: Match[] = [];
const fragment = document.createDocumentFragment();
let lastIndex = 0;
// Regex to find $$...$$ (unescaped) or $...$ (unescaped) or escaped \$ or \$\$
const mathRegex = /(?<!\\)\$\$(.*?)(?<!\\)\$\$|(?<!\\)\$(?!\s)(.*?)(?<!\s)(?<!\\)\$|\\(\$\$|\$)/gs;
let mathMatch;
while ((mathMatch = mathRegex.exec(currentText)) !== null) {
allMatches.push({
type: 'math',
matched: mathMatch[0],
displayMath: mathMatch[1],
inlineMath: mathMatch[2],
escapedMath: mathMatch[3],
startIndex: mathMatch.index,
endIndex: mathMatch.index + mathMatch[0].length
});
}
const codeBlockRegex = /(`+)((?:(?!\1).)*?)\1/gs;
let codeMatch;
while ((codeMatch = codeBlockRegex.exec(currentText)) !== null) {
allMatches.push({
type: 'code',
matched: codeMatch[0],
delimiter: codeMatch[1],
content: codeMatch[2],
startIndex: codeMatch.index,
endIndex: codeMatch.index + codeMatch[0].length
});
}
// Sort all matches by start index
allMatches.sort((a, b) => a.startIndex - b.startIndex);
// Track active matches (open but not yet closed)
const activeMatches: Match[] = [];
// Process text with matches
for (let i = 0; i < allMatches.length; i++) {
const current = allMatches[i];
// Add text before this match
if (current.startIndex > lastIndex) {
fragment.appendChild(document.createTextNode(
currentText.substring(lastIndex, current.startIndex)
));
}
// Check if this match is inside any active match
const isInsideCode = activeMatches.some(m =>
m.type === 'code' && current.startIndex > m.startIndex && current.endIndex <= m.endIndex
);
if (isInsideCode) {
continue;
}
// Process based on type
if (current.type === 'code') {
fragment.appendChild(document.createTextNode(current.matched));
activeMatches.push(current);
// Remove from active once processed
setTimeout(() => {
const index = activeMatches.indexOf(current);
if (index > -1) activeMatches.splice(index, 1);
}, 0);
} else if (current.type === 'math') {
if (current.displayMath !== undefined) {
try {
const mathNode = window.MathJax.tex2chtml(current.displayMath, { display: true });
fragment.appendChild(mathNode);
} catch (e) {
console.error("MathJax display math error:", e);
fragment.appendChild(document.createTextNode(current.matched));
}
} else if (current.inlineMath !== undefined) {
try {
const mathNode = window.MathJax.tex2chtml(current.inlineMath, { display: false });
fragment.appendChild(mathNode);
} catch (e) {
console.error("MathJax inline math error:", e);
fragment.appendChild(document.createTextNode(current.matched));
}
} else if (current.escapedMath !== undefined) {
fragment.appendChild(document.createTextNode(current.escapedMath));
}
}
lastIndex = current.endIndex;
}
// Add remaining text
if (lastIndex < currentText.length) {
fragment.appendChild(document.createTextNode(
currentText.substring(lastIndex)
));
}
return fragment;
};
const processMathInElement = (element: HTMLElement) => {
const textNodesToReplace: { originalNode: Text, newFragment: DocumentFragment }[] = [];
const walker = document.createTreeWalker(
element,
NodeFilter.SHOW_TEXT,
null
);
let currentNode;
while ((currentNode = walker.nextNode())) {
if (currentNode instanceof Text) {
const newContentFragment = processSingleTextNode(currentNode);
if (newContentFragment) {
textNodesToReplace.push({ originalNode: currentNode, newFragment: newContentFragment });
}
}
}
// Perform DOM replacements after identifying all nodes to avoid issues with the walker
textNodesToReplace.forEach(({ originalNode, newFragment }) => {
originalNode.parentNode?.replaceChild(newFragment, originalNode);
});
};
const typesetMath = async () => {
if (!window.MathJax || !containerRef.current) return;
try {
// Ensure MathJax startup is complete (important for initial loads)
if (window.MathJax.startup && typeof window.MathJax.startup.promise === 'object') {
await window.MathJax.startup.promise;
} else if (typeof window.MathJax.startup?.promise === 'function' ) { // older MathJax might have it as a function
await window.MathJax.startup.promise();
}
processMathInElement(container);
// Only call typesetPromise if we actually processed some math and if the container still exists
if (mathJaxProcessed && containerRef.current && typeof window.MathJax.typesetPromise === 'function') {
await window.MathJax.typesetPromise([containerRef.current]);
console.log("MathJax typesetting complete for:", containerRef.current);
}
} catch (e) {
console.error("Error during MathJax processing or typesetting:", e);
}
};
typesetMath();
// Cleanup function
return () => {
try {
// Clear MathJax's knowledge of this container when the component unmounts
// or if the children prop changes, leading to this effect re-running.
if (window.MathJax && containerRef.current && typeof window.MathJax.typesetClear === 'function') {
window.MathJax.typesetClear([containerRef.current]);
console.log("MathJax state cleared for:", containerRef.current);
}
} catch (e) {
console.warn("MathJax cleanup error:", e);
}
};
}, [children]); // Re-run if children prop changes, implying new content to process.
return containerRef;
}
export default useMathJax;

View file

@ -1,5 +1,6 @@
{ {
"1.0.2": "1.4.0", "1.0.2": "1.4.0",
"1.0.3": "1.4.0", "1.0.3": "1.4.0",
"1.0.4": "1.4.0" "1.0.4": "1.4.0",
"1.0.5": "1.4.0"
} }