beta/code-blocks (#1)

Reviewed-on: #1
Co-authored-by: obvTiger <obvtiger@epilogue.team>
Co-committed-by: obvTiger <obvtiger@epilogue.team>
This commit is contained in:
obvTiger 2025-04-01 15:22:15 +02:00 committed by obvtiger
parent 362b7aa15e
commit d125640fe7
26 changed files with 1816 additions and 102 deletions

View file

@ -49,6 +49,22 @@ class ButtonElementGenerator {
let attributes = "";
const idProp = node.props.find((p) => typeof p === "string" && p.startsWith("id:"));
if (idProp) {
const idValue = idProp.substring(idProp.indexOf(":") + 1).trim().replace(/^"|"$/g, "");
attributes += ` id="${idValue}"`;
node.elementId = idValue;
if (this.options.debug) {
console.log(`[ButtonElementGenerator] Added explicit ID attribute: ${idValue}`);
}
}
else if (node.elementId) {
attributes += ` id="${node.elementId}"`;
if (this.options.debug) {
console.log(`[ButtonElementGenerator] Adding generated ID attribute: ${node.elementId}`);
}
}
if (node.parent?.tag === "link") {
const linkInfo = this.linkProcessor.processLink(node.parent);
attributes += this.linkProcessor.getButtonClickHandler(linkInfo);

View file

@ -55,6 +55,23 @@ class InputElementGenerator {
attributes = ' type="range"';
}
// Extract and handle ID attribute
let idAttr = "";
const idProp = node.props.find(p => typeof p === "string" && p.startsWith("id:"));
if (idProp) {
const idValue = idProp.substring(idProp.indexOf(":") + 1).trim().replace(/^"|"$/g, "");
idAttr = ` id="${idValue}"`;
node.elementId = idValue;
// Register as reactive element with the parent generator
if (this.parentGenerator && this.parentGenerator.jsGenerator) {
this.parentGenerator.jsGenerator.registerReactiveElement(idValue);
if (this.options.debug) {
console.log(`[InputElementGenerator] Registered checkbox with ID: ${idValue} as reactive`);
}
}
}
const valueProp = node.props.find((p) => p.startsWith("value:"));
if (valueProp) {
const value = valueProp.substring(valueProp.indexOf(":") + 1).trim();
@ -70,7 +87,7 @@ class InputElementGenerator {
if (node.children.length > 0) {
let html = `<label class="${className}-container">`;
html += `<input class="${className}"${attributes}>`;
html += `<input class="${className}"${attributes}${idAttr}>`;
node.children.forEach((child) => {
child.parent = node;
@ -80,7 +97,7 @@ class InputElementGenerator {
html += `</label>`;
return html;
} else {
return `<input class="${className}"${attributes}>`;
return `<input class="${className}"${attributes}${idAttr}>`;
}
}
}

View file

@ -0,0 +1,239 @@
const StringUtils = require("../utils/StringUtils");
/**
* Generates JavaScript code for client and server blocks with reactive data handling.
*/
class JavaScriptGenerator {
/**
* Creates a new JavaScript generator.
* @param {Object} options - Options for the generator
* @param {ServerCodeGenerator} [serverGenerator] - Server code generator instance
*/
constructor(options = {}, serverGenerator = null) {
this.options = options;
this.clientScripts = new Map();
this.serverScripts = [];
this.reactiveElements = new Set();
this.serverGenerator = serverGenerator;
}
/**
* Sets the server code generator instance
* @param {ServerCodeGenerator} serverGenerator - Server code generator instance
*/
setServerGenerator(serverGenerator) {
this.serverGenerator = serverGenerator;
}
/**
* Registers a client-side script to be executed when an element is clicked.
* @param {string} elementId - The ID of the element to attach the event to
* @param {string} code - The JavaScript code to execute
*/
addClientScript(elementId, code) {
if (this.options.debug) {
console.log(`[JavaScriptGenerator] Adding client script for element ${elementId}`);
}
this.clientScripts.set(elementId, code);
}
/**
* Adds a server-side script to be executed on the server.
* @param {string} elementId - The ID of the element that triggers the server action
* @param {string} code - The JavaScript code to execute
* @param {Array<string>} params - The input parameters to retrieve from the client
*/
addServerScript(elementId, code, params = []) {
if (this.options.debug) {
console.log(`[JavaScriptGenerator] Adding server script for element ${elementId}`);
if (params.length > 0) {
console.log(`[JavaScriptGenerator] Script parameters: ${params.join(", ")}`);
}
}
this.serverScripts.push({
elementId,
code,
params
});
if (this.serverGenerator) {
this.serverGenerator.addServerRoute(elementId, code, params);
}
this.clientScripts.set(elementId, `_bp_serverAction_${elementId}(e);`);
}
/**
* Registers an element as reactive, meaning it will have state management functions
* @param {string} elementId - The ID of the element to make reactive
*/
registerReactiveElement(elementId) {
if (this.options.debug) {
console.log(`[JavaScriptGenerator] Registering reactive element: ${elementId}`);
}
this.reactiveElements.add(elementId);
}
/**
* Generates a unique element ID.
* @returns {string} - A unique element ID with bp_ prefix
*/
generateElementId() {
return StringUtils.generateRandomId();
}
/**
* Generates the reactive store and helper functions for state management
* @returns {string} - JavaScript code for reactive functionality
*/
generateReactiveStore() {
if (this.reactiveElements.size === 0) {
return '';
}
return `
const _bp_store = {
listeners: new Map(),
subscribe: function(id, callback) {
if (!this.listeners.has(id)) {
this.listeners.set(id, []);
}
this.listeners.get(id).push(callback);
},
notify: function(id, newValue) {
if (this.listeners.has(id)) {
this.listeners.get(id).forEach(callback => callback(newValue));
}
}
};
function _bp_makeElementReactive(id) {
const element = document.getElementById(id);
if (!element) {
console.log(\`[Blueprint] Element with ID \${id} not found\`);
return null;
}
return {
element: element,
get value() {
return element.textContent;
},
set: function(newValue) {
const valueString = String(newValue);
element.textContent = valueString;
_bp_store.notify(id, valueString);
return this;
},
setNumber: function(num) {
const valueString = String(Number(num));
element.textContent = valueString;
_bp_store.notify(id, valueString);
return this;
},
setHtml: function(html) {
element.innerHTML = html;
_bp_store.notify(id, html);
return this;
},
setStyle: function(property, value) {
element.style[property] = value;
return this;
},
setClass: function(className, add = true) {
if (add) {
element.classList.add(className);
} else {
element.classList.remove(className);
}
return this;
},
on: function(event, callback) {
element.addEventListener(event, callback);
return this;
},
subscribe: function(callback) {
_bp_store.subscribe(id, callback);
return this;
},
get textValue() {
return element.textContent;
},
get numberValue() {
return Number(element.textContent);
},
get booleanValue() {
const text = element.textContent.toLowerCase();
return text === 'true' || text === '1' || text === 'yes';
}
};
}
// Initialize reactive elements`;
}
/**
* Generates initialization code for reactive elements
* @returns {string} - JavaScript initialization code for reactive elements
*/
generateReactiveElementInit() {
if (this.reactiveElements.size === 0) {
return '';
}
const initCode = Array.from(this.reactiveElements)
.map(id => `const ${id} = _bp_makeElementReactive('${id}');`)
.join('\n ');
return `
${initCode}`;
}
/**
* Generates all client-side JavaScript code.
* @returns {string} - The generated JavaScript code
*/
generateClientScripts() {
if (this.clientScripts.size === 0 && this.reactiveElements.size === 0 && !this.serverGenerator) {
return '';
}
let initCode = '';
if (this.reactiveElements.size > 0) {
initCode = this.generateReactiveElementInit();
}
let scripts = '';
this.clientScripts.forEach((code, elementId) => {
scripts += ` document.getElementById('${elementId}').addEventListener('click', function(e) {
${code}
});\n`;
});
let serverClientCode = '';
if (this.serverGenerator) {
serverClientCode = this.serverGenerator.generateClientAPICalls();
}
return `<script>
${this.generateReactiveStore()}
${serverClientCode}
document.addEventListener('DOMContentLoaded', function() {
${initCode}
${scripts}});
</script>`;
}
/**
* Gets all server-side scripts.
* @returns {Array<Object>} - Array of server-side script objects
*/
getServerScripts() {
return this.serverScripts;
}
}
module.exports = JavaScriptGenerator;

View file

@ -0,0 +1,236 @@
const crypto = require('crypto');
const StringUtils = require("../utils/StringUtils");
/**
* Generates server-side Express.js routes for server blocks.
*/
class ServerCodeGenerator {
/**
* Creates a new server code generator.
* @param {Object} options - Options for the generator
*/
constructor(options = {}) {
this.options = options;
this.serverRoutes = new Map();
this.hasServerCode = false;
}
/**
* Registers a server-side route to be executed when requested.
* @param {string} elementId - The ID of the element that triggers the route
* @param {string} code - The JavaScript code to execute
* @param {Array<string>} params - The input parameters to retrieve from the client
*/
addServerRoute(elementId, code, params = []) {
if (this.options.debug) {
console.log(`[ServerCodeGenerator] Adding server route for element ${elementId}`);
if (params.length > 0) {
console.log(`[ServerCodeGenerator] Route parameters: ${params.join(", ")}`);
}
}
const endpoint = this.generateEndpointPath(elementId);
this.serverRoutes.set(elementId, {
endpoint,
code,
params
});
this.hasServerCode = true;
}
/**
* Generates a unique endpoint path for a server route.
* @param {string} elementId - The element ID for the route
* @returns {string} - A unique endpoint path
*/
generateEndpointPath(elementId) {
const hash = crypto.createHash('sha256')
.update(elementId + Math.random().toString())
.digest('hex')
.substring(0, 12);
return `/api/${StringUtils.toKebabCase(elementId)}-${hash}`;
}
/**
* Generates client-side JavaScript for making API calls to server routes.
* @returns {string} - JavaScript code for making API calls
*/
generateClientAPICalls() {
if (this.serverRoutes.size === 0) {
return '';
}
let apiCode = `
const _bp_api = {
post: async function(url, data) {
try {
const serverPort = window.blueprintServerPort || 3001;
const fullUrl = \`http://\${window.location.hostname}:\${serverPort}\${url}\`;
const response = await fetch(fullUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(\`API request failed: \${response.status}\`);
}
return await response.json();
} catch (error) {
console.error('[Blueprint API]', error);
return { error: error.message };
}
}
};
`;
this.serverRoutes.forEach((route, elementId) => {
const { endpoint, params } = route;
apiCode += `async function _bp_serverAction_${elementId}(e) {
const data = {};
${params.map(param => ` const ${param}_element = document.getElementById('${param}');
if (${param}_element) {
console.log('Found element: ${param}', ${param}_element);
if (${param}_element.type === 'checkbox') {
data.${param} = ${param}_element.checked;
console.log('Checkbox ${param} value:', ${param}_element.checked);
} else if (${param}_element.type === 'radio') {
data.${param} = ${param}_element.checked;
} else if (${param}_element.value !== undefined) {
data.${param} = ${param}_element.value;
} else {
data.${param} = ${param}_element.textContent;
}
} else {
console.error('[Blueprint] Element with ID ${param} not found');
data.${param} = null;
}`).join('\n')}
console.log('Submitting data:', data);
try {
const result = await _bp_api.post('${endpoint}', data);
console.log('[Blueprint API] Server response:', result);
if (result && typeof result === 'object') {
Object.keys(result).forEach(key => {
if (window[key] && typeof window[key].set === 'function') {
window[key].set(result[key]);
}
else {
const element = document.getElementById(key);
if (element) {
if (element.tagName.toLowerCase() === 'input') {
element.value = result[key];
} else {
element.textContent = result[key];
}
console.log(\`[Blueprint API] Updated element #\${key} with value: \${result[key]}\`);
}
}
});
}
return result;
} catch (error) {
console.error('[Blueprint API] Error in server action:', error);
}
}\n`;
});
return apiCode;
}
/**
* Generates Express.js server code for all registered server routes.
* @returns {string} - Express.js server code
*/
generateServerCode() {
if (this.serverRoutes.size === 0) {
return '';
}
let serverCode = `
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
function createBlueprintApiServer(port = 3001) {
const app = express();
app.use(cors());
app.use(bodyParser.json());
app.use((req, res, next) => {
console.log(\`[\${new Date().toISOString()}] \${req.method} \${req.url}\`);
next();
});
`;
this.serverRoutes.forEach((route, elementId) => {
const { endpoint, code, params } = route;
serverCode += `
app.post('${endpoint}', async (req, res) => {
try {
${params.map(param => {
return `const ${param} = req.body.${param} !== undefined ? req.body.${param} : null;
if (${param} === null) {
console.error(\`Missing parameter: ${param}\`);
}`;
}).join('\n ')}
let result;
try {
${code}
} catch (error) {
console.error(\`Error in server block \${error.message}\`);
return res.status(500).json({ error: error.message });
}
return res.json(result || {});
} catch (error) {
console.error(\`Error processing request: \${error.message}\`);
return res.status(500).json({ error: error.message });
}
});`;
});
serverCode += `
app.listen(port, () => {
console.log(\`Blueprint API server running at http://localhost:\${port}\`);
});
return app;
}
module.exports = createBlueprintApiServer;
`;
return serverCode;
}
/**
* Checks if there is any server code to generate.
* @returns {boolean} - Whether there is server code
*/
hasServerCodeToGenerate() {
return this.hasServerCode;
}
}
module.exports = ServerCodeGenerator;

View file

@ -54,6 +54,22 @@ class StandardElementGenerator {
let attributes = "";
const idProp = node.props.find((p) => typeof p === "string" && p.startsWith("id:"));
if (idProp) {
const idValue = idProp.substring(idProp.indexOf(":") + 1).trim().replace(/^"|"$/g, "");
attributes += ` id="${idValue}"`;
node.elementId = idValue;
if (this.options.debug) {
console.log(`[StandardElementGenerator] Added explicit ID attribute: ${idValue}`);
}
}
else if (node.elementId) {
attributes += ` id="${node.elementId}"`;
if (this.options.debug) {
console.log(`[StandardElementGenerator] Adding generated ID attribute: ${node.elementId}`);
}
}
if (node.props.find((p) => typeof p === "string" && p.startsWith("data-"))) {
const dataProps = node.props.filter(
(p) => typeof p === "string" && p.startsWith("data-")