blinkjs/blink

View on GitHub
lib/Rule.ts

Summary

Maintainability
A
2 hrs
Test Coverage
var extend = require('node.extend');

import a = require('./helpers/array');
import Configuration = require('./browser/Configuration');
import Formatter = require('./Formatter');
import HashTable = require('./interfaces/HashTable');
import RuleBody = require('./interfaces/RuleBody');
import s = require('./helpers/string');


class Rule {

    private config: Configuration;
    private decs: any[][];

    public get responders() {
        return this.body.respond;
    }

    private _selectors: string[] = [];

    public get selectors() {
        return this._selectors;
    }

    public set selectors(value: any) {
        this._selectors = value.map(selector => {
            return selector.trim();
        });
    }

    private splitSelectors(selectors: string) {
        return selectors.split(/ *, */);
    }

    constructor(selectors: string[], body?: RuleBody);
    constructor(selectors: string, body?: RuleBody);
    constructor(selectors: any, public body?: RuleBody) {
        if (Array.isArray(selectors)) {
            this.selectors = selectors;
        } else {
            this.selectors = this.splitSelectors(selectors);
        }
    }

    public resolve(config: Configuration) {
        this.config = config;
        var body = this.clone().body;
        var rules = this.resolveTree(this.selectors.join(), body);
        return Object.keys(rules).map(key => {
            return [this.splitSelectors(key), rules[key]];
        });
    }

    public clone() {
        return new Rule(extend([], this.selectors), extend({}, this.body));
    }

    private resolveTree(selectors: string, body: any, seed?: any, key?: string) {
        seed = seed || {};
        key = key || '';
        Object.keys(body).forEach(k2 => {
            if (k2 === 'respond') {
                this.resolveResponders(selectors, body.respond, seed);
                return;
            }
            if (k2[0] === ':') {
                this.resolveTree(this.joinSelectors(selectors, k2), body[k2], seed);
                return;
            }
            var joinedKey = this.combineKeys(key, k2);
            var overrideKey = s.camelize(joinedKey);
            if (this.config.overrides.hasOwnProperty(overrideKey)) {
                this.resolveOverride(overrideKey, selectors, seed, body[k2]);
                return;
            }
            var value = body[k2];
            if (this.isDeclarationValue(value)) {
                seed[selectors] = seed[selectors] || [];
                seed[selectors].push([
                    s.dasherize(joinedKey),
                    this.compileDeclarationValue(value)
                ]);
                return;
            }
            this.resolveTree(selectors, value, seed, joinedKey);
        });
        return seed;
    }

    private resolveResponders(selectors: string, body: any, seed: any) {
        Object.keys(body).forEach(condition => {
            var mediaQuery = '@media ' + condition;
            var resolved = this.resolveTree(selectors, body[condition]);
            var keys = Object.keys(resolved);
            if (keys.length === 0) {
                return;
            }
            seed[mediaQuery] = keys.map(key => {
                return [this.splitSelectors(key), resolved[key]];
            });
        });
    }

    private joinSelectors(left: string, right: string) {
        var result = [];
        this.splitSelectors(left).forEach(s1 => {
            this.splitSelectors(right).forEach(s2 => {
                result.push(s1 + s2);
            });
        });
        return result.join();
    }

    private combineKeys(k1: string, k2: string) {
        if (k1 !== '' && k2[0] !== ':') {
            return k1 + '-' + k2;
        }
        return k1 + k2;
    }

    private resolveOverride(overrideKey: string, selectors: string, seed: any, value: any) {
        var override = this.config.overrides[overrideKey];
        if (typeof override !== 'function') {
            throw new Error('Override "' + overrideKey + '" must be of type: Function');
        }
        var fn: Function;
        if (Array.isArray(value)) {
            fn = override(value[0], value[1]);
        } else {
            fn = override(value);
        }
        if (typeof fn !== 'function') {
            throw new Error('Override "' + overrideKey + '" must return a function');
        }
        var result = fn(this.config);
        if (Array.isArray(result)) {
            seed[selectors] = result;
            return;
        }
        Object.keys(result).forEach(key => {
            seed[this.joinSelectors(selectors, key)] = result[key];
        });
    }

    private isDeclarationValue(value: any) {
        if (Array.isArray(value)) {
            return true;
        }
        switch (typeof value) {
            case 'string':
            case 'number':
            case 'function':
                return true;
        }
        return false;
    }

    private compileDeclarationValue(value: any) {
        if (Array.isArray(value)) {
            return this.compileArray(value);
        }
        return this.compilePrimitive(value);
    }

    private compileArray(arr: any[]) {
        return arr.map(primitive => {
            return this.compilePrimitive(primitive);
        }).join(' ');
    }

    private compilePrimitive(value: any) {
        switch (typeof value) {
            case 'number':
                if (value === 0) {
                    return '0';
                }
                return value + 'px';
            case 'function':
                return this.compilePrimitive(value(this.config));
            case 'string':
                if (value === '') {
                    return s.repeat(this.config.quote, 2);
                }
                if (value.indexOf(' ') > -1) {
                    var quote = this.config.quote;
                    return quote + value.replace(new RegExp(quote, 'g'), '\\' + quote) + quote;
                }
        }
        return value;
    }

    public compile(config: Configuration) {
        return new Formatter().format(config, this.resolve(config));
    }

}

export = Rule;