generated from coulomb/repo-seed
Clarified that testdrive-jsui is a JavaScript library with optional backend adapters, not a Python package with JavaScript assets. Added ARCHITECTURE.md: - Core principle: JavaScript provides all functionality - Python/Ruby/Java are integration adapters only - Clear layer separation: JS library vs language adapters - Distribution models: CDN, npm, with adapters - Anti-patterns to avoid - Success metrics Added JS_FIRST_REFACTORING.md: - Phase 1: Create standalone JavaScript bundle - Phase 2: Refactor Python as thin adapter - Phase 3: Build and distribution - Concrete implementation steps with code examples - Timeline: 2-4 days of focused work Key Changes in Approach: - JavaScript does ALL rendering (using marked.js) - Backend adapters only serve assets and pass config - No HTML generation in Python/Ruby/Java - Library works completely standalone in browser This establishes foundation for true language-agnostic design. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
710 lines
18 KiB
Markdown
710 lines
18 KiB
Markdown
# JavaScript-First Refactoring Plan
|
|
|
|
**Goal**: Transform testdrive-jsui from a Python package with JS assets into a **pure JavaScript library** with optional backend adapters.
|
|
|
|
**Date**: 2025-12-16
|
|
**Status**: Planning
|
|
|
|
---
|
|
|
|
## 🎯 Vision
|
|
|
|
### Before (Current - Hybrid)
|
|
```
|
|
Python (TestDriveJSUIEngine)
|
|
↓ generates HTML template
|
|
↓ does fallback markdown rendering
|
|
↓ injects configuration
|
|
Browser receives: HTML with embedded JS config
|
|
JavaScript: Enhances the pre-rendered HTML
|
|
```
|
|
|
|
### After (Target - JS-First)
|
|
```
|
|
JavaScript Library (testdrive-jsui.js)
|
|
↓ Standalone, works in any browser
|
|
↓ Renders markdown using marked.js
|
|
↓ Creates full UI
|
|
↓ Handles all interactions
|
|
|
|
Optional: Python/Ruby/Java Adapter
|
|
↓ Just serves JS/CSS files
|
|
↓ Passes config as JSON
|
|
↓ Provides testing helpers
|
|
```
|
|
|
|
---
|
|
|
|
## 📋 Phase 1: Create Standalone JavaScript Bundle
|
|
|
|
### Goal
|
|
Create a single `testdrive-jsui.js` file that works completely standalone in a browser.
|
|
|
|
### Tasks
|
|
|
|
#### 1.1: Create Main Library Entry Point
|
|
|
|
**Create**: `js/testdrive-jsui.js`
|
|
|
|
```javascript
|
|
/**
|
|
* TestDrive-JSUI - Standalone JavaScript Markdown Editor
|
|
*
|
|
* A complete markdown editing and viewing UI that runs entirely in the browser.
|
|
* No backend required!
|
|
*
|
|
* @version 1.0.0
|
|
* @requires marked.js (for markdown parsing)
|
|
*/
|
|
|
|
class TestDriveJSUI {
|
|
/**
|
|
* Initialize TestDrive-JSUI
|
|
*
|
|
* @param {Object} config - Configuration options
|
|
* @param {string|HTMLElement} config.container - Container element or selector
|
|
* @param {string} config.mode - 'edit' or 'view'
|
|
* @param {string} config.markdown - Initial markdown content
|
|
* @param {string} config.theme - Theme name (default: 'github')
|
|
* @param {boolean} config.autosave - Enable autosave (default: false)
|
|
* @param {Object} config.callbacks - Event callbacks
|
|
*/
|
|
constructor(config) {
|
|
this.config = this._validateConfig(config);
|
|
this.container = this._resolveContainer(config.container);
|
|
|
|
// Initialize core systems
|
|
this.debugSystem = new MarkitectDebugSystem();
|
|
this.sectionManager = new SectionManager();
|
|
this.domRenderer = new DOMRenderer(this.sectionManager, this.container);
|
|
|
|
// Initialize controls based on mode
|
|
this._initializeControls();
|
|
|
|
// Load initial content
|
|
if (config.markdown) {
|
|
this.setContent(config.markdown);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set markdown content
|
|
* @param {string} markdown - Markdown text
|
|
*/
|
|
setContent(markdown) {
|
|
// Parse markdown to sections
|
|
const sections = this.sectionManager.createSectionsFromMarkdown(markdown);
|
|
|
|
// Render to DOM
|
|
this.domRenderer.renderAllSections(sections);
|
|
|
|
// Emit event
|
|
this._emit('contentLoaded', { markdown, sections });
|
|
}
|
|
|
|
/**
|
|
* Get current markdown content
|
|
* @returns {string} Current markdown
|
|
*/
|
|
getContent() {
|
|
return this.sectionManager.getAllSectionsAsMarkdown();
|
|
}
|
|
|
|
/**
|
|
* Switch between edit and view modes
|
|
* @param {string} mode - 'edit' or 'view'
|
|
*/
|
|
setMode(mode) {
|
|
if (!['edit', 'view'].includes(mode)) {
|
|
throw new Error(`Invalid mode: ${mode}`);
|
|
}
|
|
|
|
this.config.mode = mode;
|
|
this._reinitializeControls();
|
|
this._emit('modeChanged', { mode });
|
|
}
|
|
|
|
/**
|
|
* Save current content (triggers callback)
|
|
*/
|
|
save() {
|
|
const markdown = this.getContent();
|
|
|
|
if (this.config.callbacks?.onSave) {
|
|
this.config.callbacks.onSave(markdown);
|
|
} else if (this.config.autosave) {
|
|
// Default: save to localStorage
|
|
localStorage.setItem('testdrive-jsui-content', markdown);
|
|
}
|
|
|
|
this._emit('saved', { markdown });
|
|
}
|
|
|
|
/**
|
|
* Destroy the editor and clean up
|
|
*/
|
|
destroy() {
|
|
// Clean up controls
|
|
this.controls?.forEach(control => control?.destroy?.());
|
|
|
|
// Clear container
|
|
this.container.innerHTML = '';
|
|
|
|
this._emit('destroyed');
|
|
}
|
|
|
|
// Internal methods
|
|
_validateConfig(config) {
|
|
const defaults = {
|
|
mode: 'edit',
|
|
theme: 'github',
|
|
autosave: false,
|
|
shortcuts: true,
|
|
sections: true,
|
|
debug: false,
|
|
callbacks: {}
|
|
};
|
|
|
|
return { ...defaults, ...config };
|
|
}
|
|
|
|
_resolveContainer(selector) {
|
|
if (typeof selector === 'string') {
|
|
const element = document.querySelector(selector);
|
|
if (!element) {
|
|
throw new Error(`Container not found: ${selector}`);
|
|
}
|
|
return element;
|
|
}
|
|
return selector;
|
|
}
|
|
|
|
_initializeControls() {
|
|
this.controls = [];
|
|
|
|
if (this.config.mode === 'edit') {
|
|
this.controls.push(
|
|
new EditControl(this),
|
|
new StatusControl(this),
|
|
new ContentsControl(this)
|
|
);
|
|
}
|
|
|
|
if (this.config.debug) {
|
|
this.controls.push(new DebugControl(this));
|
|
}
|
|
|
|
// Initialize all controls
|
|
this.controls.forEach(control => control.initialize());
|
|
}
|
|
|
|
_reinitializeControls() {
|
|
// Destroy old controls
|
|
this.controls?.forEach(control => control?.destroy?.());
|
|
|
|
// Create new controls for current mode
|
|
this._initializeControls();
|
|
}
|
|
|
|
_emit(event, data) {
|
|
const callback = this.config.callbacks?.[`on${event.charAt(0).toUpperCase()}${event.slice(1)}`];
|
|
if (callback) {
|
|
callback(data);
|
|
}
|
|
|
|
// Also emit as custom event
|
|
this.container.dispatchEvent(new CustomEvent(`testdrive:${event}`, { detail: data }));
|
|
}
|
|
}
|
|
|
|
// Expose globally
|
|
if (typeof window !== 'undefined') {
|
|
window.TestDriveJSUI = TestDriveJSUI;
|
|
}
|
|
|
|
// ES module export
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = TestDriveJSUI;
|
|
}
|
|
```
|
|
|
|
#### 1.2: Create Standalone HTML Example
|
|
|
|
**Create**: `examples/standalone.html`
|
|
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>TestDrive-JSUI Standalone Demo</title>
|
|
|
|
<!-- TestDrive-JSUI Styles -->
|
|
<link rel="stylesheet" href="../static/css/editor.css">
|
|
<link rel="stylesheet" href="../static/css/controls.css">
|
|
<link rel="stylesheet" href="../static/css/themes/github.css">
|
|
</head>
|
|
<body>
|
|
<div id="editor-container"></div>
|
|
|
|
<!-- Dependencies -->
|
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
|
|
<!-- TestDrive-JSUI Core Components -->
|
|
<script src="../js/core/debug-system.js"></script>
|
|
<script src="../js/core/section-manager.js"></script>
|
|
<script src="../js/components/dom-renderer.js"></script>
|
|
<script src="../js/components/debug-panel.js"></script>
|
|
<script src="../js/controls/control-base.js"></script>
|
|
<script src="../js/controls/edit-control.js"></script>
|
|
<script src="../js/controls/debug-control.js"></script>
|
|
<script src="../js/controls/status-control.js"></script>
|
|
<script src="../js/controls/contents-control.js"></script>
|
|
|
|
<!-- Main library -->
|
|
<script src="../js/testdrive-jsui.js"></script>
|
|
|
|
<!-- Initialize -->
|
|
<script>
|
|
// Example markdown content
|
|
const sampleMarkdown = `# TestDrive-JSUI Standalone Demo
|
|
|
|
## Welcome!
|
|
|
|
This is a **completely standalone** markdown editor running entirely in your browser. No backend required!
|
|
|
|
### Features
|
|
|
|
- ✅ Real-time markdown rendering
|
|
- ✅ Section-based editing
|
|
- ✅ Interactive controls
|
|
- ✅ Keyboard shortcuts
|
|
- ✅ Debug mode
|
|
|
|
### Try It Out
|
|
|
|
1. Click the edit button on any section
|
|
2. Modify the markdown
|
|
3. Save your changes
|
|
|
|
## Code Example
|
|
|
|
\`\`\`javascript
|
|
const editor = new TestDriveJSUI({
|
|
container: '#editor-container',
|
|
mode: 'edit',
|
|
markdown: '# Hello World'
|
|
});
|
|
\`\`\`
|
|
|
|
That's it! No backend needed.`;
|
|
|
|
// Initialize TestDrive-JSUI
|
|
const editor = new TestDriveJSUI({
|
|
container: '#editor-container',
|
|
mode: 'edit',
|
|
markdown: sampleMarkdown,
|
|
theme: 'github',
|
|
autosave: true,
|
|
debug: true,
|
|
callbacks: {
|
|
onSave: (markdown) => {
|
|
console.log('Content saved:', markdown);
|
|
alert('Content saved to localStorage!');
|
|
},
|
|
onContentLoaded: (data) => {
|
|
console.log('Content loaded:', data);
|
|
},
|
|
onModeChanged: (data) => {
|
|
console.log('Mode changed to:', data.mode);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Expose for debugging
|
|
window.editor = editor;
|
|
|
|
console.log('✅ TestDrive-JSUI initialized!');
|
|
console.log('Try: editor.getContent(), editor.setMode("view"), editor.save()');
|
|
</script>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
#### 1.3: Update Components to Work Standalone
|
|
|
|
**Tasks**:
|
|
- Ensure all JS components work without Python-generated HTML
|
|
- Remove any assumptions about server-side rendering
|
|
- Make all components initialize from JavaScript
|
|
|
|
#### 1.4: Add Markdown Rendering Helper
|
|
|
|
**Update**: `js/utils/markdown-helper.js` (create if needed)
|
|
|
|
```javascript
|
|
/**
|
|
* Markdown rendering utilities
|
|
* Uses marked.js for parsing
|
|
*/
|
|
const MarkdownHelper = {
|
|
/**
|
|
* Render markdown to HTML
|
|
* @param {string} markdown - Markdown text
|
|
* @returns {string} HTML string
|
|
*/
|
|
render(markdown) {
|
|
if (!window.marked) {
|
|
throw new Error('marked.js is required but not loaded');
|
|
}
|
|
|
|
// Configure marked
|
|
marked.setOptions({
|
|
gfm: true,
|
|
breaks: true,
|
|
headerIds: true,
|
|
mangle: false
|
|
});
|
|
|
|
return marked.parse(markdown);
|
|
},
|
|
|
|
/**
|
|
* Extract sections from markdown
|
|
* @param {string} markdown - Markdown text
|
|
* @returns {Array} Array of section objects
|
|
*/
|
|
extractSections(markdown) {
|
|
// Split on headers
|
|
const sections = [];
|
|
const lines = markdown.split('\n');
|
|
let currentSection = { level: 0, title: '', content: '' };
|
|
|
|
for (const line of lines) {
|
|
const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
|
|
if (headerMatch) {
|
|
// Save previous section
|
|
if (currentSection.content) {
|
|
sections.push(currentSection);
|
|
}
|
|
|
|
// Start new section
|
|
currentSection = {
|
|
level: headerMatch[1].length,
|
|
title: headerMatch[2],
|
|
content: line + '\n'
|
|
};
|
|
} else {
|
|
currentSection.content += line + '\n';
|
|
}
|
|
}
|
|
|
|
// Add final section
|
|
if (currentSection.content) {
|
|
sections.push(currentSection);
|
|
}
|
|
|
|
return sections;
|
|
}
|
|
};
|
|
|
|
if (typeof window !== 'undefined') {
|
|
window.MarkdownHelper = MarkdownHelper;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📋 Phase 2: Refactor Python as Thin Adapter
|
|
|
|
### Goal
|
|
Transform Python code from "rendering engine" to "integration adapter".
|
|
|
|
### Tasks
|
|
|
|
#### 2.1: Rename Python Package Structure
|
|
|
|
```
|
|
src/testdrive_jsui/ # OLD NAME
|
|
↓
|
|
src/testdrive_jsui_python/ # NEW NAME (makes it clear it's an adapter)
|
|
```
|
|
|
|
Or keep name but rename classes to show they're adapters:
|
|
|
|
```python
|
|
# Old (implies it does rendering)
|
|
class TestDriveJSUIEngine
|
|
|
|
# New (clear it's just an adapter)
|
|
class TestDriveJSUIAdapter
|
|
class PythonIntegrationAdapter
|
|
```
|
|
|
|
#### 2.2: Simplify Python Adapter
|
|
|
|
**New**: `src/testdrive_jsui/adapter.py`
|
|
|
|
```python
|
|
"""
|
|
Python Integration Adapter for TestDrive-JSUI
|
|
|
|
This is NOT the rendering engine - it's a helper for Python projects.
|
|
The JavaScript library does all the actual work.
|
|
"""
|
|
from pathlib import Path
|
|
from typing import Dict, Optional
|
|
import json
|
|
|
|
class TestDriveJSUIAdapter:
|
|
"""
|
|
Python integration adapter for TestDrive-JSUI JavaScript library.
|
|
|
|
Purpose:
|
|
- Serve JS/CSS assets
|
|
- Generate HTML templates with config
|
|
- Provide testing helpers
|
|
|
|
Does NOT:
|
|
- Render markdown (JS does this)
|
|
- Implement UI (JS does this)
|
|
- Transform content (JS does this)
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.js_library_path = self._find_js_library()
|
|
|
|
def _find_js_library(self) -> Path:
|
|
"""Locate the JavaScript library files."""
|
|
# Look in capability directory
|
|
current = Path(__file__).parent
|
|
while current.parent != current:
|
|
js_dir = current / "js"
|
|
if js_dir.exists() and (js_dir / "testdrive-jsui.js").exists():
|
|
return js_dir
|
|
current = current.parent
|
|
raise FileNotFoundError("testdrive-jsui.js not found")
|
|
|
|
def get_asset_urls(self, base_url: str = "/static") -> Dict[str, list]:
|
|
"""
|
|
Get URLs for JS/CSS assets.
|
|
|
|
Helper for web frameworks to include assets in templates.
|
|
"""
|
|
return {
|
|
"css": [
|
|
f"{base_url}/testdrive-jsui/css/editor.css",
|
|
f"{base_url}/testdrive-jsui/css/controls.css",
|
|
f"{base_url}/testdrive-jsui/css/themes/github.css",
|
|
],
|
|
"js": [
|
|
"https://cdn.jsdelivr.net/npm/marked/marked.min.js",
|
|
f"{base_url}/testdrive-jsui/js/testdrive-jsui.js",
|
|
]
|
|
}
|
|
|
|
def generate_html(self,
|
|
markdown: str,
|
|
mode: str = "edit",
|
|
config: Optional[Dict] = None) -> str:
|
|
"""
|
|
Generate HTML that loads and initializes the JS library.
|
|
|
|
This is just a convenience helper - you can write your own template!
|
|
"""
|
|
config = config or {}
|
|
config.update({
|
|
"markdown": markdown,
|
|
"mode": mode
|
|
})
|
|
|
|
assets = self.get_asset_urls()
|
|
|
|
return f"""<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>TestDrive-JSUI</title>
|
|
{''.join(f'<link rel="stylesheet" href="{url}">' for url in assets['css'])}
|
|
</head>
|
|
<body>
|
|
<div id="editor"></div>
|
|
|
|
{''.join(f'<script src="{url}"></script>' for url in assets['js'])}
|
|
|
|
<script>
|
|
const editor = new TestDriveJSUI({json.dumps(config, indent=2)});
|
|
</script>
|
|
</body>
|
|
</html>"""
|
|
|
|
def run_js_tests(self):
|
|
"""Run JavaScript tests (existing functionality - keep this!)."""
|
|
from .testing import JavaScriptTestRunner
|
|
runner = JavaScriptTestRunner(self.js_library_path.parent)
|
|
return runner.run_js_tests()
|
|
```
|
|
|
|
#### 2.3: Update Documentation
|
|
|
|
**Update**: README.md to show Python as optional
|
|
|
|
```markdown
|
|
## Installation
|
|
|
|
### Option 1: Use JavaScript Directly (No Backend)
|
|
|
|
```html
|
|
<script src="https://cdn.jsdelivr.net/npm/testdrive-jsui@1.0.0"></script>
|
|
<script>
|
|
const editor = new TestDriveJSUI({ ... });
|
|
</script>
|
|
```
|
|
|
|
### Option 2: With Python Adapter (for Django/Flask)
|
|
|
|
```bash
|
|
pip install testdrive-jsui-python
|
|
```
|
|
|
|
```python
|
|
from testdrive_jsui import TestDriveJSUIAdapter
|
|
adapter = TestDriveJSUIAdapter()
|
|
html = adapter.generate_html(markdown="# Hello")
|
|
```
|
|
|
|
### Option 3: With Ruby Adapter (for Rails) - Coming Soon
|
|
|
|
### Option 4: With Java Adapter (for Spring) - Coming Soon
|
|
```
|
|
|
|
---
|
|
|
|
## 📋 Phase 3: Build and Distribution
|
|
|
|
### Goal
|
|
Package JavaScript library for easy distribution.
|
|
|
|
### Tasks
|
|
|
|
#### 3.1: Create Build Script
|
|
|
|
**Create**: `js/build.js`
|
|
|
|
```javascript
|
|
/**
|
|
* Build script for testdrive-jsui
|
|
* Combines all modules into single distributable file
|
|
*/
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// Files to concatenate in order
|
|
const files = [
|
|
'utils/markdown-helper.js',
|
|
'core/debug-system.js',
|
|
'core/section-manager.js',
|
|
'components/dom-renderer.js',
|
|
'components/debug-panel.js',
|
|
'controls/control-base.js',
|
|
'controls/edit-control.js',
|
|
'controls/debug-control.js',
|
|
'controls/status-control.js',
|
|
'controls/contents-control.js',
|
|
'testdrive-jsui.js' // Main class last
|
|
];
|
|
|
|
// Read and concatenate
|
|
let bundle = '// TestDrive-JSUI v1.0.0 - MIT License\n';
|
|
bundle += '(function() {\n';
|
|
|
|
for (const file of files) {
|
|
const content = fs.readFileSync(path.join(__dirname, file), 'utf8');
|
|
bundle += '\n// === ' + file + ' ===\n';
|
|
bundle += content + '\n';
|
|
}
|
|
|
|
bundle += '})();\n';
|
|
|
|
// Write bundle
|
|
fs.writeFileSync(path.join(__dirname, 'dist', 'testdrive-jsui.js'), bundle);
|
|
|
|
console.log('✅ Built: dist/testdrive-jsui.js');
|
|
```
|
|
|
|
#### 3.2: Create package.json for npm
|
|
|
|
**Update**: `package.json`
|
|
|
|
```json
|
|
{
|
|
"name": "testdrive-jsui",
|
|
"version": "1.0.0",
|
|
"description": "Standalone JavaScript markdown editor with interactive UI",
|
|
"main": "js/dist/testdrive-jsui.js",
|
|
"types": "js/testdrive-jsui.d.ts",
|
|
"files": [
|
|
"js/dist/",
|
|
"static/css/",
|
|
"README.md"
|
|
],
|
|
"scripts": {
|
|
"build": "node js/build.js",
|
|
"test": "jest",
|
|
"prepublish": "npm run build"
|
|
},
|
|
"keywords": [
|
|
"markdown", "editor", "javascript", "standalone", "ui"
|
|
],
|
|
"dependencies": {
|
|
"marked": "^4.0.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## ✅ Success Criteria
|
|
|
|
### Phase 1 Complete When:
|
|
- [ ] Can open `standalone.html` in browser (no server needed)
|
|
- [ ] Can edit markdown completely in JavaScript
|
|
- [ ] All UI controls work without Python
|
|
- [ ] marked.js renders markdown in browser
|
|
|
|
### Phase 2 Complete When:
|
|
- [ ] Python adapter is < 200 lines of code
|
|
- [ ] Python doesn't render markdown
|
|
- [ ] Python doesn't generate HTML structure
|
|
- [ ] Can use testdrive-jsui with Flask/Django via adapter
|
|
|
|
### Phase 3 Complete When:
|
|
- [ ] Single `testdrive-jsui.js` file works everywhere
|
|
- [ ] Published to npm
|
|
- [ ] Available via CDN
|
|
- [ ] < 100KB minified + gzipped
|
|
|
|
---
|
|
|
|
## 📅 Timeline Estimate
|
|
|
|
- **Phase 1**: 1-2 days (core JavaScript refactoring)
|
|
- **Phase 2**: 1 day (simplify Python adapter)
|
|
- **Phase 3**: 0.5 day (build and distribution)
|
|
|
|
**Total**: 2-4 days of focused work
|
|
|
|
---
|
|
|
|
## 🎯 Priority Order
|
|
|
|
1. **Phase 1.2**: Create standalone.html example (proves concept)
|
|
2. **Phase 1.4**: Markdown rendering in JS
|
|
3. **Phase 1.1**: Main library class
|
|
4. **Phase 2.2**: Simplify Python adapter
|
|
5. **Phase 3**: Build and distribution
|
|
|
|
Start with #1 to validate the approach!
|