@ordojs/core
Version:
Core compiler and runtime for OrdoJS framework
320 lines • 9.71 kB
JavaScript
/**
* @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