378 lines
11 KiB
JavaScript
378 lines
11 KiB
JavaScript
const BlueprintError = require("./BlueprintError");
|
|
const { ELEMENT_MAPPINGS } = require("./mappings");
|
|
|
|
class ASTBuilder {
|
|
/**
|
|
* Initializes a new instance of the ASTBuilder class.
|
|
*
|
|
* @param {Object} options - Configuration options for the ASTBuilder.
|
|
* @param {boolean} [options.debug=false] - If true, enables debug logging for the builder.
|
|
*/
|
|
|
|
constructor(options = {}) {
|
|
this.options = options;
|
|
if (this.options.debug) {
|
|
console.log(
|
|
"[ASTBuilder] Initialized with options:",
|
|
JSON.stringify(options, null, 2)
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Converts a node object into a JSON string representation.
|
|
* Handles circular references by replacing them with a predefined string.
|
|
*
|
|
* @param {Object} node - The node object to stringify.
|
|
* @returns {string} - The JSON string representation of the node.
|
|
* If unable to stringify, returns an error message.
|
|
*/
|
|
|
|
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}]`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Constructs an Abstract Syntax Tree (AST) from a sequence of tokens.
|
|
*
|
|
* This function iterates over the provided tokens to build a hierarchical
|
|
* AST structure. It identifies elements, their properties, and any nested
|
|
* child elements, converting them into structured nodes. Each node is
|
|
* represented as an object containing type, tag, properties, children,
|
|
* and position information (line and column).
|
|
*
|
|
* Throws an error if unexpected tokens are encountered or if there are
|
|
* mismatched braces.
|
|
*
|
|
* @param {Array} tokens - The list of tokens to be parsed into an AST.
|
|
* @returns {Object} - The constructed AST root node with its children.
|
|
* Each child represents either an element or text node.
|
|
* @throws {BlueprintError} - If unexpected tokens or structural issues are found.
|
|
*/
|
|
|
|
buildAST(tokens) {
|
|
if (this.options.debug) {
|
|
console.log("\n[ASTBuilder] Starting AST construction");
|
|
console.log(`[ASTBuilder] Processing ${tokens.length} tokens`);
|
|
console.log(
|
|
"[ASTBuilder] First few tokens:",
|
|
tokens
|
|
.slice(0, 3)
|
|
.map((t) => this.debugStringify(t))
|
|
.join(", ")
|
|
);
|
|
}
|
|
|
|
let current = 0;
|
|
|
|
/**
|
|
* Walks the token list to construct a hierarchical AST structure.
|
|
*
|
|
* This function is responsible for processing each token and constructing
|
|
* the corresponding node in the AST. It handles elements, their properties,
|
|
* and any nested child elements, converting them into structured nodes.
|
|
* Each node is represented as an object containing type, tag, properties,
|
|
* children, and position information (line and column).
|
|
*
|
|
* Throws an error if unexpected tokens are encountered or if there are
|
|
* mismatched braces.
|
|
*
|
|
* @returns {Object} - The constructed AST node with its children.
|
|
* Each child represents either an element or text node.
|
|
* @throws {BlueprintError} - If unexpected tokens or structural issues are found.
|
|
*/
|
|
const walk = () => {
|
|
if (this.options.debug) {
|
|
console.log(
|
|
`\n[ASTBuilder] Walking tokens at position ${current}/${tokens.length}`
|
|
);
|
|
console.log(
|
|
"[ASTBuilder] Current token:",
|
|
this.debugStringify(tokens[current])
|
|
);
|
|
}
|
|
|
|
let token = tokens[current];
|
|
|
|
if (!token) {
|
|
if (this.options.debug) {
|
|
console.log(
|
|
"[ASTBuilder] Unexpected end of input while walking tokens"
|
|
);
|
|
console.log(
|
|
"[ASTBuilder] Last processed token:",
|
|
this.debugStringify(tokens[current - 1])
|
|
);
|
|
}
|
|
throw new BlueprintError(
|
|
"Unexpected end of input",
|
|
tokens[tokens.length - 1]?.line || 1,
|
|
tokens[tokens.length - 1]?.column || 0
|
|
);
|
|
}
|
|
|
|
if (token.type === "client" || token.type === "server") {
|
|
if (this.options.debug) {
|
|
console.log(
|
|
`\n[ASTBuilder] Processing ${token.type} block at line ${token.line}, column ${token.column}`
|
|
);
|
|
}
|
|
|
|
const node = {
|
|
type: token.type,
|
|
script: token.value,
|
|
line: token.line,
|
|
column: token.column,
|
|
};
|
|
|
|
if (token.type === "server" && token.params) {
|
|
node.params = token.params;
|
|
if (this.options.debug) {
|
|
console.log(
|
|
`[ASTBuilder] Server block parameters: ${node.params.join(", ")}`
|
|
);
|
|
}
|
|
}
|
|
|
|
if (this.options.debug) {
|
|
console.log(`[ASTBuilder] Created node for ${token.type} block`);
|
|
console.log(
|
|
"[ASTBuilder] Script content (first 50 chars):",
|
|
node.script.substring(0, 50) + (node.script.length > 50 ? "..." : "")
|
|
);
|
|
}
|
|
|
|
current++;
|
|
return node;
|
|
}
|
|
|
|
if (token.type === "identifier") {
|
|
if (this.options.debug) {
|
|
console.log(
|
|
`\n[ASTBuilder] Processing identifier: "${token.value}" at line ${token.line}, column ${token.column}`
|
|
);
|
|
}
|
|
|
|
const elementType = token.value;
|
|
if (!ELEMENT_MAPPINGS[elementType]) {
|
|
if (this.options.debug) {
|
|
console.log(
|
|
`[ASTBuilder] Error: Unknown element type "${elementType}"`
|
|
);
|
|
console.log(
|
|
"[ASTBuilder] Available element types:",
|
|
Object.keys(ELEMENT_MAPPINGS).join(", ")
|
|
);
|
|
}
|
|
throw new BlueprintError(
|
|
`Unknown element type: ${elementType}`,
|
|
token.line,
|
|
token.column
|
|
);
|
|
}
|
|
|
|
const mapping = ELEMENT_MAPPINGS[elementType];
|
|
const node = {
|
|
type: "element",
|
|
tag: elementType,
|
|
props:
|
|
elementType === "page" ? [] : [...(mapping.defaultProps || [])],
|
|
children: [],
|
|
line: token.line,
|
|
column: token.column,
|
|
};
|
|
|
|
if (this.options.debug) {
|
|
console.log(`[ASTBuilder] Created node for element "${elementType}"`);
|
|
console.log(
|
|
"[ASTBuilder] Initial node state:",
|
|
this.debugStringify(node)
|
|
);
|
|
}
|
|
|
|
current++;
|
|
|
|
if (
|
|
current < tokens.length &&
|
|
tokens[current].type === "props"
|
|
) {
|
|
const props = tokens[current].value.split(",").map((p) => p.trim());
|
|
if (this.options.debug) {
|
|
console.log(
|
|
`[ASTBuilder] Processing ${props.length} properties for "${elementType}"`
|
|
);
|
|
console.log("[ASTBuilder] Properties:", props);
|
|
}
|
|
|
|
props.forEach((prop) => {
|
|
const [name, ...valueParts] = prop.split(":");
|
|
const value = valueParts.join(":").trim();
|
|
if (this.options.debug) {
|
|
console.log(
|
|
`[ASTBuilder] Processing property - name: "${name}", value: "${value}"`
|
|
);
|
|
}
|
|
|
|
if (value) {
|
|
if (elementType === "page") {
|
|
const processedProp = {
|
|
name,
|
|
value: value.replace(/^"|"$/g, ""),
|
|
};
|
|
node.props.push(processedProp);
|
|
if (this.options.debug) {
|
|
console.log(
|
|
`[ASTBuilder] Added page property:`,
|
|
processedProp
|
|
);
|
|
}
|
|
} else {
|
|
node.props.push(`${name}:${value}`);
|
|
if (this.options.debug) {
|
|
console.log(
|
|
`[ASTBuilder] Added property: "${name}:${value}"`
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
node.props.push(name);
|
|
if (this.options.debug) {
|
|
console.log(`[ASTBuilder] Added flag property: "${name}"`);
|
|
}
|
|
}
|
|
});
|
|
current++;
|
|
}
|
|
|
|
if (
|
|
current < tokens.length &&
|
|
tokens[current].type === "brace" &&
|
|
tokens[current].value === "{"
|
|
) {
|
|
if (this.options.debug) {
|
|
console.log(
|
|
`\n[ASTBuilder] Processing child elements for "${elementType}"`
|
|
);
|
|
}
|
|
current++;
|
|
|
|
while (
|
|
current < tokens.length &&
|
|
!(tokens[current].type === "brace" && tokens[current].value === "}")
|
|
) {
|
|
if (this.options.debug) {
|
|
console.log(
|
|
`[ASTBuilder] Processing child at position ${current}`
|
|
);
|
|
}
|
|
const child = walk();
|
|
child.parent = node;
|
|
node.children.push(child);
|
|
if (this.options.debug) {
|
|
console.log(
|
|
`[ASTBuilder] Added child to "${elementType}":`,
|
|
this.debugStringify(child)
|
|
);
|
|
}
|
|
}
|
|
|
|
if (current >= tokens.length) {
|
|
if (this.options.debug) {
|
|
console.log(
|
|
`[ASTBuilder] Error: Missing closing brace for "${elementType}"`
|
|
);
|
|
}
|
|
throw new BlueprintError(
|
|
"Missing closing brace",
|
|
node.line,
|
|
node.column
|
|
);
|
|
}
|
|
|
|
current++;
|
|
}
|
|
|
|
if (this.options.debug) {
|
|
console.log(`[ASTBuilder] Completed node for "${elementType}"`);
|
|
console.log(
|
|
"[ASTBuilder] Final node state:",
|
|
this.debugStringify(node)
|
|
);
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
if (token.type === "text") {
|
|
if (this.options.debug) {
|
|
console.log(
|
|
`[ASTBuilder] Processing text node at line ${token.line}, column ${token.column}`
|
|
);
|
|
console.log(`[ASTBuilder] Text content: "${token.value}"`);
|
|
}
|
|
current++;
|
|
return {
|
|
type: "text",
|
|
value: token.value,
|
|
line: token.line,
|
|
column: token.column,
|
|
};
|
|
}
|
|
|
|
if (this.options.debug) {
|
|
console.log(`[ASTBuilder] Error: Unexpected token type: ${token.type}`);
|
|
console.log("[ASTBuilder] Token details:", this.debugStringify(token));
|
|
}
|
|
throw new BlueprintError(
|
|
`Unexpected token type: ${token.type}`,
|
|
token.line,
|
|
token.column
|
|
);
|
|
};
|
|
|
|
const ast = {
|
|
type: "root",
|
|
children: [],
|
|
};
|
|
|
|
while (current < tokens.length) {
|
|
if (this.options.debug) {
|
|
console.log(
|
|
`\n[ASTBuilder] Processing root-level token at position ${current}`
|
|
);
|
|
}
|
|
ast.children.push(walk());
|
|
}
|
|
|
|
if (this.options.debug) {
|
|
console.log("\n[ASTBuilder] AST construction complete");
|
|
console.log(`[ASTBuilder] Total nodes: ${ast.children.length}`);
|
|
console.log(
|
|
"[ASTBuilder] Root children types:",
|
|
ast.children.map((c) => c.type).join(", ")
|
|
);
|
|
}
|
|
|
|
return ast;
|
|
}
|
|
}
|
|
|
|
module.exports = ASTBuilder;
|