glam
Version:
inline css for your jsx
228 lines (204 loc) • 5.91 kB
JavaScript
// @flow
import type { RuleGroup, AST } from './types';
// import clean from './clean';
import flatten from './flatten';
import hashify from './hash';
/**** labels ****/
// toggle for debug labels.
// *shouldn't* have to mess with this manually
let hasLabels = process.env.NODE_ENV !== 'production';
export function cssLabels(bool: boolean) {
hasLabels = !!bool;
}
const prefixedPseudoSelectors = {
'::placeholder': [
'::-webkit-input-placeholder',
'::-moz-placeholder',
'::-ms-input-placeholder',
],
':fullscreen': [
':-webkit-full-screen',
':-moz-full-screen',
':-ms-fullscreen',
],
};
function isSelector(key) {
let possibles = [':', '.', '[', '>', ' '],
found = false,
ch = key.charAt(0);
for (let i = 0; i < possibles.length; i++) {
if (ch === possibles[i]) {
found = true;
break;
}
}
return found || key.indexOf('&') >= 0;
}
// from https://github.com/j2css/j2c/blob/5d381c2d721d04b54fabe6a165d587247c3087cb/src/helpers.js#L28-L61
// "Tokenizes" the selectors into parts relevant for the next function.
// Strings and comments are matched, but ignored afterwards.
// This is not a full tokenizers. It only recognizes comas, parentheses,
// strings and comments.
// regexp generated by scripts/regexps.js then trimmed by hand
var selectorTokenizer = /[(),]|"(?:\\.|[^"\n])*"|'(?:\\.|[^'\n])*'|\/\*[\s\S]*?\*\//g;
/**
* This will split a coma-separated selector list into individual selectors,
* ignoring comas in strings, comments and in :pseudo-selectors(parameter, lists).
*
* @param {string} selector
* @return {string[]}
*/
function splitSelector(selector) {
if (selector.indexOf(',') === -1) {
return [selector];
}
var indices = [],
res = [],
inParen = 0,
o;
/*eslint-disable no-cond-assign*/
while ((o = selectorTokenizer.exec(selector))) {
/*eslint-enable no-cond-assign*/
switch (o[0]) {
case '(':
inParen++;
break;
case ')':
inParen--;
break;
case ',':
if (inParen) break;
indices.push(o.index);
}
}
for (o = indices.length; o--; ) {
res.unshift(selector.slice(indices[o] + 1));
selector = selector.slice(0, indices[o]);
}
res.unshift(selector);
return res;
}
function joinSelectors(a, b) {
let as = splitSelector(a).map(a => (!(a.indexOf('&') >= 0) ? '&' + a : a));
let bs = splitSelector(b).map(b => (!(b.indexOf('&') >= 0) ? '&' + b : b));
return bs
.reduce((arr, b) => arr.concat(as.map(a => b.replace(/\&/g, a))), [])
.join(',');
}
function joinMediaQueries(a, b) {
return a ? `@media ${a.substring(6)} and ${b.substring(6)}` : b;
}
function isMediaQuery(key) {
return key.indexOf('@media') === 0;
}
function isSupports(key) {
return key.indexOf('@supports') === 0;
}
function joinSupports(a, b) {
return a ? `@supports ${a.substring(9)} and ${b.substring(9)}` : b;
}
// mutable! modifies dest.
function construct(
dest: AST,
{
selector = '',
mq = '',
supp = '',
inputs = {},
}: { selector?: string, mq?: string, supp?: string, inputs?: RuleGroup },
): Object {
const inputArray = !Array.isArray(inputs) ? [inputs] : flatten(inputs);
inputArray.filter(x => !!x).forEach(input => {
const src = input;
Object.keys(src || {}).forEach(key => {
if (isSelector(key)) {
// todo - regex test the string to look for prefixedpseudos
if (prefixedPseudoSelectors[key]) {
prefixedPseudoSelectors[key].forEach(p =>
construct(dest, {
selector: joinSelectors(selector, p),
mq,
supp,
inputs: src[key],
}),
);
}
construct(dest, {
selector: joinSelectors(selector, key),
mq,
supp,
inputs: src[key],
});
} else if (isMediaQuery(key)) {
construct(dest, {
selector,
mq: joinMediaQueries(mq, key),
supp,
inputs: src[key],
});
} else if (isSupports(key)) {
construct(dest, {
selector,
mq,
supp: joinSupports(supp, key),
inputs: src[key],
});
} else {
let _dest = dest;
if (supp) {
_dest[supp] = _dest[supp] || {};
_dest = _dest[supp];
}
if (mq) {
_dest[mq] = _dest[mq] || {};
_dest = _dest[mq];
}
if (selector) {
_dest[selector] = _dest[selector] || {};
_dest = _dest[selector];
}
if (key === 'label') {
if (hasLabels) {
// concat at root of object
dest.label = dest.label.concat(src.label);
}
} else {
_dest[key] = src[key];
}
}
});
});
return dest;
}
function groupByType(style: Object): AST {
// we can be sure it's not infinitely nested here
let plain, selects, medias, supports;
Object.keys(style).forEach(key => {
if (key.indexOf('&') >= 0) {
selects = selects || {};
selects[key] = style[key];
} else if (key.indexOf('@media') === 0) {
medias = medias || {};
medias[key] = groupByType(style[key]);
} else if (key.indexOf('@supports') === 0) {
supports = supports || {};
supports[key] = groupByType(style[key]);
} else if (key === 'label') {
if (style.label.length > 0) {
plain = plain || {};
plain.label = hasLabels ? style.label.join('.') : '';
}
} else {
plain = plain || {};
plain[key] = style[key];
}
});
return { plain, selects, medias, supports };
}
export default function parse(
css: RuleGroup,
): { className: string, parsed: AST } {
const parsed = groupByType(construct({ label: [] }, { inputs: css }));
const className = 'css-' + hashify(parsed);
return { className, parsed };
}