314 lines
10 KiB
JavaScript
314 lines
10 KiB
JavaScript
const { ELEMENT_MAPPINGS } = require("./mappings");
|
|
const TextNodeGenerator = require("./generators/TextNodeGenerator");
|
|
const ButtonElementGenerator = require("./generators/ButtonElementGenerator");
|
|
const LinkElementGenerator = require("./generators/LinkElementGenerator");
|
|
const StandardElementGenerator = require("./generators/StandardElementGenerator");
|
|
const RootNodeGenerator = require("./generators/RootNodeGenerator");
|
|
const InputElementGenerator = require("./generators/InputElementGenerator");
|
|
const MediaElementGenerator = require("./generators/MediaElementGenerator");
|
|
const JavaScriptGenerator = require("./generators/JavaScriptGenerator");
|
|
const ServerCodeGenerator = require("./generators/ServerCodeGenerator");
|
|
const HTMLTemplate = require("./templates/HTMLTemplate");
|
|
const StringUtils = require("./utils/StringUtils");
|
|
|
|
class HTMLGenerator {
|
|
/**
|
|
* Creates a new HTML generator instance.
|
|
* @param {Object} [options] - Options object
|
|
* @param {boolean} [options.minified=true] - Minify generated HTML
|
|
* @param {boolean} [options.debug=false] - Enable debug logging
|
|
* @param {CSSGenerator} cssGenerator - CSS generator instance
|
|
*/
|
|
constructor(options = {}, cssGenerator) {
|
|
this.options = options;
|
|
this.cssGenerator = cssGenerator;
|
|
this.htmlTemplate = new HTMLTemplate(options);
|
|
this.serverGenerator = new ServerCodeGenerator(options);
|
|
this.jsGenerator = new JavaScriptGenerator(options, this.serverGenerator);
|
|
this.currentElement = null;
|
|
|
|
|
|
this.generators = [
|
|
new TextNodeGenerator(this.options),
|
|
new RootNodeGenerator(this.options, this),
|
|
new ButtonElementGenerator(this.options, this.cssGenerator, this),
|
|
new LinkElementGenerator(this.options, this.cssGenerator, this),
|
|
new InputElementGenerator(this.options, this.cssGenerator, this),
|
|
new MediaElementGenerator(this.options, this.cssGenerator, this),
|
|
|
|
new StandardElementGenerator(this.options, this.cssGenerator, this)
|
|
];
|
|
|
|
if (this.options.debug) {
|
|
console.log(
|
|
"[HTMLGenerator] Initialized with options:",
|
|
JSON.stringify(options, null, 2)
|
|
);
|
|
console.log(
|
|
"[HTMLGenerator] Registered generators:",
|
|
this.generators.map(g => g.constructor.name).join(", ")
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Converts a node to a string for debugging purposes, avoiding circular
|
|
* references.
|
|
* @param {Object} node - Node to stringify
|
|
* @returns {string} String representation of the node
|
|
*/
|
|
debugStringify(node) {
|
|
const getCircularReplacer = () => {
|
|
const seen = new WeakSet();
|
|
return (key, value) => {
|
|
if (key === "parent") return "[Circular:Parent]";
|
|
if (typeof value === "object" && value !== null) {
|
|
if (seen.has(value)) {
|
|
return "[Circular]";
|
|
}
|
|
seen.add(value);
|
|
}
|
|
return value;
|
|
};
|
|
};
|
|
|
|
try {
|
|
return JSON.stringify(node, getCircularReplacer(), 2);
|
|
} catch (err) {
|
|
return `[Unable to stringify: ${err.message}]`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates HTML for a node, delegating to the appropriate generator.
|
|
* @param {Object} node - Node to generate HTML for
|
|
* @returns {string} Generated HTML
|
|
*/
|
|
generateHTML(node) {
|
|
if (this.options.debug) {
|
|
console.log(`\n[HTMLGenerator] Generating HTML for node`);
|
|
console.log(`[HTMLGenerator] Node type: "${node.type}"`);
|
|
console.log("[HTMLGenerator] Node details:", StringUtils.safeStringify(node));
|
|
}
|
|
|
|
// Handle client and server blocks
|
|
if (node.type === "client" || node.type === "server") {
|
|
return this.handleScriptBlock(node);
|
|
}
|
|
|
|
if (node.type === "element" && node.tag === "page") {
|
|
if (this.options.debug) {
|
|
console.log("[HTMLGenerator] Skipping page node - metadata only");
|
|
}
|
|
return "";
|
|
}
|
|
|
|
const prevElement = this.currentElement;
|
|
this.currentElement = node;
|
|
|
|
// Check if this element has an explicit ID in its props
|
|
if (node.type === "element") {
|
|
const idProp = node.props.find(p => typeof p === "string" && p.startsWith("id:"));
|
|
if (idProp) {
|
|
const idValue = idProp.substring(idProp.indexOf(":") + 1).trim().replace(/^"|"$/g, "");
|
|
node.elementId = idValue;
|
|
|
|
// Register this element as reactive
|
|
this.jsGenerator.registerReactiveElement(idValue);
|
|
|
|
if (this.options.debug) {
|
|
console.log(`[HTMLGenerator] Found explicit ID: ${idValue}, registered as reactive`);
|
|
}
|
|
}
|
|
}
|
|
|
|
let result = "";
|
|
for (const generator of this.generators) {
|
|
if (generator.canHandle(node)) {
|
|
if (this.options.debug) {
|
|
console.log(`[HTMLGenerator] Using ${generator.constructor.name} for node`);
|
|
}
|
|
|
|
// If this is an element that might have event handlers,
|
|
// add a unique ID to it for client scripts if it doesn't already have one
|
|
if (node.type === "element" && node.children.some(child => child.type === "client")) {
|
|
// Generate a unique ID for this element if it doesn't already have one
|
|
if (!node.elementId) {
|
|
node.elementId = this.jsGenerator.generateElementId();
|
|
if (this.options.debug) {
|
|
console.log(`[HTMLGenerator] Generated ID for element: ${node.elementId}`);
|
|
}
|
|
}
|
|
|
|
result = generator.generate(node);
|
|
|
|
// Process all client blocks inside this element
|
|
node.children
|
|
.filter(child => child.type === "client")
|
|
.forEach(clientBlock => {
|
|
this.handleScriptBlock(clientBlock, node.elementId);
|
|
});
|
|
} else {
|
|
result = generator.generate(node);
|
|
}
|
|
|
|
this.currentElement = prevElement;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
if (this.options.debug) {
|
|
console.log(`[HTMLGenerator] No generator found for node type: ${node.type}`);
|
|
}
|
|
|
|
this.currentElement = prevElement;
|
|
return "";
|
|
}
|
|
|
|
/**
|
|
* Handles client and server script blocks.
|
|
* @param {Object} node - The script node to handle
|
|
* @param {string} [elementId] - The ID of the parent element, if any
|
|
* @returns {string} - Empty string as script blocks don't directly generate HTML
|
|
*/
|
|
handleScriptBlock(node, elementId = null) {
|
|
if (this.options.debug) {
|
|
console.log(`\n[HTMLGenerator] Processing ${node.type} script block`);
|
|
console.log(`[HTMLGenerator] Script content (first 50 chars): "${node.script.substring(0, 50)}..."`);
|
|
if (elementId) {
|
|
console.log(`[HTMLGenerator] Attaching to element: ${elementId}`);
|
|
}
|
|
}
|
|
|
|
if (node.type === "client") {
|
|
if (!elementId && this.currentElement) {
|
|
if (!this.currentElement.elementId) {
|
|
this.currentElement.elementId = this.jsGenerator.generateElementId();
|
|
}
|
|
elementId = this.currentElement.elementId;
|
|
}
|
|
|
|
if (elementId) {
|
|
this.jsGenerator.addClientScript(elementId, node.script);
|
|
} else {
|
|
if (this.options.debug) {
|
|
console.log(`[HTMLGenerator] Warning: Client script with no parent element`);
|
|
}
|
|
}
|
|
} else if (node.type === "server") {
|
|
if (!elementId && this.currentElement) {
|
|
if (!this.currentElement.elementId) {
|
|
this.currentElement.elementId = this.jsGenerator.generateElementId();
|
|
}
|
|
elementId = this.currentElement.elementId;
|
|
}
|
|
|
|
if (elementId) {
|
|
const params = node.params || [];
|
|
if (this.options.debug && params.length > 0) {
|
|
console.log(`[HTMLGenerator] Server block parameters: ${params.join(", ")}`);
|
|
}
|
|
|
|
this.jsGenerator.addServerScript(elementId, node.script, params);
|
|
} else {
|
|
if (this.options.debug) {
|
|
console.log(`[HTMLGenerator] Warning: Server script with no parent element`);
|
|
}
|
|
}
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
/**
|
|
* Processes a link node, extracting the href attribute and converting it
|
|
* to an internal link if it doesn't start with http:// or https://.
|
|
*
|
|
* If no href property is found, the default value of # is used.
|
|
*
|
|
* @param {Object} node - The link node to process
|
|
* @returns {Object} - An object containing the final href value
|
|
*/
|
|
processLink(node) {
|
|
if (this.options.debug) {
|
|
console.log("\n[HTMLGenerator] Processing link node");
|
|
console.log(
|
|
"[HTMLGenerator] Link properties:",
|
|
this.debugStringify(node.props)
|
|
);
|
|
}
|
|
|
|
const hrefProp = node.props.find((p) => p.startsWith("href:"));
|
|
let href = "#";
|
|
|
|
if (hrefProp) {
|
|
let hrefTarget = hrefProp
|
|
.substring(hrefProp.indexOf(":") + 1)
|
|
.trim()
|
|
.replace(/^"|"$/g, "");
|
|
if (
|
|
!hrefTarget.startsWith("http://") &&
|
|
!hrefTarget.startsWith("https://")
|
|
) {
|
|
hrefTarget = "/" + hrefTarget;
|
|
if (this.options.debug) {
|
|
console.log(
|
|
`[HTMLGenerator] Converted to internal link: "${hrefTarget}"`
|
|
);
|
|
}
|
|
} else {
|
|
if (this.options.debug) {
|
|
console.log(
|
|
`[HTMLGenerator] External link detected: "${hrefTarget}"`
|
|
);
|
|
}
|
|
}
|
|
href = hrefTarget;
|
|
} else {
|
|
if (this.options.debug) {
|
|
console.log(
|
|
"[HTMLGenerator] No href property found, using default: '#'"
|
|
);
|
|
}
|
|
}
|
|
|
|
if (this.options.debug) {
|
|
console.log(`[HTMLGenerator] Final href value: "${href}"`);
|
|
}
|
|
return { href };
|
|
}
|
|
|
|
/**
|
|
* Generates the final HTML document as a string.
|
|
*
|
|
* @param {string} headContent - The HTML content to be placed within the <head> tag.
|
|
* @param {string} bodyContent - The HTML content to be placed within the <body> tag.
|
|
* @returns {string} - A complete HTML document containing the provided head and body content.
|
|
*/
|
|
generateFinalHtml(headContent, bodyContent) {
|
|
const clientScripts = this.jsGenerator.generateClientScripts();
|
|
|
|
return this.htmlTemplate.generateDocument(
|
|
headContent,
|
|
bodyContent + clientScripts
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Generates server-side code for Express.js API routes.
|
|
* @returns {string} - Express.js server code
|
|
*/
|
|
generateServerCode() {
|
|
return this.serverGenerator.generateServerCode();
|
|
}
|
|
|
|
/**
|
|
* Checks if there is any server code to generate.
|
|
* @returns {boolean} - Whether there is server code
|
|
*/
|
|
hasServerCode() {
|
|
return this.serverGenerator.hasServerCodeToGenerate();
|
|
}
|
|
}
|
|
|
|
module.exports = HTMLGenerator;
|