feat: code blocks beta
This commit is contained in:
parent
1ecb6d8682
commit
73c8048c9d
5 changed files with 980 additions and 0 deletions
190
examples/reactive-example.bp
Normal file
190
examples/reactive-example.bp
Normal file
|
@ -0,0 +1,190 @@
|
|||
page(favicon:"/favicon.ico") {
|
||||
title { "Blueprint - Reactive Example" }
|
||||
description { "Example of reactive values in Blueprint" }
|
||||
keywords { "blueprint, javascript, reactive, state" }
|
||||
author { "Blueprint Team" }
|
||||
}
|
||||
|
||||
navbar {
|
||||
horizontal {
|
||||
link(href:index) { text(bold) { "Blueprint" } }
|
||||
links {
|
||||
link(href:index) { "Home" }
|
||||
link(href:examples) { "Examples" }
|
||||
link(href:docs) { "Docs" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section(wide, centered) {
|
||||
vertical(centered) {
|
||||
title(huge) { "Reactive Values Demo" }
|
||||
text { "Demonstration of reactive state management in Blueprint" }
|
||||
}
|
||||
}
|
||||
|
||||
section(wide) {
|
||||
title { "Counter Example" }
|
||||
|
||||
vertical(centered) {
|
||||
// Notice the ID uses underscore format. - is not allowed
|
||||
text(id:counter_value) { "0" }
|
||||
|
||||
horizontal(centered) {
|
||||
button-secondary(id:decrease_btn) {
|
||||
"Decrease"
|
||||
|
||||
@client {
|
||||
// Use the reactive counter_value with numberValue
|
||||
const currentValue = counter_value.numberValue;
|
||||
counter_value.setNumber(currentValue - 1);
|
||||
console.log("Counter decreased to", currentValue - 1);
|
||||
}
|
||||
}
|
||||
|
||||
button(id:increase_btn) {
|
||||
"Increase"
|
||||
|
||||
@client {
|
||||
// Use the reactive counter_value with numberValue
|
||||
const currentValue = counter_value.numberValue;
|
||||
counter_value.setNumber(currentValue + 1);
|
||||
console.log("Counter increased to", currentValue + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section(wide) {
|
||||
title { "Color Changer with Reactive Elements" }
|
||||
|
||||
vertical(centered) {
|
||||
// Element with explicit ID that will be styled
|
||||
card(id:color_target, raised) {
|
||||
title { "Change My Background" }
|
||||
text { "Click the buttons below to change my background color" }
|
||||
}
|
||||
|
||||
// Display the current color
|
||||
text(subtle, id:color_display) { "Current color: None" }
|
||||
|
||||
horizontal(centered) {
|
||||
button-secondary(id:red_btn) {
|
||||
"Red"
|
||||
|
||||
@client {
|
||||
// Using reactive methods
|
||||
color_target.setStyle("backgroundColor", "#e74c3c");
|
||||
color_display.set("Current color: Red");
|
||||
}
|
||||
}
|
||||
|
||||
button-secondary(id:green_btn) {
|
||||
"Green"
|
||||
|
||||
@client {
|
||||
// Using reactive methods
|
||||
color_target.setStyle("backgroundColor", "#2ecc71");
|
||||
color_display.set("Current color: Green");
|
||||
}
|
||||
}
|
||||
|
||||
button-secondary(id:blue_btn) {
|
||||
"Blue"
|
||||
|
||||
@client {
|
||||
// Using reactive methods
|
||||
color_target.setStyle("backgroundColor", "#3498db");
|
||||
color_display.set("Current color: Blue");
|
||||
}
|
||||
}
|
||||
|
||||
button-secondary(id:reset_btn) {
|
||||
"Reset"
|
||||
|
||||
@client {
|
||||
// Using reactive methods
|
||||
color_target.setStyle("backgroundColor", "");
|
||||
color_display.set("Current color: None");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section(wide) {
|
||||
title { "Data Types Example" }
|
||||
|
||||
vertical(centered) {
|
||||
horizontal(centered) {
|
||||
vertical {
|
||||
text(bold) { "Number:" }
|
||||
text(id:number_display) { "42" }
|
||||
}
|
||||
|
||||
vertical {
|
||||
text(bold) { "Text:" }
|
||||
text(id:text_display) { "Hello Blueprint" }
|
||||
}
|
||||
|
||||
vertical {
|
||||
text(bold) { "Boolean:" }
|
||||
text(id:boolean_display) { "true" }
|
||||
}
|
||||
}
|
||||
|
||||
horizontal(centered) {
|
||||
button-secondary(id:modify_values_btn) {
|
||||
"Modify Values"
|
||||
|
||||
@client {
|
||||
// Use type-specific methods
|
||||
number_display.setNumber(number_display.numberValue * 2);
|
||||
text_display.set(text_display.value + "!");
|
||||
boolean_display.set(!boolean_display.booleanValue);
|
||||
|
||||
console.log("Number value:", number_display.numberValue);
|
||||
console.log("Text value:", text_display.textValue);
|
||||
console.log("Boolean value:", boolean_display.booleanValue);
|
||||
}
|
||||
}
|
||||
|
||||
button-secondary(id:reset_values_btn) {
|
||||
"Reset Values"
|
||||
|
||||
@client {
|
||||
number_display.setNumber(42);
|
||||
text_display.set("Hello Blueprint");
|
||||
boolean_display.set("true");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section(wide) {
|
||||
title { "Subscription Example" }
|
||||
|
||||
vertical(centered) {
|
||||
// Input and subscribed elements
|
||||
input(id:user_input) { "Type something..." }
|
||||
|
||||
text(bold) { "Live Preview:" }
|
||||
text(id:preview_output) { "Type something..." }
|
||||
|
||||
// Add a client block to handle typing
|
||||
@client {
|
||||
// Set up the input to update the preview on input
|
||||
user_input.element.addEventListener("input", function(e) {
|
||||
// Use the reactive API to update the preview
|
||||
preview_output.set(e.target.value);
|
||||
});
|
||||
|
||||
// Example of subscription - will log changes to the console
|
||||
preview_output.subscribe(function(newValue) {
|
||||
console.log("Preview content changed to:", newValue);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
163
examples/server-form-example.bp
Normal file
163
examples/server-form-example.bp
Normal file
|
@ -0,0 +1,163 @@
|
|||
page(favicon:"/favicon.ico") {
|
||||
title { "Blueprint - Server Form Example" }
|
||||
description { "Example of server-side form processing in Blueprint" }
|
||||
keywords { "blueprint, javascript, server, api, form" }
|
||||
author { "Blueprint Team" }
|
||||
}
|
||||
|
||||
navbar {
|
||||
horizontal {
|
||||
link(href:index) { text(bold) { "Blueprint" } }
|
||||
links {
|
||||
link(href:index) { "Home" }
|
||||
link(href:examples) { "Examples" }
|
||||
link(href:docs) { "Docs" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section(wide, centered) {
|
||||
vertical(centered) {
|
||||
title(huge) { "Server-Side Form Processing" }
|
||||
text { "Demonstration of server-side form handling with Blueprint" }
|
||||
}
|
||||
}
|
||||
|
||||
section(wide) {
|
||||
title { "Contact Form Example" }
|
||||
|
||||
vertical(centered) {
|
||||
card(raised) {
|
||||
title { "Submit a Message" }
|
||||
text { "Fill out the form below to submit a message to the server." }
|
||||
|
||||
vertical {
|
||||
text(bold) { "Your Name" }
|
||||
input(id:user_name) { "John Doe" }
|
||||
|
||||
text(bold) { "Your Email" }
|
||||
input(id:user_email) { "john@example.com" }
|
||||
|
||||
text(bold) { "Your Message" }
|
||||
textarea(id:user_message) { "Hello from Blueprint!" }
|
||||
|
||||
// Display the server response
|
||||
text(id:form_result) { "" }
|
||||
|
||||
button(id:submit_form) {
|
||||
"Submit Form"
|
||||
|
||||
// Server block with parameters specifying which input values to include
|
||||
@server(user_name, user_email, user_message) {
|
||||
console.log("Form submission received:");
|
||||
console.log("Name:", user_name);
|
||||
console.log("Email:", user_email);
|
||||
console.log("Message:", user_message);
|
||||
|
||||
// Validate inputs
|
||||
const errors = [];
|
||||
|
||||
if (!user_name || user_name.length < 2) {
|
||||
errors.push("Name is too short");
|
||||
}
|
||||
|
||||
if (!user_email || !user_email.includes('@')) {
|
||||
errors.push("Invalid email address");
|
||||
}
|
||||
|
||||
if (!user_message || user_message.length < 5) {
|
||||
errors.push("Message is too short");
|
||||
}
|
||||
|
||||
// Return error message if validation fails
|
||||
if (errors.length > 0) {
|
||||
return res.status(400).json({
|
||||
form_result: "Error: " + errors.join(", ")
|
||||
});
|
||||
}
|
||||
|
||||
// Process the form (in a real app, this might save to a database)
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
// Return success response that will update the form_result element
|
||||
return res.status(200).json({
|
||||
form_result: `Thank you, ${user_name}! Your message was received at ${timestamp}.`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section(wide) {
|
||||
title { "User Registration Example" }
|
||||
|
||||
vertical(centered) {
|
||||
card(raised) {
|
||||
title { "Register a New Account" }
|
||||
text { "Fill out the form below to register a new account." }
|
||||
|
||||
vertical {
|
||||
text(bold) { "Username" }
|
||||
input(id:username) { "newuser123" }
|
||||
|
||||
text(bold) { "Email" }
|
||||
input(id:email) { "newuser@example.com" }
|
||||
|
||||
text(bold) { "Password" }
|
||||
input(id:password) { "password123" }
|
||||
|
||||
text(bold) { "Confirm Password" }
|
||||
input(id:confirm_password) { "password123" }
|
||||
|
||||
// Display the registration status
|
||||
text(id:registration_status) { "" }
|
||||
|
||||
button(id:register_user) {
|
||||
"Register"
|
||||
|
||||
@server(username, email, password, confirm_password) {
|
||||
console.log("Registration request for username:", username);
|
||||
|
||||
// Validate username
|
||||
if (!username || username.length < 4) {
|
||||
return res.status(400).json({
|
||||
registration_status: "Error: Username must be at least 4 characters"
|
||||
});
|
||||
}
|
||||
|
||||
// Validate email
|
||||
if (!email || !email.includes('@')) {
|
||||
return res.status(400).json({
|
||||
registration_status: "Error: Invalid email address"
|
||||
});
|
||||
}
|
||||
|
||||
// Validate password
|
||||
if (!password || password.length < 8) {
|
||||
return res.status(400).json({
|
||||
registration_status: "Error: Password must be at least 8 characters"
|
||||
});
|
||||
}
|
||||
|
||||
// Check password matching
|
||||
if (password !== confirm_password) {
|
||||
return res.status(400).json({
|
||||
registration_status: "Error: Passwords do not match"
|
||||
});
|
||||
}
|
||||
|
||||
// In a real app, this would create the user account
|
||||
const userId = Math.floor(Math.random() * 10000);
|
||||
|
||||
// Return success response
|
||||
return res.status(200).json({
|
||||
registration_status: `Success! User ${username} registered with ID #${userId}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
179
lib/StandardElementGenerator.js
Normal file
179
lib/StandardElementGenerator.js
Normal file
|
@ -0,0 +1,179 @@
|
|||
/**
|
||||
* Generates HTML for standard HTML elements.
|
||||
*/
|
||||
class StandardElementGenerator {
|
||||
/**
|
||||
* Creates a new StandardElementGenerator instance.
|
||||
* @param {Object} options - Generator options
|
||||
* @param {CSSGenerator} cssGenerator - CSS generator instance
|
||||
* @param {HTMLGenerator} htmlGenerator - HTML generator instance
|
||||
*/
|
||||
constructor(options = {}, cssGenerator, htmlGenerator) {
|
||||
this.options = options;
|
||||
this.cssGenerator = cssGenerator;
|
||||
this.htmlGenerator = htmlGenerator;
|
||||
this.parentGenerator = htmlGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether this generator can handle a given node.
|
||||
* @param {Object} node - Node to check
|
||||
* @returns {boolean} - True if this generator can handle the node
|
||||
*/
|
||||
canHandle(node) {
|
||||
return node.type === "element";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates HTML for a standard element.
|
||||
* @param {Object} node - Node to generate HTML for
|
||||
* @returns {string} - Generated HTML
|
||||
*/
|
||||
generate(node) {
|
||||
if (this.options.debug) {
|
||||
console.log(`[StandardElementGenerator] Processing element node: ${node.tag || node.name}`);
|
||||
}
|
||||
|
||||
const mapping = ELEMENT_MAPPINGS ? ELEMENT_MAPPINGS[node.tag] : null;
|
||||
|
||||
const tagName = mapping ? mapping.tag : (node.name || node.tag || "div");
|
||||
|
||||
let className = "";
|
||||
if (this.cssGenerator) {
|
||||
className = this.cssGenerator.generateClassName(node.tag);
|
||||
const { cssProps, nestedRules } = this.cssGenerator.nodeToCSSProperties(node);
|
||||
this.cssGenerator.cssRules.set(`.${className}`, {
|
||||
cssProps,
|
||||
nestedRules,
|
||||
});
|
||||
}
|
||||
|
||||
const fullClassName = this.generateClassName(node);
|
||||
if (className && fullClassName) {
|
||||
className = `${className} ${fullClassName}`;
|
||||
} else if (fullClassName) {
|
||||
className = fullClassName;
|
||||
}
|
||||
|
||||
const attributes = this.generateAttributes(node, node.elementId, className);
|
||||
|
||||
let content = "";
|
||||
const children = node.children || [];
|
||||
|
||||
for (const child of children) {
|
||||
if (
|
||||
child.type === "client" ||
|
||||
child.type === "server" ||
|
||||
(child.type === "element" && (child.name === "script" || child.tag === "script"))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
child.parent = node;
|
||||
content += this.htmlGenerator.generateHTML(child);
|
||||
if (!this.options.minified) {
|
||||
content += "\n";
|
||||
}
|
||||
}
|
||||
|
||||
return `<${tagName}${attributes}>${this.options.minified ? "" : "\n"}${content}${this.options.minified ? "" : ""}</${tagName}>${this.options.minified ? "" : "\n"}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a className string for the element.
|
||||
* @param {Object} node - The node to generate class for
|
||||
* @returns {string} - The generated class name
|
||||
*/
|
||||
generateClassName(node) {
|
||||
let classNames = [];
|
||||
|
||||
if (node.attributes && Array.isArray(node.attributes)) {
|
||||
const classAttr = node.attributes.find(attr => attr.name === "class");
|
||||
if (classAttr && classAttr.value) {
|
||||
classNames.push(classAttr.value);
|
||||
}
|
||||
}
|
||||
|
||||
if (node.props && Array.isArray(node.props)) {
|
||||
for (const prop of node.props) {
|
||||
if (typeof prop === "string") {
|
||||
if (prop.startsWith("class:")) {
|
||||
classNames.push(prop.substring(prop.indexOf(":") + 1).trim().replace(/^"|"$/g, ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return classNames.join(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an attributes string for the element.
|
||||
* @param {Object} node - The node to generate attributes for
|
||||
* @param {string} id - The element ID
|
||||
* @param {string} className - The element class name
|
||||
* @returns {string} - The generated attributes string
|
||||
*/
|
||||
generateAttributes(node, id, className) {
|
||||
let attributes = "";
|
||||
|
||||
if (id) {
|
||||
attributes += ` id="${id}"`;
|
||||
} else if (node.props && Array.isArray(node.props)) {
|
||||
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 (className) {
|
||||
attributes += ` class="${className}"`;
|
||||
}
|
||||
|
||||
if (node.props && Array.isArray(node.props)) {
|
||||
const dataProps = node.props.filter(p => typeof p === "string" && p.startsWith("data-"));
|
||||
if (dataProps.length) {
|
||||
attributes += " " + dataProps.join(" ");
|
||||
}
|
||||
}
|
||||
|
||||
if (node.attributes && Array.isArray(node.attributes)) {
|
||||
for (const attr of node.attributes) {
|
||||
if (attr.name === "id" || attr.name === "class") continue;
|
||||
|
||||
if (attr.value === true) {
|
||||
attributes += ` ${attr.name}`;
|
||||
} else if (attr.value !== false && attr.value !== undefined && attr.value !== null) {
|
||||
attributes += ` ${attr.name}="${attr.value}"`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (node.props && Array.isArray(node.props)) {
|
||||
for (const prop of node.props) {
|
||||
if (typeof prop === "string") {
|
||||
if (prop.startsWith("id:") || prop.startsWith("class:") || prop.startsWith("data-")) continue;
|
||||
|
||||
const colonIndex = prop.indexOf(":");
|
||||
if (colonIndex !== -1) {
|
||||
const name = prop.substring(0, colonIndex).trim();
|
||||
const value = prop.substring(colonIndex + 1).trim().replace(/^"|"$/g, "");
|
||||
|
||||
if (!attributes.includes(` ${name}="`)) {
|
||||
attributes += ` ${name}="${value}"`;
|
||||
}
|
||||
} else {
|
||||
if (!attributes.includes(` ${prop}`)) {
|
||||
attributes += ` ${prop}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StandardElementGenerator;
|
239
lib/generators/JavaScriptGenerator.js
Normal file
239
lib/generators/JavaScriptGenerator.js
Normal 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;
|
209
lib/generators/ServerCodeGenerator.js
Normal file
209
lib/generators/ServerCodeGenerator.js
Normal file
|
@ -0,0 +1,209 @@
|
|||
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 => ` data.${param} = ${param} ? ${param}.value : null;`).join('\n')}
|
||||
|
||||
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) {
|
||||
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 => `const ${param} = req.body.${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;
|
Loading…
Add table
Reference in a new issue