UNPKG

enb

Version:

Faster BEM/BEViS assembler

607 lines (522 loc) 18.1 kB
/** * --- (C) Original BEM Tools, modified for compatibility. * * Инструментарий для раскрытия deps'ов. * Заимствованный. */ var inherit = require('inherit'); var vowFs = require('../lib/fs/async-fs'); var vm = require('vm'); var Vow = require('vow'); module.exports.OldDeps = (function () { /** * Класс, раскрывающий зависимости. Взят из bem-tools. * * @name OldDeps */ var OldDeps = inherit({ /** * Конструктор. * Принимает блоки из bemdecl. * * @param {Array} deps */ __constructor: function (deps) { this.items = {}; this.itemsByOrder = []; this.uniqExpand = {}; // Force adding of root item to this.items var rootItem = this.rootItem = new OldDepsItem({}); this.items[rootItem.buildKey()] = rootItem; deps && this.parse(deps); }, /** * Добавляет зависимость в коллекцию. * * @param {OldDepsItem} target * @param {String} depsType shouldDeps/mustDeps * @param {OldDepsItem} item */ add: function (target, depsType, item) { var items = this.items; var targetKey = target.buildKey(); var itemKey = item.buildKey(); if (!items[itemKey]) { items[itemKey] = item; this.itemsByOrder.push(itemKey); } (items[targetKey] || (items[targetKey] = target))[depsType].push(itemKey); }, /** * Удаляет зависимость из коллекции. * * @param {OldDepsItem} target * @param {OldDepsItem} item */ remove: function (target, item) { target = this.items[target.buildKey()]; var itemKey = item.buildKey(); removeFromArray(target.shouldDeps, itemKey); removeFromArray(target.mustDeps, itemKey); }, /** * Клонирует резолвер зависимостей. * * @param {OldDeps} [target] * @returns {OldDeps} */ clone: function (target) { target || (target = new this.__self()); var items = this.items; for (var i in items) { if (!items.hasOwnProperty(i)) { continue; } target.items[i] = items[i].clone(); } target.itemsByOrder = this.itemsByOrder.concat(); target.tech = this.tech; target.uniqExpand = this.uniqExpand; return target; }, /** * Разбирает bemdecl. * * @param {Array} deps * @param {Object} [ctx] * @param {Function} [fn] * @returns {OldDeps} */ parse: function (deps, ctx, fn) { fn || (fn = function (i) { this.add(this.rootItem, 'shouldDeps', i); }); var _this = this; var forEachItem = function (type, items, ctx) { items && !isEmptyObject(items) && (Array.isArray(items) ? items : [items]).forEach(function (item) { if (isSimple(item)) { var i = item; (item = {})[type] = i; } item.name && (item[type] = item.name); var depsItem = new OldDepsItem(item, ctx); fn.call(_this, depsItem); // _this.add(rootItem, 'shouldDeps', depsItem); _this.parse( item.mustDeps, depsItem, function (i) { this.add(depsItem, 'mustDeps', i); }); _this.parse( item.shouldDeps, depsItem, function (i) { this.add(depsItem, 'shouldDeps', i); }); _this.parse( item.noDeps, depsItem, function (i) { this.remove(depsItem, i); }); forEachItem('elem', item.elems, depsItem); var mods = item.mods; if (mods && !Array.isArray(mods)) { // Object var modsArr = []; for (var m in mods) { if (!mods.hasOwnProperty(m)) { continue; } modsArr.push({ mod: m }); var mod = { mod: m }; var v = mods[m]; Array.isArray(v) ? (mod.vals = v) : (mod.val = v); modsArr.push(mod); } mods = modsArr; } forEachItem('mod', mods, depsItem); forEachItem('val', item.vals, depsItem); }); }; forEachItem('block', deps, ctx); return this; }, /** * Раскрывает зависимости, используя deps.js-файлы. * * @param {Object} tech * @returns {Promise} */ expandByFS: function (tech) { this.tech = tech; var _this = this; var depsCount1 = this.getCount(); var depsCount2; return Vow.when(this.expandOnceByFS()) .then(function again(newDeps) { depsCount2 = newDeps.getCount(); if (depsCount1 !== depsCount2) { depsCount1 = depsCount2; return Vow.when(newDeps.expandOnceByFS(), again); } return newDeps.clone(_this); }); }, /** * Раскрывает зависимости, используя deps.js-файлы без повторений. * * @returns {Promise} */ expandOnceByFS: function () { var newDeps = this.clone(); var items = this.filter(function (item) { return !newDeps.uniqExpand.hasOwnProperty(item.buildKey()); }); function keepWorking(item) { newDeps.uniqExpand[item.buildKey()] = true; return newDeps.expandItemByFS(item).then(function () { if (items.length > 0) { return keepWorking(items.shift()); } else { return null; } }); } if (items.length > 0) { return keepWorking(items.shift()).then(function () { return newDeps; }); } else { return Vow.fulfill(newDeps); } }, /** * Раскрывает одну зависимость, используя deps.js-файлы. * * @param {OldDepsItem} item * @returns {Promise} */ expandItemByFS: function (item) { var _this = this; var tech = this.tech; var files = tech.levels.getFilesByDecl(item.item.block, item.item.elem, item.item.mod, item.item.val) .filter(function (file) { return file.suffix === 'deps.js'; }); var promise = Vow.fulfill(); files.forEach(function (file) { promise = promise.then(function () { return vowFs.read(file.fullname, 'utf8').then(function (content) { try { _this.parse(vm.runInThisContext(content, file.fullname), item); } catch (e) { throw new Error('Syntax error in file "' + file.fullname + '": ' + e.message); } }); }); }); return promise; }, /** * Вычитает зависимости из переданного OldDeps. * * @param {OldDeps} deps * @returns {OldDeps} */ subtract: function (deps) { var items1 = this.items; var items2 = deps.items; for (var k in items2) { if (k && items2.hasOwnProperty(k)) { delete items1[k]; } } return this; }, /** * Сохраняет пересечение с другим OldDeps. * * @param {OldDeps} deps * @returns {OldDeps} */ intersect: function (deps) { var items1 = this.items; var items2 = deps.items; var newItems = {}; for (var k in items2) { if ((items2.hasOwnProperty(k) && items1.hasOwnProperty(k)) || !k) { newItems[k] = items1[k]; } } this.items = newItems; return this; }, /** * Возвращает количество зависимостей. * * @returns {Number} */ getCount: function () { var res = 0; var items = this.items; for (var k in items) { items.hasOwnProperty(k) && res++; } return res; }, /** * Итерирует по набору зависимостей. * * @param {Function} fn * @param {Object} [uniq] * @param {Array} [itemsByOrder] * @param {Object} [ctx] */ forEach: function (fn, uniq, itemsByOrder, ctx) { uniq || (uniq = {}); var _this = this; (itemsByOrder || this.items[''].shouldDeps).forEach(function (i) { if (i = _this.items[i]) { var key = i.buildKey(); if (!uniq.hasOwnProperty(key)) { uniq[key] = true; var newCtx = ctx || i; _this.forEach(fn, uniq, i.mustDeps, newCtx); fn.call(_this, i, newCtx); _this.forEach(fn, uniq, i.shouldDeps, newCtx); } } }) }, /** * Вызывает map для набора зависимостей. * * @param {Function} fn * @returns {Array} */ map: function (fn) { var res = []; this.forEach(function (item) { res.push(fn.call(this, item)); }); return res; }, /** * Фильтрует зависимости, возвращает результат. * @param {Function} fn * @returns {Array} */ filter: function (fn) { var res = []; this.forEach(function (item) { if (fn.call(this, item)) { res.push(item); } }); return res; }, /** * Возвращает результат резолвинга. * * @returns {Object} */ serialize: function () { var byTech = {}; this.forEach(function (item, ctx) { var t1 = ctx.item.tech || ''; var t2 = item.item.tech || ''; var techsByTech = byTech[t1] || (byTech[t1] = {}); var i = item.serialize(); if (i) { (techsByTech[t2] || (techsByTech[t2] = [])).push(i); } }); return byTech; }, /** * Сериализует в строку. * * @returns {String} */ stringify: function () { var res = []; var deps = this.serialize(); if (deps['']) { res.push('exports.deps = ' + JSON.stringify(deps[''][''], null, 4) + ';\n'); delete deps['']['']; } else { res.push('exports.deps = [];\n'); } isEmptyObject(deps) || res.push('exports.depsByTechs = ' + JSON.stringify(deps, null, 4) + ';\n'); return res.join(''); }, /** * Возвращает результат раскрытия зависимостей. * * @returns {Object|*|*|Array} */ getDeps: function () { var serializedData = this.serialize(); return (serializedData && serializedData[''] && serializedData['']['']) || []; } }); /** * Элемент зависимостей. * * @name OldDepsItem */ var OldDepsItem = inherit({ __constructor: function (item, ctx) { this.shouldDeps = []; this.mustDeps = []; this.item = {}; this.extendByCtx({ item: item }); this.extendByCtx(ctx); }, /** * Раскрывает зависимости. * * @param {Object} ctx * @returns {OldDepsItem} */ extendByCtx: function (ctx) { if (ctx && (ctx = ctx.item)) { var ks = ['tech', 'block', 'elem', 'mod', 'val']; var k; while (k = ks.shift()) { if (this.item[k]) { break; } else { ctx[k] && (this.item[k] = ctx[k]); } } } return this; }, /** * Возвращает копию. * * @returns {OldDepsItem} */ clone: function () { var res = new this.__self({}, this); res.shouldDeps = this.shouldDeps.concat(); res.mustDeps = this.mustDeps.concat(); this.hasOwnProperty('key') && (res.key = this.key); return res; }, /** * Расширяет зависимость. * * @param {OldDepsItem} item * @returns {OldDepsItem} */ extend: function (item) { if (!item) { return this; } var ds = ['mustDeps', 'shouldDeps']; var d; var thisDeps; var itemDeps; while (d = ds.shift()) { itemDeps = item[d] || (item[d] = {}); if (thisDeps = this.item[d]) { for (var k in thisDeps) { if (thisDeps.hasOwnProperty(k)) { if (!thisDeps[k].extend) { throw 'bla'; // FIXME: WTF? } (itemDeps[k] = thisDeps[k].extend(itemDeps[k])); } } } } return item; }, /** * Записывает зависимость в кэш по ключу. * * @param {Object} cache * @returns {OldDepsItem} */ cache: function (cache) { var key = this.buildKey(); return cache[key] = this.extend(cache[key]); }, /** * Строит ключ для зависимости. * * @returns {String} */ buildKey: function () { if ('key' in this) { return this.key; } var i = this.item; var k = ''; if (i.block) { k += i.block; i.elem && (k += '__' + i.elem); if (i.mod) { k += '_' + i.mod; i.val && (k += '_' + i.val); } } if (i.tech) { k += '.' + i.tech; } return this.key = k; }, /** * Сериализует зависимость в объект. * * @returns {Object} */ serialize: function () { var res = {}; var ks = ['tech', 'block', 'elem', 'mod', 'val']; var k; while (k = ks.shift()) { if (this.item[k]) { res[k] = this.item[k]; } } if (res.block) { return res; } } }); exports.DepsItem = OldDepsItem; /** * Возвращает true при String/Number. * @param {*} value * @returns {Boolean} */ function isSimple(value) { var t = typeof value; return t === 'string' || t === 'number'; } /** * Хэлпер для удаления значений из массива. * Возвращает true в случае успеха. * * @param {Array} arr * @param {*} value * @returns {Boolean} */ function removeFromArray(arr, value) { var i = arr.indexOf(value); if (i >= 0) { arr.splice(i, 1); return true; } else { return false; } } /** * Возвращает true, если переданный объект пуст. * @param {Object} obj * @returns {Boolean} */ function isEmptyObject(obj) { for (var i in obj) { return false; } return true; } return OldDeps; })();