src/transformStyleSheetObjectIntoSpecification.js
import assert from 'assert';
import foreach from 'foreach';
import splitSelector from './utils/splitSelector';
const isMediaQueryDeclaration = /^@/;
const hasAttachedSelector = /[^:\[]+[:\[]/;
const isStandaloneSelector = /^[:\[]/;
const isValidStyleName = /^([_a-zA-Z]+[ _a-zA-Z0-9-]*[_a-zA-Z0-9-]*)|(\$[_a-zA-Z0-9-\s,\.]+)$/;
export default function transformStyleSheetObjectIntoSpecification(content, styles = {}, parent) {
assertPlainObject(content);
foreach(content, (value, key) => {
if (isMediaQueryDeclaration.test(key)) {
processMediaQuery(styles, key.substring(1), value, parent);
} else if (isStandaloneSelector.test(key)) {
assert(false, 'stand-alone selectors are not allowed at the top-level');
} else if (hasAttachedSelector.test(key)) {
const [styleName, selectorName] = splitSelector(key);
processStyleAndSelector(styles, styleName, selectorName, value, parent);
} else if (key.charAt(0) === '$') {
const firstSubKey = Object.keys(value)[0];
if (!isPlainObject(value[firstSubKey])) {
// needs to be deprecated
processStyle(styles, key, value);
} else {
transformStyleSheetObjectIntoSpecification(value, styles, key.substring(1));
}
} else {
processStyle(styles, key, value, parent);
}
});
return styles;
}
function processMediaQuery(styles, mediaQueryName, content, parent) {
assertPlainObject(content);
foreach(content, (value, key) => {
if (isMediaQueryDeclaration.test(key)) {
assert(false, 'media queries cannot be nested into each other');
} else if (isStandaloneSelector.test(key)) {
assert(false, 'stand-alone selectors are not allowed in top-level media queries');
} else if (hasAttachedSelector.test(key)) {
const [styleName, selectorName] = splitSelector(key);
processStyleAndMediaQueryAndSelector(styles, styleName, mediaQueryName, selectorName, value, parent);
} else {
processStyleAndMediaQuery(styles, key, mediaQueryName, value, parent);
}
});
}
function processStyle(styles, styleName, content, parent) {
assertPlainObject(content);
const style = initStyleSpec(styles, styleName, parent);
foreach(content, (value, key) => {
if (isMediaQueryDeclaration.test(key)) {
processStyleAndMediaQuery(styles, styleName, key.substring(1), value, parent);
} else if (isStandaloneSelector.test(key)) {
processStyleAndSelector(styles, styleName, key, value, parent);
} else if (hasAttachedSelector.test(key)) {
assert(false, 'styles cannot be nested into each other');
} else {
processRule(style.rules, key, value);
}
});
}
function processStyleAndMediaQuery(styles, styleName, mediaQueryName, content, parent) {
assertPlainObject(content);
const style = initStyleSpec(styles, styleName, parent);
const mediaQuery = initMediaQuerySpec(style.mediaQueries, mediaQueryName);
foreach(content, (value, key) => {
if (isMediaQueryDeclaration.test(key)) {
assert(false, 'media queries cannot be nested into each other');
} else if (isStandaloneSelector.test(key)) {
processStyleAndMediaQueryAndSelector(styles, styleName, mediaQueryName, key, value, parent);
} else if (hasAttachedSelector.test(key)) {
assert(false, 'styles cannot be nested into each other');
} else {
processRule(mediaQuery.rules, key, value);
}
});
}
function processStyleAndSelector(styles, styleName, selectorName, content, parent) {
assertPlainObject(content);
const style = initStyleSpec(styles, styleName, parent);
const selector = initSelectorSpec(style.selectors, selectorName);
foreach(content, (value, key) => {
if (isMediaQueryDeclaration.test(key)) {
assert(false, 'media queries cannot be nested into selectors');
} else if (isStandaloneSelector.test(key)) {
processStyleAndSelector(styles, styleName, selectorName + key, value);
} else if (hasAttachedSelector.test(key)) {
assert(false, 'styles cannot be nested into each other');
} else {
processRule(selector.rules, key, value);
}
});
}
function processStyleAndMediaQueryAndSelector(styles, styleName, mediaQueryName, selectorName, content, parent) {
assert(isPlainObject(content), 'style value must be a plain object');
const style = initStyleSpec(styles, styleName, parent);
const mediaQuery = initMediaQuerySpec(style.mediaQueries, mediaQueryName);
const selector = initSelectorSpec(mediaQuery.selectors, selectorName);
foreach(content, (value, key) => {
if (isMediaQueryDeclaration.test(key)) {
assert(false, 'media queries cannot be nested into each other');
} else if (isStandaloneSelector.test(key)) {
processStyleAndMediaQueryAndSelector(styles, styleName, mediaQueryName, selectorName + key, value);
} else if (hasAttachedSelector.test(key)) {
assert(false, 'styles cannot be nested into each other');
} else {
processRule(selector.rules, key, value);
}
});
}
function processRule(rules, key, value) {
assert(typeof value === 'string' || typeof value === 'number', 'value must be a number or a string');
rules[key] = value;
}
function initStyleSpec(styles, name, parent, checkValid = true) {
if (checkValid) {
assert(isValidStyleName.test(name), `style name is invalid: ${name}`);
}
styles[name] = styles[name] || { rules: {}, selectors: {}, mediaQueries: {} };
if (parent) {
styles[name].parents = styles[name].parents || {};
return initStyleSpec(styles[name].parents, parent, null, false);
}
return styles[name];
}
function initMediaQuerySpec(mediaQueries, name) {
mediaQueries[name] = mediaQueries[name] || { rules: {}, selectors: {} };
return mediaQueries[name];
}
function initSelectorSpec(selectors, name) {
selectors[name] = selectors[name] || { rules: {} };
return selectors[name];
}
function isPlainObject(obj) {
return Object.prototype.toString.call(obj) === '[object Object]';
}
function assertPlainObject(content) {
assert(isPlainObject(content), 'value must be a plain object');
}