feat: reorganize builder
This commit is contained in:
parent
ff7bb041ef
commit
362b7aa15e
18 changed files with 1094 additions and 310 deletions
|
@ -1,11 +1,12 @@
|
|||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const TokenParser = require("./TokenParser");
|
||||
const ASTBuilder = require("./ASTBuilder");
|
||||
const CSSGenerator = require("./CSSGenerator");
|
||||
const HTMLGenerator = require("./HTMLGenerator");
|
||||
const MetadataManager = require("./MetadataManager");
|
||||
const BlueprintCompiler = require("./BlueprintCompiler");
|
||||
const BlueprintFileHandler = require("./BlueprintFileHandler");
|
||||
|
||||
/**
|
||||
* BlueprintBuilder coordinates the entire build process from reading Blueprint files
|
||||
* to writing compiled HTML and CSS files.
|
||||
*/
|
||||
class BlueprintBuilder {
|
||||
/**
|
||||
* Create a new Blueprint builder instance.
|
||||
|
@ -20,14 +21,10 @@ class BlueprintBuilder {
|
|||
...options,
|
||||
};
|
||||
|
||||
this.tokenParser = new TokenParser(this.options);
|
||||
this.astBuilder = new ASTBuilder(this.options);
|
||||
this.cssGenerator = new CSSGenerator(this.options);
|
||||
this.htmlGenerator = new HTMLGenerator(this.options, this.cssGenerator);
|
||||
this.metadataManager = new MetadataManager(this.options);
|
||||
this.compiler = new BlueprintCompiler(this.options);
|
||||
this.fileHandler = new BlueprintFileHandler(this.options);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Builds a Blueprint file.
|
||||
* @param {string} inputPath - Path to the Blueprint file to build
|
||||
|
@ -40,42 +37,23 @@ class BlueprintBuilder {
|
|||
}
|
||||
|
||||
try {
|
||||
if (!inputPath.endsWith(".bp")) {
|
||||
throw new Error("Input file must have .bp extension");
|
||||
}
|
||||
|
||||
const input = fs.readFileSync(inputPath, "utf8");
|
||||
|
||||
const tokens = this.tokenParser.tokenize(input);
|
||||
|
||||
const ast = this.astBuilder.buildAST(tokens);
|
||||
|
||||
const pageNode = ast.children.find((node) => node.tag === "page");
|
||||
if (pageNode) {
|
||||
this.metadataManager.processPageMetadata(pageNode);
|
||||
}
|
||||
|
||||
const html = this.htmlGenerator.generateHTML(ast);
|
||||
const css = this.cssGenerator.generateCSS();
|
||||
const input = this.fileHandler.readBlueprintFile(inputPath);
|
||||
|
||||
const baseName = path.basename(inputPath, ".bp");
|
||||
const headContent = this.metadataManager.generateHeadContent(baseName);
|
||||
const finalHtml = this.generateFinalHtml(headContent, html);
|
||||
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
const result = this.compiler.compile(input, baseName);
|
||||
|
||||
fs.writeFileSync(path.join(outputDir, `${baseName}.html`), finalHtml);
|
||||
fs.writeFileSync(path.join(outputDir, `${baseName}.css`), css);
|
||||
if (result.success) {
|
||||
this.fileHandler.writeCompiledFiles(outputDir, baseName, result.html, result.css);
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log("[DEBUG] Build completed successfully");
|
||||
if (this.options.debug) {
|
||||
console.log("[DEBUG] Build completed successfully");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
errors: [],
|
||||
success: result.success,
|
||||
errors: result.errors,
|
||||
};
|
||||
} catch (error) {
|
||||
if (this.options.debug) {
|
||||
|
@ -94,51 +72,6 @@ class BlueprintBuilder {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
${headContent}
|
||||
<style>
|
||||
:root {
|
||||
--navbar-height: 4rem;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-top: var(--navbar-height);
|
||||
background-color: #0d1117;
|
||||
color: #e6edf3;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
line-height: 1.5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
::selection {
|
||||
background-color: rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${bodyContent}
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BlueprintBuilder;
|
||||
|
|
89
lib/BlueprintCompiler.js
Normal file
89
lib/BlueprintCompiler.js
Normal file
|
@ -0,0 +1,89 @@
|
|||
const TokenParser = require("./TokenParser");
|
||||
const ASTBuilder = require("./ASTBuilder");
|
||||
const CSSGenerator = require("./CSSGenerator");
|
||||
const HTMLGenerator = require("./HTMLGenerator");
|
||||
const MetadataManager = require("./MetadataManager");
|
||||
|
||||
/**
|
||||
* BlueprintCompiler handles the core compilation process of transforming Blueprint syntax
|
||||
* into HTML and CSS, without handling file I/O operations.
|
||||
*/
|
||||
class BlueprintCompiler {
|
||||
/**
|
||||
* Create a new Blueprint compiler instance.
|
||||
* @param {Object} [options] - Options object
|
||||
* @param {boolean} [options.minified=true] - Minify generated HTML and CSS
|
||||
* @param {boolean} [options.debug=false] - Enable debug logging
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
minified: true,
|
||||
debug: false,
|
||||
...options,
|
||||
};
|
||||
|
||||
this.tokenParser = new TokenParser(this.options);
|
||||
this.astBuilder = new ASTBuilder(this.options);
|
||||
this.cssGenerator = new CSSGenerator(this.options);
|
||||
this.htmlGenerator = new HTMLGenerator(this.options, this.cssGenerator);
|
||||
this.metadataManager = new MetadataManager(this.options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles Blueprint code into HTML and CSS.
|
||||
* @param {string} blueprintCode - Blueprint source code to compile
|
||||
* @param {string} baseName - Base name for the generated files
|
||||
* @returns {Object} - Compilation result with HTML and CSS content
|
||||
*/
|
||||
compile(blueprintCode, baseName) {
|
||||
if (this.options.debug) {
|
||||
console.log(`[DEBUG] Starting compilation`);
|
||||
}
|
||||
|
||||
try {
|
||||
const tokens = this.tokenParser.tokenize(blueprintCode);
|
||||
const ast = this.astBuilder.buildAST(tokens);
|
||||
|
||||
const pageNode = ast.children.find((node) => node.tag === "page");
|
||||
if (pageNode) {
|
||||
this.metadataManager.processPageMetadata(pageNode);
|
||||
}
|
||||
|
||||
const html = this.htmlGenerator.generateHTML(ast);
|
||||
const css = this.cssGenerator.generateCSS();
|
||||
|
||||
const headContent = this.metadataManager.generateHeadContent(baseName);
|
||||
const finalHtml = this.htmlGenerator.generateFinalHtml(headContent, html);
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log("[DEBUG] Compilation completed successfully");
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
html: finalHtml,
|
||||
css: css,
|
||||
errors: [],
|
||||
};
|
||||
} catch (error) {
|
||||
if (this.options.debug) {
|
||||
console.log("[DEBUG] Compilation failed with error:", error);
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
html: null,
|
||||
css: null,
|
||||
errors: [
|
||||
{
|
||||
message: error.message,
|
||||
type: error.name,
|
||||
line: error.line,
|
||||
column: error.column,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BlueprintCompiler;
|
55
lib/BlueprintFileHandler.js
Normal file
55
lib/BlueprintFileHandler.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
const path = require("path");
|
||||
const BlueprintFileReader = require("./file/BlueprintFileReader");
|
||||
const BlueprintFileWriter = require("./file/BlueprintFileWriter");
|
||||
|
||||
/**
|
||||
* BlueprintFileHandler coordinates file I/O operations for the Blueprint compiler.
|
||||
*/
|
||||
class BlueprintFileHandler {
|
||||
/**
|
||||
* Create a new BlueprintFileHandler instance.
|
||||
* @param {Object} [options] - Options object
|
||||
* @param {boolean} [options.debug=false] - Enable debug logging
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
debug: false,
|
||||
...options,
|
||||
};
|
||||
|
||||
this.reader = new BlueprintFileReader(this.options);
|
||||
this.writer = new BlueprintFileWriter(this.options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a Blueprint file from the file system.
|
||||
* @param {string} inputPath - Path to the Blueprint file
|
||||
* @returns {string} - The content of the file
|
||||
* @throws {Error} - If the file does not exist or has an invalid extension
|
||||
*/
|
||||
readBlueprintFile(inputPath) {
|
||||
return this.reader.readFile(inputPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the compiled HTML and CSS to the file system.
|
||||
* @param {string} outputDir - Directory to write the files to
|
||||
* @param {string} baseName - Base name for the files
|
||||
* @param {string} html - HTML content to write
|
||||
* @param {string} css - CSS content to write
|
||||
* @throws {Error} - If the output directory cannot be created or the files cannot be written
|
||||
*/
|
||||
writeCompiledFiles(outputDir, baseName, html, css) {
|
||||
this.writer.writeCompiledFiles(outputDir, baseName, html, css);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that a directory exists, creating it if it does not.
|
||||
* @param {string} dir - Directory path
|
||||
*/
|
||||
ensureDirectoryExists(dir) {
|
||||
this.writer.ensureDirectoryExists(dir);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BlueprintFileHandler;
|
|
@ -1,4 +1,13 @@
|
|||
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 HTMLTemplate = require("./templates/HTMLTemplate");
|
||||
const StringUtils = require("./utils/StringUtils");
|
||||
|
||||
class HTMLGenerator {
|
||||
/**
|
||||
|
@ -11,11 +20,29 @@ class HTMLGenerator {
|
|||
constructor(options = {}, cssGenerator) {
|
||||
this.options = options;
|
||||
this.cssGenerator = cssGenerator;
|
||||
this.htmlTemplate = new HTMLTemplate(options);
|
||||
|
||||
|
||||
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(", ")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,7 +75,7 @@ class HTMLGenerator {
|
|||
}
|
||||
|
||||
/**
|
||||
* Converts a node to a string of HTML.
|
||||
* Generates HTML for a node, delegating to the appropriate generator.
|
||||
* @param {Object} node - Node to generate HTML for
|
||||
* @returns {string} Generated HTML
|
||||
*/
|
||||
|
@ -56,239 +83,32 @@ class HTMLGenerator {
|
|||
if (this.options.debug) {
|
||||
console.log(`\n[HTMLGenerator] Generating HTML for node`);
|
||||
console.log(`[HTMLGenerator] Node type: "${node.type}"`);
|
||||
console.log("[HTMLGenerator] Node details:", this.debugStringify(node));
|
||||
console.log("[HTMLGenerator] Node details:", StringUtils.safeStringify(node));
|
||||
}
|
||||
|
||||
if (node.type === "text") {
|
||||
if (node.parent?.tag === "codeblock") {
|
||||
if (this.options.debug) {
|
||||
console.log("[HTMLGenerator] Rendering raw text for codeblock");
|
||||
console.log(`[HTMLGenerator] Raw text content: "${node.value}"`);
|
||||
}
|
||||
return node.value;
|
||||
}
|
||||
const escapedText = node.value
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
|
||||
if (node.type === "element" && node.tag === "page") {
|
||||
if (this.options.debug) {
|
||||
console.log("[HTMLGenerator] Generated escaped text");
|
||||
console.log(`[HTMLGenerator] Original: "${node.value}"`);
|
||||
console.log(`[HTMLGenerator] Escaped: "${escapedText}"`);
|
||||
console.log("[HTMLGenerator] Skipping page node - metadata only");
|
||||
}
|
||||
return escapedText;
|
||||
return "";
|
||||
}
|
||||
|
||||
let html = "";
|
||||
if (node.type === "element") {
|
||||
if (node.tag === "page") {
|
||||
|
||||
for (const generator of this.generators) {
|
||||
if (generator.canHandle(node)) {
|
||||
if (this.options.debug) {
|
||||
console.log("[HTMLGenerator] Skipping page node - metadata only");
|
||||
console.log(`[HTMLGenerator] Using ${generator.constructor.name} for node`);
|
||||
}
|
||||
return "";
|
||||
return generator.generate(node);
|
||||
}
|
||||
|
||||
const mapping = ELEMENT_MAPPINGS[node.tag];
|
||||
let tag = mapping ? mapping.tag : "div";
|
||||
const className = this.cssGenerator.generateClassName(node.tag);
|
||||
const { cssProps, nestedRules } =
|
||||
this.cssGenerator.nodeToCSSProperties(node);
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log(`\n[HTMLGenerator] Processing element node`);
|
||||
console.log(`[HTMLGenerator] Tag: "${node.tag}" -> "${tag}"`);
|
||||
console.log(`[HTMLGenerator] Generated class name: "${className}"`);
|
||||
console.log(
|
||||
"[HTMLGenerator] CSS properties:",
|
||||
this.debugStringify(Object.fromEntries(cssProps))
|
||||
);
|
||||
console.log(
|
||||
"[HTMLGenerator] Nested rules:",
|
||||
this.debugStringify(Object.fromEntries(nestedRules))
|
||||
);
|
||||
}
|
||||
|
||||
let attributes = "";
|
||||
if (tag === "input") {
|
||||
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"';
|
||||
}
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[HTMLGenerator] Added input attributes: "${attributes}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (node.tag === "media") {
|
||||
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";
|
||||
|
||||
if (type === "video") {
|
||||
tag = "video";
|
||||
attributes = ` src="${src}" controls`;
|
||||
} else {
|
||||
tag = "img";
|
||||
attributes = ` src="${src}" alt="${node.children.map(child => this.generateHTML(child)).join("")}"`;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.tag === "link") {
|
||||
const linkInfo = this.processLink(node);
|
||||
attributes += ` href="${linkInfo.href}"`;
|
||||
if (
|
||||
linkInfo.href.startsWith("http://") ||
|
||||
linkInfo.href.startsWith("https://")
|
||||
) {
|
||||
attributes += ` target="_blank" rel="noopener noreferrer"`;
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[HTMLGenerator] Added external link attributes for: ${linkInfo.href}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[HTMLGenerator] Added internal link attributes for: ${linkInfo.href}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 (this.options.debug) {
|
||||
console.log(
|
||||
`[HTMLGenerator] Added data attributes:`,
|
||||
this.debugStringify(dataProps)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.cssGenerator.cssRules.set(`.${className}`, {
|
||||
cssProps,
|
||||
nestedRules,
|
||||
});
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[HTMLGenerator] Registered CSS rules for class: .${className}`
|
||||
);
|
||||
}
|
||||
|
||||
if (node.tag === "button" || node.tag.startsWith("button-")) {
|
||||
if (node.parent?.tag === "link") {
|
||||
const linkInfo = this.processLink(node.parent);
|
||||
if (
|
||||
linkInfo.href.startsWith("http://") ||
|
||||
linkInfo.href.startsWith("https://")
|
||||
) {
|
||||
attributes += ` onclick="window.open('${linkInfo.href}', '_blank', 'noopener,noreferrer')"`;
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[HTMLGenerator] Added external button click handler for: ${linkInfo.href}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
attributes += ` onclick="window.location.href='${linkInfo.href}'"`;
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[HTMLGenerator] Added internal button click handler for: ${linkInfo.href}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
html += `<button class="${className}"${attributes}>${
|
||||
this.options.minified ? "" : "\n"
|
||||
}`;
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[HTMLGenerator] Generated button opening tag with attributes:`,
|
||||
this.debugStringify({ class: className, ...attributes })
|
||||
);
|
||||
}
|
||||
node.children.forEach((child) => {
|
||||
child.parent = node;
|
||||
html += this.generateHTML(child);
|
||||
});
|
||||
html += `${this.options.minified ? "" : "\n"}</button>${
|
||||
this.options.minified ? "" : "\n"
|
||||
}`;
|
||||
} else if (
|
||||
node.tag === "link" &&
|
||||
node.children.length === 1 &&
|
||||
(node.children[0].tag === "button" ||
|
||||
node.children[0].tag?.startsWith("button-"))
|
||||
) {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
"[HTMLGenerator] Processing button inside link - using button's HTML"
|
||||
);
|
||||
}
|
||||
node.children[0].parent = node;
|
||||
html += this.generateHTML(node.children[0]);
|
||||
} else {
|
||||
html += `<${tag} class="${className}"${attributes}>${
|
||||
this.options.minified ? "" : "\n"
|
||||
}`;
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[HTMLGenerator] Generated opening tag: <${tag}> with attributes:`,
|
||||
this.debugStringify({ class: className, ...attributes })
|
||||
);
|
||||
}
|
||||
node.children.forEach((child) => {
|
||||
child.parent = node;
|
||||
html += this.generateHTML(child);
|
||||
});
|
||||
html += `${this.options.minified ? "" : "\n"}</${tag}>${
|
||||
this.options.minified ? "" : "\n"
|
||||
}`;
|
||||
if (this.options.debug) {
|
||||
console.log(`[HTMLGenerator] Completed element: ${tag}`);
|
||||
}
|
||||
}
|
||||
} else if (node.type === "root") {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[HTMLGenerator] Processing root node with ${node.children.length} children`
|
||||
);
|
||||
}
|
||||
node.children.forEach((child, index) => {
|
||||
if (this.options.debug) {
|
||||
console.log(
|
||||
`[HTMLGenerator] Processing root child ${index + 1}/${
|
||||
node.children.length
|
||||
}`
|
||||
);
|
||||
}
|
||||
html += this.generateHTML(child);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (this.options.debug) {
|
||||
console.log("[HTMLGenerator] Generated HTML:", html);
|
||||
console.log(`[HTMLGenerator] No generator found for node type: ${node.type}`);
|
||||
}
|
||||
return html;
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -348,6 +168,17 @@ class HTMLGenerator {
|
|||
}
|
||||
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) {
|
||||
return this.htmlTemplate.generateDocument(headContent, bodyContent);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HTMLGenerator;
|
||||
|
|
|
@ -9,6 +9,7 @@ const options = {
|
|||
minified: !args.includes("--readable"),
|
||||
srcDir: "./src",
|
||||
outDir: "./dist",
|
||||
debug: args.includes("--debug"),
|
||||
};
|
||||
|
||||
const server = new BlueprintServer(options);
|
||||
|
|
42
lib/file/BlueprintFileReader.js
Normal file
42
lib/file/BlueprintFileReader.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
const fs = require("fs");
|
||||
|
||||
/**
|
||||
* Handles reading Blueprint files from the file system.
|
||||
*/
|
||||
class BlueprintFileReader {
|
||||
/**
|
||||
* Creates a new BlueprintFileReader instance.
|
||||
* @param {Object} [options] - Options object
|
||||
* @param {boolean} [options.debug=false] - Enable debug logging
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
debug: false,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a Blueprint file from the file system.
|
||||
* @param {string} inputPath - Path to the Blueprint file
|
||||
* @returns {string} - The content of the file
|
||||
* @throws {Error} - If the file does not exist or has an invalid extension
|
||||
*/
|
||||
readFile(inputPath) {
|
||||
if (this.options.debug) {
|
||||
console.log(`[DEBUG] Reading Blueprint file: ${inputPath}`);
|
||||
}
|
||||
|
||||
if (!inputPath.endsWith(".bp")) {
|
||||
throw new Error("Input file must have .bp extension");
|
||||
}
|
||||
|
||||
if (!fs.existsSync(inputPath)) {
|
||||
throw new Error(`File not found: ${inputPath}`);
|
||||
}
|
||||
|
||||
return fs.readFileSync(inputPath, "utf8");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BlueprintFileReader;
|
78
lib/file/BlueprintFileWriter.js
Normal file
78
lib/file/BlueprintFileWriter.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
/**
|
||||
* Handles writing compiled Blueprint files to the file system.
|
||||
*/
|
||||
class BlueprintFileWriter {
|
||||
/**
|
||||
* Creates a new BlueprintFileWriter instance.
|
||||
* @param {Object} [options] - Options object
|
||||
* @param {boolean} [options.debug=false] - Enable debug logging
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
debug: false,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes HTML content to a file.
|
||||
* @param {string} outputPath - Path to write the HTML file
|
||||
* @param {string} content - HTML content to write
|
||||
*/
|
||||
writeHtmlFile(outputPath, content) {
|
||||
if (this.options.debug) {
|
||||
console.log(`[DEBUG] Writing HTML file: ${outputPath}`);
|
||||
}
|
||||
this.ensureDirectoryExists(path.dirname(outputPath));
|
||||
fs.writeFileSync(outputPath, content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes CSS content to a file.
|
||||
* @param {string} outputPath - Path to write the CSS file
|
||||
* @param {string} content - CSS content to write
|
||||
*/
|
||||
writeCssFile(outputPath, content) {
|
||||
if (this.options.debug) {
|
||||
console.log(`[DEBUG] Writing CSS file: ${outputPath}`);
|
||||
}
|
||||
this.ensureDirectoryExists(path.dirname(outputPath));
|
||||
fs.writeFileSync(outputPath, content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes both HTML and CSS files for a Blueprint compilation result.
|
||||
* @param {string} outputDir - Directory to write the files to
|
||||
* @param {string} baseName - Base name for the files (without extension)
|
||||
* @param {string} html - HTML content to write
|
||||
* @param {string} css - CSS content to write
|
||||
*/
|
||||
writeCompiledFiles(outputDir, baseName, html, css) {
|
||||
if (this.options.debug) {
|
||||
console.log(`[DEBUG] Writing compiled files to: ${outputDir}/${baseName}`);
|
||||
}
|
||||
|
||||
this.ensureDirectoryExists(outputDir);
|
||||
|
||||
this.writeHtmlFile(path.join(outputDir, `${baseName}.html`), html);
|
||||
this.writeCssFile(path.join(outputDir, `${baseName}.css`), css);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that a directory exists, creating it if it does not.
|
||||
* @param {string} dir - Directory path
|
||||
*/
|
||||
ensureDirectoryExists(dir) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
if (this.options.debug) {
|
||||
console.log(`[DEBUG] Created directory: ${dir}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BlueprintFileWriter;
|
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;
|
61
lib/templates/HTMLTemplate.js
Normal file
61
lib/templates/HTMLTemplate.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
* HTMLTemplate provides templates for generating the final HTML document.
|
||||
*/
|
||||
class HTMLTemplate {
|
||||
/**
|
||||
* Creates a new HTML template instance.
|
||||
* @param {Object} [options] - Options object
|
||||
* @param {boolean} [options.minified=true] - Whether to minify the output
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
minified: true,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the final HTML document using the provided head and body content.
|
||||
* @param {string} headContent - HTML content for the <head> section
|
||||
* @param {string} bodyContent - HTML content for the <body> section
|
||||
* @returns {string} - Complete HTML document
|
||||
*/
|
||||
generateDocument(headContent, bodyContent) {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
${headContent}
|
||||
<style>
|
||||
:root {
|
||||
--navbar-height: 4rem;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-top: var(--navbar-height);
|
||||
background-color: #0d1117;
|
||||
color: #e6edf3;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
line-height: 1.5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
::selection {
|
||||
background-color: rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${bodyContent}
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HTMLTemplate;
|
88
lib/utils/LinkProcessor.js
Normal file
88
lib/utils/LinkProcessor.js
Normal file
|
@ -0,0 +1,88 @@
|
|||
/**
|
||||
* LinkProcessor provides utilities for processing link elements.
|
||||
*/
|
||||
class LinkProcessor {
|
||||
/**
|
||||
* Creates a new link processor instance.
|
||||
* @param {Object} [options] - Options object
|
||||
* @param {boolean} [options.debug=false] - Enable debug logging
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
debug: false,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a link node and extracts the href attribute.
|
||||
* Converts to an internal link if it doesn't start with http:// or https://.
|
||||
*
|
||||
* @param {Object} node - The link node to process
|
||||
* @returns {Object} - An object containing link properties
|
||||
*/
|
||||
processLink(node) {
|
||||
if (this.options.debug) {
|
||||
console.log("\n[LinkProcessor] Processing link node");
|
||||
}
|
||||
|
||||
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(`[LinkProcessor] Converted to internal link: "${hrefTarget}"`);
|
||||
}
|
||||
} else {
|
||||
if (this.options.debug) {
|
||||
console.log(`[LinkProcessor] External link detected: "${hrefTarget}"`);
|
||||
}
|
||||
}
|
||||
href = hrefTarget;
|
||||
} else {
|
||||
if (this.options.debug) {
|
||||
console.log("[LinkProcessor] No href property found, using default: '#'");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
href,
|
||||
isExternal: href.startsWith("http://") || href.startsWith("https://")
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets appropriate HTML attributes for a link based on its type
|
||||
* @param {Object} linkInfo - Link information object
|
||||
* @returns {string} - HTML attributes for the link
|
||||
*/
|
||||
getLinkAttributes(linkInfo) {
|
||||
if (linkInfo.isExternal) {
|
||||
return ` href="${linkInfo.href}" target="_blank" rel="noopener noreferrer"`;
|
||||
} else {
|
||||
return ` href="${linkInfo.href}"`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the click handler for a button inside a link
|
||||
* @param {Object} linkInfo - Link information object
|
||||
* @returns {string} - onclick attribute value
|
||||
*/
|
||||
getButtonClickHandler(linkInfo) {
|
||||
if (linkInfo.isExternal) {
|
||||
return ` onclick="window.open('${linkInfo.href}', '_blank', 'noopener,noreferrer')"`;
|
||||
} else {
|
||||
return ` onclick="window.location.href='${linkInfo.href}'"`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LinkProcessor;
|
59
lib/utils/StringUtils.js
Normal file
59
lib/utils/StringUtils.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* Utilities for string operations used throughout the Blueprint compiler.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Escapes special HTML characters in a string to prevent XSS attacks.
|
||||
* @param {string} text - The text to escape
|
||||
* @returns {string} - The escaped text
|
||||
*/
|
||||
const escapeHTML = (text) => {
|
||||
return text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a camelCase string to kebab-case (lowercase with hyphens).
|
||||
* @param {string} str - The string to convert
|
||||
* @returns {string} - The converted string
|
||||
*/
|
||||
const toKebabCase = (str) => {
|
||||
return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
||||
};
|
||||
|
||||
/**
|
||||
* Stringify an object while handling circular references.
|
||||
* @param {Object} obj - The object to stringify
|
||||
* @returns {string} - JSON string representation
|
||||
*/
|
||||
const safeStringify = (obj) => {
|
||||
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(obj, getCircularReplacer(), 2);
|
||||
} catch (err) {
|
||||
return `[Unable to stringify: ${err.message}]`;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
escapeHTML,
|
||||
toKebabCase,
|
||||
safeStringify
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "blueprint",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"description": "A modern UI component compiler with live reload support",
|
||||
"main": "lib/BlueprintBuilder.js",
|
||||
"scripts": {
|
||||
|
|
Loading…
Add table
Reference in a new issue