Skip to content

Commit ac25a19

Browse files
staredclaude
andcommitted
Add interactive markdown editor with real-time preview
Implement collapsible right sidebar editor with syntax highlighting and live equation preview. Users can now edit equations in real-time and contribute new equations via pull requests. Features: - CodeJar + Prism.js editor (~4.5KB total, minimal bundle impact) - Custom syntax highlighting for \mark[], []{.class}, ## .class - Term colors in editor match equation color scheme - Real-time preview with 500ms debouncing - Two modes: * Existing equation: collapsed by default, loads current markdown * New equation: visible by default with template - Download markdown as .md file - Contribute workflow instructions for GitHub PR submission - Responsive: collapses to horizontal bar on mobile - Smooth animations and Tufte-style minimal design Technical changes: - Add CodeJar and Prism.js dependencies - Create src/prism-custom.ts with custom language definition - Update parser.ts to support parsing from string - Extend main.ts with editor initialization and controls - Add three-column layout CSS with collapse transitions - Add toolbar with toggle, new, download, contribute buttons 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent ffba54a commit ac25a19

File tree

7 files changed

+576
-6
lines changed

7 files changed

+576
-6
lines changed

index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@ <h1 id="equation-title">Interactive Equations</h1>
3939
</p>
4040
</footer>
4141
</main>
42+
43+
<aside id="editor-sidebar" class="editor-sidebar collapsed">
44+
<div class="editor-toolbar">
45+
<button id="toggle-editor-btn" class="toolbar-btn" title="Show/hide editor">
46+
<span class="icon"></span>
47+
</button>
48+
<button id="new-equation-btn" class="toolbar-btn" title="Create new equation">New</button>
49+
<button id="download-btn" class="toolbar-btn" title="Download markdown">Download</button>
50+
<button id="contribute-btn" class="toolbar-btn" title="Contribute to repository">Contribute</button>
51+
</div>
52+
<div id="editor-container" class="editor-container"></div>
53+
</aside>
4254
</div>
4355
<script type="module" src="/src/main.ts"></script>
4456
</body>

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
"vite": "^7.1.12"
3030
},
3131
"dependencies": {
32-
"katex": "^0.16.25"
32+
"codejar": "^4.3.0",
33+
"katex": "^0.16.25",
34+
"prismjs": "^1.30.0"
3335
}
3436
}

pnpm-lock.yaml

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/main.ts

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import katex from 'katex';
22
import './style.css';
33
import { loadContent, type ParsedContent } from './parser';
4+
import { CodeJar } from 'codejar';
5+
import Prism from 'prismjs';
6+
import './prism-custom';
7+
import { applyTermColors } from './prism-custom';
48

59
// Equation metadata
610
interface EquationInfo {
@@ -65,6 +69,12 @@ const colorSchemes: Record<string, ColorScheme> = {
6569
let currentScheme = 'vibrant';
6670
let parsedContent: ParsedContent | null = null;
6771

72+
// Editor state
73+
let editor: any = null;
74+
let isNewEquationMode = false;
75+
let currentMarkdown = '';
76+
let previewTimeout: number | null = null;
77+
6878
function applyColorScheme(schemeName: string) {
6979
const scheme = colorSchemes[schemeName];
7080
if (!scheme || !parsedContent) return;
@@ -252,6 +262,11 @@ async function loadEquation(equationId: string, updateHash = true) {
252262
}
253263
});
254264
}
265+
266+
// Load markdown into editor (for existing equation mode)
267+
if (!isNewEquationMode) {
268+
await loadMarkdownIntoEditor(`./examples/${equation.file}`);
269+
}
255270
}
256271

257272
// Create equation selector buttons
@@ -285,6 +300,209 @@ window.addEventListener('hashchange', async () => {
285300
await loadEquation(equationId, false); // Don't update hash again
286301
});
287302

