UNPKG

@ordojs/core

Version:

Core compiler and runtime for OrdoJS framework

320 lines 9.71 kB
/** * @fileoverview CSS Parser for OrdoJS Framework * Parses CSS content into an AST structure for further processing */ import {} from '../types/index.js'; /** * Default CSS Parser options */ const DEFAULT_CSS_PARSER_OPTIONS = { generateSourceMaps: true, validateSyntax: true }; /** * CSS Parser for OrdoJS components */ export class OrdoJSCSSParser { options; source = ''; currentPosition = 0; currentLine = 1; currentColumn = 0; errors = []; constructor(options = {}) { this.options = { ...DEFAULT_CSS_PARSER_OPTIONS, ...options }; } /** * Parse CSS content into a StyleBlockNode */ parse(source, scoped = true) { this.source = source; this.currentPosition = 0; this.currentLine = 1; this.currentColumn = 0; this.errors = []; const rules = this.parseRules(); if (this.options.validateSyntax && this.errors.length > 0) { throw new Error(`CSS parsing errors: ${this.errors.join(', ')}`); } return { type: 'StyleBlock', rules, scoped, range: this.createSourceRange(0, this.currentPosition) }; } /** * Parse CSS rules */ parseRules() { const rules = []; this.skipWhitespace(); while (this.currentPosition < this.source.length) { const rule = this.parseRule(); if (rule) { rules.push(rule); } this.skipWhitespace(); } return rules; } /** * Parse a single CSS rule */ parseRule() { const startPos = this.currentPosition; // Parse selector const selector = this.parseSelector(); if (!selector) { return null; } this.skipWhitespace(); // Expect opening brace if (!this.consume('{')) { this.errors.push(`Expected '{' after selector '${selector}' at line ${this.currentLine}, column ${this.currentColumn}`); this.recoverToNextRule(); return null; } this.skipWhitespace(); // Parse declarations const declarations = this.parseDeclarations(); // Expect closing brace if (!this.consume('}')) { this.errors.push(`Expected '}' after declarations for selector '${selector}' at line ${this.currentLine}, column ${this.currentColumn}`); this.recoverToNextRule(); return null; } return { type: 'CSSRule', selector, declarations, range: this.createSourceRange(startPos, this.currentPosition) }; } /** * Parse a CSS selector */ parseSelector() { const startPos = this.currentPosition; // Skip any leading whitespace this.skipWhitespace(); // Read until we find an opening brace or end of input let selector = ''; while (this.currentPosition < this.source.length && this.source[this.currentPosition] !== '{' && this.source[this.currentPosition] !== '}') { selector += this.source[this.currentPosition]; this.advance(); } selector = selector.trim(); if (!selector) { this.errors.push(`Empty selector at line ${this.currentLine}, column ${this.currentColumn}`); return null; } return selector; } /** * Parse CSS declarations */ parseDeclarations() { const declarations = []; this.skipWhitespace(); while (this.currentPosition < this.source.length && this.source[this.currentPosition] !== '}') { const declaration = this.parseDeclaration(); if (declaration) { declarations.push(declaration); } // Skip semicolon and whitespace this.consume(';'); this.skipWhitespace(); } return declarations; } /** * Parse a single CSS declaration */ parseDeclaration() { const startPos = this.currentPosition; // Parse property const property = this.parseIdentifier(); if (!property) { this.recoverToNextDeclaration(); return null; } this.skipWhitespace(); // Expect colon if (!this.consume(':')) { this.errors.push(`Expected ':' after property '${property}' at line ${this.currentLine}, column ${this.currentColumn}`); this.recoverToNextDeclaration(); return null; } this.skipWhitespace(); // Parse value const value = this.parseValue(); if (!value) { this.recoverToNextDeclaration(); return null; } return { type: 'CSSDeclaration', property, value, important: false, range: this.createSourceRange(startPos, this.currentPosition) }; } /** * Parse a CSS property name */ parseIdentifier() { const startPos = this.currentPosition; // CSS identifiers can start with a letter, underscore, or hyphen if (this.currentPosition < this.source.length && !this.isIdentifierStart(this.source[this.currentPosition])) { this.errors.push(`Invalid identifier start at line ${this.currentLine}, column ${this.currentColumn}`); return null; } let identifier = ''; while (this.currentPosition < this.source.length && this.isIdentifierPart(this.source[this.currentPosition])) { identifier += this.source[this.currentPosition]; this.advance(); } if (!identifier) { this.errors.push(`Empty identifier at line ${this.currentLine}, column ${this.currentColumn}`); return null; } return identifier; } /** * Parse a CSS property value */ parseValue() { let value = ''; let parenDepth = 0; while (this.currentPosition < this.source.length) { const char = this.source[this.currentPosition]; // Handle nested parentheses in values like url(), calc(), etc. if (char === '(') { parenDepth++; } else if (char === ')') { parenDepth--; } // End of value when we hit a semicolon or closing brace (if not in parentheses) if ((char === ';' || char === '}') && parenDepth <= 0) { break; } value += char; this.advance(); } value = value.trim(); if (!value) { this.errors.push(`Empty value at line ${this.currentLine}, column ${this.currentColumn}`); return null; } return value; } /** * Check if a character is valid as the start of an identifier */ isIdentifierStart(char) { return /[a-zA-Z_\-]/.test(char); } /** * Check if a character is valid as part of an identifier */ isIdentifierPart(char) { return /[a-zA-Z0-9_\-]/.test(char); } /** * Advance the current position */ advance() { if (this.source[this.currentPosition] === '\n') { this.currentLine++; this.currentColumn = 0; } else { this.currentColumn++; } this.currentPosition++; } /** * Skip whitespace characters */ skipWhitespace() { while (this.currentPosition < this.source.length && /\s/.test(this.source[this.currentPosition])) { this.advance(); } } /** * Consume an expected character */ consume(expected) { if (this.currentPosition < this.source.length && this.source[this.currentPosition] === expected) { this.advance(); return true; } return false; } /** * Recover from an error by advancing to the next rule */ recoverToNextRule() { while (this.currentPosition < this.source.length && this.source[this.currentPosition] !== '}') { this.advance(); } // Skip the closing brace if found if (this.currentPosition < this.source.length) { this.advance(); } } /** * Recover from an error by advancing to the next declaration */ recoverToNextDeclaration() { while (this.currentPosition < this.source.length && this.source[this.currentPosition] !== ';' && this.source[this.currentPosition] !== '}') { this.advance(); } // Skip the semicolon if found if (this.currentPosition < this.source.length && this.source[this.currentPosition] === ';') { this.advance(); } } /** * Create a source location object */ createSourceLocation(start, end) { return { start, end, line: this.currentLine, column: this.currentColumn }; } /** * Create a source range object */ createSourceRange(start, end) { return { start: { line: this.currentLine, column: this.currentColumn, offset: start }, end: { line: this.currentLine, column: this.currentColumn, offset: end } }; } } //# sourceMappingURL=css-parser.js.map