feat: reorganize builder
This commit is contained in:
parent
ff7bb041ef
commit
362b7aa15e
18 changed files with 1094 additions and 310 deletions
81
lib/generators/ButtonElementGenerator.js
Normal file
81
lib/generators/ButtonElementGenerator.js
Normal file
|
@ -0,0 +1,81 @@
|
|||
const LinkProcessor = require("../utils/LinkProcessor");
|
||||
|
||||
/**
|
||||
* Generates HTML for button elements.
|
||||
*/
|
||||
class ButtonElementGenerator {
|
||||
/**
|
||||
* Creates a new button element generator.
|
||||
* @param {Object} options - Options for the generator
|
||||
* @param {CSSGenerator} cssGenerator - CSS generator instance
|
||||
* @param {Object} parentGenerator - Parent HTML generator for recursion
|
||||
*/
|
||||
constructor(options, cssGenerator, parentGenerator) {
|
||||
this.options = options;
|
||||
this.cssGenerator = cssGenerator;
|
||||
this.parentGenerator = parentGenerator;
|
||||
this.linkProcessor = new LinkProcessor(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this generator can handle the given node.
|
||||
* @param {Object} node - The node to check
|
||||
* @returns {boolean} - True if this generator can handle the node
|
||||
*/
|
||||
canHandle(node) {
|
||||
return (
|
||||
node.type === "element" &&
|
||||
(node.tag === "button" || node.tag.startsWith("button-"))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates HTML for a button element.
|
||||
* @param {Object} node - The node to generate HTML for
|
||||
* @returns {string} - The generated HTML
|
||||
*/
|
||||
generate(node) {
|
||||
if (this.options.debug) {
|
||||
console.log(`\n[ButtonElementGenerator] Processing button: ${node.tag}`);
|
||||
}
|
||||
|
||||
const className = this.cssGenerator.generateClassName(node.tag);
|
||||
const { cssProps, nestedRules } = this.cssGenerator.nodeToCSSProperties(node);
|
||||
|
||||
this.cssGenerator.cssRules.set(`.${className}`, {
|
||||
cssProps,
|
||||
nestedRules,
|
||||
});
|
||||
|
||||
let attributes = "";
|
||||
|
||||
if (node.parent?.tag === "link") {
|
||||
const linkInfo = this.linkProcessor.processLink(node.parent);
|
||||
attributes += this.linkProcessor.getButtonClickHandler(linkInfo);
|
||||
}
|
||||
|
||||
if (node.props.find((p) => typeof p === "string" && p.startsWith("data-"))) {
|
||||
const dataProps = node.props.filter(
|
||||
(p) => typeof p === "string" && p.startsWith("data-")
|
||||
);
|
||||
attributes += " " + dataProps.map((p) => `${p}`).join(" ");
|
||||
}
|
||||
|
||||
let html = `<button class="${className}"${attributes}>${
|
||||
this.options.minified ? "" : "\n"
|
||||
}`;
|
||||
|
||||
node.children.forEach((child) => {
|
||||
child.parent = node;
|
||||
html += this.parentGenerator.generateHTML(child);
|
||||
});
|
||||
|
||||
html += `${this.options.minified ? "" : "\n"}</button>${
|
||||
this.options.minified ? "" : "\n"
|
||||
}`;
|
||||
|
||||
return html;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ButtonElementGenerator;
|
88
lib/generators/InputElementGenerator.js
Normal file
88
lib/generators/InputElementGenerator.js
Normal file
|
@ -0,0 +1,88 @@
|
|||
/**
|
||||
* Generates HTML for input elements (checkbox, radio, switch, slider).
|
||||
*/
|
||||
class InputElementGenerator {
|
||||
/**
|
||||
* Creates a new input element generator.
|
||||
* @param {Object} options - Options for the generator
|
||||
* @param {CSSGenerator} cssGenerator - CSS generator instance
|
||||
* @param {Object} parentGenerator - Parent HTML generator for recursion
|
||||
*/
|
||||
constructor(options, cssGenerator, parentGenerator) {
|
||||
this.options = options;
|
||||
this.cssGenerator = cssGenerator;
|
||||
this.parentGenerator = parentGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this generator can handle the given node.
|
||||
* @param {Object} node - The node to check
|
||||
* @returns {boolean} - True if this generator can handle the node
|
||||
*/
|
||||
canHandle(node) {
|
||||
return (
|
||||
node.type === "element" &&
|
||||
["checkbox", "radio", "switch", "slider"].includes(node.tag)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates HTML for an input element.
|
||||
* @param {Object} node - The node to generate HTML for
|
||||
* @returns {string} - The generated HTML
|
||||
*/
|
||||
generate(node) {
|
||||
if (this.options.debug) {
|
||||
console.log(`\n[InputElementGenerator] Processing input: ${node.tag}`);
|
||||
}
|
||||
|
||||
const className = this.cssGenerator.generateClassName(node.tag);
|
||||
const { cssProps, nestedRules } = this.cssGenerator.nodeToCSSProperties(node);
|
||||
|
||||
this.cssGenerator.cssRules.set(`.${className}`, {
|
||||
cssProps,
|
||||
nestedRules,
|
||||
});
|
||||
|
||||
let attributes = "";
|
||||
if (node.tag === "checkbox") {
|
||||
attributes = ' type="checkbox"';
|
||||
} else if (node.tag === "radio") {
|
||||
attributes = ' type="radio"';
|
||||
} else if (node.tag === "switch") {
|
||||
attributes = ' type="checkbox" role="switch"';
|
||||
} else if (node.tag === "slider") {
|
||||
attributes = ' type="range"';
|
||||
}
|
||||
|
||||
const valueProp = node.props.find((p) => p.startsWith("value:"));
|
||||
if (valueProp) {
|
||||
const value = valueProp.substring(valueProp.indexOf(":") + 1).trim();
|
||||
attributes += ` value="${value}"`;
|
||||
}
|
||||
|
||||
if (node.props.find((p) => typeof p === "string" && p.startsWith("data-"))) {
|
||||
const dataProps = node.props.filter(
|
||||
(p) => typeof p === "string" && p.startsWith("data-")
|
||||
);
|
||||
attributes += " " + dataProps.map((p) => `${p}`).join(" ");
|
||||
}
|
||||
|
||||
if (node.children.length > 0) {
|
||||
let html = `<label class="${className}-container">`;
|
||||
html += `<input class="${className}"${attributes}>`;
|
||||
|
||||
node.children.forEach((child) => {
|
||||
child.parent = node;
|
||||
html += this.parentGenerator.generateHTML(child);
|
||||
});
|
||||
|
||||
html += `</label>`;
|
||||
return html;
|
||||
} else {
|
||||
return `<input class="${className}"${attributes}>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = InputElementGenerator;
|
78
lib/generators/LinkElementGenerator.js
Normal file
78
lib/generators/LinkElementGenerator.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
const LinkProcessor = require("../utils/LinkProcessor");
|
||||
|
||||
/**
|
||||
* Generates HTML for link elements.
|
||||
*/
|
||||
class LinkElementGenerator {
|
||||
/**
|
||||
* Creates a new link element generator.
|
||||
* @param {Object} options - Options for the generator
|
||||
* @param {CSSGenerator} cssGenerator - CSS generator instance
|
||||
* @param {Object} parentGenerator - Parent HTML generator for recursion
|
||||
*/
|
||||
constructor(options, cssGenerator, parentGenerator) {
|
||||
this.options = options;
|
||||
this.cssGenerator = cssGenerator;
|
||||
this.parentGenerator = parentGenerator;
|
||||
this.linkProcessor = new LinkProcessor(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this generator can handle the given node.
|
||||
* @param {Object} node - The node to check
|
||||
* @returns {boolean} - True if this generator can handle the node
|
||||
*/
|
||||
canHandle(node) {
|
||||
return node.type === "element" && node.tag === "link";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates HTML for a link element.
|
||||
* @param {Object} node - The node to generate HTML for
|
||||
* @returns {string} - The generated HTML
|
||||
*/
|
||||
generate(node) {
|
||||
if (this.options.debug) {
|
||||
console.log(`\n[LinkElementGenerator] Processing link`);
|
||||
}
|
||||
|
||||
if (
|
||||
node.children.length === 1 &&
|
||||
(node.children[0].tag === "button" || node.children[0].tag?.startsWith("button-"))
|
||||
) {
|
||||
if (this.options.debug) {
|
||||
console.log("[LinkElementGenerator] Processing button inside link - using button's HTML");
|
||||
}
|
||||
node.children[0].parent = node;
|
||||
return this.parentGenerator.generateHTML(node.children[0]);
|
||||
}
|
||||
|
||||
const className = this.cssGenerator.generateClassName(node.tag);
|
||||
const { cssProps, nestedRules } = this.cssGenerator.nodeToCSSProperties(node);
|
||||
|
||||
this.cssGenerator.cssRules.set(`.${className}`, {
|
||||
cssProps,
|
||||
nestedRules,
|
||||
});
|
||||
|
||||
const linkInfo = this.linkProcessor.processLink(node);
|
||||
const attributes = this.linkProcessor.getLinkAttributes(linkInfo);
|
||||
|
||||
let html = `<a class="${className}"${attributes}>${
|
||||
this.options.minified ? "" : "\n"
|
||||
}`;
|
||||
|
||||
node.children.forEach((child) => {
|
||||
child.parent = node;
|
||||
html += this.parentGenerator.generateHTML(child);
|
||||
});
|
||||
|
||||
html += `${this.options.minified ? "" : "\n"}</a>${
|
||||
this.options.minified ? "" : "\n"
|
||||
}`;
|
||||
|
||||
return html;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LinkElementGenerator;
|
121
lib/generators/MediaElementGenerator.js
Normal file
121
lib/generators/MediaElementGenerator.js
Normal file
|
@ -0,0 +1,121 @@
|
|||
const BlueprintError = require("../BlueprintError");
|
||||
|
||||
/**
|
||||
* Generates HTML for media elements (images and videos).
|
||||
*/
|
||||
class MediaElementGenerator {
|
||||
/**
|
||||
* Creates a new media element generator.
|
||||
* @param {Object} options - Options for the generator
|
||||
* @param {CSSGenerator} cssGenerator - CSS generator instance
|
||||
* @param {Object} parentGenerator - Parent HTML generator for recursion
|
||||
*/
|
||||
constructor(options, cssGenerator, parentGenerator) {
|
||||
this.options = options;
|
||||
this.cssGenerator = cssGenerator;
|
||||
this.parentGenerator = parentGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this generator can handle the given node.
|
||||
* @param {Object} node - The node to check
|
||||
* @returns {boolean} - True if this generator can handle the node
|
||||
*/
|
||||
canHandle(node) {
|
||||
return node.type === "element" && node.tag === "media";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates HTML for a media element.
|
||||
* @param {Object} node - The node to generate HTML for
|
||||
* @returns {string} - The generated HTML
|
||||
*/
|
||||
generate(node) {
|
||||
if (this.options.debug) {
|
||||
console.log(`\n[MediaElementGenerator] Processing media element`);
|
||||
}
|
||||
|
||||
const className = this.cssGenerator.generateClassName(node.tag);
|
||||
const { cssProps, nestedRules } = this.cssGenerator.nodeToCSSProperties(node);
|
||||
|
||||
this.cssGenerator.cssRules.set(`.${className}`, {
|
||||
cssProps,
|
||||
nestedRules,
|
||||
});
|
||||
|
||||
const srcProp = node.props.find((p) => p.startsWith("src:"));
|
||||
const typeProp = node.props.find((p) => p.startsWith("type:"));
|
||||
|
||||
if (!srcProp) {
|
||||
throw new BlueprintError("Media element requires src property", node.line, node.column);
|
||||
}
|
||||
|
||||
const src = srcProp.substring(srcProp.indexOf(":") + 1).trim();
|
||||
const type = typeProp ? typeProp.substring(typeProp.indexOf(":") + 1).trim() : "img";
|
||||
|
||||
let tag, attributes;
|
||||
|
||||
if (type === "video") {
|
||||
tag = "video";
|
||||
attributes = ` src="${src}" controls`;
|
||||
|
||||
const autoProp = node.props.find((p) => p === "autoplay");
|
||||
if (autoProp) {
|
||||
attributes += ` autoplay`;
|
||||
}
|
||||
|
||||
const loopProp = node.props.find((p) => p === "loop");
|
||||
if (loopProp) {
|
||||
attributes += ` loop`;
|
||||
}
|
||||
|
||||
const mutedProp = node.props.find((p) => p === "muted");
|
||||
if (mutedProp) {
|
||||
attributes += ` muted`;
|
||||
}
|
||||
} else {
|
||||
tag = "img";
|
||||
const altText = node.children.length > 0
|
||||
? node.children.map(child =>
|
||||
this.parentGenerator.generateHTML(child)).join("")
|
||||
: src.split('/').pop();
|
||||
|
||||
attributes = ` src="${src}" alt="${altText}"`;
|
||||
|
||||
const loadingProp = node.props.find((p) => p.startsWith("loading:"));
|
||||
if (loadingProp) {
|
||||
const loadingValue = loadingProp.substring(loadingProp.indexOf(":") + 1).trim();
|
||||
if (["lazy", "eager"].includes(loadingValue)) {
|
||||
attributes += ` loading="${loadingValue}"`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (node.props.find((p) => typeof p === "string" && p.startsWith("data-"))) {
|
||||
const dataProps = node.props.filter(
|
||||
(p) => typeof p === "string" && p.startsWith("data-")
|
||||
);
|
||||
attributes += " " + dataProps.map((p) => `${p}`).join(" ");
|
||||
}
|
||||
|
||||
if (tag === "img") {
|
||||
return `<${tag} class="${className}"${attributes}>`;
|
||||
} else {
|
||||
let html = `<${tag} class="${className}"${attributes}>${
|
||||
this.options.minified ? "" : "\n"
|
||||
}`;
|
||||
|
||||
if (node.children.length > 0 && tag === "video") {
|
||||
html += `<p>Your browser doesn't support video playback.</p>`;
|
||||
}
|
||||
|
||||
html += `${this.options.minified ? "" : "\n"}</${tag}>${
|
||||
this.options.minified ? "" : "\n"
|
||||
}`;
|
||||
|
||||
return html;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MediaElementGenerator;
|
47
lib/generators/RootNodeGenerator.js
Normal file
47
lib/generators/RootNodeGenerator.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* Generates HTML for the root node of the AST.
|
||||
*/
|
||||
class RootNodeGenerator {
|
||||
/**
|
||||
* Creates a new root node generator.
|
||||
* @param {Object} options - Options for the generator
|
||||
* @param {Object} parentGenerator - Parent HTML generator for recursion
|
||||
*/
|
||||
constructor(options, parentGenerator) {
|
||||
this.options = options;
|
||||
this.parentGenerator = parentGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this generator can handle the given node.
|
||||
* @param {Object} node - The node to check
|
||||
* @returns {boolean} - True if this generator can handle the node
|
||||
*/
|
||||
canHandle(node) {
|
||||
return node.type === "root";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates HTML for the root node.
|
||||
* @param {Object} node - The node to generate HTML for
|
||||
* @returns {string} - The generated HTML
|
||||
*/
|
||||
generate(node) {
|
||||
if (this.options.debug) {
|
||||
console.log(`\n[RootNodeGenerator] Processing root node with ${node.children.length} children`);
|
||||
}
|
||||
|
||||
let html = "";
|
||||
|
||||
node.children.forEach((child, index) => {
|
||||
if (this.options.debug) {
|
||||
console.log(`[RootNodeGenerator] Processing child ${index + 1}/${node.children.length}`);
|
||||
}
|
||||
html += this.parentGenerator.generateHTML(child);
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RootNodeGenerator;
|
81
lib/generators/StandardElementGenerator.js
Normal file
81
lib/generators/StandardElementGenerator.js
Normal file
|
@ -0,0 +1,81 @@
|
|||
const { ELEMENT_MAPPINGS } = require("../mappings");
|
||||
const StringUtils = require("../utils/StringUtils");
|
||||
|
||||
/**
|
||||
* Generates HTML for standard elements.
|
||||
*/
|
||||
class StandardElementGenerator {
|
||||
/**
|
||||
* Creates a new standard element generator.
|
||||
* @param {Object} options - Options for the generator
|
||||
* @param {CSSGenerator} cssGenerator - CSS generator instance
|
||||
* @param {Object} parentGenerator - Parent HTML generator for recursion
|
||||
*/
|
||||
constructor(options, cssGenerator, parentGenerator) {
|
||||
this.options = options;
|
||||
this.cssGenerator = cssGenerator;
|
||||
this.parentGenerator = parentGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this generator can handle the given node.
|
||||
* @param {Object} node - The node to check
|
||||
* @returns {boolean} - True if this generator can handle the node
|
||||
*/
|
||||
canHandle(node) {
|
||||
return (
|
||||
node.type === "element" &&
|
||||
node.tag !== "page" &&
|
||||
!node.tag.startsWith("button") &&
|
||||
node.tag !== "link" &&
|
||||
node.tag !== "media"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates HTML for a standard element.
|
||||
* @param {Object} node - The node to generate HTML for
|
||||
* @returns {string} - The generated HTML
|
||||
*/
|
||||
generate(node) {
|
||||
if (this.options.debug) {
|
||||
console.log(`\n[StandardElementGenerator] Processing element node: ${node.tag}`);
|
||||
}
|
||||
|
||||
const mapping = ELEMENT_MAPPINGS[node.tag];
|
||||
const tag = mapping ? mapping.tag : "div";
|
||||
const className = this.cssGenerator.generateClassName(node.tag);
|
||||
const { cssProps, nestedRules } = this.cssGenerator.nodeToCSSProperties(node);
|
||||
|
||||
this.cssGenerator.cssRules.set(`.${className}`, {
|
||||
cssProps,
|
||||
nestedRules,
|
||||
});
|
||||
|
||||
let attributes = "";
|
||||
|
||||
if (node.props.find((p) => typeof p === "string" && p.startsWith("data-"))) {
|
||||
const dataProps = node.props.filter(
|
||||
(p) => typeof p === "string" && p.startsWith("data-")
|
||||
);
|
||||
attributes += " " + dataProps.map((p) => `${p}`).join(" ");
|
||||
}
|
||||
|
||||
let html = `<${tag} class="${className}"${attributes}>${
|
||||
this.options.minified ? "" : "\n"
|
||||
}`;
|
||||
|
||||
node.children.forEach((child) => {
|
||||
child.parent = node;
|
||||
html += this.parentGenerator.generateHTML(child);
|
||||
});
|
||||
|
||||
html += `${this.options.minified ? "" : "\n"}</${tag}>${
|
||||
this.options.minified ? "" : "\n"
|
||||
}`;
|
||||
|
||||
return html;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StandardElementGenerator;
|
51
lib/generators/TextNodeGenerator.js
Normal file
51
lib/generators/TextNodeGenerator.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
const StringUtils = require("../utils/StringUtils");
|
||||
|
||||
/**
|
||||
* Generates HTML for text nodes.
|
||||
*/
|
||||
class TextNodeGenerator {
|
||||
/**
|
||||
* Creates a new text node generator.
|
||||
* @param {Object} options - Options for the generator
|
||||
*/
|
||||
constructor(options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this generator can handle the given node.
|
||||
* @param {Object} node - The node to check
|
||||
* @returns {boolean} - True if this generator can handle the node
|
||||
*/
|
||||
canHandle(node) {
|
||||
return node.type === "text";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates HTML for a text node.
|
||||
* @param {Object} node - The node to generate HTML for
|
||||
* @returns {string} - The generated HTML
|
||||
*/
|
||||
generate(node) {
|
||||
if (this.options.debug) {
|
||||
console.log(`\n[TextNodeGenerator] Processing text node`);
|
||||
}
|
||||
|
||||
if (node.parent?.tag === "codeblock") {
|
||||
if (this.options.debug) {
|
||||
console.log("[TextNodeGenerator] Raw text content for codeblock");
|
||||
}
|
||||
return node.value;
|
||||
}
|
||||
|
||||
const escapedText = StringUtils.escapeHTML(node.value);
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log("[TextNodeGenerator] Generated escaped text");
|
||||
}
|
||||
|
||||
return escapedText;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TextNodeGenerator;
|
Loading…
Add table
Add a link
Reference in a new issue