UNPKG

40.9 kBJavaScriptView Raw
1'use strict';
2
3var PATH = require('./path'),
4 FS = require('fs'),
5 INHERIT = require('inherit'),
6 createTech = require('./tech').createTech,
7 U = require('util'),
8 bemUtil = require('./util'),
9 LOGGER = require('./logger'),
10 isRequireable = bemUtil.isRequireable,
11
12 BEM,
13
14 // Use this as a function because of circular dependency which occurs
15 // if require is placed in the global scope of the module. BEM is also cached
16 // in a var to avoid require call every time (which is much slower than if statement.
17 getBem = function() {
18 if (!BEM) BEM = require('..');
19
20 return BEM;
21 },
22
23 getLevelClass = function(path, optional, stack) {
24 stack = stack || [];
25
26 var level;
27 try {
28 level = optional && !isRequireable(path) ? {} : requireLevel(path);
29 } catch(error) {
30 throw new Error(U.format(
31 'level module %s can not be found %s %s%s\n',
32 path,
33 stack.length > 0? 'but required by the level': '',
34 stack.length > 1? 'inheritance tree:\n\t': '',
35 stack.join('\n\t')));
36 }
37
38 stack.push(path);
39
40 if (typeof level === 'function') level = level(getBem());
41
42 if (level.Level) return level.Level;
43
44 var baseLevelPath = (level.baseLevelName?
45 PATH.join(__dirname, 'levels', level.baseLevelName):
46 null) || level.baseLevelPath;
47
48 return INHERIT(baseLevelPath? getLevelClass(baseLevelPath, false, stack) : Level, level);
49 },
50
51 requireLevel = function(path) {
52 return bemUtil.requireWrapper(require)(path, true);
53 },
54
55 checkedLevels = {},
56
57 checkLevel = function(path) {
58 if (checkedLevels[path]) {
59 return;
60 }
61 checkedLevels[path] = true;
62 if (!bemUtil.isDirectory(path)) {
63 LOGGER.fwarn('Level at %s is not a directory', path);
64 return;
65 }
66 if (!bemUtil.isDirectory(PATH.join(path, '.bem'))) {
67 LOGGER.fwarn('Level at %s does not contain .bem subdirectory', path);
68 }
69 },
70
71 levelCache = {},
72 useCache = false,
73 exceptLevels = [],
74
75 allRe = /(?:^([^_.\/]+)\/__([^_.\/]+)\/(?:_([^_.\/]+)\/\1__\2_\3(?:_([^_.\/]+))?|\1__\2)(.*?)$|^([^_.\/]+)\/(?:(?:\6)|(?:_([^_.\/]+)\/\6_\7(?:_([^_.\/]+))?))(.*?)$)/,
76 elemAllRe = /^([^_.\/]+)\/__([^_.\/]+)\/(?:_([^_.\/]+)\/\1__\2_\3(?:_([^_.\/]+))?|\1__\2)(.*?)$/,
77 blockAllRe = /^([^_.\/]+)\/(?:(?:\1)|(?:_([^_.\/]+)\/\1_\2(?:_([^_.\/]+))?))(.*?)$/;
78
79/**
80 * Create level object from path on filesystem.
81 *
82 * @param {String | Object} level Path to level directory.
83 * @param {Object} [opts] Optional parameters
84 * @return {Level} Level object.
85 */
86exports.createLevel = function(level, opts) {
87 // NOTE: в директории .bem внутри уровня переопределения
88 // лежит модуль-конфиг для уровня переопределения
89 var path = level.path || level;
90
91 opts = opts || {};
92
93 checkLevel(level);
94
95 if (!opts.noCache && levelCache[path]) return levelCache[path];
96 level = new (getLevelClass(PATH.resolve(path, '.bem', 'level.js'), true))(level, opts);
97 levelCache[path] = level;
98
99 return level;
100};
101
102exports.setCachePolicy = function(useCacheByDefault, except) {
103 useCache = useCacheByDefault;
104 exceptLevels = except || [];
105};
106
107exports.resetLevelsCache = function(all) {
108 for(var l in levelCache) {
109 var level = levelCache[l];
110
111 if (!level.cache || all) level.files = null;
112 }
113};
114
115
116var Level = exports.Level = INHERIT(/** @lends Level.prototype */{
117
118 /**
119 * Construct an instance of Level.
120 *
121 * @class Level base class.
122 * @constructs
123 * @param {String | Object} path Level directory path.
124 * @param {Object} [opts] Optional parameters
125 */
126 __constructor: function(path, opts) {
127 opts = opts || {};
128 this.dir = PATH.resolve(path.path || path);
129 this.projectRoot = opts.projectRoot || PATH.resolve('');
130
131 // NOTE: keep this.path for backwards compatibility
132 this.path = this.bemDir = PATH.join(this.dir, '.bem');
133
134 path = PATH.relative(this.projectRoot, this.dir);
135 this.cache = useCache;
136 for(var e in exceptLevels) {
137 var except = exceptLevels[e];
138 if (path.substr(0, except.length) === except) {
139 this.cache = !this.cache;
140 break;
141 }
142 }
143
144 // NOTE: tech modules cache
145 this._techsCache = {};
146 },
147
148 /**
149 * Return level type.
150 *
151 * Default is `['level']`.
152 *
153 * @return {String[]}
154 */
155 getTypes: function() {
156 return ['level'];
157 },
158
159 /**
160 * Place to store uncommon level configurations
161 *
162 * @return {Object}
163 */
164 getConfig: function() {
165 return {};
166 },
167
168 /**
169 * Tech module definitions for level
170 *
171 * @return {Object} Tech module definitions
172 */
173 getTechs: function() {
174 // NOTE: this.techs is for backwards compatibility with legacy level configs
175 return this.techs || {};
176 },
177
178 /**
179 * Get tech object from its name and optional path to tech module.
180 *
181 * Object will be created and stored in cache. All following calls
182 * to getTech() with same name will return the same object.
183 *
184 * Is you need unique object every time, use createTech() method
185 * with same signature.
186 *
187 * @param {String} name Tech name
188 * @param {String} [path] Path to tech module
189 * @return {Tech}
190 */
191 getTech: function(name, path) {
192 if(!this._techsCache.hasOwnProperty(name)) {
193 this._techsCache[name] = this.createTech(name, path || name);
194 }
195 return this._techsCache[name];
196 },
197
198 /**
199 * Create tech object from its name and optional path to tech module.
200 *
201 * @param {String} name Tech name
202 * @param {String} [path] Path to tech module
203 * @return {Tech}
204 */
205 createTech: function(name, path) {
206 return createTech(this.resolveTech(path || name), name, this);
207 },
208
209 /**
210 * Resolve tech identifier into tech module path.
211 *
212 * @param {String} techIdent Tech identifier.
213 * @param {Object|Boolean} [opts] Options to use during resolution. If boolean value
214 * is passed, it gets used as `options.force` for backward compatibility.
215 * @param {Boolean} [opts.force=false] Flag to not use tech name resolution.
216 * @param {Boolean} [opts.throwWhenUnresolved=false] Throw an error, if tech cannot be
217 * resoled. If false, will return base tech instead of unresolved.
218 * @param {Number} [version=1] Version of a tech to load, for bem-tools techs.
219 * @return {String} Tech module path.
220 */
221 resolveTech: function(techIdent, opts) {
222 if (typeof opts === 'boolean') {
223 //legacy code used `force` second argument
224 opts = {force: opts};
225 }
226 opts = opts || {};
227 if(bemUtil.isPath(techIdent)) {
228 return this.resolveTechPath(techIdent);
229 }
230 if(!opts.force && this.getTechs().hasOwnProperty(techIdent)) {
231 return this.resolveTechName(techIdent);
232 }
233 return bemUtil.getBemTechPath(techIdent, opts);
234 },
235
236 /**
237 * Resolve tech name into tech module path.
238 *
239 * @param {String} techName Tech name.
240 * @return {String} Tech module path.
241 */
242 resolveTechName: function(techName) {
243 var p = this.getTechs()[techName];
244 return typeof p !== 'undefined'? this.resolveTech(p, {force: true}) : null;
245 },
246
247 /**
248 * Resolve tech module path.
249 *
250 * @throws {Error} In case when tech module is not found.
251 * @param {String} techPath Tech path (relative or absolute).
252 * @return {String} Tech module path.
253 */
254 resolveTechPath: function(techPath) {
255 // Get absolute path if path starts with "."
256 // NOTE: Can not replace check to !isAbsolute()
257 if(techPath.substring(0, 1) === '.') {
258 // Resolve relative path starting at level `.bem/` directory
259 techPath = PATH.join(this.bemDir, techPath);
260
261 /* jshint -W109 */
262 if(!isRequireable(techPath)) {
263 throw new Error("Tech module on path '" + techPath + "' not found");
264 }
265 /* jshint +W109 */
266
267 return techPath;
268 }
269
270 // Trying absolute of relative-without-dot path
271 if(isRequireable(techPath)) {
272 return techPath;
273 }
274
275
276 /* jshint -W109 */
277 try {
278 return require.resolve('./' + PATH.join('./techs', techPath));
279 } catch (err) {
280 throw new Error("Tech module with path '" + techPath + "' not found on require search paths");
281 }
282 /* jshint +W109 */
283 },
284
285 /**
286 * Get list of default techs to create with `bem create {block,elem,mod}`
287 * commands.
288 *
289 * Returns all declared techs in `defaultTechs` property or keys of result
290 * of `getTech()` method if `defaultTechs` is undefined.
291 *
292 * @return {String[]} Array of tech names.
293 */
294 getDefaultTechs: function() {
295 return this.defaultTechs || Object.keys(this.getTechs());
296 },
297
298 /**
299 * Resolve relative paths using level config directory `.bem/`
300 * as a base for them.
301 *
302 * Absolute paths (and keys of object) will be left untouched.
303 * Returns new Array of strings or Object.
304 *
305 * @param {Object|String[]} paths Paths to resolve.
306 * @return {Object|String[]} Resolved paths.
307 */
308 resolvePaths: function(paths) {
309
310 // resolve array of paths
311 if (Array.isArray(paths)) {
312 return paths.map(function(path) {
313 return this.resolvePath(path);
314 }, this);
315 }
316
317 // resolve paths in object values
318 var resolved = {};
319 Object.keys(paths).forEach(function(key) {
320 resolved[key] = this.resolvePath(paths[key]);
321 }, this);
322
323 return resolved;
324
325 },
326
327 /**
328 * Resolve relative path using level config directory `.bem/`
329 * as a base.
330 *
331 * Absolute path will be left untouched.
332 *
333 * @param {String} path Path to resolve.
334 * @return {String} Resolved path.
335 */
336 resolvePath: function(path) {
337 return PATH.resolve(this.path, path);
338 },
339
340 /**
341 * Construct path to tech file / directory from
342 * prefix and tech name.
343 *
344 * @param {String} prefix Path prefix.
345 * @param {String} tech Tech name.
346 * @return {String} Absolute path.
347 */
348 getPath: function(prefix, tech) {
349 return this.getTech(tech).getPath(prefix);
350 },
351
352 getPaths: function(prefix, tech) {
353 return (typeof tech === 'string'? this.getTech(tech): tech).getPaths(prefix);
354 },
355
356 /**
357 * Construct absolute path to tech file / directory from
358 * BEM entity object and tech name.
359 *
360 * @param {Object} item BEM entity object.
361 * @param {String} item.block Block name.
362 * @param {String} item.elem Element name.
363 * @param {String} item.mod Modifier name.
364 * @param {String} item.val Modifier value.
365 * @param {String} tech Tech name.
366 * @return {String} Absolute path.
367 */
368 getPathByObj: function(item, tech) {
369 return PATH.join(this.dir, this.getRelPathByObj(item, tech));
370 },
371
372 /**
373 * Construct relative path to tech file / directory from
374 * BEM entity object and tech name.
375 *
376 * @param {Object} item BEM entity object.
377 * @param {String} item.block Block name.
378 * @param {String} item.elem Element name.
379 * @param {String} item.mod Modifier name.
380 * @param {String} item.val Modifier value.
381 * @param {String} tech Tech name.
382 * @return {String} Relative path.
383 */
384 getRelPathByObj: function(item, tech) {
385 return this.getPath(this.getRelByObj(item), tech);
386 },
387
388 getFileByObjIfExists: function(item, tech) {
389 if (!this.files) return;
390
391 var blocks = this.files.tree,
392 block = blocks[item.block];
393
394 if (!block) return [];
395
396 if (item.mod && !item.elem) {
397 block = block.mods[item.mod];
398 if (block && item.val) block = block.vals[item.val];
399
400 } else if (item.elem) {
401 block = block.elems[item.elem];
402 if (block && item.mod) {
403 block = block.mods[item.mod];
404 if (block && item.val) block = block.vals[item.val];
405 }
406 }
407
408
409 var files = block? block.files: null;
410
411 if (!files || files.length === 0) return [];
412
413 var suffixes = tech.getSuffixes(),
414 res = [];
415
416 for(var i = 0; i < suffixes.length; i++) {
417 var suffix = suffixes[i],
418 filesBySuffix = files[suffix];
419
420 if (filesBySuffix) res = res.concat(filesBySuffix);
421
422 }
423
424
425 return res;
426 },
427
428 /**
429 * Get absolute path prefix on the filesystem to specified
430 * BEM entity described as an object with special properties.
431 *
432 * @param {Object} item BEM entity object.
433 * @param {String} item.block Block name.
434 * @param {String} item.elem Element name.
435 * @param {String} item.mod Modifier name.
436 * @param {String} item.val Modifier value.
437 * @return {String} Absolute path prefix.
438 */
439 getByObj: function(item) {
440 return PATH.join(this.dir, this.getRelByObj(item));
441 },
442
443 /**
444 * Get relative to level directory path prefix on the filesystem
445 * to specified BEM entity described as an object with special
446 * properties.
447 *
448 * @param {Object} item BEM entity object.
449 * @param {String} item.block Block name.
450 * @param {String} item.elem Element name.
451 * @param {String} item.mod Modifier name.
452 * @param {String} item.val Modifier value.
453 * @return {String} Relative path prefix.
454 */
455 getRelByObj: function(item) {
456 var getter, args;
457 if (item.block) {
458 getter = 'block';
459 args = [item.block];
460 if (item.elem) {
461 getter = 'elem';
462 args.push(item.elem);
463 }
464 if (item.mod) {
465 getter += '-mod';
466 args.push(item.mod);
467 if (item.val) {
468 getter += '-val';
469 args.push(item.val);
470 }
471 }
472 return this.getRel(getter, args);
473 }
474 return '';
475 },
476
477 /**
478 * Get absolute path prefix on the filesystem to specified
479 * BEM entity described as a pair of entity type and array.
480 *
481 * @param {String} what BEM entity type.
482 * @param {String[]} args Array of BEM entity meta.
483 * @return {String}
484 */
485 get: function(what, args) {
486 return PATH.join(this.dir, this.getRel(what, args));
487 },
488
489 /**
490 * Get relative to level directory path prefix on the
491 * filesystem to specified BEM entity described as a pair
492 * of entity type and array.
493 *
494 * @param what
495 * @param args
496 * @return {String}
497 */
498 getRel: function(what, args) {
499 return this['get-' + what].apply(this, args);
500 },
501
502 /**
503 * Get relative path prefix for block.
504 *
505 * @param {String} block Block name.
506 * @return {String} Path prefix.
507 */
508 'get-block': function(block) {
509 return PATH.join.apply(null, [block, block]);
510 },
511
512 /**
513 * Get relative path prefix for block modifier.
514 *
515 * @param {String} block Block name.
516 * @param {String} mod Modifier name.
517 * @return {String} Path prefix.
518 */
519 'get-block-mod': function(block, mod) {
520 return PATH.join.apply(null,
521 [block,
522 '_' + mod,
523 block + '_' + mod]);
524 },
525
526 /**
527 * Get relative path prefix for block modifier-with-value.
528 *
529 * @param {String} block Block name.
530 * @param {String} mod Modifier name.
531 * @param {String} val Modifier value.
532 * @return {String} Path prefix.
533 */
534 'get-block-mod-val': function(block, mod, val) {
535 return PATH.join.apply(null,
536 [block,
537 '_' + mod,
538 block + '_' + mod + '_' + val]);
539 },
540
541 /**
542 * Get relative path prefix for elem.
543 *
544 * @param {String} block Block name.
545 * @param {String} elem Element name.
546 * @return {String} Path prefix.
547 */
548 'get-elem': function(block, elem) {
549 return PATH.join.apply(null,
550 [block,
551 '__' + elem,
552 block + '__' + elem]);
553 },
554
555 /**
556 * Get relative path prefix for element modifier.
557 *
558 * @param {String} block Block name.
559 * @param {String} elem Element name.
560 * @param {String} mod Modifier name.
561 * @return {String} Path prefix.
562 */
563 'get-elem-mod': function(block, elem, mod) {
564 return PATH.join.apply(null,
565 [block,
566 '__' + elem,
567 '_' + mod,
568 block + '__' + elem + '_' + mod]);
569 },
570
571 /**
572 * Get relative path prefix for element modifier-with-value.
573 *
574 * @param {String} block Block name.
575 * @param {String} elem Element name.
576 * @param {String} mod Modifier name.
577 * @param {String} val Modifier value.
578 * @return {String} Path prefix.
579 */
580 'get-elem-mod-val': function(block, elem, mod, val) {
581 return PATH.join.apply(null,
582 [block,
583 '__' + elem,
584 '_' + mod,
585 block + '__' + elem + '_' + mod + '_' + val]);
586 },
587
588 /**
589 * Get regexp string to match parts of path that represent
590 * BEM entity on filesystem.
591 *
592 * @return {String}
593 */
594 matchRe: function() {
595 return '[^_.' + PATH.dirSepRe + ']+';
596 },
597
598 /**
599 * Get order of matchers to apply during introspection.
600 *
601 * @return {String[]} Array of matchers names.
602 */
603 matchOrder: function() {
604
605 return ['elem-all', 'block-all', 'elem-mod-val', 'elem-mod', 'block-mod-val',
606 'block-mod', 'elem', 'block'];
607 },
608
609 /**
610 * Get order of techs to match during introspection.
611 *
612 * @return {String[]} Array of techs names.
613 */
614 matchTechsOrder: function() {
615 return Object.keys(this.getTechs());
616 },
617
618 /**
619 * Match path against all matchers and return first match.
620 *
621 * Match object will contain `block`, `suffix` and `tech` fields
622 * and can also contain any of the `elem`, `mod` and `val` fields
623 * or all of them.
624 *
625 * @param {String} path Path to match (absolute or relative).
626 * @return {Boolean|Object} BEM entity object in case of positive match and false otherwise.
627 */
628 matchAny: function(path) {
629 if (PATH.isAbsolute(path)) path = PATH.relative(this.dir, path);
630
631 var matchTechs = this.matchTechsOrder().map(function(t) {
632 return this.getTech(t);
633 }, this);
634
635 return this.matchOrder().reduce(function(match, matcher) {
636
637 // Skip if already matched
638 if (match) return match;
639
640 // Try matcher
641 match = this.match(matcher, path);
642
643 // Skip if not matched
644 if (!match) return false;
645
646 // Try to match for tech
647 match.tech = matchTechs.reduce(function(tech, t) {
648 if (tech || !t.matchSuffix(match.suffix)) return tech;
649 return t.getTechName();
650 }, match.tech);
651
652 return match;
653
654 }.bind(this), false);
655 },
656
657 /**
658 * Match ralative path against specified matcher.
659 *
660 * Match object will contain `block` and `suffix` fields and
661 * can also contain any of the `elem`, `mod` and `val` fields
662 * or all of them.
663 *
664 * @param {String} what Matcher to match against.
665 * @param {String} path Path to match.
666 * @return {Boolean|Object} BEM entity object in case of positive match and false otherwise.
667 */
668 match: function(what, path) {
669 return this['match-' + what].call(this, path);
670 },
671
672 /**
673 * Match if specified path represents block entity.
674 *
675 * Match object will contain `block` and `suffix` fields.
676
677 * @param {String} path Path to match.
678 * @return {Boolean|Object} BEM block object in case of positive match and false otherwise.
679 */
680 'match-block': function(path) {
681 var match = new RegExp(['^(' + this.matchRe() + ')',
682 '\\1(.*?)$'].join(PATH.dirSepRe)).exec(path);
683
684 if (!match) return false;
685 return {
686 block: match[1],
687 suffix: match[2]
688 };
689 },
690
691 /**
692 * Match if specified path represents block modifier entity.
693 *
694 * Match object will contain `block`, `mod` and `suffix` fields.
695 *
696 * @param {String} path Path to match.
697 * @return {Boolean|Object} BEM block modifier object in case of positive match and false otherwise.
698 */
699 'match-block-mod': function(path) {
700 var m = this.matchRe(),
701 match = new RegExp(['^(' + m + ')',
702 '_(' + m + ')',
703 '\\1_\\2(.*?)$'].join(PATH.dirSepRe)).exec(path);
704
705 if (!match) return false;
706 return {
707 block: match[1],
708 mod: match[2],
709 suffix: match[3]
710 };
711 },
712
713 /**
714 * Match if specified path represents block modifier-with-value entity.
715 *
716 * Match object will contain `block`, `mod`, `val` and `suffix` fields.
717 *
718 * @param {String} path Path to match.
719 * @return {Boolean|Object} BEM block modifier-with-value object in case of positive match and false otherwise.
720 */
721 'match-block-mod-val': function(path) {
722 var m = this.matchRe(),
723 match = new RegExp(['^(' + m + ')',
724 '_(' + m + ')',
725 '\\1_\\2_(' + m + ')(.*?)$'].join(PATH.dirSepRe)).exec(path);
726
727 if (!match) return false;
728 return {
729 block: match[1],
730 mod: match[2],
731 val: match[3],
732 suffix: match[4]
733 };
734 },
735
736 'match-block-all': function(path) {
737 var match = blockAllRe.exec(path);
738 if (!match) return false;
739
740 var res = {
741 block: match[1]
742 };
743
744 if (match[2]) {
745 res.mod = match[2];
746
747 if (match[3]) res.val = match[3];
748 }
749
750 if (match[4]) res.suffix = match[4];
751
752 return res;
753 },
754
755 'get-block-all': function() {
756
757 },
758
759 'get-elem-all': function() {
760
761 },
762
763 /**
764 * Match if specified path represents element entity.
765 *
766 * Match object will contain `block`, `elem` and `suffix` fields.
767 *
768 * @param {String} path Path to match.
769 * @return {Boolean|Object} BEM element object in case of positive match and false otherwise.
770 */
771 'match-elem': function(path) {
772 var m = this.matchRe(),
773 match = new RegExp(['^(' + m + ')',
774 '__(' + m + ')',
775 '\\1__\\2(.*?)$'].join(PATH.dirSepRe)).exec(path);
776
777 if (!match) return false;
778 return {
779 block: match[1],
780 elem: match[2],
781 suffix: match[3]
782 };
783 },
784
785 /**
786 * Match if specified path represents element modifier entity.
787 *
788 * Match object will contain `block`, `elem`, `mod` and `suffix` fields.
789 *
790 * @param {String} path Path to match.
791 * @return {Boolean|Object} BEM element modifier object in case of positive match and false otherwise.
792 */
793 'match-elem-mod': function(path) {
794 var m = this.matchRe(),
795 match = new RegExp(['^(' + m + ')',
796 '__(' + m + ')',
797 '_(' + m + ')',
798 '\\1__\\2_\\3(.*?)$'].join(PATH.dirSepRe)).exec(path);
799
800
801 if (!match) return false;
802 return {
803 block: match[1],
804 elem: match[2],
805 mod: match[3],
806 suffix: match[4]
807 };
808 },
809
810 /**
811 * Match if specified path represents element modifier-with-value entity.
812 *
813 * Match object will contain `block`, `elem`, `mod`, `val` and `suffix` fields.
814 *
815 * @param {String} path Path to match.
816 * @return {Boolean|Object} BEM element modifier-with-value object in case of positive match and false otherwise.
817 */
818 'match-elem-mod-val': function(path) {
819 var m = this.matchRe(),
820 match = new RegExp(['^(' + m + ')',
821 '__(' + m + ')',
822 '_(' + m + ')',
823 '\\1__\\2_\\3_(' + m + ')(.*?)$'].join(PATH.dirSepRe)).exec(path);
824
825 if (!match) return false;
826 return {
827 block: match[1],
828 elem: match[2],
829 mod: match[3],
830 val: match[4],
831 suffix: match[5]
832 };
833 },
834
835 'match-elem-all': function(path) {
836 var match = elemAllRe.exec(path);
837 if (!match) return false;
838
839 var res = {
840 block: match[1],
841 elem: match[2]
842 };
843
844 if (match[3]) res.mod = match[3];
845 if (match[4]) res.val = match[4];
846 if (match[5]) res.suffix = match[5];
847
848 return res;
849 },
850
851 'match-all': function(path) {
852 var match = allRe.exec(path);
853 if (!match) return false;
854
855 var res = {};
856
857 if (match[1]) {
858 res.block = match[1];
859 res.elem = match[2];
860
861 if (match[3]) res.mod = match[3];
862 if (match[4]) res.val = match[4];
863 if (match[5]) res.suffix = match[5];
864 } else if (match[6]) {
865
866 res.block = match[6];
867
868 if (match[7]) {
869 res.mod = match[7];
870
871 if (match[8]) res.val = match[8];
872 }
873
874 if (match[9]) res.suffix = match[9];
875 }
876
877
878 return res;
879 },
880
881 /**
882 * Get declaration for block.
883 *
884 * @param {String} blockName Block name to get declaration for.
885 * @return {Object} Block declaration object.
886 */
887 getBlockByIntrospection: function(blockName) {
888 // TODO: support any custom naming scheme, e.g. flat, when there are
889 // no directories for blocks
890 var decl = this.getDeclByIntrospection(PATH.dirname(this.get('block', [blockName])));
891 return decl.length? decl.shift() : {};
892 },
893
894 /**
895 * Get declaration of level directory or one of its subdirectories.
896 *
897 * @param {String} [from] Relative path to subdirectory of level directory to start introspection from.
898 * @return {Array} Array of declaration.
899 */
900 getDeclByIntrospection: function(from) {
901
902 this._declIntrospector || (this._declIntrospector = this.createIntrospector({
903
904 creator: function(res, match) {
905 if (match && match.tech) {
906 return this._mergeMatchToDecl(match, res);
907 }
908 return res;
909 }
910
911 }));
912
913 return this._declIntrospector(from);
914 },
915
916 /**
917 * Get BEM entities from level directory or one of its subdirectories.
918 *
919 * @param {String} [from] Relative path to subdirectory of level directory to start introspection from.
920 * @return {Array} Array of entities.
921 */
922 getItemsByIntrospection: function(from) {
923
924 this._itemsIntrospector || (this._itemsIntrospector = this.createIntrospector());
925 return this._itemsIntrospector(from);
926
927 },
928
929 scanFiles: function(force) {
930 var list = {},
931 blocks = {},
932 flat = [],
933 files = {
934 files: list,
935 tree: blocks,
936 blocks: flat
937 },
938 items = {
939 push: function(file, item) {
940 file.suffix = item.suffix[0] === '.'?item.suffix.substr(1):item.suffix;
941 (list[file.suffix] || (list[file.suffix] = [])).push(file);
942 flat.push(item);
943
944 var block = blocks[item.block] || (blocks[item.block] = {elems: {}, mods: {}, files: {}});
945 if (item.mod && !item.elem) {
946 block = block.mods[item.mod] || (block.mods[item.mod] = {vals: {}, files: {}});
947
948 if (item.val) block = block.vals[item.val] || (block.vals[item.val] = {files: {}});
949 }
950
951 if (item.elem) {
952 block = block.elems[item.elem] || (block.elems[item.elem] = {mods: {}, files: {}});
953
954 if (item.mod) block = block.mods[item.mod] || (block.mods[item.mod] = {vals: {}, files: {}});
955 if (item.val) block = block.vals[item.val] || (block.vals[item.val] = {files: {}});
956 }
957
958 (block.files[file.suffix] || (block.files[file.suffix] = [])).push(file);
959 }
960 };
961
962 if (this.files && !force) return this.files;
963
964 var _this = this,
965 cachePath = PATH.join(this.projectRoot, '.bem', 'cache', PATH.relative(this.projectRoot, this.dir));
966
967 if (this.cache) {
968 try {
969 _this.files = JSON.parse(FS.readFileSync(PATH.join(cachePath, 'files.json')));
970 return _this.files;
971
972 } catch(err) {
973 LOGGER.fdebug('cache for level not found', _this.dir);
974 }
975 }
976
977 _this.scan(items);
978 _this.files = files;
979
980
981 return bemUtil
982 .mkdirp(cachePath)
983 .then(function() {
984 return bemUtil.writeFile(PATH.join(cachePath, 'files.json'), JSON.stringify(files));
985 })
986 .then(function() {
987 return files;
988 });
989 },
990
991 scan: function(items) {
992
993 if (!bemUtil.isDirectory(this.dir)) return;
994
995 LOGGER.time('scan ' + this.dir);
996
997 var _this = this;
998
999 this.suffixToTech = {};
1000 Object.keys(this.getTechs()).forEach(function(tech) {
1001 try {
1002 tech = this.getTech(tech);
1003
1004 tech.getSuffixes().forEach(function(s) {
1005 this.suffixToTech['.' + s] = tech.getTechName();
1006 }, this);
1007 } catch(err) {
1008 LOGGER.fwarn(err.message);
1009 }
1010
1011 }, this);
1012
1013 _this.scanBlocks(_this.dir, items);
1014
1015 LOGGER.timeEndLevel('debug', 'scan ' + _this.dir);
1016 },
1017
1018 scanBlocks: function(path, items) {
1019 var dirs = [],
1020 _this = this;
1021
1022 bemUtil.getDirsFilesSync(path, dirs);
1023
1024 return dirs
1025 .filter(function(dir) {
1026 dir = dir.file;
1027 return dir[0] !== '_' && dir[0] !== '.';
1028 })
1029 .forEach(function(block) {
1030 return _this.scanBlock(_this.dir, block.file, items);
1031 });
1032 },
1033
1034 scanBlock: function(path, block, items) {
1035 var _this = this,
1036 dirs = [], files = [];
1037
1038 bemUtil.getDirsFilesSync(PATH.join(path, block), dirs, files);
1039
1040 var blockPart = block + '.',
1041 blockPartL = blockPart.length;
1042
1043 files.forEach(function(f) {
1044 var file = f.file;
1045 if (file.substr(0, blockPartL) !== blockPart) return;
1046
1047 var suffix = file.substr(blockPartL - 1);
1048
1049 items.push(f, {
1050 block: block,
1051 suffix: suffix,
1052 tech: _this.suffixToTech[suffix]
1053 });
1054 });
1055
1056 dirs.forEach(function(d) {
1057 var dir = d.file;
1058 if (_this.isElemDir(dir)) return _this.scanElem(path, block, dir, items);
1059 if (_this.isModDir(dir)) return _this.scanMod(path, block, null, dir, items);
1060 if (dir.substr(0, blockPartL) !== blockPart) return;
1061
1062 var suffix = dir.substr(blockPartL - 1);
1063
1064 items.push(d, {
1065 block: block,
1066 suffix: suffix,
1067 tech: _this.suffixToTech[suffix]
1068 });
1069
1070 files = [];
1071 bemUtil.getDirsFilesSync(PATH.join(path, block, dir), files, files);
1072
1073 files.forEach(function(file) {
1074 var suffix = (dir + PATH.dirSep + file.file).substr(blockPartL - 1);
1075
1076 items.push(file, {
1077 block: block,
1078 suffix: suffix,
1079 tech: _this.suffixToTech[suffix]
1080 });
1081 });
1082 });
1083 },
1084
1085 isElemDir: function(dir) {
1086 return dir[0] === '_' && dir[1] === '_' && !~dir.indexOf('.');
1087 },
1088
1089 blockElemFileSeparator: '__',
1090 elemDirPrefix: '__',
1091
1092 scanElem: function(path, block, elem, items) {
1093 var _this = this,
1094 dir = path + PATH.dirSep + block + PATH.dirSep + elem,
1095 dirs = [], files = [];
1096
1097 bemUtil.getDirsFilesSync(dir, dirs, files);
1098
1099 var blockPart = block + _this.blockElemFileSeparator + elem.substr(_this.elemDirPrefix.length) + '.',
1100 blockPartL = blockPart.length,
1101 prefixLen = _this.elemDirPrefix.length;
1102
1103 files.forEach(function(f) {
1104 var file = f.file;
1105 if (file.substr(0, blockPartL) !== blockPart) return;
1106
1107 var suffix = file.substr(blockPartL - 1);
1108
1109 items.push(f, {
1110 block: block,
1111 elem: elem.substr(prefixLen),
1112 suffix: suffix,
1113 tech: _this.suffixToTech[suffix]
1114 });
1115 });
1116
1117 dirs.forEach(function(d) {
1118 if (_this.isModDir(d.file)) return _this.scanMod(path, block, elem, d.file, items);
1119 if (d.file.substr(0, blockPartL) !== blockPart) return;
1120
1121 var suffix = d.file.substr(blockPartL - 1);
1122
1123 items.push(d, {
1124 block: block,
1125 elem: elem.substr(prefixLen),
1126 suffix: suffix,
1127 tech: _this.suffixToTech[suffix]
1128 });
1129
1130 files = [];
1131
1132 bemUtil.getDirsFilesSync(PATH.join(dir, d.file), null, files);
1133
1134 files.forEach(function(file) {
1135 var suffix = (d.file + PATH.dirSep + file.file).substr(blockPartL - 1);
1136
1137 items.push(file, {
1138 block: block,
1139 elem: elem.substr(prefixLen),
1140 suffix: suffix,
1141 tech: _this.suffixToTech[suffix]
1142 });
1143 });
1144 });
1145 },
1146
1147 isModDir: function(dir) {
1148 return dir[0] === '_' && dir[1] !== '_';
1149 },
1150
1151 scanMod: function(path, block, elem, mod, items) {
1152 var _this = this,
1153 dir = path + PATH.dirSep + block + PATH.dirSep + (elem?elem+PATH.dirSep:'') + mod,
1154 dirs = [], files = [];
1155
1156 bemUtil.getDirsFilesSync(dir, dirs, files);
1157
1158 var blockPart = block + (elem?_this.blockElemFileSeparator + elem.substr(_this.elemDirPrefix.length):'') + mod,
1159 blockPartL = blockPart.length;
1160
1161 files.forEach(function(f) {
1162 var file = f.file;
1163 if (file.substr(0, blockPartL) !== blockPart) return;
1164
1165 var val,
1166 modval = file.substr(blockPartL);
1167
1168 if (modval[0] === '_') val = modval.substr(1);
1169 else if (modval[0] !== '.') return;
1170
1171 var suffix = modval.substr(modval.indexOf('.')),
1172 item = {
1173 block: block,
1174 mod: mod.substr(1),
1175 suffix: suffix,
1176 tech: _this.suffixToTech[suffix]
1177 };
1178
1179 if (elem) item.elem = elem.substr(_this.elemDirPrefix.length);
1180 if (val) item.val = val.substr(0, val.indexOf('.'));
1181
1182 items.push(f, item);
1183 });
1184
1185 dirs.forEach(function(d) {
1186 if (d.file.substr(0, blockPartL) !== blockPart) return;
1187
1188 var val,
1189 modval = d.file.substr(blockPartL);
1190
1191 if (modval[0] === '_') val = modval.substr(1);
1192 else if (modval[0] !== '.') return;
1193
1194 var suffix = modval.substr(modval.indexOf('.')),
1195 item = {
1196 block: block,
1197 mod: mod.substr(1),
1198 suffix: suffix,
1199 tech: _this.suffixToTech[suffix]
1200 };
1201
1202 if (elem) item.elem = elem.substr(_this.elemDirPrefix.length);
1203 if (val) item.val = val.substr(0, val.indexOf('.'));
1204
1205 items.push(d, item);
1206
1207 files = [];
1208
1209 bemUtil.getDirsFilesSync(PATH.join(dir, d.file), null, files);
1210
1211 files.forEach(function(file) {
1212 var suffix = modval.substr(modval.indexOf('.')) + PATH.dirSep + file.file,
1213 item = {
1214 block: block,
1215 mod: mod.substr(1),
1216 suffix: suffix,
1217 tech: _this.suffixToTech[suffix]
1218 };
1219
1220 if (elem) item.elem = elem.substr(_this.elemDirPrefix.length);
1221 if (val) item.val = val.substr(0, val.indexOf('.'));
1222
1223 items.push(file, item);
1224 });
1225 });
1226 },
1227
1228 /**
1229 * Creates preconfigured introspection functions.
1230 *
1231 * @param {Object} [opts] Introspector options.
1232 * @param {String} [opts.from] Relative path to subdirectory of level directory to start introspection from.
1233 * @param {Function} [opts.init] Function to return initial value of introspection.
1234 * @param {Function} [opts.filter] Function to filter paths to introspect, must return {Boolean}.
1235 * @param {Function} [opts.matcher] Function to perform match of paths, must return introspected value.
1236 * @param {Function} [opts.creator] Function to modify introspection object with matched value, must return new introspection.
1237 * @return {Function} Introspection function.
1238 */
1239 createIntrospector: function(opts) {
1240
1241 var level = this;
1242
1243 if (!opts) opts = {
1244 opts: false
1245 };
1246
1247 // clone opts
1248 opts = bemUtil.extend({}, opts);
1249
1250 // set default options
1251 opts.from || (opts.from = '.');
1252
1253 // initial value initializer
1254 opts.init || (opts.init = function() {
1255 return [];
1256 });
1257
1258 // paths filter function
1259 opts.filter || (opts.filter = function(path) {
1260 return !this.isIgnorablePath(path);
1261 });
1262
1263 // matcher function
1264 opts.matcher || (opts.matcher = function(path) {
1265 return this.matchAny(path);
1266 });
1267
1268 // result creator function
1269 opts.creator || (opts.creator = function(res, match) {
1270 if (match && match.tech) res.push(match);
1271 return res;
1272 });
1273
1274 /**
1275 * Introspection function.
1276 *
1277 * @param {String} [from] Relative path to subdirectory of level directory to start introspection from.
1278 * @param {*} [res] Initial introspection value to extend.
1279 * @return {*}
1280 */
1281 return function(from, res) {
1282
1283 if (opts.opts === false) {
1284 level.scanFiles();
1285 return level.files.blocks;
1286 }
1287
1288 from = PATH.resolve(level.dir, from || opts.from);
1289 res || (res = opts.init.call(level));
1290
1291 bemUtil.fsWalkTree(from, function(path) {
1292 res = opts.creator.call(level, res, opts.matcher.call(level, path));
1293 },
1294 opts.filter,
1295 level);
1296
1297 return res;
1298
1299 };
1300
1301 },
1302
1303 _ignorePathRe: /\.(svn|git)$/,
1304
1305 /**
1306 * Check path if it must be ignored during introspection.
1307 *
1308 * @param {String} path Path to check.
1309 * @return {Boolean} True if path must be ignored.
1310 */
1311 isIgnorablePath: function(path) {
1312 return this._ignorePathRe.test(path);
1313 },
1314
1315 _mergeMatchToDecl: function(match, decl) {
1316 var blocks, elems, mods, vals,
1317 techAdded = false,
1318 addTech = function(o) {
1319 if(!techAdded && match.tech) {
1320 o.techs = [{ name: match.tech }];
1321 techAdded = true;
1322 }
1323 return o;
1324 };
1325
1326 match.val &&
1327 (vals = [addTech({name: match.val})]);
1328 match.mod && match.val &&
1329 (mods = [addTech({name: match.mod, vals: vals})]);
1330 match.mod && !match.val &&
1331 (mods = [addTech({name: match.mod})]);
1332 match.elem &&
1333 (elems = [addTech({name: match.elem, mods: mods})]) &&
1334 (blocks = [addTech({name: match.block, elems: elems})]);
1335 !match.elem &&
1336 (blocks = [addTech({name: match.block, mods: mods})]);
1337
1338 return bemUtil.mergeDecls(decl, blocks);
1339 }
1340
1341});