UNPKG

bem

Version:
774 lines (690 loc) 24.3 kB
var PATH = require('./path'), INHERIT = require('inherit'), createTech = require('./tech').createTech, bemUtil = require('./util'), isRequireable = bemUtil.isRequireable, getLevelClass = function(path, optional) { var level = optional && !isRequireable(path) ? {} : requireLevel(path); if (level.Level) return level.Level; return INHERIT(level.baseLevelPath? getLevelClass(level.baseLevelPath) : Level, level); }, requireLevel = function(path) { return bemUtil.requireWrapper(require)(path, true); }; /** * Create level object from path on filesystem. * * @param {String} path Path to level directory. * @return {Level} Level object. */ exports.createLevel = function(path) { // NOTE: в директории .bem внутри уровня переопределения // лежит модуль-конфиг для уровня переопределения return new (getLevelClass(PATH.resolve(path, '.bem', 'level.js'), true))(path); }; var Level = exports.Level = INHERIT(/** @lends Level.prototype */{ /** * Construct an instance of Level. * * @class Level base class. * @constructs * @param {String} path Level directory path. */ __constructor: function(path) { this.dir = PATH.resolve(path); // NOTE: keep this.path for backwards compatability this.path = this.bemDir = PATH.join(this.dir, '.bem'); // NOTE: tech modules cache this._techsCache = {}; }, /** * Place to store uncommon level configurations * * @return {Object} */ getConfig: function() { return {}; }, /** * Tech module definitions for level * * @return {Object} Tech module definitions */ getTechs: function() { // NOTE: this.techs if for backwards compatability with legacy level configs return this.techs || {}; }, /** * Get tech object from its name and optional path to tech module. * * Object will be created and stored in cache. All following calls * to getTech() with same name will return the same object. * * Is you need unique object every time, use createTech() method * with same signature. * * @param {String} name Tech name * @param {String} [path] Path to tech module * @return {Tech} */ getTech: function(name, path) { if(!this._techsCache.hasOwnProperty(name)) { this._techsCache[name] = this.createTech(name, path || name); } return this._techsCache[name]; }, /** * Create tech object from its name and optional path to tech module. * * @param {String} name Tech name * @param {String} [path] Path to tech module * @return {Tech} */ createTech: function(name, path) { return createTech(this.resolveTech(path || name), name, this); }, /** * Resolve tech identifier into tech module path. * * @param {String} techIdent Tech identifier. * @param {Boolean} [force] Flag to not use tech name resolution. * @return {String} Tech module path. */ resolveTech: function(techIdent, force) { if(bemUtil.isPath(techIdent)) { return this.resolveTechPath(techIdent); } if(!force && this.getTechs().hasOwnProperty(techIdent)) { return this.resolveTechName(techIdent); } return bemUtil.getBemTechPath(techIdent); }, /** * Resolve tech name into tech module path. * * @param {String} techName Tech name. * @return {String} Tech module path. */ resolveTechName: function(techName) { var p = this.getTechs()[techName]; return typeof p !== 'undefined'? this.resolveTech(p, true) : null; }, /** * Resolve tech module path. * * @throws {Error} In case when tech module is not found. * @param {String} techPath Tech path (relative or absolute). * @return {String} Tech module path. */ resolveTechPath: function(techPath) { // Get absolute path if path starts with "." // NOTE: Can not replace check to !isAbsolute() if(techPath.substring(0, 1) === '.') { // Resolve relative path starting at level `.bem/` directory techPath = PATH.join(this.bemDir, techPath); if(!isRequireable(techPath)) { throw new Error("Tech module on path '" + techPath + "' not found"); } return techPath; } // Trying absolute of relative-withot-dot path if(isRequireable(techPath)) { return techPath; } throw new Error("Tech module with path '" + techPath + "' not found on require search paths"); }, /** * Get list of default techs to create with `bem create {block,elem,mod}` * commands. * * Returns all declared techs in `defaultTechs` property or keys of result * of `getTech()` method if `defaultTechs` is undefined. * * @return {String[]} Array of tech names. */ getDefaultTechs: function() { return this.defaultTechs || Object.keys(this.getTechs()); }, /** * Resolve relative paths using level config directory `.bem/` * as a base for them. * * Absolute paths (and keys of object) will be left untouched. * Returns new Array of strings or Object. * * @param {Object|String[]} paths Paths to resolve. * @return {Object|String[]} Resolved paths. */ resolvePaths: function(paths) { // resolve array of paths if (Array.isArray(paths)) { return paths.map(function(path) { return this.resolvePath(path); }, this); } // resolve paths in object values var resolved = {}; Object.keys(paths).forEach(function(key) { resolved[key] = this.resolvePath(paths[key]); }, this); return resolved; }, /** * Resolve relative path using level config directory `.bem/` * as a base. * * Absolute path will be left untouched. * * @param {String} path Path to resolve. * @return {String} Resolved path. */ resolvePath: function(path) { return PATH.resolve(this.path, path); }, /** * Construct path to tech file / directory from * prefix and tech name. * * @param {String} prefix Path prefix. * @param {String} tech Tech name. * @return {String} Absolute path. */ getPath: function(prefix, tech) { return this.getTech(tech).getPath(prefix); }, /** * Construct absolute path to tech file / directory from * BEM entity object and tech name. * * @param {Object} item BEM entity object. * @param {String} item.block Block name. * @param {String} item.elem Element name. * @param {String} item.mod Modifier name. * @param {String} item.val Modifier value. * @param {String} tech Tech name. * @return {String} Absolute path. */ getPathByObj: function(item, tech) { return PATH.join(this.dir, this.getRelPathByObj(item, tech)); }, /** * Construct relative path to tech file / directory from * BEM entity object and tech name. * * @param {Object} item BEM entity object. * @param {String} item.block Block name. * @param {String} item.elem Element name. * @param {String} item.mod Modifier name. * @param {String} item.val Modifier value. * @param {String} tech Tech name. * @return {String} Relative path. */ getRelPathByObj: function(item, tech) { return this.getPath(this.getRelByObj(item), tech); }, /** * Get absolute path prefix on the filesystem to specified * BEM entity described as an object with special properties. * * @param {Object} item BEM entity object. * @param {String} item.block Block name. * @param {String} item.elem Element name. * @param {String} item.mod Modifier name. * @param {String} item.val Modifier value. * @return {String} Absolute path prefix. */ getByObj: function(item) { return PATH.join(this.dir, this.getRelByObj(item)); }, /** * Get relative to level directory path prefix on the filesystem * to specified BEM entity described as an object with special * properties. * * @param {Object} item BEM entity object. * @param {String} item.block Block name. * @param {String} item.elem Element name. * @param {String} item.mod Modifier name. * @param {String} item.val Modifier value. * @return {String} Relative path prefix. */ getRelByObj: function(item) { var getter, args; if (item.block) { getter = 'block'; args = [item.block]; if (item.elem) { getter = 'elem'; args.push(item.elem); } if (item.mod) { getter += '-mod'; args.push(item.mod); if (item.val) { getter += '-val'; args.push(item.val); } } return this.getRel(getter, args); } return ''; }, /** * Get absolute path prefix on the filesystem to specified * BEM entity described as a pair of entity type and array. * * @param {String} what BEM entity type. * @param {String[]} args Array of BEM entity meta. * @return {String} */ get: function(what, args) { return PATH.join(this.dir, this.getRel(what, args)); }, /** * Get relative to level directory path prefix on the * filesystem to specified BEM entity described as a pair * of entity type and array. * * @param what * @param args * @return {String} */ getRel: function(what, args) { return this['get-' + what].apply(this, args); }, /** * Get relative path prefix for block. * * @param {String} block Block name. * @return {String} Path prefix. */ 'get-block': function(block) { return PATH.join.apply(null, [block, block]); }, /** * Get relative path prefix for block modifier. * * @param {String} block Block name. * @param {String} mod Modifier name. * @return {String} Path prefix. */ 'get-block-mod': function(block, mod) { return PATH.join.apply(null, [block, '_' + mod, block + '_' + mod]); }, /** * Get relative path prefix for block modifier-with-value. * * @param {String} block Block name. * @param {String} mod Modifier name. * @param {String} val Modifier value. * @return {String} Path prefix. */ 'get-block-mod-val': function(block, mod, val) { return PATH.join.apply(null, [block, '_' + mod, block + '_' + mod + '_' + val]); }, /** * Get relative path prefix for elem. * * @param {String} block Block name. * @param {String} elem Element name. * @return {String} Path prefix. */ 'get-elem': function(block, elem) { return PATH.join.apply(null, [block, '__' + elem, block + '__' + elem]); }, /** * Get relative path prefix for element modifier. * * @param {String} block Block name. * @param {String} elem Element name. * @param {String} mod Modifier name. * @return {String} Path prefix. */ 'get-elem-mod': function(block, elem, mod) { return PATH.join.apply(null, [block, '__' + elem, '_' + mod, block + '__' + elem + '_' + mod]); }, /** * Get relative path prefix for element modifier-with-value. * * @param {String} block Block name. * @param {String} elem Element name. * @param {String} mod Modifier name. * @param {String} val Modifier value. * @return {String} Path prefix. */ 'get-elem-mod-val': function(block, elem, mod, val) { return PATH.join.apply(null, [block, '__' + elem, '_' + mod, block + '__' + elem + '_' + mod + '_' + val]); }, /** * Get regexp string to match parts of path that represent * BEM entity on filesystem. * * @return {String} */ matchRe: function() { return '[^_.' + PATH.dirSepRe + ']+'; }, /** * Get order of matchers to apply during introspection. * * @return {String[]} Array of matchers names. */ matchOrder: function() { return ['elem-mod-val', 'elem-mod', 'block-mod-val', 'block-mod', 'elem', 'block']; }, /** * Get order of techs to match during introspection. * * @return {String[]} Array of techs names. */ matchTechsOrder: function() { return Object.keys(this.getTechs()); }, /** * Match path against all matchers and return first match. * * Match object will contain `block`, `suffix` and `tech` fields * and can also contain any of the `elem`, `mod` and `val` fields * or all of them. * * @param {String} path Path to match (absolute or relative). * @return {Boolean|Object} BEM entity object in case of positive match and false otherwise. */ matchAny: function(path) { if (PATH.isAbsolute(path)) path = PATH.relative(this.dir, path); var matchTechs = this.matchTechsOrder().map(function(t) { return this.getTech(t); }, this); return this.matchOrder().reduce(function(match, matcher) { // Skip if already matched if (match) return match; // Try matcher match = this.match(matcher, path); // Skip if not matched if (!match) return false; // Try to match for tech match.tech = matchTechs.reduce(function(tech, t) { if (tech || !t.matchSuffix(match.suffix)) return tech; return t.getTechName(); }, match.tech); return match; }.bind(this), false); }, /** * Match ralative path against specified matcher. * * Match object will contain `block` and `suffix` fields and * can also contain any of the `elem`, `mod` and `val` fields * or all of them. * * @param {String} what Matcher to match against. * @param {String} path Path to match. * @return {Boolean|Object} BEM entity object in case of positive match and false otherwise. */ match: function(what, path) { return this['match-' + what].call(this, path); }, /** * Match if specified path represents block entity. * * Match object will contain `block` and `suffix` fields. * @param {String} path Path to match. * @return {Boolean|Object} BEM block object in case of positive match and false otherwise. */ 'match-block': function(path) { var match = new RegExp(['^(' + this.matchRe() + ')', '\\1(.*?)$'].join(PATH.dirSepRe)).exec(path); if (!match) return false; return { block: match[1], suffix: match[2] }; }, /** * Match if specified path represents block modifier entity. * * Match object will contain `block`, `mod` and `suffix` fields. * * @param {String} path Path to match. * @return {Boolean|Object} BEM block modifier object in case of positive match and false otherwise. */ 'match-block-mod': function(path) { var m = this.matchRe(), match = new RegExp(['^(' + m + ')', '_(' + m + ')', '\\1_\\2(.*?)$'].join(PATH.dirSepRe)).exec(path); if (!match) return false; return { block: match[1], mod: match[2], suffix: match[3] }; }, /** * Match if specified path represents block modifier-with-value entity. * * Match object will contain `block`, `mod`, `val` and `suffix` fields. * * @param {String} path Path to match. * @return {Boolean|Object} BEM block modifier-with-value object in case of positive match and false otherwise. */ 'match-block-mod-val': function(path) { var m = this.matchRe(), match = new RegExp(['^(' + m + ')', '_(' + m + ')', '\\1_\\2_(' + m + ')(.*?)$'].join(PATH.dirSepRe)).exec(path); if (!match) return false; return { block: match[1], mod: match[2], val: match[3], suffix: match[4] }; }, /** * Match if specified path represents element entity. * * Match object will contain `block`, `elem` and `suffix` fields. * * @param {String} path Path to match. * @return {Boolean|Object} BEM element object in case of positive match and false otherwise. */ 'match-elem': function(path) { var m = this.matchRe(), match = new RegExp(['^(' + m + ')', '__(' + m + ')', '\\1__\\2(.*?)$'].join(PATH.dirSepRe)).exec(path); if (!match) return false; return { block: match[1], elem: match[2], suffix: match[3] }; }, /** * Match if specified path represents element modifier entity. * * Match object will contain `block`, `elem`, `mod` and `suffix` fields. * * @param {String} path Path to match. * @return {Boolean|Object} BEM element modifier object in case of positive match and false otherwise. */ 'match-elem-mod': function(path) { var m = this.matchRe(), match = new RegExp(['^(' + m + ')', '__(' + m + ')', '_(' + m + ')', '\\1__\\2_\\3(.*?)$'].join(PATH.dirSepRe)).exec(path); if (!match) return false; return { block: match[1], elem: match[2], mod: match[3], suffix: match[4] }; }, /** * Match if specified path represents element modifier-with-value entity. * * Match object will contain `block`, `elem`, `mod`, `val` and `suffix` fields. * * @param {String} path Path to match. * @return {Boolean|Object} BEM element modifier-with-value object in case of positive match and false otherwise. */ 'match-elem-mod-val': function(path) { var m = this.matchRe(), match = new RegExp(['^(' + m + ')', '__(' + m + ')', '_(' + m + ')', '\\1__\\2_\\3_(' + m + ')(.*?)$'].join(PATH.dirSepRe)).exec(path); if (!match) return false; return { block: match[1], elem: match[2], mod: match[3], val: match[4], suffix: match[5] }; }, /** * Get declaration for block. * * @param {String} blockName Block name to get declaration for. * @return {Object} Block declaration object. */ getBlockByIntrospection: function(blockName) { // TODO: support any custom naming scheme, e.g. flat, when there are // no directories for blocks var decl = this.getDeclByIntrospection(PATH.dirname(this.get('block', [blockName]))); return decl.length? decl.shift() : {}; }, /** * Get declaration of level directory or one of its subdirectories. * * @param {String} [from] Relative path to subdirectory of level directory to start introspection from. * @return {Array} Array of declaration. */ getDeclByIntrospection: function(from) { this._declIntrospector || (this._declIntrospector = this.createIntrospector({ creator: function(res, match) { if (match && match.tech) { return this._mergeMatchToDecl(match, res); } return res; } })); return this._declIntrospector(from); }, /** * Get BEM entities from level directory or one of its subdirectories. * * @param {String} [from] Relative path to subdirectory of level directory to start introspection from. * @return {Array} Array of entities. */ getItemsByIntrospection: function(from) { this._itemsIntrospector || (this._itemsIntrospector = this.createIntrospector()); return this._itemsIntrospector(from); }, /** * Creates preconfigured introspection functions. * * @param {Object} [opts] Introspector options. * @param {String} [opts.from] Relative path to subdirectory of level directory to start introspection from. * @param {Function} [opts.init] Function to return initial value of introspection. * @param {Function} [opts.filter] Function to filter paths to introspect, must return {Boolean}. * @param {Function} [opts.matcher] Function to perform match of paths, must return introspected value. * @param {Function} [opts.creator] Function to modify introspection object with matched value, must return new introspection. * @return {Function} Introspection function. */ createIntrospector: function(opts) { var level = this; // clone opts opts = bemUtil.extend({}, opts); // set default options opts.from || (opts.from = '.'); // initial value initializer opts.init || (opts.init = function() { return []; }); // paths filter function opts.filter || (opts.filter = function(path) { return !this.isIgnorablePath(path); }); // matcher function opts.matcher || (opts.matcher = function(path) { return this.matchAny(path); }); // result creator function opts.creator || (opts.creator = function(res, match) { if (match && match.tech) res.push(match); return res; }); /** * Introspection function. * * @param {String} [from] Relative path to subdirectory of level directory to start introspection from. * @param {*} [res] Initial introspection value to extend. * @return {*} */ return function(from, res) { from = PATH.resolve(level.dir, from || opts.from); res || (res = opts.init.call(level)); bemUtil.fsWalkTree(from, function(path) { res = opts.creator.call(level, res, opts.matcher.call(level, path)); }, opts.filter, level); return res; }; }, /** * Check path if it must be ignored during introspection. * * @param {String} path Path to check. * @return {Boolean} True if path must be ignored. */ isIgnorablePath: function(path) { return /\.(svn|git)$/.test(path); }, _mergeMatchToDecl: function(match, decl) { var blocks, elems, mods, vals, techAdded = false, addTech = function(o) { if(!techAdded && match.tech) { o.techs = [{ name: match.tech }]; techAdded = true; } return o; }; match.val && (vals = [addTech({name: match.val})]); match.mod && match.val && (mods = [addTech({name: match.mod, vals: vals})]); match.mod && !match.val && (mods = [addTech({name: match.mod})]); match.elem && (elems = [addTech({name: match.elem, mods: mods})]) && (blocks = [addTech({name: match.block, elems: elems})]); !match.elem && (blocks = [addTech({name: match.block, mods: mods})]); return bemUtil.mergeDecls(decl, blocks); } });