From 08b6eafabdddd52e43c93f53fad340983f7f0138 Mon Sep 17 00:00:00 2001 From: Fedor Indutny Date: Thu, 18 Apr 2013 15:30:11 -0400 Subject: [PATCH] initial --- .gitignore | 2 + README.md | 1 + lib/bemhtml.js | 15 ++ lib/bemhtml/api.js | 13 ++ lib/bemhtml/compiler.js | 405 ++++++++++++++++++++++++++++++++++ lib/bemhtml/i-bem.js | 475 ++++++++++++++++++++++++++++++++++++++++ lib/bemhtml/runtime.js | 145 ++++++++++++ package.json | 21 ++ test/api-test.js | 38 ++++ 9 files changed, 1115 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 lib/bemhtml.js create mode 100644 lib/bemhtml/api.js create mode 100644 lib/bemhtml/compiler.js create mode 100644 lib/bemhtml/i-bem.js create mode 100644 lib/bemhtml/runtime.js create mode 100644 package.json create mode 100644 test/api-test.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..91fa8cf8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/node_modules/ +npm-debug.log diff --git a/README.md b/README.md new file mode 100644 index 00000000..93312d24 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# BEM.js diff --git a/lib/bemhtml.js b/lib/bemhtml.js new file mode 100644 index 00000000..168609b1 --- /dev/null +++ b/lib/bemhtml.js @@ -0,0 +1,15 @@ +var bemhtml = exports; + +// Runtime +bemhtml.runtime = require('./bemhtml/runtime'); + +// i-bem +bemhtml.ibem = require('./bemhtml/i-bem'); + +// Compiler +bemhtml.Compiler = require('./bemhtml/compiler').Compiler; + +// API functions +bemhtml.translate = require('./bemhtml/api').translate; +bemhtml.generate = require('./bemhtml/api').generate; +bemhtml.compile = require('./bemhtml/api').compile; diff --git a/lib/bemhtml/api.js b/lib/bemhtml/api.js new file mode 100644 index 00000000..b76c606e --- /dev/null +++ b/lib/bemhtml/api.js @@ -0,0 +1,13 @@ +var bemhtml = require('../bemhtml'); + +exports.translate = function translate(ast, options) { + return new bemhtml.Compiler(options).translate(ast); +}; + +exports.generate = function generate(code, options) { + return new bemhtml.Compiler(options).generate(code); +}; + +exports.compile = function compile(code, options) { + return new bemhtml.Compiler(options).compile(code); +}; diff --git a/lib/bemhtml/compiler.js b/lib/bemhtml/compiler.js new file mode 100644 index 00000000..9caaa162 --- /dev/null +++ b/lib/bemhtml/compiler.js @@ -0,0 +1,405 @@ +var bemhtml = require('../bemhtml'); +var assert = require('assert'); +var vm = require('vm'); +var esprima = require('esprima'); +var estraverse = require('estraverse'); +var uglify = require('uglify-js'); +var xjst = require('xjst'); + +function Compiler(options) { + this.options = options || {}; + + this.matches = { + match: true, block: true, elem: true, mode: true, + def: true, tag: true, attrs: true, cls: true, + js: true, jsAttr: true, bem: true, mix: true, content: true + }; +}; +exports.Compiler = Compiler; + +var ibemAst = null; + +Compiler.prototype.translate = function translate(ast) { + // Lazily parse i-bem + if (!ibemAst) { + ibemAst = esprima.parse('function ibem() {' + bemhtml.ibem + '\n}'); + ibemAst = ibemAst.body[0].body.body; + } + + // Add i-bem block to the AST + assert.equal(ast.type, 'Program'); + ast = { + type: 'Program', + body: ibemAst.concat(ast.body) + }; + + // Ok, I admit it. Translation process is a bit fucked. + var self = this; + var allowed = { + Program: true, + ExpressionStatement: true, + CallExpression: true, + MemberExpression: true + }; + ast = estraverse.replace(ast, { + enter: function(ast, parent, notify) { + // Do not get too deep + if (!allowed[ast.type]) { + this.skip(); + } + }, + leave: function(ast) { + // 1. mark all match calls + ast = self.markMatches(ast); + + // 2. replace all custom matches with match() calls + ast = self.replaceCustom(ast); + + // 3. Merge match(cond).match(cond) into match(cond, cond) and + // match(cond)(match(cond)) into match()(match(cond, cond) + ast = self.mergeMatches(ast); + + return ast; + } + }); + + // 4. Flatten every statement and replace local/apply/applyNext/applyCtx + for (var i = 0; i < ast.body.length; i++) { + var stmt = ast.body[i]; + if (stmt.type !== 'ExpressionStatement' || + !stmt.expression.bemMarked) { + continue; + } + + var exprs = this.flatten(stmt.expression); + ast.body.splice.apply(ast.body, [ i, 1 ].concat(exprs.map(function(expr) { + return { + type: 'ExpressionStatement', + expression: expr + } + }))); + i += exprs.length - 1; + } + + return xjst.translate(ast, this.options); +}; + +Compiler.prototype.markMatches = function markMatches(ast) { + // Propagate marks through member expressions + if (ast.type === 'MemberExpression' && + ast.object.type === 'CallExpression' && ast.object.bemMarked) { + return { + type: 'MemberExpression', + computed: ast.computed, + object: ast.object, + property: ast.property, + bemMarked: true + }; + } + + if (ast.type !== 'CallExpression') return ast; + + // Propagate marks through calls + if (ast.callee.type === 'CallExpression' && ast.callee.bemMarked) { + return { + type: 'CallExpression', + callee: ast.callee, + arguments: ast.arguments, + bemMarked: true + }; + } + + // match() + // NOTE: Marks are created here + if (ast.callee.type === 'Identifier') { + var name = ast.callee.name; + if (!this.matches[name]) return ast; + return { + type: 'CallExpression', + callee: ast.callee, + arguments: ast.arguments, + bemMarked: true + }; + } + + // .match() + if (ast.callee.type === 'MemberExpression' && ast.callee.bemMarked) { + var type = ast.callee.property.type; + assert(type === 'Literal' || type === 'Identifier', + 'match().prop could be only literal or identifier'); + var prop = ast.callee.property.name || ast.callee.property.value; + if (!this.matches[prop]) return ast; + return { + type: 'CallExpression', + callee: ast.callee, + arguments: ast.arguments, + bemMarked: true + }; + } + + return ast; +}; + +Compiler.prototype.getBinop = function getBinop(name, args) { + var lhs; + var rhs; + if (name === 'block' || name === 'elem' || name === 'mode') { + lhs = name === 'mode' ? '_mode' : name; + rhs = args[0]; + assert.equal(args.length, 1, + 'block/elem/mode predicates must have only one argument'); + } else { + // Mode predicates + assert.equal(args.length, 0, + 'mode literal predicates can\'t have arguments'); + lhs = '_mode'; + rhs = { type: 'Literal', value: name === 'def' ? 'default' : name }; + } + assert(lhs && rhs); + + return { + type: 'BinaryExpression', + operator: '===', + left: { + type: 'MemberExpression', + computed: false, + object: { type: 'ThisExpression' }, + property: { type: 'Identifier', name: lhs } + }, + right: rhs + }; +}; + +Compiler.prototype.replaceCustom = function replaceCustom(ast) { + if (ast.type !== 'CallExpression' || !ast.bemMarked) return ast; + + var callee = ast.callee; + if (callee.type === 'Identifier') { + var name = callee.name; + + if (!this.matches[name]) { + return ast; + } + + // Just replace predicates + if (name === 'match') return ast; + + return { + type: 'CallExpression', + callee: { type: 'Identifier', name: 'match' }, + arguments: [this.getBinop(name, ast.arguments)], + bemMarked: true + }; + } else if (callee.type === 'MemberExpression') { + var property = callee.property; + var name = property.name || property.value; + if (!this.matches[name]) { + return ast; + } + + // Just replace predicates + if (name === 'match') return ast; + + return { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + computed: false, + object: callee.object, + property: { type: 'Identifier', name: 'match' } + }, + arguments: [this.getBinop(name, ast.arguments)], + bemMarked: true + }; + } + + return ast; +}; + +Compiler.prototype.mergeMatches = function mergeMatches(ast) { + if (ast.type !== 'CallExpression' || !ast.bemMarked) return ast; + + // (match(...).match)(...) => match(...) + if (ast.callee.type === 'MemberExpression') { + var obj = ast.callee.object; + assert.equal(obj.type, 'CallExpression'); + ast = { + type: 'CallExpression', + callee: obj.callee, + arguments: ast.arguments.concat(obj.arguments), + bemMarked: true + }; + } + + return ast; +}; + +Compiler.prototype.flatten = function flatten(expr) { + function dive(expr) { + // At this point only match(...)(match(...)(...), body) expressions are + // present. + assert.equal(expr.type, 'CallExpression'); + assert.equal(expr.callee.type, 'CallExpression'); + var predicates = expr.callee.arguments; + + var res = []; + + // Visit sub-templates and bodies + expr.arguments.forEach(function(arg) { + if (arg.bemMarked) { + dive(arg).forEach(function(t) { + res.push({ + predicates: predicates.concat(t.predicates), + body: t.body + }); + }); + } else { + // Body + res.push({ + predicates: predicates, + body: arg + }); + } + }); + + return res; + } + + return dive(expr).map(function(t) { + return { + type: 'CallExpression', + callee: { + type: 'CallExpression', + callee: { type: 'Identifier', name: 'template' }, + arguments: t.predicates + }, + arguments: [ this.replaceBody(t.body) ] + }; + }, this); +}; + +Compiler.prototype.replaceBody = function replaceBody(body) { + var self = this; + + return estraverse.replace(body, { + leave: function(node) { + if (node.type !== 'CallExpression' || + node.callee.type !== 'CallExpression') return; + + var callee = node.callee; + + // apply(ctx)(locals) + if (callee.callee.type === 'Identifier') { + var name = callee.callee.name; + if (name !== 'apply' && + name !== 'applyNext' && + name !== 'applyCtx') { + return; + } + + return name === 'applyCtx' ? self.replaceApplyCtx(node) : + self.replaceApply(node); + // local(ctx)(locals)(body) + } else if (callee.callee.type === 'CallExpression' && + callee.callee.callee.type === 'Identifier') { + var name = callee.callee.callee.name; + if (name !== 'local') return; + + return self.replaceLocal(node); + } + } + }); +}; + +Compiler.prototype.getModeObj = function getModeObj(mode) { + return { + type: 'ObjectExpression', + properties: [{ + type: 'Property', + key: { type: 'Literal', value: '_mode' }, + value: mode, + kind: 'init' + }] + }; +}; + +Compiler.prototype.replaceApplyCtx = function replaceApplyCtx(ast) { + // (applyCtx(this))(newCtx) => (apply(this))({ _mode: '', ctx: newCtx }) + assert.equal(ast.arguments.length, 1, 'applyCtx() must have one argument'); + return { + type: 'CallExpression', + callee: { + type: 'CallExpression', + callee: { type: 'Identifier', name: 'apply' }, + arguments: ast.callee.arguments + }, + arguments: [{ + type: 'ObjectExpression', + properties: [{ + type: 'Property', + key: { type: 'Literal', value: '_mode' }, + value: { type: 'Literal', value: '' }, + kind: 'init' + }, { + type: 'Property', + key: { type: 'Literal', value: 'ctx' }, + value: ast.arguments[0], + kind: 'init' + }] + }] + }; +}; + +Compiler.prototype.replaceApply = function replaceApply(ast) { + // (apply(this))(mode, {}) + return { + type: 'CallExpression', + callee: ast.callee, + arguments: ast.arguments.map(function(arg) { + if (arg.type !== 'Literal') return arg; + return this.getModeObj(arg); + }, this) + }; +}; + +Compiler.prototype.replaceLocal = function replaceLocal(ast) { + // (local(this))(mode, {})(body) + return { + type: 'CallExpression', + callee: { + type: 'CallExpression', + callee: ast.callee.callee, + arguments: ast.callee.arguments.map(function(arg) { + if (arg.type !== 'Literal') return arg; + return this.getModeObj(arg); + }, this) + }, + arguments: ast.arguments + }; +}; + +Compiler.prototype.generate = function generate(code) { + if (this.options['no-opt'] || this.options.optimize === false) { + return xjst.generate(bemhtml.runtime + ';\n' + + bemhtml.ibem + ';\n' + + code + ';\n' + + '__$flush();\n', + this.options); + } + + var ast = esprima.parse(code); + + ast = this.translate(ast); + + return uglify.AST_Node.from_mozilla_ast(ast).print_to_string({ beautify: true }); +}; + +Compiler.prototype.compile = function compile(code) { + var out = this.generate(code), + exports = {}; + + require('fs').writeFileSync('/tmp/1.js', out); + vm.runInNewContext(out, { exports: exports, console: console }); + + return exports; +}; diff --git a/lib/bemhtml/i-bem.js b/lib/bemhtml/i-bem.js new file mode 100644 index 00000000..80ff20bc --- /dev/null +++ b/lib/bemhtml/i-bem.js @@ -0,0 +1,475 @@ +module.exports = function() { + +if (this.$override) (function() { + +var BEM_ = {}, + toString = Object.prototype.toString, + SHORT_TAGS = { // хэш для быстрого определения, является ли тэг коротким + area : 1, base : 1, br : 1, col : 1, command : 1, embed : 1, hr : 1, img : 1, + input : 1, keygen : 1, link : 1, meta : 1, param : 1, source : 1, wbr : 1 }; + +/** @fileOverview - module for internal BEM helpers */ +/** @requires BEM */ + +(function(BEM, undefined) { + +/** + * Separator for modifiers and their values + * @const + * @type String + */ +var MOD_DELIM = '_', + +/** + * Separator between block names and a nested element + * @const + * @type String + */ + ELEM_DELIM = '__', + +/** + * Pattern for acceptable names of elements and modifiers + * @const + * @type String + */ + NAME_PATTERN = '[a-zA-Z0-9-]+'; + +function buildModPostfix(modName, modVal, buffer) { + + buffer.push(MOD_DELIM, modName, MOD_DELIM, modVal); + +} + +function buildBlockClass(name, modName, modVal, buffer) { + + buffer.push(name); + modVal && buildModPostfix(modName, modVal, buffer); + +} + +function buildElemClass(block, name, modName, modVal, buffer) { + + buildBlockClass(block, undefined, undefined, buffer); + buffer.push(ELEM_DELIM, name); + modVal && buildModPostfix(modName, modVal, buffer); + +} + +BEM.INTERNAL = { + + NAME_PATTERN : NAME_PATTERN, + + MOD_DELIM : MOD_DELIM, + ELEM_DELIM : ELEM_DELIM, + + buildModPostfix : function(modName, modVal, buffer) { + + var res = buffer || []; + buildModPostfix(modName, modVal, res); + return buffer? res : res.join(''); + + }, + + /** + * Builds the class for a block or element with a modifier + * @private + * @param {String} block Block name + * @param {String} [elem] Element name + * @param {String} [modName] Modifier name + * @param {String} [modVal] Element name + * @param {Array} [buffer] Buffer + * @returns {String|Array} Class or buffer string (depending on whether the buffer parameter is present) + */ + buildClass : function(block, elem, modName, modVal, buffer) { + + var typeOf = typeof modName; + if(typeOf == 'string') { + if(typeof modVal != 'string') { + buffer = modVal; + modVal = modName; + modName = elem; + elem = undefined; + } + } else if(typeOf != 'undefined') { + buffer = modName; + modName = undefined; + } else if(elem && typeof elem != 'string') { + buffer = elem; + elem = undefined; + } + + if(!(elem || modName || buffer)) { // оптимизация для самого простого случая + return block; + } + + var res = buffer || []; + + elem? + buildElemClass(block, elem, modName, modVal, res) : + buildBlockClass(block, modName, modVal, res); + + return buffer? res : res.join(''); + + }, + + /** + * Builds modifier classes + * @private + * @param {String} block Block name + * @param {String} [elem] Element name + * @param {Object} [mods] Modifier name + * @param {Array} [buffer] Buffer + * @returns {String|Array} Class or buffer string (depending on whether the buffer parameter is present) + */ + buildModsClasses : function(block, elem, mods, buffer) { + + var res = buffer || []; + + if(mods) { + var modName; // TODO: разобраться с OmetaJS и YUI Compressor + for(modName in mods) { + if(!mods.hasOwnProperty(modName)) continue; + + var modVal = mods[modName]; + if (modVal === null) continue; + + modVal = mods[modName] + ''; + if (!modVal) continue; + + res.push(' '); + if (elem) { + buildElemClass(block, elem, modName, modVal, res) + } else { + buildBlockClass(block, modName, modVal, res); + } + } + } + + return buffer? res : res.join(''); + + }, + + /** + * Builds full classes for a block or element with modifiers + * @private + * @param {String} block Block name + * @param {String} [elem] Element name + * @param {Object} [mods] Modifier name + * @param {Array} [buffer] Buffer + * @returns {String|Array} Class or buffer string (depending on whether the buffer parameter is present) + */ + buildClasses : function(block, elem, mods, buffer) { + + var res = buffer || []; + + elem? + buildElemClass(block, elem, undefined, undefined, res) : + buildBlockClass(block, undefined, undefined, res); + + this.buildModsClasses(block, elem, mods, buffer); + + return buffer? res : res.join(''); + + } + +}; + +})(BEM_); + +var buildEscape = (function() { + var ts = { '"': '"', '&': '&', '<': '<', '>': '>' }, + f = function(t) { return ts[t] || t }; + return function(r) { + r = new RegExp(r, 'g'); + return function(s) { return ('' + s).replace(r, f) } + } +})(); + +function BEMContext(context, apply_) { + this.ctx = typeof context === null ? '' : context; + this.apply = apply_; + this._buf = []; + this._ = this; + + // Stub out fields that will be used later + this._start = true; + this._mode = ''; + this._listLength = 0; + this._notNewList = false; + this.position = 0; + this.block = undefined; + this.elem = undefined; + this.mods = undefined; + this.elemMods = undefined; +}; + +BEMContext.prototype.isArray = function isArray(obj) { + return toString.call(obj) === "[object Array]"; +}; + +BEMContext.prototype.isSimple = function isSimple(obj) { + var t = typeof obj; + return t === 'string' || t === 'number' || t === 'boolean'; +}; + +BEMContext.prototype.isShortTag = function isShortTag(t) { + return SHORT_TAGS.hasOwnProperty(t); +}; + +BEMContext.prototype.extend = function extend(o1, o2) { + if(!o1 || !o2) return o1 || o2; + var res = {}, n; + for(n in o1) o1.hasOwnProperty(n) && (res[n] = o1[n]); + for(n in o2) o2.hasOwnProperty(n) && (res[n] = o2[n]); + return res; +}; + +BEMContext.prototype.identify = (function() { + var cnt = 0, + id = BEM_.__id = (+new Date()), + expando = '__' + id, + get = function() { return 'uniq' + id + ++cnt; }; + return function(obj, onlyGet) { + if(!obj) return get(); + if(onlyGet || obj[expando]) return obj[expando]; + else return (obj[expando] = get()); + }; +})(); + +BEMContext.prototype.xmlEscape = buildEscape('[&<>]'); +BEMContext.prototype.attrEscape = buildEscape('["&<>]'); + +BEMContext.prototype.BEM = BEM_; + +BEMContext.prototype.isFirst = function isFirst() { + return this.position === 1; +}; + +BEMContext.prototype.isLast = function isLast() { + return this.position === this._listLength; +}; + +BEMContext.prototype.generateId = function generateId() { + return this.identify(this.ctx); +}; + +var oldApply = exports.apply; + +// Wrap xjst's apply and export our own +this.$exports.apply = BEMContext.apply = function _apply() { + var ctx = new BEMContext(this, oldApply); + ctx.apply(); + return ctx._buf.join(''); +}; + +}).call(this); // this.$override + +match(this._mode === '')( + match()(function() { + var vBlock = this.ctx.block, + vElem = this.ctx.elem, + block = this._currBlock || this.block; + + this.ctx || (this.ctx = {}); + + local(this)('default', { + _links: this.ctx.links || this._links, + block: vBlock || (vElem ? block : undefined), + _currBlock: vBlock || vElem ? undefined : block, + elem: this.ctx.elem, + mods: (vBlock ? this.ctx.mods : this.mods) || {}, + elemMods: this.ctx.elemMods || {} + })(function() { + (this.block || this.elem) ? + (this.position = (this.position || 0) + 1) : + this._listLength--; + apply(this)(); + }); + }), + + match(function() { return this._.isArray(this.ctx) })(function() { + var v = this.ctx, + l = v.length, + i = 0, + prevPos = this.position, + prevNotNewList = this._notNewList; + + if(prevNotNewList) { + this._listLength += l - 1; + } else { + this.position = 0; + this._listLength = l; + } + + this._notNewList = true; + + while(i < l) { + var newCtx = v[i++]; + apply(this)({ ctx: newCtx === null ? '' : newCtx }); + } + + prevNotNewList || (this.position = prevPos); + }), + + match(!this.ctx)(function() { + this._listLength--; + }), + + match(function() { return this._.isSimple(this.ctx) })(function() { + this._listLength--; + + var ctx = this.ctx; + (ctx && ctx !== true || ctx === 0) && this._buf.push(ctx); + }) +); + +def()(function() { + var _this = this, + BEM_ = _this.BEM, + v = this.ctx, + buf = this._buf, + tag; + + tag = apply(this)('tag'); + typeof tag != 'undefined' || (tag = v.tag); + typeof tag != 'undefined' || (tag = 'div'); + + if(tag) { + var jsParams, js; + if(this.block && v.js !== false) { + js = apply(this)('js'); + js = js? this._.extend(v.js, js === true? {} : js) : v.js === true? {} : v.js; + js && ((jsParams = {})[BEM_.INTERNAL.buildClass(this.block, v.elem)] = js); + } + + buf.push('<', tag); + + var isBEM = apply(this)('bem'); + typeof isBEM != 'undefined' || (isBEM = typeof v.bem != 'undefined' ? v.bem : v.block || v.elem); + + var cls = apply(this)('cls'); + cls || (cls = v.cls); + + var addJSInitClass = v.block && jsParams; + if(isBEM || cls) { + buf.push(' class="'); + if(isBEM) { + + BEM_.INTERNAL.buildClasses(this.block, v.elem, v.elemMods || v.mods, buf); + + var mix = apply(this)('mix'); + v.mix && (mix = mix? mix.concat(v.mix) : v.mix); + + if(mix) { + var visited = {}; + + function visitedKey(block, elem) { + return (block || '') + '__' + (elem || ''); + } + + visited[visitedKey(this.block, this.elem)] = true; + + // Transform mix to the single-item array if it's not array + if (!this._.isArray(mix)) mix = [mix]; + for (var i = 0; i < mix.length; i++) { + var mixItem = mix[i], + hasItem = mixItem.block || mixItem.elem, + block = mixItem.block || mixItem._block || _this.block, + elem = mixItem.elem || mixItem._elem || _this.elem; + + hasItem && buf.push(' '); + BEM_.INTERNAL[hasItem? 'buildClasses' : 'buildModsClasses']( + block, + mixItem.elem || mixItem._elem || + (mixItem.block ? undefined : _this.elem), + mixItem.elemMods || mixItem.mods, + buf); + + if(mixItem.js) { + (jsParams || (jsParams = {}))[BEM_.INTERNAL.buildClass(block, mixItem.elem)] = mixItem.js === true? {} : mixItem.js; + addJSInitClass || (addJSInitClass = block && !mixItem.elem); + } + + // Process nested mixes + if (hasItem && !visited[visitedKey(block, elem)]) { + visited[visitedKey(block, elem)] = true; + var nestedMix = apply(this)({ + block: block, + elem: elem + }, 'mix'); + + if (nestedMix) { + for (var j = 0; j < nestedMix.length; j++) { + var nestedItem = nestedMix[j]; + if (!nestedItem.block && + !nestedItem.elem || + !visited[visitedKey( + nestedItem.block, + nestedItem.elem + )]) { + nestedItem._block = block; + nestedItem._elem = elem; + mix.splice(i + 1, 0, nestedItem); + } + } + } + } + } + } + } + + cls && buf.push(isBEM? ' ' : '', cls); + + addJSInitClass && buf.push(' i-bem'); + buf.push('"'); + } + + if(jsParams) { + var jsAttr = apply(this)('jsAttr'); + buf.push( + ' ', jsAttr || 'onclick', '="return ', + this._.attrEscape(JSON.stringify(jsParams)), + '"'); + } + + var attrs = apply(this)('attrs'); + attrs = this._.extend(attrs, v.attrs); // NOTE: возможно стоит делать массив, чтобы потом быстрее сериализовывать + if(attrs) { + var name; // TODO: разобраться с OmetaJS и YUI Compressor + for(name in attrs) { + if (attrs[name] === undefined) continue; + buf.push(' ', name, '="', this._.attrEscape(attrs[name]), '"'); + } + } + } + + if(this._.isShortTag(tag)) { + buf.push('/>'); + } else { + tag && buf.push('>'); + + var content = apply(this)('content'); + if(content || content === 0) { + var isBEM = this.block || this.elem; + apply(this, '', { + _notNewList: false, + position: isBEM ? 1 : this.position, + _listLength: isBEM ? 1 : this._listLength, + ctx: content + }); + } + + tag && buf.push(''); + } +}); + +tag()(undefined); +attrs()(undefined); +cls()(undefined); +js()(undefined); +jsAttr()(undefined); +bem()(undefined); +mix()(undefined); +content()(function() { this.ctx.content }); + +}.toString().replace(/^function\s*\(\)\s*{|}$/g, ''); // module.exports diff --git a/lib/bemhtml/runtime.js b/lib/bemhtml/runtime.js new file mode 100644 index 00000000..41dd3f31 --- /dev/null +++ b/lib/bemhtml/runtime.js @@ -0,0 +1,145 @@ +module.exports = function() { + var __$that = this, + __$queue = []; + + // Called after all matches + function __$flush() { + __$queue.filter(function(item) { + return !item.__$parent; + }).forEach(function(item) { + function apply(conditions, item) { + if (item && item.__$children) { + // Sub-template + var subcond = conditions.concat(item.__$cond); + item.__$children.forEach(function(child) { + apply(subcond, child); + }); + } else { + // Body + template.apply(null, conditions)(item); + } + } + apply([], item); + }); + }; + + // Matching + function match() { + function fn() { + var args = Array.prototype.slice.call(arguments); + + args.forEach(function(arg) { + if (arg && arg.__$children) { + // Sub-template + arg.__$parent = fn; + } + fn.__$children.push(arg); + }); + + // Handle match().match() + var res = fn; + while (res.__$parent) res = res.__$parent; + return res; + }; + __$queue.push(fn); + fn.__$children = []; + fn.__$parent = null; + fn.__$cond = Array.prototype.slice.call(arguments); + + fn.match = match; + fn.block = block; + fn.elem = elem; + fn.mode = mode; + fn.def = def; + fn.tag = tag; + fn.attrs = attrs; + fn.cls = cls; + fn.js = js; + fn.jsAttr = jsAttr; + fn.bem = bem; + fn.mix = mix; + fn.content = content; + + // match().match() + if (this && this.__$children) { + this.__$children.push(fn); + fn.__$parent = this; + } + + return fn; + }; + + function block(name) { + return match.call(this, __$that.block === name); + }; + + function elem(name) { + return match.call(this, __$that.elem === name); + }; + + function mode(name) { + return match.call(this, __$that._mode === name); + }; + + function def() { return mode.call(this, 'default'); }; + function tag() { return mode.call(this, 'tag'); }; + function attrs() { return mode.call(this,'attrs'); }; + function cls() { return mode.call(this, 'cls'); }; + function js() { return mode.call(this, 'js'); }; + function jsAttr() { return mode.call(this, 'jsAttr'); }; + function bem() { return mode.call(this, 'bem'); }; + function mix() { return mode.call(this, 'mix'); }; + function content() { return mode.call(this, 'content'); }; + + // Apply by mode, local by mode and applyCtx + apply = function(apply) { + return function(ctx) { + return function() { + var args = Array.prototype.map.call(arguments, function(arg) { + if (typeof arg === 'string') { + return { _mode: arg }; + } else { + return arg; + } + }); + return apply(ctx).apply(null, args); + }; + }; + }(apply); + + applyNext = function(applyNext) { + return function(ctx) { + return function() { + var args = Array.prototype.map.call(arguments, function(arg) { + if (typeof arg === 'string') { + return { _mode: arg }; + } else { + return arg; + } + }); + return applyNext(ctx).apply(null, args); + }; + }; + }(applyNext); + + local = function(local) { + return function(ctx) { + return function() { + var args = Array.prototype.map.call(arguments, function(arg) { + if (typeof arg === 'string') { + return { _mode: arg }; + } else { + return arg; + } + }); + return local(ctx).apply(null, args); + }; + }; + }(local); + + function applyCtx(ctx) { + return function(context) { + return applyNext(ctx)({ _mode: '', ctx: context }); + }; + }; +}.toString().replace(/^function\s*\(\)\s*{|}$/g, ''); diff --git a/package.json b/package.json new file mode 100644 index 00000000..ab66be03 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "bemhtml.js", + "version": "0.0.0", + "description": "ERROR: No README.md file found!", + "main": "lib/bemhtml.js", + "scripts": { + "test": "mocha --reporter spec test/*-test.js" + }, + "repository": "", + "author": "Fedor Indutny", + "license": "MIT", + "dependencies": { + "estraverse": "~1.1.1", + "esprima": "~1.0.2", + "ometajs": "~3.2.2", + "uglify-js": "~2.2.5" + }, + "devDependencies": { + "mocha": "~1.9.0" + } +} diff --git a/test/api-test.js b/test/api-test.js new file mode 100644 index 00000000..823d3d48 --- /dev/null +++ b/test/api-test.js @@ -0,0 +1,38 @@ +var bem = require('..'); +var assert = require('assert'); + +describe('BEM.js compiler', function() { + function test(fn, data, expected) { + var body = fn.toString().replace(/^function\s*\(\)\s*{|}$/g, ''); + var fns = [ + bem.compile(body, { optimize: false }), + bem.compile(body) + ]; + + fns.forEach(function(fn) { + assert.equal(fn.apply.call(data || {}), expected); + }); + } + + it('should compile example code', function() { + test(function() { + block('b1').tag()( + elem('e1')('a'), + elem('e2').match(function() { + return this.ctx.hooray(); + })(function() { + return apply(this)('mode', { 'a': 1 }); + }) + ); + }, { block: 'b1', elem: 'e1' }, ''); + }); + + it('should understand applyCtx', function() { + test(function() { + block('b1').content()(function() { + return applyCtx(this)({ block: 'b2' }); + }); + block('b2').tag()('li'); + }, { block: 'b1' }, '
  • '); + }); +});