Skip to content

Instantly share code, notes, and snippets.

@espretto
Last active November 5, 2015 20:14
Show Gist options
  • Save espretto/b1ca36e0ef073f5403e5 to your computer and use it in GitHub Desktop.
Save espretto/b1ca36e0ef073f5403e5 to your computer and use it in GitHub Desktop.
JavaScript: css parser
;(function (window, jQuery){
/* ---------------------------------------------------------------------------
* utilities
*/
var document = window.document;
var location = window.location;
var home = [location.protocol, location.hostname].join('//');
// check if needs cross-domain-request
function isLocal (url) {
return url.indexOf(home) === 0;
}
// ES6 rest operator as higher order function
function rest (func) {
var arity = Math.max(0, func.length-1);
return function () {
for (var i = arguments.length, args = Array(i); i--;) args[i] = arguments[i];
if (arity) args.push(args.splice(arity, args.length-arity));
else args = [args];
return func.apply(this, args);
};
}
// tackle type system
function type (any) {
var typo = typeof any;
return (
typo !== 'object' ? typo :
any === null ? any+'' :
Object.prototype.toString.call(any).slice(8, -1).toLowerCase()
);
}
// simplify try-catch-blocks
var attempt = rest(function (func, args) {
var result;
try {
result = func.apply(this, args);
} catch (e) {
result = e;
}
return result;
});
// split while preserving remainder after n-th split operation
function split (string, delimiter, n) {
var result = string.split(delimiter);
n = (n === undefined ? -1 : n) >>> 0;
if (n < result.length) {
var remaining = result.splice(n, result.length-n);
result.push(remaining.join(delimiter));
}
return result;
}
// round with precision
function round (num, frac) {
return +num.toFixed(frac);
}
/* ---------------------------------------------------------------------------
* css selector specificity
*/
var reStripStrings = /("|')(?:\\\1|[^\1])*?\1/g;
var rePseudoElements = /first-(?:line|letter)|before|after/i;
var reMatchModifiers = /(\[[^\]]*?\]|[\.#:]?:?[^\*\+>~ \[\.#:]+)/g;
var reClassParams = /(:not|:matches)?\(([^\)]*)\)/gi;
function specificityFromSelector (selector) {
var specificity = 0;
/**
* increments specficity according to selector segment
*/
function cbMatchModifiers (match, modifier) {
var prefix = modifier.charAt(0);
var importance = (
prefix === '#' ? 2 : // id
prefix === '[' || // attr
prefix === '.' || // class
prefix === ':' && // pseudo-selector, not -element
modifier.charAt(1) !== ':' &&
!rePseudoElements.test(modifier.substring(1)) ? 1 : 0
);
specificity += 1 << importance*8;
return '';
}
/**
* removes parameters for pseudo-classes:
* - :lang
* - :nth-child
* - :nth-last-child
* - :nth-of-type
* - :nth-last-of-type
*
* extracts and recursively analyzes the parameter of :not and :matches.
*/
function cbClassParams (match, klass, selector) {
if (klass) selector.replace(reMatchModifiers, cbMatchModifiers);
return '';
}
selector
.replace(reStripStrings, '')
.replace(reClassParams, cbClassParams)
.replace(reMatchModifiers, cbMatchModifiers);
return specificity;
}
function specificityToArray (specificity) {
var i = 4, result = new Array(i);
while (i--) result[i] = specificity >> i*8 & 255;
return result.reverse();
}
function specificityFromArray (array) {
return array.reverse().reduce(function (specificity, value, i) {
return specificity + (value << i*8);
}, 0);
}
/* ---------------------------------------------------------------------------
* parse cssText
*/
// cleaners
var reBom = /^\u00EF\u00BB\u00BF/, cbBom = '';
var reHtmlComment = /<!--[\s\S]*?-->/g, cbHtmlComment = '';
var reCdata = /<!\[CDATA\[([\s\S]*?)\]\]>/g, cbCdata = '$1';
var reCssComments = /\/\*[\s\S]*?\*\//g, cbCssComments = '';
var reStripQuotes = /("|')((?:\\\1|[^\1])*?)\1/, cbStripQuotes = '$2';
// preserve strings
var rePreserveStrings = /("|')(?:\\\1|[^\1])*?\1/g;
var reRestoreStrings = /\[(\d+)\]/g;
// tokenize
var reSplitBraces = /({|})/g;
var reAtRules = /@(import|charset)([^;\r\n]*);/g;
// filters
var trim = String.prototype.trim;
var isTruthy = function (any) { return !!any; };
// main
function parseCssText (cssText) {
var result = {
rules: []
};
var keychain = [];
var strings = [];
function cbPreserveStrings (string) {
var index = strings.push(string)-1;
return '[' + index + ']';
}
function cbRestoreStrings (match, index) {
return strings[+index];
}
function cbAtRules (match, type, value) {
type += 's';
var atList = result[type] || (result[type] = []);
atList.push(value
.replace(reRestoreStrings, cbRestoreStrings).trim()
.replace(reStripQuotes, cbStripQuotes).trim()
);
return '';
}
var tokens = cssText
// strip byte order mark
.replace(reBom, cbBom).trim()
// clean inline stylesheets
.replace(reHtmlComment, cbHtmlComment).trim()
.replace(reCdata, cbCdata)
// strip comments
.replace(reCssComments, cbCssComments)
// cut out and preserve strings
.replace(rePreserveStrings, cbPreserveStrings)
// cut out import and charset rules
.replace(reAtRules, cbAtRules)
// split by scope seperators
.split(reSplitBraces)
// compact array
.map(trim.call, trim).filter(isTruthy)
// traverse
.forEach(function (item, i, array) {
var prev = array[i-1],
next = array[i+1],
last = i === array.length-1;
// entering next scope
if (next === '{') return keychain.push(item.trim());
// leaving current scope
if (next === '}' || last) var selstring = keychain.pop();
// process leaf node
if (prev !== '{') return;
var selectors = {};
var properties = {};
selstring
.split(',')
.map(trim.call, trim)
.forEach(function (sel) {
selectors[sel.replace(reRestoreStrings, cbRestoreStrings)] = specificityFromSelector(sel);
});
item
.split(';')
.filter(isTruthy)
.forEach(function (key_value) {
key_value = split(key_value, ':', 1).map(trim.call, trim);
properties[key_value[0]] = key_value[1].replace(reRestoreStrings, cbRestoreStrings);
});
result.rules.push({
scope: keychain.slice(),
selectors: selectors,
properties: properties
});
});
return result;
}
/* ---------------------------------------------------------------------------
* main
*/
function fetchCssText (url) {
var options = {
crossDomain: true,
dataType: 'json',
jsonp: 'yqlCallback',
url: 'http://query.yahooapis.com/v1/public/yql',
data: {
q: ['select * from html where url="', url, '"'].join(''),
format: 'json'
}
};
return jQuery.ajax(options).then(function (data) {
return data.query.results.body;
});
}
function parseStyleSheet (sheet) {
var href = attempt(function () {
// may fail due to cross-domain-policy
return sheet.href;
});
var fetched;
if (href) {
if (type(href) === 'error' || !isLocal(href)) {
fetched = fetchCssText(href);
} else {
fetched = jQuery.get(href);
}
} else {
fetched = jQuery.when(sheet.cssText || sheet.ownerNode.innerHTML);
}
return fetched.then(function (cssText) {
console.time(href || 'inlined sheet');
var content = parseCssText(cssText);
var time = console.timeEnd(href || 'inlined sheet');
return {
content: content,
href: href,
inline: !href,
size: round(cssText.length/1024, 2) + 'kb',
time: round(time) + 'ms'
};
});
}
/* ---------------------------------------------------------------------------
* export
*/
var Csson = window.Csson = {};
Csson.specificityToArray = specificityToArray;
Csson.specificityFromArray = specificityFromArray;
Csson.specificityFromSelector = specificityFromSelector;
Csson.fetchCssText = fetchCssText;
Csson.parseCssText = parseCssText;
Csson.parseStyleSheet = parseStyleSheet;
console.log([
'Welcome to Csson',
'----------------',
'include jQuery, then try me:',
'```js',
'var sheets = Array.apply(null, document.styleSheets);',
'var jobs = sheets.map(Csson.parseStyleSheet);',
'jQuery.when.apply(null, jobs).then(function () {',
' console.dir(arguments);',
'});',
'```'
].join('\n'));
}(this, jQuery));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment