martinandert/babel-plugin-css-in-js

View on GitHub
src/transformStyleSheetObjectIntoSpecification.js

Summary

Maintainability
A
2 hrs
Test Coverage
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');
}