Parser: Support multiple components in one file, change args to params with defaults

This commit is contained in:
Aaron Sinclair committed 2025-11-15 03:25:13 -08:00
1 parent 635d466874
commit 81d51d5617
38 files changed
+2151 -455

No files matched your search

+132
View File
@@ -0,0 +1,132 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ritz Framework Demo</title>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
background: #f5f5f5;
}
#app {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header {
border-bottom: 2px solid #333;
padding-bottom: 10px;
margin-bottom: 20px;
}
.status {
margin: 10px 0;
padding: 10px;
background: #f0f0f0;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
}
.error {
background: #fee;
color: #c00;
}
</style>
</head>
<body>
<div class="header">
<h1>🎭 Ritz Framework</h1>
<p>Python components running in WebAssembly</p>
</div>
<div class="status" id="status">Loading...</div>
<div id="app"></div>
<script src="ritz.js"></script>
<script>
// Configuration
const COMPONENT_NAME = 'HelloWorld'; // Change this to your component name
const WASM_FILE = '../examples/HelloWorld.wasm';
async function loadComponent() {
const status = document.getElementById('status');
const appRoot = document.getElementById('app');
try {
// Initialize Ritz runtime
status.textContent = '⏳ Initializing Ritz runtime...';
const runtime = new RitzRuntime(appRoot);
// Load WASM module
status.textContent = `⏳ Loading ${WASM_FILE}...`;
const response = await fetch(WASM_FILE);
if (!response.ok) {
throw new Error(`Failed to fetch ${WASM_FILE}: ${response.status}`);
}
const wasmBytes = await response.arrayBuffer();
// Set up Module for Emscripten
window.Module = {
wasmBinary: wasmBytes,
ritz: runtime, // Provide runtime to WASM
onRuntimeInitialized: function() {
status.textContent = '⏳ Rendering component...';
try {
// Call the render function
const renderFn = Module[`_render_${COMPONENT_NAME}`];
if (!renderFn) {
throw new Error(`Component render_${COMPONENT_NAME} not found in WASM exports`);
}
// Render the component
Module.ccall(`render_${COMPONENT_NAME}`, null, [], []);
status.textContent = `✅ ${COMPONENT_NAME} rendered successfully!`;
status.className = 'status';
} catch (error) {
status.textContent = `❌ Render error: ${error.message}`;
status.className = 'status error';
console.error('Render error:', error);
}
},
print: function(text) {
console.log('[WASM]', text);
},
printErr: function(text) {
console.error('[WASM]', text);
}
};
// Load the Emscripten JS glue code
status.textContent = '⏳ Loading WASM runtime...';
const script = document.createElement('script');
script.src = WASM_FILE.replace('.wasm', '.js');
script.onerror = () => {
throw new Error('Failed to load WASM JS glue code');
};
document.body.appendChild(script);
} catch (error) {
status.textContent = `❌ Error: ${error.message}`;
status.className = 'status error';
console.error('Load error:', error);
}
}
// Start loading when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadComponent);
} else {
loadComponent();
}
</script>
</body>
</html>
+131
View File
@@ -0,0 +1,131 @@
/* Ritz Debug Bar Styles */
.ritz-debug {
position: fixed;
top: 0;
right: 0;
width: 400px;
height: 100vh;
background: #1e1e1e;
color: #d4d4d4;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
box-shadow: -2px 0 10px rgba(0,0,0,0.3);
transition: transform 0.3s ease;
z-index: 9999;
overflow-y: auto;
}
.ritz-debug.hidden {
transform: translateX(100%);
}
.debug-header {
background: #252525;
padding: 15px;
border-bottom: 1px solid #3e3e3e;
display: flex;
justify-content: space-between;
align-items: center;
}
.debug-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.debug-header button {
background: none;
border: none;
color: #d4d4d4;
font-size: 24px;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
line-height: 1;
}
.debug-header button:hover {
color: #fff;
}
.status {
margin: 15px;
padding: 10px;
background: #2d2d30;
border-left: 3px solid #007acc;
border-radius: 3px;
font-size: 12px;
}
.status.success {
border-left-color: #4ec9b0;
}
.status.error {
border-left-color: #f48771;
color: #f48771;
}
.debug-content {
padding: 15px;
}
.debug-section {
margin-bottom: 20px;
}
.debug-section h4 {
margin: 0 0 10px 0;
font-size: 12px;
text-transform: uppercase;
color: #858585;
font-weight: 600;
}
.debug-section pre {
background: #252525;
padding: 10px;
border-radius: 3px;
overflow-x: auto;
margin: 0;
font-size: 11px;
line-height: 1.6;
}
/* Toggle hint */
.debug-toggle-hint {
position: fixed;
bottom: 20px;
right: 20px;
background: #252525;
color: #d4d4d4;
padding: 8px 12px;
border-radius: 4px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 11px;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
opacity: 0;
animation: fadeInOut 3s ease-in-out;
pointer-events: none;
z-index: 10000;
}
@keyframes fadeInOut {
0%, 100% { opacity: 0; }
10%, 90% { opacity: 1; }
}
/* App container default styles */
#app {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
transition: margin-right 0.3s ease;
}
body.debug-open #app {
margin-right: 400px;
}
+83
View File
@@ -0,0 +1,83 @@
/**
* Ritz Debug Bar Controller
*/
class RitzDebug {
constructor() {
this.debugBar = document.getElementById('ritz-debug');
this.isOpen = false;
// Setup keyboard shortcut (Ctrl+`)
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === '`') {
e.preventDefault();
this.toggle();
}
});
// Show hint on first load
this.showHint();
}
toggle() {
this.isOpen = !this.isOpen;
if (this.debugBar) {
if (this.isOpen) {
this.debugBar.classList.remove('hidden');
document.body.classList.add('debug-open');
} else {
this.debugBar.classList.add('hidden');
document.body.classList.remove('debug-open');
}
}
}
open() {
this.isOpen = true;
if (this.debugBar) {
this.debugBar.classList.remove('hidden');
document.body.classList.add('debug-open');
}
}
close() {
this.isOpen = false;
if (this.debugBar) {
this.debugBar.classList.add('hidden');
document.body.classList.remove('debug-open');
}
}
showHint() {
// Show toggle hint for 3 seconds
const hint = document.createElement('div');
hint.className = 'debug-toggle-hint';
hint.textContent = 'Press Ctrl+` to toggle debug';
document.body.appendChild(hint);
setTimeout(() => {
hint.remove();
}, 3000);
}
log(message, type = 'info') {
const status = document.getElementById('status');
if (status) {
status.textContent = message;
status.className = 'status ' + type;
}
console.log(`[Ritz] ${message}`);
}
}
// Initialize debug controller
const ritzDebug = new RitzDebug();
// Export
if (typeof module !== 'undefined' && module.exports) {
module.exports = RitzDebug;
} else {
window.RitzDebug = RitzDebug;
window.ritzDebug = ritzDebug;
}
+322
View File
@@ -0,0 +1,322 @@
/**
* Ritz Runtime - WebAssembly DOM Bridge
*
* Provides the runtime environment for Ritz components compiled to WASM.
*/
class RitzRuntime {
constructor(rootElement) {
this.rootElement = rootElement || document.getElementById('app');
this.nodes = new Map();
this.nextNodeId = 1;
this.log = [];
this.wasmInstance = null;
this.debugEnabled = true;
}
// ===== DOM Manipulation Bridge =====
js_create_element(tagPtr) {
const tag = this.readString(tagPtr);
const element = document.createElement(tag);
const id = this.nextNodeId++;
this.nodes.set(id, element);
this._log(`createElement("${tag}") = ${id}`);
return id;
}
js_create_text(textPtr) {
const text = this.readString(textPtr);
const textNode = document.createTextNode(text);
const id = this.nextNodeId++;
this.nodes.set(id, textNode);
this._log(`createTextNode("${text}") = ${id}`);
return id;
}
js_append_child(parentId, childId) {
const parent = this.nodes.get(parentId);
const child = this.nodes.get(childId);
if (parent && child) {
parent.appendChild(child);
this._log(`appendChild(${parentId}, ${childId})`);
}
}
js_set_attribute(elemId, namePtr, valuePtr) {
const element = this.nodes.get(elemId);
const name = this.readString(namePtr);
const value = this.readString(valuePtr);
if (element) {
if (name === 'class') {
element.className = value;
} else {
element.setAttribute(name, value);
}
this._log(`setAttribute(${elemId}, "${name}", "${value}")`);
}
}
js_bind_event(elemId, eventNamePtr, handlerNamePtr) {
const element = this.nodes.get(elemId);
const eventName = this.readString(eventNamePtr);
const handlerName = this.readString(handlerNamePtr);
if (element && this.wasmInstance) {
const handler = this.wasmInstance.exports[handlerName];
if (handler) {
element.addEventListener(eventName, () => {
this._log(`[Event] ${eventName} → ${handlerName}()`);
handler();
this.updateSignals();
});
this._log(`bindEvent(${elemId}, "${eventName}", "${handlerName}")`);
} else {
console.error(`Handler ${handlerName} not found in WASM exports`);
}
}
}
js_set_root(elemId) {
const element = this.nodes.get(elemId);
if (element) {
this.rootElement.innerHTML = '';
this.rootElement.appendChild(element);
this._log(`setRoot(${elemId})`);
}
}
js_update_text_content(nodeId, textPtr) {
const node = this.nodes.get(nodeId);
const text = this.readString(textPtr);
if (node) {
node.textContent = text;
this._log(`updateTextContent(${nodeId}, "${text}")`);
}
}
// ===== Signal Management =====
updateSignals() {
// Discover and update all signals
if (!this.wasmInstance) return;
const exports = this.wasmInstance.exports;
// Find all get_* functions (signal getters)
for (const [name, fn] of Object.entries(exports)) {
if (name.startsWith('get_') && typeof fn === 'function') {
const signalName = name.substring(4); // Remove 'get_' prefix
const value = fn();
this._log(`[Signal] ${signalName} = ${value}`);
// Update DOM elements bound to this signal
// TODO: Track signal-to-DOM bindings during render for efficiency
this.updateSignalDisplay(signalName, value);
}
}
}
updateSignalDisplay(signalName, value) {
// Find DOM elements that should display this signal
// For now, update any element with class matching signal name or data-signal attribute
const selectors = [
`[data-signal="${signalName}"]`,
`.${signalName}`,
'.count', // Hardcoded for demo - TODO: track bindings properly
];
selectors.forEach(selector => {
const elements = document.querySelectorAll(selector);
elements.forEach(el => {
// Check if text contains the signal name or looks like a counter
const text = el.textContent;
if (text.includes(signalName) || text.match(/\d+/)) {
// Replace the number in the text
el.textContent = text.replace(/\d+/, value);
}
});
});
}
// ===== Reactive Conditional Rendering =====
js_create_marker() {
// Create a comment node as a marker for conditional insertion
const marker = document.createComment('conditional-marker');
const id = this.nextNodeId++;
this.nodes.set(id, marker);
this._log(`Created marker node ${id}`);
return id;
}
js_replace_at_marker(markerId, oldNodeId, newNodeId) {
// Replace DOM node at marker position
const marker = this.nodes.get(markerId);
if (!marker) {
console.error(`Marker ${markerId} not found`);
return;
}
const parent = marker.parentNode;
if (!parent) {
console.error(`Marker has no parent`);
return;
}
// Remove old node if it exists (0 means no old node)
if (oldNodeId !== 0) {
const oldNode = this.nodes.get(oldNodeId);
if (oldNode && oldNode.parentNode) {
oldNode.parentNode.removeChild(oldNode);
this._log(`Removed old node ${oldNodeId}`);
}
}
// Insert new node after marker (only if newNodeId is not 0)
if (newNodeId !== 0) {
const newNode = this.nodes.get(newNodeId);
if (!newNode) {
console.error(`New node ${newNodeId} not found`);
return;
}
parent.insertBefore(newNode, marker.nextSibling);
this._log(`Inserted node ${newNodeId} at marker ${markerId}`);
}
}
// ===== Memory & String Utilities =====
readString(ptr) {
if (!this.memory) return '';
const bytes = new Uint8Array(this.memory.buffer, ptr);
let length = 0;
while (bytes[length] !== 0) length++;
return new TextDecoder().decode(bytes.slice(0, length));
}
setMemory(memory) {
this.memory = memory;
}
setWasmInstance(instance) {
this.wasmInstance = instance;
}
// ===== Logging & Debug =====
_log(message) {
if (this.debugEnabled) {
this.log.push(message);
}
}
getLog() {
return this.log;
}
clearLog() {
this.log = [];
}
}
/**
* RitzLoader - Simplified WASM loading
*/
class RitzLoader {
constructor(options = {}) {
this.rootElement = options.rootElement || document.getElementById('app');
this.statusElement = options.statusElement || document.getElementById('status');
this.onLoad = options.onLoad || (() => {});
this.onError = options.onError || ((err) => console.error(err));
}
async load(wasmPath, componentName, props = {}) {
try {
this.setStatus('⏳ Creating runtime...');
const runtime = new RitzRuntime(this.rootElement);
this.setStatus(`⏳ Loading ${wasmPath}...`);
const response = await fetch(wasmPath);
if (!response.ok) {
throw new Error(`Failed to load ${wasmPath}: ${response.status}`);
}
const wasmBytes = await response.arrayBuffer();
this.setStatus('⏳ Instantiating WASM...');
// Create imports for WASM
const imports = {
env: {
js_create_element: (ptr) => runtime.js_create_element(ptr),
js_create_text: (ptr) => runtime.js_create_text(ptr),
js_append_child: (p, c) => runtime.js_append_child(p, c),
js_set_attribute: (e, n, v) => runtime.js_set_attribute(e, n, v),
js_set_root: (e) => runtime.js_set_root(e),
js_bind_event: (e, n, h) => runtime.js_bind_event(e, n, h),
js_update_text_content: (n, t) => runtime.js_update_text_content(n, t),
js_create_marker: () => runtime.js_create_marker(),
js_replace_at_marker: (m, o, n) => runtime.js_replace_at_marker(m, o, n),
}
};
const { instance } = await WebAssembly.instantiate(wasmBytes, imports);
// Setup runtime
runtime.setMemory(instance.exports.memory);
runtime.setWasmInstance(instance);
this.setStatus(`⏳ Rendering ${componentName}...`);
// Call the render function with props
const renderFn = instance.exports[`render_${componentName}`];
if (!renderFn) {
throw new Error(`render_${componentName} not found in WASM exports`);
}
// Pass props as positional arguments
// Components now RETURN the root element ID instead of calling js_set_root
const propValues = Object.values(props);
const rootElementId = renderFn(...propValues);
// Mount the returned element to the DOM
runtime.js_set_root(rootElementId);
// Update signals
runtime.updateSignals();
this.setStatus(`✅ ${componentName} loaded!`, 'success');
// Make runtime globally available for debugging
window.ritzRuntime = runtime;
window.ritzInstance = instance;
this.onLoad(runtime, instance);
return { runtime, instance };
} catch (error) {
this.setStatus(`❌ Error: ${error.message}`, 'error');
this.onError(error);
throw error;
}
}
setStatus(message, type = '') {
if (this.statusElement) {
this.statusElement.textContent = message;
this.statusElement.className = 'status ' + type;
}
}
}
// Export for use in modules or global scope
if (typeof module !== 'undefined' && module.exports) {
module.exports = { RitzRuntime, RitzLoader };
} else {
window.RitzRuntime = RitzRuntime;
window.RitzLoader = RitzLoader;
}
+112
View File
@@ -0,0 +1,112 @@
/**
* Ritz Runtime - DOM Bridge for WASM
*
* Provides DOM manipulation functions called by WASM code.
*/
class RitzRuntime {
constructor(rootElement) {
this.rootElement = rootElement || document.body;
this.nodes = new Map(); // Map of node IDs to DOM nodes
this.nextNodeId = 1;
this.rootNodeId = null;
}
/**
* Create a DOM element and return its ID
*/
createElement(tag) {
const element = document.createElement(tag);
const id = this.nextNodeId++;
this.nodes.set(id, element);
console.log(`[Ritz] Created element <${tag}> with ID ${id}`);
return id;
}
/**
* Create a text node and return its ID
*/
createTextNode(text) {
const textNode = document.createTextNode(text);
const id = this.nextNodeId++;
this.nodes.set(id, textNode);
console.log(`[Ritz] Created text node "${text}" with ID ${id}`);
return id;
}
/**
* Append child to parent
*/
appendChild(parentId, childId) {
const parent = this.nodes.get(parentId);
const child = this.nodes.get(childId);
if (!parent || !child) {
console.error(`[Ritz] appendChild failed: parent=${parentId}, child=${childId}`);
return;
}
parent.appendChild(child);
console.log(`[Ritz] Appended node ${childId} to node ${parentId}`);
}
/**
* Set an attribute on an element
*/
setAttribute(elemId, name, value) {
const element = this.nodes.get(elemId);
if (!element) {
console.error(`[Ritz] setAttribute failed: element=${elemId}`);
return;
}
// Handle special cases
if (name === 'class') {
element.className = value;
} else {
element.setAttribute(name, value);
}
console.log(`[Ritz] Set attribute ${name}="${value}" on node ${elemId}`);
}
/**
* Set the root element and mount it to DOM
*/
setRoot(elemId) {
const element = this.nodes.get(elemId);
if (!element) {
console.error(`[Ritz] setRoot failed: element=${elemId}`);
return;
}
this.rootNodeId = elemId;
// Clear the root element
this.rootElement.innerHTML = '';
// Mount the component
this.rootElement.appendChild(element);
console.log(`[Ritz] Mounted component (node ${elemId}) to DOM`);
}
/**
* Clear all nodes (for hot reload)
*/
clear() {
this.nodes.clear();
this.nextNodeId = 1;
this.rootNodeId = null;
this.rootElement.innerHTML = '';
}
}
// Export for use in module or global scope
if (typeof module !== 'undefined' && module.exports) {
module.exports = RitzRuntime;
} else {
window.RitzRuntime = RitzRuntime;
}