lib/CSSTree.js
var rwhitespace = /(\s|\t|\r\n|\r|\n)/,
leftTrim = /^(\r\n|\r|\n|\t|\s)*/,
StringIterator = global.StringIterator;
function CSSTree( css ) {
var self = this, m;
// Force instance of CSSTree
if ( ! ( self instanceof CSSTree ) ) {
return new CSSTree( css );
}
// Internals
self.iter = new StringIterator( css );
self.css = css;
self.branches = [];
// Begin rendering
self.render();
}
// Methods
CSSTree.prototype = {
// Starts process of reading the stylesheet
render: function(){
var self = this,
iter = self.iter;
iter.each(function( c ) {
// Comment
if ( c == '/' && iter.next == '*' ) {
iter.reverse();
self.comment();
}
// Skip over whitespace, but assume anything else is a selector/atrule
else if ( ! rwhitespace.exec( c ) ) {
iter.reverse();
self.selector();
}
});
// Apply line/character information to all positions
self._positions( self.branches );
},
// Ignore comment blocks
comment: function( nested ) {
var self = this,
iter = self.iter,
position = new CSSTree.Position( iter.index + 1 ),
comment = iter.each(function( c, iter ) {
if ( iter.c == '/' && iter.prev == '*' ) {
return false;
}
});
self.branches.push(
new CSSTree.Comment( comment, nested || false, position.markEnd( iter.index ) )
);
},
// Selctor looks for opening rule set or closing semicolon for oneliners
selector: function(){
var self = this,
iter = self.iter,
position = new CSSTree.Position( iter.index + 1 ),
selector = '',
branch;
iter.each(function( c ) {
// Comment block
if ( c == '/' && iter.next == '*' ) {
iter.reverse();
position.markChunkEnd( iter.index );
self.comment();
position.markChunkStart( iter.index + 1 );
}
// Atrule
else if ( c == ';' ) {
return false;
}
// Media atrule
else if ( c == '{' && selector.trim()[ 0 ] == '@' ) {
branch = self.nested( selector, position );
branch.position = position.markEnd( iter.index );
self.branches.push( branch );
selector = null;
return false;
}
// Selector for ruleset
else if ( c == '{' ) {
branch = new CSSTree.Selector( selector, self.rules( position ), position );
position.markEnd( iter.index );
self.branches.push( branch );
selector = null;
return false;
}
// Escape string
else if ( c == "\\" ) {
iter.skip();
selector += c + iter.c;
}
// Seek
else if ( c == "'" || c == '"' ) {
selector += c + iter.seek( c );
}
// Seek
else if ( c == '(' ) {
selector += c + iter.seek( ')' );
}
// Add to selector string
else {
selector += c;
}
});
// Single line queries
if ( selector && selector.trim().length ) {
self.branches.push(
new CSSTree.AtRule( selector, position.markEnd( iter.index - 1 ) )
);
}
},
// Rule Sets
rules: function( parentPos ) {
var self = this,
iter = self.iter,
rules = [],
rule, position;
iter.each(function( c ) {
// Nested Comment block
if ( c == '/' && iter.next == '*' ) {
iter.reverse();
parentPos.markChunkEnd( iter.index );
self.comment( true );
parentPos.markChunkStart( iter.index + 1 );
}
// End of rules
else if ( c == '}' ) {
return false;
}
// New rule
else if ( ! rwhitespace.exec( c ) ) {
position = new CSSTree.Position( iter.index, parentPos );
iter.reverse();
rule = new CSSTree.Rule(
self.property( position ).trim(),
self.value( position ).trim(),
position
);
// Break down the parts of the value, and push it
position.markEnd( iter.index - ( iter.c == ';' ? 1 : 0 ), false );
// Only add if there is an actual property
if ( rule.property.length ) {
rules.push( rule );
}
}
});
return rules;
},
// Property Names
property: function( position ) {
var self = this,
iter = self.iter,
property = '';
iter.each(function( c ) {
// Nested Comment block
if ( c == '/' && iter.next == '*' ) {
iter.reverse();
position.markChunkEnd( iter.index );
self.comment( true );
position.markChunkStart( iter.index + 1 );
}
// End of property
else if ( c == ':' ) {
return false;
}
// Invalid CSS, but still end of property
else if ( c == ';' || c == '}' ) {
iter.reverse();
return false;
}
else {
property += c;
}
});
return property;
},
// Values
value: function( position ) {
var self = this,
iter = self.iter,
value = '';
iter.each(function( c ) {
// Nested Comment block
if ( c == '/' && iter.next == '*' ) {
iter.reverse();
position.markChunkEnd( iter.index );
self.comment( true );
position.markChunkStart( iter.index + 1 );
}
// End of value
else if ( c == ';' ) {
return false;
}
// Watch for no semi-colon at end of set
else if ( c == '}' ) {
iter.reverse();
return false;
}
// Seek strings
else if ( c == "'" || c == '"' ) {
value += c + iter.seek( c );
}
// Seek groupings
else if ( c == '(' ) {
value += c + iter.seek( ')' );
}
// Append
else {
value += c;
}
});
return value;
},
// Nested atrules
nested: function( atrule, position ) {
var self = this,
iter = self.iter,
startPos = null,
string = '',
peek, index, rule, subPosition, character,
block = atrule.trim()[ 0 ] == '@' ?
new CSSTree.AtRule( atrule, position ) :
new CSSTree.Selector( atrule, null, position );
iter.each(function( c ) {
// Nested Comment block
if ( c == '/' && iter.next == '*' ) {
iter.reverse();
position.markChunkEnd( iter.index );
self.comment( true );
position.markChunkStart( iter.index );
}
// End of property:value
else if ( c == ';' ) {
subPosition = new CSSTree.Position( startPos );
subPosition.markEnd( iter.index - ( iter.c == ';' ? 1 : 0 ) );
// String trimming & positioning
string = string.trim();
index = string.indexOf( ':' );
// Build up rule property
rule = new CSSTree.Rule(
string.substr( 0, index ).trim(),
string.substr( index + 1 ).trim(),
subPosition
);
// Atrules don't startoff with rules
if ( ! block.rules ) {
block.rules = [];
}
// Parse out parts and add to branches rules
block.rules.push( rule );
startPos = null;
string = '';
}
// Nested Block
else if ( c == '{' ) {
if ( ! block.branches ) {
block.branches = [];
}
// Nested branches should start tracking at the first character, not whitespace
string = string.replace( leftTrim, '' );
// Travel down the tree
subPosition = new CSSTree.Position( startPos, position );
block.branches.push( self.nested( string, subPosition ) );
startPos = null;
string = '';
}
// Seek
else if ( c == "'" || c == '"' ) {
string += c + iter.seek( c );
}
// Seek
else if ( c == '(' ) {
string += c + iter.seek( ')' );
}
// End of block
else if ( c == '}' ) {
subPosition = new CSSTree.Position( startPos );
// Assume any string left is a property:value definition
if ( ( string = string.trim() ).length ) {
index = string.indexOf( ':' );
rule = new CSSTree.Rule(
string.substr( 0, index ).trim(),
string.substr( index + 1 ).trim(),
subPosition.markEnd( iter.index - 1 )
);
// Atrules don't startoff with rules
if ( ! block.rules ) {
block.rules = [];
}
// Parse out value parts and add to rules
block.rules.push( rule );
}
block.position.markEnd( iter.index, false );
return false;
}
// Append
else {
if ( startPos === null && ! rwhitespace.exec( c ) ) {
startPos = iter.index;
}
string += c;
}
});
return block;
},
// Crawl the tree to finish position markup
_positions: function( branches ) {
var self = this;
// Cycle through each branch for markings
branches.forEach(function( branch ) {
self._markPosition( branch.position );
// Markup each rule positions
if ( branch.rules ) {
branch.rules.forEach(function( rule ) {
if ( rule.position ) {
self._markPosition( rule.position );
}
});
}
// Crawl nested branches
if ( branch.branches ) {
self._positions( branch.branches );
}
});
},
// Marks up position
_markPosition: function( position ) {
var self = this;
// Mark branch positions
position.start = self._charPosition( position.range.start );
position.end = self._charPosition( position.range.end );
},
// Marks the line & character for the character position passed
_charPosition: function( pos ) {
var self = this,
iter = self.iter.goto( pos );
return {
line: iter.line,
character: iter.character
};
}
};
// Keep Reference
CSSTree.StringIterator = StringIterator;
// Expose to NodeJS/Window
if ( typeof module == 'object' && typeof module.exports == 'object' ) {
module.exports = CSSTree;
}
else {
global.CSSTree = CSSTree;
}