Last active
November 5, 2015 20:14
-
-
Save espretto/b1ca36e0ef073f5403e5 to your computer and use it in GitHub Desktop.
JavaScript: css parser
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;(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