Source: Template.js

definer('Template', /** @exports Template */ function( /* jshint maxparams: false */
    Match, classify, Node, Selector, Helpers, object, string, is
) {

    /**
     * Модуль шаблонизации BEMJSON-узла.
     *
     * @constructor
     * @param {...string} pattern Шаблоны для матчинга
     * @param {object} modes Моды для преобразования узла
     */
    function Template(pattern, modes) { /* jshint unused: false */

        /**
         * Шаблоны для матчинга.
         *
         * @private
         * @type {string[]}
         */
        this._patterns = [].slice.call(arguments, 0, -1);

        /**
         * Моды для преобразования узла.
         *
         * @type {object}
         */
        this.modes = [].slice.call(arguments, -1)[0];

        /**
         * Имена мод шаблона.
         *
         * @private
         * @type {string[]}
         */
        this._modesNames = Object.keys(this.modes);

        /**
         * Вес шаблона.
         *
         * Рассчитать вес шаблона возможно при наличии только одного селектора.
         * При наличии в шаблоне нескольких селекторов его вес устанавливается как `null`.
         *
         * @type {?number}
         */
        this.weight = this._patterns.length === 1
            ? new Selector(this._patterns[0]).weight()
            : null;

        /**
         * Функции-помощники.
         *
         * @private
         * @type {Helpers}
         */
        this._helpers = new Helpers();

        /**
         * Экземпляры матчера.
         *
         * @private
         * @type {Match[]}
         */
        this._matches = Object.keys(this._patterns).map(function(key) {
            return new Match(this._patterns[key]);
        }, this);

        /**
         * Класс по модам.
         *
         * @private
         * @type {Function}
         */
        this.Modes = this._classifyModes();
    }

    /**
     * Получить БЭМ-узел на основе BEMJSON по базовому шаблону.
     *
     * @param {object} bemjson Входящий BEMJSON
     * @param {object} [data] Данные по сущности в дереве
     * @returns {Node}
     */
    Template.base = function(bemjson, data) {
        return Template.baseTemplate.transform(bemjson, data);
    };

    Template.prototype = {

        /**
         * Применить BEMJSON к шаблону.
         *
         * @param {object} bemjson Входящий BEMJSON
         * @param {object} [data] Данные по сущности в дереве
         * @param {object} [baseBemjson] Базовый BEMJSON из входящих данных
         * @param {string[]} [modesFromAnotherTemplates] Список полей, которые были установлены из других шаблонов
         * @param {number} [index] Порядковый номер шаблона в общем списке
         * @returns {Node|null} Экземпляр БЭМ-узла или null при несоответствии BEMJSON шаблону
         */
        match: function(bemjson, data, baseBemjson, modesFromAnotherTemplates, index) {
            for(var i = 0; i < this._matches.length; i++) {
                if(this._matches[i].is(bemjson)) {
                    return this.transform(object.clone(bemjson), data, baseBemjson, modesFromAnotherTemplates, index);
                }
            }
            return null;
        },

        /**
         * Получить БЭМ-узел на основе BEMJSON.
         *
         * @param {object} bemjson Входящий BEMJSON
         * @param {object} [data] Данные по сущности в дереве
         * @param {object} [baseBemjson=bemjson] Базовый BEMJSON из входящих данных
         * @param {string[]} [modesFromAnotherTemplates={}] Список полей, которые были установлены из других шаблонов
         * @param {number} [index=0] Порядковый номер шаблона в общем списке
         * @returns {Node}
         */
        transform: function(bemjson, data, baseBemjson, modesFromAnotherTemplates, index) {
            var modes = new this.Modes(bemjson, data);

            for(var i = 0, len = Template._defaultModesNames.length; i < len; i++) {
                var mode = Template._defaultModesNames[i];
                bemjson[mode] = this._getMode(
                    modes,
                    bemjson,
                    mode,
                    baseBemjson || bemjson,
                    modesFromAnotherTemplates || {},
                    index || 0
                );
            }

            return new Node(bemjson);
        },

        /**
         * Наследовать шаблон.
         *
         * @param {Template} template Базовый шаблон
         * @returns {Template}
         */
        extend: function(template) {
            template.Modes = classify(this.Modes, template.modes);
            return template;
        },

        /**
         * Разбить шаблон на шаблоны с единичными селекторами.
         *
         * @returns {Template[]}
         */
        split: function() {
            return Object.keys(this._patterns).map(function(key) {
                return new Template(this._patterns[key], this.modes).helper(this._helpers.get());
            }, this);
        },

        /**
         * Проверить шаблон на соответствие.
         *
         * Вернёт `true`, если хотя бы один селектор
         * текущего шаблона и проверяемого пройдёт неточную проверку.
         *
         * @param {Template} template Шаблон
         * @returns {boolean}
         */
        is: function(template) {
            return this._matches.some(function(match) {
                return Object.keys(template._patterns).some(function(key) {
                    return match.is(template._patterns[key]);
                });
            });
        },

        /**
         * Добавить одну или несколько
         * пользовательских функций-помощников.
         *
         * @param {string|object} nameOrList Имя функции или карта помощников
         * @param {function} [callback] Тело функции
         * @returns {Template}
         */
        helper: function(nameOrList, callback) {

            if(is.string(nameOrList)) {
                this._helpers.add(nameOrList, callback);
            } else {
                object.each(nameOrList, function(name, callback) {
                    this._helpers.add(name, callback);
                }, this);
            }

            this.Modes = this._classifyModes();
            return this;
        },

        /**
         * Сформировать класс на основе базовых полей.
         *
         * @private
         * @returns {Function}
         */
        _classifyModes: function() {
            return classify(classify(this._getBaseModes()), this._functionifyModes(this.modes));
        },

        /**
         * Получить базовые поля для класса.
         *
         * @private
         * @returns {object}
         */
        _getBaseModes: function() {
            return object.extend(this._functionifyModes(this._getDefaultModes()), this._helpers.get());
        },

        /**
         * Обернуть все поля в функции.
         *
         * Все поля должны являться функциями для
         * возможности вызова `__base` в любой ситуации.
         *
         * Поля, не являющиеся функциями, оборачиваются
         * в анонимную функцию со свойством `__wrapped__`.
         *
         * @private
         * @param {object} modes Поля
         * @returns {object}
         */
        _functionifyModes: function(modes) {
            object.each(modes, function(name, val) {
                if(!is.function(val)) {
                    modes[name] = function() { return val; };
                    modes[name].__wrapped__ = true;
                }
            }, this);
            return modes;
        },

        /**
         * Получить стандартные моды.
         *
         * @private
         * @returns {object}
         */
        _getDefaultModes: function() {
            return {
                js: false,
                bem: true,
                mods: {},
                elemMods: {},
                attrs: {},
                mix: [],
                tag: true,
                single: undefined,
                cls: '',
                content: '',
                options: {}
            };
        },

        /**
         * Получить значение моды.
         *
         * Если значение в шаблоне скалярное, то массивы конкатенируются,
         * а объекты (карты) наследуются с приоритетом у BEMJSON.
         *
         * @private
         * @param {Object} modes Экземпляр класса по модам
         * @param {object} bemjson Входящий BEMJSON
         * @param {string} name Имя требуемой моды
         * @param {object} baseBemjson Базовый BEMJSON из входящих данных
         * @param {string[]} modesFromAnotherTemplates Список полей, которые были установлены из других шаблонов
         * @param {number} index Порядковый номер шаблона в общем списке
         * @returns {*}
         */
        _getMode: function(modes, bemjson, name, baseBemjson, modesFromAnotherTemplates, index) {
            var isValFunc = !modes[name].__wrapped__,
                bemjsonVal = bemjson[name],
                baseBemjsonVal = baseBemjson[name],
                val = modes[name].call(modes, bemjsonVal),
                priorityVal = this._getPriorityValue(
                    name,
                    val,
                    bemjsonVal,
                    baseBemjsonVal,
                    isValFunc,
                    modesFromAnotherTemplates,
                    { weight: this.weight, index: index }
                );

            if(!isValFunc) {
                if(is.array(val, bemjsonVal)) {
                    priorityVal = bemjsonVal.concat(val);
                } else if(is.map(val, bemjsonVal)) {
                    priorityVal = this._isThisTemplatePriority(index, modesFromAnotherTemplates[name], isValFunc)
                        ? object.extend(object.clone(bemjsonVal), val)
                        : object.extend(object.clone(val), bemjsonVal);
                }
            }

            return priorityVal;
        },

        /**
         * Получить приоритетное значение моды.
         *
         * Если значение моды в шаблоне задано функцией,
         * то оно является приоритетным.
         *
         * @private
         * @param {string} name Имя требуемой моды
         * @param {*} val Значение моды в шаблоне
         * @param {*} bemjsonVal Значение моды в BEMJSON
         * @param {*} baseBemjsonVal Значение моды базового BEMJSON из входящих данных
         * @param {boolean} isValFunc Значение моды в шаблоне может быть задано функцией
         * @param {string[]} modesFromAnotherTemplates Список полей, которые были установлены из других шаблонов
         * @param {object} info Информация для добавления в список полей, установленных из шаблонов
         * @returns {*}
         */
        _getPriorityValue: function(name, val, bemjsonVal, baseBemjsonVal, isValFunc, modesFromAnotherTemplates, info) {
            var isOwn = !!~this._modesNames.indexOf(name),
                isThisTemplatePriority = this._isThisTemplatePriority(
                    info.index,
                    modesFromAnotherTemplates[name],
                    isValFunc
                );

            if(isThisTemplatePriority && isOwn) {
                modesFromAnotherTemplates[name] = info;
                return val;
            }

            if(!is.undefined(baseBemjsonVal)) {
                return baseBemjsonVal;
            }

            if(is.undefined(val) || modesFromAnotherTemplates[name] && (!isOwn || !isThisTemplatePriority)) {
                return bemjsonVal;
            }

            if(isOwn) {
                modesFromAnotherTemplates[name] = info;
            }
            return val;
        },

        /**
         * Проверить приоритет моды текущего шаблона.
         *
         * @private
         * @param {number} index Порядковый номер текущего шаблона в общем списке
         * @param {object} modeFromAnotherTemplate Информация об установке моды из другого шаблона
         * @param {boolean} isValFunc Значение моды в шаблоне может быть задано функцией
         * @returns {boolean}
         */
        _isThisTemplatePriority: function(index, modeFromAnotherTemplate, isValFunc) {
            // Мода не была установлена в другом шаблоне, значит приоритет имеет BEMJSON или функция шаблона.
            if(!modeFromAnotherTemplate) return isValFunc;

            if(modeFromAnotherTemplate.weight > this.weight) return false;
            if(modeFromAnotherTemplate.weight === this.weight) {
                return modeFromAnotherTemplate.index <= index;
            }

            return true;
        }

    };

    /**
     * Базовый шаблон.
     *
     * @type {Template}
     */
    Template.baseTemplate = new Template('', {});

    /**
     * Стандартные моды базового шаблона.
     *
     * @private
     * @type {object}
     */
    Template._defaultModes = Template.baseTemplate._getDefaultModes();

    /**
     * Список имён стандартных мод.
     *
     * @private
     * @type {array}
     */
    Template._defaultModesNames = Object.keys(Template._defaultModes);

    return Template;

});