Thought it wouldn’t hurt to post a follow-up to the initial ground work. The idea/exercise was to create a parser to handle nested CSS script much like SASS. A good exercise in getting to grips or more comfortable with recursion. In addition a chance to brush up on regular expressions.
The parser converts the input text into a node-tree object. A check for closed brackets being used as the base condition.
I’m sure improvements can be made to the overall design etc. Error handling would be a good start, but it seems to be working up to a point. I have allowed for dropped semi-colons.
A test example here http://pixel-shack.com/rpgdigit/nestingTest.html
Inprog code below
;!function (win) {
// Pseudo regex's required to differentiate between CSS properties and pseudo selectors. li:hover ~= display:block
var pseudos = [
'active|checked|default|dir\\\\(|disabled|empty|enabled|first|fullscreen|',
'focus|hover|indeterminate|in-range|invalid|lang\\\\(|last|left|link|not\\\\(|',
'nth|only|optional|out\\\\-|read|required|right|root|scope|target|valid|visited'
].join(''),
cssRX = new RegExp([
'(?: *?([.#a-z](?:[^:}{\\\\s]*|:(?:' + pseudos + ')| \\\\b)+))?', //[1] Selector
'(?:\\\\s*({))?', //[2] Open Bracket
'(?:[ \\\
]*(', //[3] Vars and Props
'(?:\\\\$?[-a-z]+ *?:(?!' + pseudos + ')(?:[ -;#\\\\w)(,%]*\\\\s*))*',//
'))?', //
'(?:\\\\s*(})[\\\
]*)?' //[4] Closing Bracket
].join(''), 'gmi'),
splitProps = /\\$?[\\-\\w]+ *?:[\\$\\-\\w #)(,%]+(?=[}\\s;]|$)/gi, // properties and $variables
cleanUpRX = /\\r|\\/\\*[^\\*]*\\*\\//gm, // Carriage returns and comments.
// ------------------------------------------------------------------------------------------------------------------
slice = function(obj){ return [].slice.call(obj); },
// Filters properties using given regex. Returns an array of properties. [{ name: prop, value: value }, { ... ]
filterMatches = function(matches, reg){
return slice(matches)
.filter( function (prop) {
return ((prop.match(reg)) && prop)
})
.map(function (prop) {
var props = prop.split(/\\s*:\\s*/);
return {name: props[0], value: props[1]}
});
},
// ---------------------------------- CSS Compiler ------------------------------------------------
// Returns an array like object tree of CSS text broken down into selectors, properties and variables.
compileCSS = function (cssInput) {
cssRX.lastIndex = 0;
cssInput = cssInput.replace(cleanUpRX,'');
var mch = null;
return (function walkCSS(child) {
var obj, props;
child = child || Object.create(null, {
selector: { enumerable: true, value: '' }
});
while ((mch = cssRX.exec(cssInput)) && mch[0]) {
props = (mch[3] && mch[3].match(splitProps));
if (mch[1] === undefined && mch[4]) break;
obj = Object.create(null, {
selector: { enumerable: true, value: (mch[1] !== undefined) ? mch[1] : ''},
parent: { enumerable: true, value: child },
props: { enumerable: true, value: filterMatches(props, /^[a-z]/i ) }, // Match a property: value
variables: { enumerable: true, value: filterMatches(props, /^\\$/) } // Match a $variable: value
});
[].push.call(child, (mch[4] === undefined) ? walkCSS(obj) : obj);
}
return child;
}());
},
// ----------------------------------------------------------------------------------------------
// Parent lookup. Takes an optional context argument to bind callback to.
parentLookUp = function (child, fn, obj) {
if (obj) {
while (child = child.parent) {
obj = fn.call(obj, child);
}
return obj;
} else {
while (child = child.parent) fn(child);
}
},
// Checks property values against $variables and replaces matches.
parseVars = function(child, prop){
parentLookUp(child, function(child){
if (child.variables) {
child.variables.forEach(
function(vars){
if (prop.value === vars.name) { prop.value = vars.value; }
}
)
}
});
return prop.name + ': ' + prop.value;
},
// ---------------------------------- CSS Compiler ------------------------------------------------
parseCSS = function (cssStrg) {
var cssRules = compileCSS(cssStrg),
output = '';
console.log(cssRules); // Just for reference. See console.
return (function parse_CSS(cssRule) {
var parent = cssRule.parent,
selector = cssRule.selector,
props = cssRule.props;
if (parent && props.length) {
// Empty string passed to lookUp will be our initial 'this' context. Note: an empty string literal"" passes as null.
output += [ parentLookUp(cssRule, function (child) {
return (child.selector) ? this.replace(/^/, child.selector + ' ') : this;
}, new String("")),// <-----
selector,
' {'
].join('');
props.forEach(function (prop) { output += '\\r\
' + parseVars(cssRule, prop) + ';'; });
output += '\\r\
}\\r\
\\r\
';
}
if (cssRule.length) {
[].slice.call(cssRule).forEach(function (rule) { parse_CSS(rule); });
}
return output.trim();
}(cssRules));
};
win.parseCSS = parseCSS;
}(window);