blueprint/lib/TokenParser.js
2025-03-27 17:28:51 +01:00

506 lines
14 KiB
JavaScript

const BlueprintError = require("./BlueprintError");
class TokenParser {
/**
* Creates a new TokenParser instance.
* @param {Object} [options] - Options object
* @param {boolean} [options.debug=false] - Enable debug logging
*/
constructor(options = {}) {
this.options = options;
if (this.options.debug) {
console.log(
"[TokenParser] Initialized with options:",
JSON.stringify(options, null, 2)
);
}
}
/**
* Tokenizes the input string into an array of tokens.
* Tokens can be of the following types:
* - `identifier`: A sequence of letters, numbers, underscores, and hyphens.
* Represents a CSS selector or a property name.
* - `props`: A sequence of characters enclosed in parentheses.
* Represents a list of CSS properties.
* - `text`: A sequence of characters enclosed in quotes.
* Represents a string of text.
* - `brace`: A single character, either `{` or `}`.
* Represents a brace in the input.
*
* @param {string} input - Input string to tokenize
* @returns {Array<Object>} - Array of tokens
* @throws {BlueprintError} - If the input contains invalid syntax
*/
tokenize(input) {
if (this.options.debug) {
console.log("\n[TokenParser] Starting tokenization");
console.log(`[TokenParser] Input length: ${input.length} characters`);
console.log(`[TokenParser] First 100 chars: ${input.slice(0, 100)}...`);
}
const tokens = [];
let current = 0;
let line = 1;
let column = 1;
const startTime = Date.now();
const TIMEOUT_MS = 5000;
while (current < input.length) {
let char = input[current];
if (Date.now() - startTime > TIMEOUT_MS) {
if (this.options.debug) {
console.log(
`[TokenParser] Tokenization timeout at position ${current}, line ${line}, column ${column}`
);
}
throw new BlueprintError(
"Parsing timeout - check for unclosed brackets or quotes",
line,
column
);
}
if (char === "\n") {
if (this.options.debug) {
console.log(
`[TokenParser] Line break at position ${current}, moving to line ${
line + 1
}`
);
}
line++;
column = 1;
current++;
continue;
}
if (/\s/.test(char)) {
column++;
current++;
continue;
}
if (char === "/" && input[current + 1] === "/") {
if (this.options.debug) {
console.log(
`[TokenParser] Comment found at line ${line}, column ${column}`
);
const commentEnd = input.indexOf("\n", current);
const comment = input.slice(
current,
commentEnd !== -1 ? commentEnd : undefined
);
console.log(`[TokenParser] Comment content: ${comment}`);
}
while (current < input.length && input[current] !== "\n") {
current++;
column++;
}
continue;
}
if (char === "@") {
const startPos = current;
const startColumn = column;
const startLine = line;
current++;
column++;
let blockType = "";
char = input[current];
while (current < input.length && /[a-zA-Z]/.test(char)) {
blockType += char;
current++;
column++;
char = input[current];
}
if (blockType === "client" || blockType === "server") {
if (this.options.debug) {
console.log(`[TokenParser] ${blockType} block found at line ${startLine}, column ${startColumn}`);
}
while (current < input.length && /\s/.test(char)) {
if (char === "\n") {
line++;
column = 1;
} else {
column++;
}
current++;
char = input[current];
}
let params = [];
if (blockType === "server" && char === "(") {
current++;
column++;
let paramString = "";
let depth = 1;
while (current < input.length && depth > 0) {
char = input[current];
if (char === "(") depth++;
if (char === ")") depth--;
if (depth === 0) break;
paramString += char;
if (char === "\n") {
line++;
column = 1;
} else {
column++;
}
current++;
}
current++;
column++;
params = paramString.split(",").map(p => p.trim()).filter(p => p);
if (this.options.debug) {
console.log(`[TokenParser] Server block parameters: ${params.join(", ")}`);
}
char = input[current];
while (current < input.length && /\s/.test(char)) {
if (char === "\n") {
line++;
column = 1;
} else {
column++;
}
current++;
char = input[current];
}
}
if (char === "{") {
current++;
column++;
let script = "";
let braceCount = 1;
while (current < input.length && braceCount > 0) {
char = input[current];
if (char === "{") braceCount++;
if (char === "}") braceCount--;
if (braceCount === 0) break;
script += char;
if (char === "\n") {
line++;
column = 1;
} else {
column++;
}
current++;
}
current++;
column++;
tokens.push({
type: blockType,
value: script.trim(),
params: params,
line: startLine,
column: startColumn,
});
if (this.options.debug) {
console.log(`[TokenParser] ${blockType} block script: "${script.trim().substring(0, 50)}..."`);
}
continue;
} else {
throw new BlueprintError(
`Expected opening brace after @${blockType}${params.length ? '(...)' : ''}`,
line,
column
);
}
} else {
throw new BlueprintError(
`Unknown block type: @${blockType}`,
startLine,
startColumn
);
}
}
if (/[a-zA-Z]/.test(char)) {
let value = "";
const startColumn = column;
const startPos = current;
while (current < input.length && /[a-zA-Z0-9_-]/.test(char)) {
value += char;
current++;
column++;
char = input[current];
}
if (this.options.debug) {
console.log(
`[TokenParser] Identifier found at line ${line}, column ${startColumn}`
);
console.log(`[TokenParser] Identifier value: "${value}"`);
console.log(
`[TokenParser] Context: ...${input.slice(
Math.max(0, startPos - 10),
startPos
)}[${value}]${input.slice(current, current + 10)}...`
);
}
tokens.push({
type: "identifier",
value,
line,
column: startColumn,
});
continue;
}
if (char === "(") {
if (this.options.debug) {
console.log(`[DEBUG] Starting property list at position ${current}`);
}
const startColumn = column;
let value = "";
let depth = 1;
let propLine = line;
let propColumn = column;
current++;
column++;
const propStartPos = current;
while (current < input.length && depth > 0) {
if (current - propStartPos > 1000) {
if (this.options.debug) {
console.log("[DEBUG] Property list too long or unclosed");
}
throw new BlueprintError(
"Property list too long or unclosed parenthesis",
propLine,
propColumn
);
}
char = input[current];
if (char === "(") depth++;
if (char === ")") depth--;
if (depth === 0) break;
value += char;
if (char === "\n") {
line++;
column = 1;
} else {
column++;
}
current++;
}
if (depth > 0) {
if (this.options.debug) {
console.log("[DEBUG] Unclosed parenthesis detected");
}
throw new BlueprintError(
"Unclosed parenthesis in property list",
propLine,
propColumn
);
}
tokens.push({
type: "props",
value: value.trim(),
line,
column: startColumn,
});
current++;
column++;
continue;
}
if (char === '"' || char === "'") {
if (this.options.debug) {
console.log(`[DEBUG] Starting string at position ${current}`);
}
const startColumn = column;
const startLine = line;
const quote = char;
let value = "";
const stringStartPos = current;
current++;
column++;
while (current < input.length) {
if (current - stringStartPos > 1000) {
if (this.options.debug) {
console.log("[DEBUG] String too long or unclosed");
}
throw new BlueprintError(
"String too long or unclosed quote",
startLine,
startColumn
);
}
char = input[current];
if (char === "\n") {
line++;
column = 1;
value += char;
} else if (char === quote && input[current - 1] !== "\\") {
break;
} else {
value += char;
column++;
}
current++;
}
tokens.push({
type: "text",
value,
line: startLine,
column: startColumn,
});
current++;
column++;
continue;
}
if (char === "{" || char === "}") {
if (this.options.debug) {
console.log(`[DEBUG] Found brace: ${char} at position ${current}`);
}
tokens.push({
type: "brace",
value: char,
line,
column,
});
current++;
column++;
continue;
}
if (this.options.debug) {
console.log(
`[DEBUG] Unexpected character at position ${current}: "${char}"`
);
}
throw new BlueprintError(`Unexpected character: ${char}`, line, column);
}
if (this.options.debug) {
console.log("\n[TokenParser] Tokenization complete");
console.log(`[TokenParser] Total tokens generated: ${tokens.length}`);
console.log(
"[TokenParser] Token summary:",
tokens.map((t) => `${t.type}:${t.value}`).join(", ")
);
}
this.validateBraces(tokens);
return tokens;
}
/**
* Validates that all braces in the token stream are properly matched.
* This function walks the token stream, counting the number of open and
* close braces. If it encounters an unmatched brace, it throws an error.
* If it encounters an extra closing brace, it throws an error.
* @throws {BlueprintError} - If there is a brace mismatch
*/
validateBraces(tokens) {
let braceCount = 0;
let lastOpenBrace = { line: 1, column: 1 };
const braceStack = [];
if (this.options.debug) {
console.log("\n[TokenParser] Starting brace validation");
}
for (const token of tokens) {
if (token.type === "brace") {
if (token.value === "{") {
braceCount++;
braceStack.push({ line: token.line, column: token.column });
lastOpenBrace = { line: token.line, column: token.column };
if (this.options.debug) {
console.log(
`[TokenParser] Opening brace at line ${token.line}, column ${token.column}, depth: ${braceCount}`
);
}
} else if (token.value === "}") {
braceCount--;
const matchingOpen = braceStack.pop();
if (this.options.debug) {
console.log(
`[TokenParser] Closing brace at line ${token.line}, column ${token.column}, depth: ${braceCount}`
);
if (matchingOpen) {
console.log(
`[TokenParser] Matches opening brace at line ${matchingOpen.line}, column ${matchingOpen.column}`
);
}
}
}
}
}
if (braceCount !== 0) {
if (this.options.debug) {
console.log(
`[TokenParser] Brace mismatch detected: ${
braceCount > 0 ? "unclosed" : "extra"
} braces`
);
console.log(`[TokenParser] Brace stack:`, braceStack);
}
if (braceCount > 0) {
throw new BlueprintError(
"Unclosed brace",
lastOpenBrace.line,
lastOpenBrace.column
);
} else {
throw new BlueprintError(
"Extra closing brace",
tokens[tokens.length - 1].line,
tokens[tokens.length - 1].column
);
}
}
if (this.options.debug) {
console.log("[TokenParser] Brace validation complete - all braces match");
}
}
}
module.exports = TokenParser;