303+
// Editor functions
304+
function highlightEditor(editorElement: HTMLElement) {
305+
// Use Prism to highlight
306+
editorElement.innerHTML = Prism.highlight(
307+
editorElement.textContent || '',
308+
Prism.languages.eqmd,
309+
'eqmd'
310+
);
311+
312+
// Apply term colors if we have parsed content
313+
if (parsedContent) {
314+
applyTermColors(editorElement, parsedContent.termOrder, colorSchemes[currentScheme].colors);
315+
}
316+
}
317+
318+
function initializeEditor() {
319+
const editorContainer = document.getElementById('editor-container');
320+
if (!editorContainer) return;
321+
322+
// Create code element for CodeJar
323+
const codeElement = document.createElement('code');
324+
codeElement.className = 'language-eqmd';
325+
editorContainer.appendChild(codeElement);
326+
327+
// Initialize CodeJar with Prism highlighting
328+
editor = CodeJar(codeElement, highlightEditor, {
329+
tab: ' ', // 2 spaces for tab
330+
indentOn: /[({[]$/,
331+
});
332+
333+
// Update preview on change (debounced)
334+
editor.onUpdate((code: string) => {
335+
currentMarkdown = code;
336+
337+
// Clear previous timeout
338+
if (previewTimeout !== null) {
339+
clearTimeout(previewTimeout);
340+
}
341+
342+
// Debounce: update after 500ms of inactivity
343+
previewTimeout = window.setTimeout(async () => {
344+
await updatePreview();
345+
}, 500);
346+
});
347+
}
348+
349+
async function updatePreview() {
350+
if (!currentMarkdown.trim()) return;
351+
352+
try {
353+
// Parse markdown content
354+
parsedContent = await loadContent(currentMarkdown, true); // true = from string
355+
356+
// Clear and re-render
357+
const equationContainer = document.getElementById('equation-container');
358+
const descriptionContainer = document.getElementById('static-description');
359+
const hoverContainer = document.getElementById('hover-explanation');
360+
361+
if (equationContainer) equationContainer.innerHTML = '';
362+
if (descriptionContainer) descriptionContainer.innerHTML = '';
363+
if (hoverContainer) {
364+
hoverContainer.innerHTML = '';
365+
hoverContainer.classList.remove('visible');
366+
}
367+
368+
// Render content
369+
renderEquation();
370+
renderDescription();
371+
372+
// Apply colors
373+
applyColorScheme(currentScheme);
374+
375+
// Setup hover effects
376+
setupHoverEffects();
377+
378+
// Update editor highlighting with new colors
379+
const codeElement = document.querySelector('#editor-container code') as HTMLElement;
380+
if (codeElement) {
381+
highlightEditor(codeElement);
382+
}
383+
} catch (error) {
384+
console.error('Failed to parse markdown:', error);
385+
// Could show error message to user
386+
}
387+
}
388+
389+
async function loadMarkdownIntoEditor(url: string) {
390+
try {
391+
const response = await fetch(url);
392+
const markdown = await response.text();
393+
currentMarkdown = markdown;
394+
395+
if (editor) {
396+
editor.updateCode(markdown);
397+
}
398+
} catch (error) {
399+
console.error('Failed to load markdown:', error);
400+
}
401+
}
402+
403+
function setupEditorControls() {
404+
// Toggle editor collapse/expand
405+
const toggleBtn = document.getElementById('toggle-editor-btn');
406+
const editorSidebar = document.getElementById('editor-sidebar');
407+
408+
if (toggleBtn && editorSidebar) {
409+
toggleBtn.addEventListener('click', () => {
410+
editorSidebar.classList.toggle('collapsed');
411+
});
412+
}
413+
414+
// New equation mode toggle
415+
const newEquationBtn = document.getElementById('new-equation-btn');
416+
if (newEquationBtn && editorSidebar) {
417+
newEquationBtn.addEventListener('click', () => {
418+
isNewEquationMode = !isNewEquationMode;
419+
420+
if (isNewEquationMode) {
421+
// Switch to new mode: show editor, load template
422+
editorSidebar.classList.remove('collapsed');
423+
newEquationBtn.textContent = 'Exit New Mode';
424+
425+
// Load template
426+
const template = `# Equation
427+
428+
$$
429+
\\mark[term1]{E} = \\mark[term2]{m} \\mark[term3]{c^2}
430+
$$
431+
432+
# Description
433+
434+
The famous [mass-energy equivalence]{.term2}: [energy]{.term1} equals [mass]{.term2} times the [speed of light]{.term3} squared.
435+
436+
## .term1
437+
438+
Energy (E) is the capacity to do work, measured in joules.
439+
440+
## .term2
441+
442+
Mass (m) is the amount of matter in an object, measured in kilograms.
443+
444+
## .term3
445+
446+
The speed of light (c) is approximately 299,792,458 meters per second.
447+
`;
448+
currentMarkdown = template;
449+
if (editor) {
450+
editor.updateCode(template);
451+
}
452+
} else {
453+
// Exit new mode: reload current equation
454+
newEquationBtn.textContent = 'New';
455+
if (currentEquationId) {
456+
loadEquation(currentEquationId);
457+
}
458+
}
459+
});
460+
}
461+
462+
// Download markdown
463+
const downloadBtn = document.getElementById('download-btn');
464+
if (downloadBtn) {
465+
downloadBtn.addEventListener('click', () => {
466+
const markdown = currentMarkdown || '';
467+
const blob = new Blob([markdown], { type: 'text/markdown' });
468+
const url = URL.createObjectURL(blob);
469+
const a = document.createElement('a');
470+
a.href = url;
471+
a.download = isNewEquationMode ? 'new-equation.md' : `${currentEquationId}.md`;
472+
a.click();
473+
URL.revokeObjectURL(url);
474+
});
475+
}
476+
477+
// Contribute instructions
478+
const contributeBtn = document.getElementById('contribute-btn');
479+
if (contributeBtn) {
480+
contributeBtn.addEventListener('click', () => {
481+
showContributeInstructions();
482+
});
483+
}
484+
}
485+
486+
function showContributeInstructions() {
487+
const instructions = `To contribute your equation to the repository:
488+
489+
1. Download your markdown file using the Download button
490+
2. Fork the repository: https://github.com/stared/equations-explained-colorfully
491+
3. Add your .md file to the public/examples/ directory
492+
4. Add an entry to public/examples/equations.json:
493+
{
494+
"id": "your-equation-id",
495+
"title": "Your Equation Name",
496+
"category": "Field (e.g., Physics, Math)",
497+
"file": "your-file.md"
498+
}
499+
5. Submit a pull request with your changes
500+
501+
Thank you for contributing!`;
502+
503+
alert(instructions);
504+
}
505+
288506
// Initialize - load content and render
289507
document.addEventListener('DOMContentLoaded', async () => {
290508
try {
@@ -297,6 +515,10 @@ document.addEventListener('DOMContentLoaded', async () => {
297515
// Create color scheme switcher
298516
createColorSchemeSwitcher();
299517

518+
// Initialize editor
519+
initializeEditor();
520+
setupEditorControls();
521+
300522
// Load equation from URL hash or default
301523
const initialEquation = getEquationFromHash();
302524
await loadEquation(initialEquation);

src/parser.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,16 @@ export function parseContent(markdown: string): ParsedContent {
167167
}
168168

169169
/**
170-
* Load and parse content from a markdown file
170+
* Load and parse content from a markdown file or string
171171
*/
172-
export async function loadContent(path: string): Promise<ParsedContent> {
173-
const response = await fetch(path);
174-
const markdown = await response.text();
175-
return parseContent(markdown);
172+
export async function loadContent(pathOrMarkdown: string, fromString = false): Promise<ParsedContent> {
173+
if (fromString) {
174+
// Parse directly from string
175+
return parseContent(pathOrMarkdown);
176+
} else {
177+
// Fetch from URL and parse
178+
const response = await fetch(pathOrMarkdown);
179+
const markdown = await response.text();
180+
return parseContent(markdown);
181+
}
176182
}

0 commit comments

Comments
 (0)