diff --git a/lib/xjst/compiler/base.js b/lib/xjst/compiler/base.js index f94e76a..d369c8a 100644 --- a/lib/xjst/compiler/base.js +++ b/lib/xjst/compiler/base.js @@ -92,7 +92,7 @@ Compiler.prototype.generate = function generate(code) { ' // Just ignore any errors\n' + '}\n' + 'function templates(template, local, apply, applyNext, oninit, ' + - '__$$fetch) {\n' + + '__$$fetch, __$$set) {\n' + '/// -------------------------------------\n' + '/// ---------- Bootstrap end ------------\n' + '/// -------------------------------------\n' + @@ -312,7 +312,7 @@ Compiler.prototype.sanitize = function sanitize(stmt) { // Replace `this` with `__$ctx` Compiler.prototype.replaceThis = function replaceThis(stmt) { var ctx = this.ctx; - estraverse.replace(stmt, { + return estraverse.replace(stmt, { enter: function(node, parent, notify) { if (node.type === 'ThisExpression') { return ctx; @@ -322,7 +322,37 @@ Compiler.prototype.replaceThis = function replaceThis(stmt) { } } }); - return stmt; +}; + +Compiler.prototype.replaceFetch = function replaceFetch(stmt) { + var self = this; + return estraverse.replace(stmt, { + enter: function(node, parent, notify) { + if (node.type === 'CallExpression' && + node.callee.type === 'Identifier') { + if (node.callee.name !== '__$$fetch' && + node.callee.name !== '__$$set') { + return; + } + assert(node.arguments.length >= 1); + assert.equal(node.arguments[0].type, 'Literal'); + + var id = self.fetchGlobal(node.arguments[0].value); + + if (node.callee.name === '__$$fetch') { + return id; + } else { + assert.equal(node.arguments.length, 2); + return { + type: 'AssignmentExpression', + operator: '=', + left: id, + right: node.arguments[1] + }; + } + } + } + }); }; Compiler.prototype.jailVars = function jailVars(stmt) { @@ -491,7 +521,7 @@ Compiler.prototype.checkRef = function checkRef(expr) { } // Fastest case, just literal - if (cantBeRef(expr)) + if (!expr || cantBeRef(expr)) return { apply: [{ type: 'ReturnStatement', argument: expr }] }; // Simple case @@ -677,9 +707,24 @@ Compiler.prototype.sortGroup = function sortGroup(templates) { return out; }; -Compiler.prototype.registerGlobal = function registerGlobal(name) { - if (name !== '__proto__') - this.globals[name] = true; +Compiler.prototype.fetchGlobal = function fetchGlobal(name) { + var parts = name.split('.'); + var parent = '$$' + parts[0]; + + if (parent !== '__proto__') + this.globals[parent] = true; + + var ret = { type: 'Identifier', name: parent }; + for (var i = 1; i < parts.length; i++) { + ret = { + type: 'MemberExpression', + computed: true, + object: ret, + property: { type: 'Literal', value: parts[i] } + }; + } + + return ret; }; Compiler.prototype.registerExtension = function registerExtension(name) { @@ -770,7 +815,7 @@ Compiler.prototype.render = function render(program, bodyOnly) { var stmts = [], initializers = program.init.slice(), applyBody = program.other.map(function(stmt) { - return this.sanitize(stmt); + return this.replaceFetch(this.sanitize(stmt)); }, this), applyContext = { type: 'LogicalExpression', @@ -855,22 +900,6 @@ Compiler.prototype.render = function render(program, bodyOnly) { }] }); - // global variables - var globals = Object.keys(this.globals); - if (globals.length !== 0) { - stmts.push({ - type: 'VariableDeclaration', - kind: 'var', - declarations: globals.map(function(name) { - return { - type: 'VariableDeclarator', - id: { type: 'Identifier', name: name }, - init: null - }; - }) - }); - } - // exports.apply = apply stmts.push(apply); stmts.push({ @@ -948,6 +977,22 @@ Compiler.prototype.render = function render(program, bodyOnly) { // Render each template var out = this.renderArray(program.templates); + // global variables + var globals = Object.keys(this.globals); + if (globals.length !== 0) { + stmts.unshift({ + type: 'VariableDeclaration', + kind: 'var', + declarations: globals.map(function(name) { + return { + type: 'VariableDeclarator', + id: { type: 'Identifier', name: name }, + init: null + }; + }) + }); + } + /// Apply to the bottom if (out.apply) applyBody = applyBody.concat(out.apply); diff --git a/lib/xjst/compiler/entities/body.js b/lib/xjst/compiler/entities/body.js index c36e5e8..09b0519 100644 --- a/lib/xjst/compiler/entities/body.js +++ b/lib/xjst/compiler/entities/body.js @@ -148,6 +148,11 @@ Body.prototype.rollOutSpecific = function rollOutSpecific(node) { assert.equal(node.arguments.length, 1); assert.equal(node.arguments[0].type, 'Literal'); return this.rollOutFetch(node.arguments[0].value); + } else if (name === '__$$set') { + assert.equal(node.arguments.length, 2); + assert.equal(node.arguments[0].type, 'Literal'); + return this.rollOutSet(node.arguments[0].value, + node.arguments[1]); } // local(locals)(body) } else if (node.callee.type === 'CallExpression' && @@ -241,37 +246,32 @@ Body.prototype.rollOutLocal = function rollOutLocal(ast, changes, body) { }); } - // `local(null, { a: 1 })` should be translated to - // `var oldA = a; a = 1; ...; a = oldA;` - var isGlobal = changes.some(function(change) { - return change.type === 'Literal' && change.value === null; - }); - // Generate list of prop/value pairs changes.forEach(function(change) { if (change.type === 'Literal') return; assert.equal(change.type, 'ObjectExpression'); change.properties.forEach(function(property) { - var keys = (property.key.name || property.key.value).split('.'), - prop = keys.reduce(function(left, right, i, l) { - if (isGlobal && left === ctx) { - var sub = { type: 'Identifier', name: '$$' + right }; - - self.compiler.registerGlobal('$$' + right); - } else { - var sub = { - type: 'MemberExpression', - computed: false, - object: left, - property: { type: 'Identifier', name: right } - }; - - self.compiler.registerExtension(right); - } - - return sub; - }, ctx); + var keys = (property.key.name || property.key.value).split('.'); + var isGlobal = keys[0] === '$$global'; + if (isGlobal) + keys.shift(); + if (isGlobal) { + var prop = this.compiler.fetchGlobal(keys.join('.')); + } else { + var prop = keys.reduce(function(left, right, i, l) { + var sub = { + type: 'MemberExpression', + computed: false, + object: left, + property: { type: 'Identifier', name: right } + }; + + self.compiler.registerExtension(right); + + return sub; + }, ctx); + } addPair(this.compiler.sanitize(prop), this.compiler.sanitize(property.value)); @@ -289,10 +289,7 @@ Body.prototype.rollOutLocal = function rollOutLocal(ast, changes, body) { arguments: [] }; - if (isGlobal) - this.compiler.addChange([]); - else - this.compiler.addChange(predicates); + this.compiler.addChange(predicates); if (typeof body === 'function') { body = body.call(this); } @@ -333,7 +330,7 @@ Body.prototype.rollOutLocal = function rollOutLocal(ast, changes, body) { left = { type: 'MemberExpression', - computed: false, + computed: left.computed, object: tmp, property: left.property }; @@ -413,7 +410,16 @@ Body.prototype.rollOutLocal = function rollOutLocal(ast, changes, body) { }; Body.prototype.rollOutFetch = function rollOutFetch(id) { - return { type: 'Identifier', name: '$$' + id }; + return this.compiler.fetchGlobal(id); +}; + +Body.prototype.rollOutSet = function rollOutSet(id, value) { + return { + type: 'AssignmentExpression', + operator: '=', + left: this.compiler.fetchGlobal(id), + right: value + }; }; // Render body diff --git a/lib/xjst/compiler/entities/group.js b/lib/xjst/compiler/entities/group.js index c6e8b73..e3cab1a 100644 --- a/lib/xjst/compiler/entities/group.js +++ b/lib/xjst/compiler/entities/group.js @@ -44,7 +44,7 @@ Group.prototype._render = function render() { declarations: [{ type: 'VariableDeclarator', id: t, - init: this.predicate.expr + init: this.predicate.getExpr() }] }); diff --git a/lib/xjst/compiler/entities/map.js b/lib/xjst/compiler/entities/map.js index 312f908..8ce854c 100644 --- a/lib/xjst/compiler/entities/map.js +++ b/lib/xjst/compiler/entities/map.js @@ -10,13 +10,13 @@ function Map(compiler, pairs) { this.shareable = false; this.pairs = {}; if (pairs && pairs.length >= 1) { - this.predicate = pairs[0].predicate.expr; + this.predicate = pairs[0].predicate.getExpr(); this.predicateId = pairs[0].predicate.id; pairs.forEach(function(pair) { pair.bodies.forEach(function(body) { assert(pair.predicate.value !== null); - this.add(pair.predicate.expr, pair.predicate.value, body); + this.add(this.predicate, pair.predicate.value, body); }, this); }, this); } else { @@ -46,7 +46,7 @@ Map.prototype.mapChildren = function mapChildren(fn, ctx) { }; Map.prototype.add = function add(predicate, value, body) { - assert(value.type === 'Literal' && typeof value.value === 'string'); + assert(value.type === 'Literal'); if (this.predicate === null) { this.predicate = predicate; this.predicateId = this.compiler.getId(predicate); diff --git a/lib/xjst/compiler/entities/predicate.js b/lib/xjst/compiler/entities/predicate.js index a6eb05a..8bf6a38 100644 --- a/lib/xjst/compiler/entities/predicate.js +++ b/lib/xjst/compiler/entities/predicate.js @@ -31,11 +31,11 @@ Predicate.prototype.render = function render() { return { type: 'BinaryExpression', operator: '===', - left: this.expr, - right: this.value + left: this.getExpr(), + right: this.compiler.replaceFetch(this.value) }; } else { - return this.expr; + return this.getExpr(); } }; @@ -49,3 +49,7 @@ Predicate.prototype.simplify = function simplify() { this.value = null; } }; + +Predicate.prototype.getExpr = function getExpr() { + return this.compiler.replaceFetch(this.expr); +}; diff --git a/lib/xjst/utils.js b/lib/xjst/utils.js index 3500d72..d48f723 100644 --- a/lib/xjst/utils.js +++ b/lib/xjst/utils.js @@ -47,14 +47,6 @@ utils.run = function run(templates, context, globalCtx) { var backup = []; var args = Array.prototype.slice.call(arguments); - var isGlobal = args.some(function(change) { - return change === null; - }); - - // Lazily allocate global ctx - if (isGlobal && !globalCtx) - globalCtx = {}; - args.forEach(function(change) { if (change === null) return; @@ -63,8 +55,19 @@ utils.run = function run(templates, context, globalCtx) { var parts = key.split('.'), newValue = change[key], oldValue, + isGlobal = parts[0] === '$$global', subContext = isGlobal ? globalCtx : context; + if (isGlobal) { + parts.shift(); + + // Lazily allocate global ctx + if (!globalCtx) { + globalCtx = {}; + subContext = globalCtx; + } + } + // Dive inside for (var i = 0; i < parts.length - 1; i++) { subContext = subContext[parts[i]]; @@ -76,6 +79,7 @@ utils.run = function run(templates, context, globalCtx) { // Push old value to backup list backup.push({ + isGlobal: isGlobal, key: parts, value: oldValue }); @@ -87,7 +91,7 @@ utils.run = function run(templates, context, globalCtx) { // Rollback old values for (var i = backup.length - 1; i >= 0; i--) { - var subContext = isGlobal ? globalCtx : context, + var subContext = backup[i].isGlobal ? globalCtx : context, change = backup[i]; // Dive inside @@ -135,6 +139,11 @@ utils.run = function run(templates, context, globalCtx) { var parts = name.split('.'), value = globalCtx; + if (!value) { + globalCtx = {}; + value = globalCtx; + } + // Dive inside for (var i = 0; i < parts.length; i++) { value = value[parts[i]]; @@ -143,7 +152,26 @@ utils.run = function run(templates, context, globalCtx) { return value; } - templates.call(context, template, local, apply, applyNext, oninit, fetch); + function set(name, val) { + var parts = name.split('.'), + value = globalCtx; + + if (!value) { + globalCtx = {}; + value = globalCtx; + } + + // Dive inside + for (var i = 0; i < parts.length - 1; i++) { + value = value[parts[i]]; + } + value[parts[i]] = val; + + return value; + }; + + templates.call(context, template, local, apply, applyNext, oninit, fetch, + set); if (!last) { if (context.$init) return; diff --git a/test/unit/compiler-test.js b/test/unit/compiler-test.js index 80e0d25..c630559 100644 --- a/test/unit/compiler-test.js +++ b/test/unit/compiler-test.js @@ -287,11 +287,13 @@ describe('XJST Compiler', function () { it('should understand context local and fetch', function() { run(function() { - template()(function() { + template(function() { return __$$fetch('xkcd.dot') })(function() { return __$$fetch('xkcd.dot'); }); template()(function() { - return local(null, { xkcd: {}, 'xkcd.dot': 'yes' })(applyNext()); + return local(null, { '$$global.xkcd': {}, '$$global.xkcd.dot': 'yes' })( + applyNext() + ); }); }, {}, 'yes') });