UNPKG

@ordojs/core

Version:

Core compiler and runtime for OrdoJS framework

748 lines 26.8 kB
/** * @fileoverview OrdoJS Parser - Full implementation for syntax analysis */ import { ExpressionType, LifecycleType, StatementType, SyntaxError, TokenType } from '../types/index.js'; export class OrdoJSParser { tokenStream; current = 0; errors = []; options; filename; constructor(tokenStream, options = {}, filename = 'unknown') { this.tokenStream = tokenStream; this.options = { allowRecovery: false, maxErrors: 1, ...options }; this.filename = filename; } parse() { try { const component = this.parseComponent(); if (this.errors.length > 0) { throw this.errors[0]; } return { component, dependencies: [], exports: [], sourceMap: { version: 3, sources: [this.filename], names: [], mappings: '', sourcesContent: [] } }; } catch (error) { if (error instanceof SyntaxError) { throw error; } throw new SyntaxError(`Parser error: ${error instanceof Error ? error.message : 'Unknown error'}`, this.getCurrentPosition(), ['valid component syntax'], this.peek().value, this.filename); } } getErrors() { return this.errors; } parseComponent() { const start = this.getCurrentPosition(); // Expect 'component' keyword if (!this.match(TokenType.COMPONENT)) { this.throwError("Expected 'component' keyword", ['component']); } // Parse component name const nameToken = this.consume(TokenType.IDENTIFIER, 'Expected component name'); const name = nameToken.value; // Validate component name (must start with uppercase) if (!/^[A-Z][a-zA-Z0-9]*$/.test(name)) { this.throwError('Component name must start with uppercase letter and contain only alphanumeric characters', []); } // Parse optional props const props = this.parseProps(); // Expect component body this.consume(TokenType.LEFT_BRACE, "Expected '{' after component declaration"); // Parse component blocks let clientBlock; let serverBlock; let markupBlock; while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) { if (this.match(TokenType.CLIENT)) { if (clientBlock) { this.throwError('Duplicate client block', []); } clientBlock = this.parseClientBlock(); } else if (this.match(TokenType.SERVER)) { if (serverBlock) { this.throwError('Duplicate server block', []); } serverBlock = this.parseServerBlock(); } else if (this.match(TokenType.MARKUP)) { if (markupBlock) { this.throwError('Duplicate markup block', []); } markupBlock = this.parseMarkupBlock(); } else { this.throwError('Expected client, server, or markup block', ['client', 'server', 'markup']); } } this.consume(TokenType.RIGHT_BRACE, "Expected '}' after component body"); // Markup block is required if (!markupBlock) { this.throwError('Component must have a markup block', ['markup']); } const end = this.getCurrentPosition(); return { type: 'Component', name, props, clientBlock, serverBlock, markupBlock: markupBlock, range: { start, end } }; } parseProps() { const props = []; if (!this.check(TokenType.LEFT_PAREN)) { return props; } this.advance(); // consume '(' while (!this.check(TokenType.RIGHT_PAREN) && !this.isAtEnd()) { props.push(this.parseProp()); if (!this.match(TokenType.COMMA)) { break; } } this.consume(TokenType.RIGHT_PAREN, "Expected ')' after props"); return props; } parseProp() { const start = this.getCurrentPosition(); const nameToken = this.consume(TokenType.IDENTIFIER, 'Expected prop name'); this.consume(TokenType.COLON, "Expected ':' after prop name"); const dataType = this.parseTypeAnnotation(); let defaultValue; let isRequired = true; if (this.match(TokenType.ASSIGN)) { isRequired = false; defaultValue = this.parseExpression(); } const end = this.getCurrentPosition(); return { type: 'PropDefinition', name: nameToken.value, dataType, defaultValue, isRequired, range: { start, end } }; } parseTypeAnnotation() { const nameToken = this.consume(TokenType.IDENTIFIER, 'Expected type name'); let isArray = false; if (this.match(TokenType.LEFT_BRACKET)) { this.consume(TokenType.RIGHT_BRACKET, "Expected ']' after '['"); isArray = true; } return { name: nameToken.value, isArray, isOptional: false, genericTypes: [] }; } parseClientBlock() { const start = this.getCurrentPosition(); this.consume(TokenType.LEFT_BRACE, "Expected '{' after 'client'"); const reactiveVariables = []; const computedValues = []; const eventHandlers = []; const functions = []; const lifecycle = []; while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) { if (this.check(TokenType.LET) || this.check(TokenType.CONST)) { reactiveVariables.push(this.parseReactiveVariable()); } else if (this.checkLifecycleHook()) { lifecycle.push(this.parseLifecycleHook()); } else if (this.check(TokenType.IDENTIFIER)) { functions.push(this.parseFunction()); } else { this.throwError('Expected variable declaration, function, or lifecycle hook', ['let', 'const', 'function']); } } this.consume(TokenType.RIGHT_BRACE, "Expected '}' after client block"); const end = this.getCurrentPosition(); return { type: 'ClientBlock', reactiveVariables, computedValues, eventHandlers, functions, lifecycle, range: { start, end } }; } parseServerBlock() { const start = this.getCurrentPosition(); this.consume(TokenType.LEFT_BRACE, "Expected '{' after 'server'"); const functions = []; const middleware = []; const dataFetchers = []; const imports = []; while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) { if (this.check(TokenType.PUBLIC) || this.check(TokenType.IDENTIFIER)) { functions.push(this.parseServerFunction()); } else { this.throwError('Expected function declaration', ['public', 'function']); } } this.consume(TokenType.RIGHT_BRACE, "Expected '}' after server block"); const end = this.getCurrentPosition(); return { type: 'ServerBlock', functions, middleware, dataFetchers, imports, range: { start, end } }; } parseMarkupBlock() { const start = this.getCurrentPosition(); this.consume(TokenType.LEFT_BRACE, "Expected '{' after 'markup'"); const elements = []; const textNodes = []; const interpolations = []; while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) { if (this.check(TokenType.HTML_TAG_OPEN)) { elements.push(this.parseHTMLElement()); } else if (this.check(TokenType.INTERPOLATION_START)) { interpolations.push(this.parseInterpolation()); } else if (this.check(TokenType.HTML_TEXT)) { textNodes.push(this.parseTextNode()); } else { // Skip whitespace and try to parse HTML if (this.peek().value.trim()) { this.advance(); } else { this.advance(); } } } this.consume(TokenType.RIGHT_BRACE, "Expected '}' after markup block"); const end = this.getCurrentPosition(); return { type: 'MarkupBlock', elements, textNodes, interpolations, range: { start, end } }; } parseReactiveVariable() { const start = this.getCurrentPosition(); const isConst = this.match(TokenType.CONST); if (!isConst) { this.consume(TokenType.LET, "Expected 'let' or 'const'"); } const nameToken = this.consume(TokenType.IDENTIFIER, 'Expected variable name'); let dataType = { name: 'any', isArray: false, isOptional: false, genericTypes: [] }; if (this.match(TokenType.COLON)) { dataType = this.parseTypeAnnotation(); } this.consume(TokenType.ASSIGN, "Expected '=' after variable declaration"); const initialValue = this.parseExpression(); this.match(TokenType.SEMICOLON); // Optional semicolon const end = this.getCurrentPosition(); return { type: 'ReactiveVariable', name: nameToken.value, initialValue, dataType, isConst, range: { start, end } }; } parseFunction() { const start = this.getCurrentPosition(); const nameToken = this.consume(TokenType.IDENTIFIER, 'Expected function name'); this.consume(TokenType.LEFT_PAREN, "Expected '(' after function name"); const parameters = this.parseParameters(); this.consume(TokenType.RIGHT_PAREN, "Expected ')' after parameters"); this.consume(TokenType.COLON, "Expected ':' after parameters"); const returnType = this.parseTypeAnnotation(); this.consume(TokenType.LEFT_BRACE, "Expected '{' after function signature"); const body = this.parseStatements(); this.consume(TokenType.RIGHT_BRACE, "Expected '}' after function body"); const end = this.getCurrentPosition(); return { type: 'Function', name: nameToken.value, parameters, body, returnType, isAsync: false, range: { start, end } }; } parseServerFunction() { const start = this.getCurrentPosition(); const isPublic = this.match(TokenType.PUBLIC); const nameToken = this.consume(TokenType.IDENTIFIER, 'Expected function name'); this.consume(TokenType.LEFT_PAREN, "Expected '(' after function name"); const parameters = this.parseParameters(); this.consume(TokenType.RIGHT_PAREN, "Expected ')' after parameters"); this.consume(TokenType.COLON, "Expected ':' after parameters"); const returnType = this.parseTypeAnnotation(); this.consume(TokenType.LEFT_BRACE, "Expected '{' after function signature"); const body = this.parseStatements(); this.consume(TokenType.RIGHT_BRACE, "Expected '}' after function body"); const end = this.getCurrentPosition(); return { type: 'ServerFunction', name: nameToken.value, parameters, body, returnType, isAsync: false, isPublic, middleware: [], permissions: [], range: { start, end } }; } parseLifecycleHook() { const start = this.getCurrentPosition(); const hookName = this.advance().value; let hookType; switch (hookName) { case 'onMount': hookType = LifecycleType.ON_MOUNT; break; case 'onUnmount': hookType = LifecycleType.ON_UNMOUNT; break; case 'onUpdate': hookType = LifecycleType.ON_UPDATE; break; default: this.throwError(`Unknown lifecycle hook: ${hookName}`, ['onMount', 'onUnmount', 'onUpdate']); hookType = LifecycleType.ON_MOUNT; } this.consume(TokenType.LEFT_PAREN, "Expected '(' after lifecycle hook"); this.consume(TokenType.RIGHT_PAREN, "Expected ')' after lifecycle hook"); this.consume(TokenType.LEFT_BRACE, "Expected '{' after lifecycle hook"); const handler = this.parseStatements(); this.consume(TokenType.RIGHT_BRACE, "Expected '}' after lifecycle hook body"); const end = this.getCurrentPosition(); return { type: 'LifecycleHook', hookType, handler, range: { start, end } }; } parseParameters() { const parameters = []; while (!this.check(TokenType.RIGHT_PAREN) && !this.isAtEnd()) { parameters.push(this.parseParameter()); if (!this.match(TokenType.COMMA)) { break; } } return parameters; } parseParameter() { const start = this.getCurrentPosition(); const nameToken = this.consume(TokenType.IDENTIFIER, 'Expected parameter name'); this.consume(TokenType.COLON, "Expected ':' after parameter name"); const dataType = this.parseTypeAnnotation(); let defaultValue; let isOptional = false; if (this.match(TokenType.ASSIGN)) { isOptional = true; defaultValue = this.parseExpression(); } const end = this.getCurrentPosition(); return { type: 'Parameter', name: nameToken.value, dataType, defaultValue, isOptional, range: { start, end } }; } parseStatements() { const statements = []; while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) { statements.push(this.parseStatement()); } return statements; } parseStatement() { const start = this.getCurrentPosition(); // For now, treat everything as expression statements const expression = this.parseExpression(); this.match(TokenType.SEMICOLON); // Optional semicolon const end = this.getCurrentPosition(); return { type: 'Statement', statementType: StatementType.EXPRESSION, expression, range: { start, end } }; } parseExpression() { return this.parseAssignment(); } parseAssignment() { const expr = this.parseLogicalOr(); if (this.match(TokenType.ASSIGN)) { const operator = this.previous().value; const right = this.parseAssignment(); return { type: 'Expression', expressionType: ExpressionType.ASSIGNMENT, operator, left: expr, right, range: { start: expr.range.start, end: right.range.end } }; } return expr; } parseLogicalOr() { let expr = this.parseLogicalAnd(); while (this.match(TokenType.LOGICAL_OR)) { const operator = this.previous().value; const right = this.parseLogicalAnd(); expr = { type: 'Expression', expressionType: ExpressionType.BINARY, operator, left: expr, right, range: { start: expr.range.start, end: right.range.end } }; } return expr; } parseLogicalAnd() { let expr = this.parseEquality(); while (this.match(TokenType.LOGICAL_AND)) { const operator = this.previous().value; const right = this.parseEquality(); expr = { type: 'Expression', expressionType: ExpressionType.BINARY, operator, left: expr, right, range: { start: expr.range.start, end: right.range.end } }; } return expr; } parseEquality() { let expr = this.parseComparison(); while (this.match(TokenType.EQUALS, TokenType.NOT_EQUALS)) { const operator = this.previous().value; const right = this.parseComparison(); expr = { type: 'Expression', expressionType: ExpressionType.BINARY, operator, left: expr, right, range: { start: expr.range.start, end: right.range.end } }; } return expr; } parseComparison() { let expr = this.parseTerm(); while (this.match(TokenType.GREATER_THAN, TokenType.GREATER_EQUAL, TokenType.LESS_THAN, TokenType.LESS_EQUAL)) { const operator = this.previous().value; const right = this.parseTerm(); expr = { type: 'Expression', expressionType: ExpressionType.BINARY, operator, left: expr, right, range: { start: expr.range.start, end: right.range.end } }; } return expr; } parseTerm() { let expr = this.parseFactor(); while (this.match(TokenType.MINUS, TokenType.PLUS)) { const operator = this.previous().value; const right = this.parseFactor(); expr = { type: 'Expression', expressionType: ExpressionType.BINARY, operator, left: expr, right, range: { start: expr.range.start, end: right.range.end } }; } return expr; } parseFactor() { let expr = this.parseUnary(); while (this.match(TokenType.DIVIDE, TokenType.MULTIPLY, TokenType.MODULO)) { const operator = this.previous().value; const right = this.parseUnary(); expr = { type: 'Expression', expressionType: ExpressionType.BINARY, operator, left: expr, right, range: { start: expr.range.start, end: right.range.end } }; } return expr; } parseUnary() { if (this.match(TokenType.LOGICAL_NOT, TokenType.MINUS)) { const operator = this.previous().value; const right = this.parseUnary(); return { type: 'Expression', expressionType: ExpressionType.UNARY, operator, right, range: { start: this.previous().range.start, end: right.range.end } }; } return this.parseCall(); } parseCall() { let expr = this.parsePrimary(); while (true) { if (this.match(TokenType.LEFT_PAREN)) { expr = this.finishCall(expr); } else if (this.match(TokenType.DOT)) { const name = this.consume(TokenType.IDENTIFIER, "Expected property name after '.'"); expr = { type: 'Expression', expressionType: ExpressionType.MEMBER, object: expr, property: { type: 'Expression', expressionType: ExpressionType.IDENTIFIER, identifier: name.value, range: name.range }, range: { start: expr.range.start, end: name.range.end } }; } else { break; } } return expr; } finishCall(callee) { const args = []; if (!this.check(TokenType.RIGHT_PAREN)) { do { args.push(this.parseExpression()); } while (this.match(TokenType.COMMA)); } const paren = this.consume(TokenType.RIGHT_PAREN, "Expected ')' after arguments"); return { type: 'Expression', expressionType: ExpressionType.CALL, callee, arguments: args, range: { start: callee.range.start, end: paren.range.end } }; } parsePrimary() { const start = this.getCurrentPosition(); if (this.match(TokenType.BOOLEAN)) { const value = this.previous().value === 'true'; return { type: 'Expression', expressionType: ExpressionType.LITERAL, value, range: { start, end: this.getCurrentPosition() } }; } if (this.match(TokenType.NUMBER)) { const value = parseFloat(this.previous().value); return { type: 'Expression', expressionType: ExpressionType.LITERAL, value, range: { start, end: this.getCurrentPosition() } }; } if (this.match(TokenType.STRING)) { const value = this.previous().value; return { type: 'Expression', expressionType: ExpressionType.LITERAL, value, range: { start, end: this.getCurrentPosition() } }; } if (this.match(TokenType.IDENTIFIER)) { const identifier = this.previous().value; return { type: 'Expression', expressionType: ExpressionType.IDENTIFIER, identifier, range: { start, end: this.getCurrentPosition() } }; } if (this.match(TokenType.LEFT_PAREN)) { const expr = this.parseExpression(); this.consume(TokenType.RIGHT_PAREN, "Expected ')' after expression"); return expr; } this.throwError('Expected expression', ['identifier', 'number', 'string', 'boolean', '(']); // Return dummy expression for error recovery return { type: 'Expression', expressionType: ExpressionType.LITERAL, value: null, range: { start, end: this.getCurrentPosition() } }; } parseHTMLElement() { const start = this.getCurrentPosition(); // This is a simplified HTML parser - in a real implementation, // you'd need much more sophisticated HTML parsing const tagName = 'div'; // Placeholder const attributes = []; const children = []; const end = this.getCurrentPosition(); return { type: 'HTMLElement', tagName, attributes, children, isSelfClosing: false, isVoidElement: false, range: { start, end } }; } parseInterpolation() { const start = this.getCurrentPosition(); this.consume(TokenType.INTERPOLATION_START, "Expected '{' for interpolation"); const expression = this.parseExpression(); this.consume(TokenType.INTERPOLATION_END, "Expected '}' after interpolation"); const end = this.getCurrentPosition(); return { type: 'Interpolation', expression, range: { start, end } }; } parseTextNode() { const start = this.getCurrentPosition(); const content = this.consume(TokenType.HTML_TEXT, 'Expected text content').value; const end = this.getCurrentPosition(); return { type: 'Text', content, range: { start, end } }; } checkLifecycleHook() { const value = this.peek().value; return value === 'onMount' || value === 'onUnmount' || value === 'onUpdate'; } // Utility methods match(...types) { for (const type of types) { if (this.check(type)) { this.advance(); return true; } } return false; } check(type) { if (this.isAtEnd()) return false; return this.peek().type === type; } advance() { if (!this.isAtEnd()) this.current++; return this.previous(); } isAtEnd() { return this.peek().type === TokenType.EOF; } peek() { return this.tokenStream.tokens[this.current]; } previous() { return this.tokenStream.tokens[this.current - 1]; } consume(type, message) { if (this.check(type)) return this.advance(); this.throwError(message, [type.toString()]); // Return dummy token for error recovery return { type: TokenType.EOF, value: '', position: this.getCurrentPosition(), range: { start: this.getCurrentPosition(), end: this.getCurrentPosition() } }; } throwError(message, expected) { const error = new SyntaxError(message, this.getCurrentPosition(), expected, this.peek().value, this.filename); if (this.options.allowRecovery && this.errors.length < (this.options.maxErrors || 1)) { this.errors.push(error); this.synchronize(); throw error; // Still throw after synchronize for proper error handling } throw error; } synchronize() { this.advance(); while (!this.isAtEnd()) { if (this.previous().type === TokenType.SEMICOLON) return; switch (this.peek().type) { case TokenType.COMPONENT: case TokenType.CLIENT: case TokenType.SERVER: case TokenType.MARKUP: case TokenType.LET: case TokenType.CONST: return; } this.advance(); } } getCurrentPosition() { return this.peek().position; } } //# sourceMappingURL=parser.js.map