Skip to content

Commit

Permalink
Merge pull request #1082 from wycats/decorators
Browse files Browse the repository at this point in the history
Decorators
  • Loading branch information
kpdecker committed Sep 1, 2015
2 parents 1c27408 + 6c45f49 commit 1aae321
Show file tree
Hide file tree
Showing 21 changed files with 453 additions and 19 deletions.
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"no-extra-boolean-cast": 2,
"no-extra-parens": 0,
"no-extra-semi": 2,
"no-func-assign": 2,
"no-func-assign": 0,

// Stylistic... might consider disallowing in the future
"no-inner-declarations": 0,
Expand Down
29 changes: 28 additions & 1 deletion docs/compiler-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,33 @@ interface CommentStatement <: Statement {
}
```


```java
interface Decorator <: Statement {
type: "Decorator";

path: PathExpression | Literal;
params: [ Expression ];
hash: Hash;

strip: StripFlags | null;
}

interface DecoratorBlock <: Statement {
type: "DecoratorBlock";
path: PathExpression | Literal;
params: [ Expression ];
hash: Hash;

program: Program | null;

openStrip: StripFlags | null;
closeStrip: StripFlags | null;
}
```

Decorator paths only utilize the `path.original` value and as a consequence do not support depthed evaluation.

### Expressions

```java
Expand Down Expand Up @@ -249,7 +276,7 @@ The `Handlebars.JavaScriptCompiler` object has a number of methods that may be c

- `parent` is the existing code in the path resolution
- `name` is the current path component
- `type` is the type of name being evaluated. May be one of `context`, `data`, `helper`, or `partial`.
- `type` is the type of name being evaluated. May be one of `context`, `data`, `helper`, `decorator`, or `partial`.

Note that this does not impact dynamic partials, which implementors need to be aware of. Overriding `VM.resolvePartial` may be required to support dynamic cases.

Expand Down
17 changes: 16 additions & 1 deletion lib/handlebars/base.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {createFrame, extend, toString} from './utils';
import Exception from './exception';
import {registerDefaultHelpers} from './helpers';
import {registerDefaultDecorators} from './decorators';
import logger from './logger';

export const VERSION = '3.0.1';
Expand All @@ -17,11 +18,13 @@ export const REVISION_CHANGES = {

const objectType = '[object Object]';

export function HandlebarsEnvironment(helpers, partials) {
export function HandlebarsEnvironment(helpers, partials, decorators) {
this.helpers = helpers || {};
this.partials = partials || {};
this.decorators = decorators || {};

registerDefaultHelpers(this);
registerDefaultDecorators(this);
}

HandlebarsEnvironment.prototype = {
Expand Down Expand Up @@ -54,6 +57,18 @@ HandlebarsEnvironment.prototype = {
},
unregisterPartial: function(name) {
delete this.partials[name];
},

registerDecorator: function(name, fn) {
if (toString.call(name) === objectType) {
if (fn) { throw new Exception('Arg not supported with multiple decorators'); }
extend(this.decorators, name);
} else {
this.decorators[name] = fn;
}
},
unregisterDecorator: function(name) {
delete this.decorators[name];
}
};

Expand Down
3 changes: 3 additions & 0 deletions lib/handlebars/compiler/code-gen.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ function CodeGen(srcFile) {
}

CodeGen.prototype = {
isEmpty() {
return !this.source.length;
},
prepend: function(source, loc) {
this.source.unshift(this.wrap(source, loc));
},
Expand Down
13 changes: 13 additions & 0 deletions lib/handlebars/compiler/compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,15 @@ Compiler.prototype = {
this.opcode('append');
},

DecoratorBlock(decorator) {
let program = decorator.program && this.compileProgram(decorator.program);
let params = this.setupFullMustacheParams(decorator, program, undefined),
path = decorator.path;

this.useDecorators = true;
this.opcode('registerDecorator', params.length, path.original);
},

PartialStatement: function(partial) {
this.usePartial = true;

Expand Down Expand Up @@ -201,6 +210,10 @@ Compiler.prototype = {
this.opcode('append');
}
},
Decorator(decorator) {
this.DecoratorBlock(decorator);
},


ContentStatement: function(content) {
if (content.value) {
Expand Down
11 changes: 9 additions & 2 deletions lib/handlebars/compiler/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,9 @@ export function prepareMustache(path, params, hash, open, strip, locInfo) {
let escapeFlag = open.charAt(3) || open.charAt(2),
escaped = escapeFlag !== '{' && escapeFlag !== '&';

let decorator = (/\*/.test(open));
return {
type: 'MustacheStatement',
type: decorator ? 'Decorator' : 'MustacheStatement',
path,
params,
hash,
Expand Down Expand Up @@ -124,12 +125,18 @@ export function prepareBlock(openBlock, program, inverseAndProgram, close, inver
validateClose(openBlock, close);
}

let decorator = (/\*/.test(openBlock.open));

program.blockParams = openBlock.blockParams;

let inverse,
inverseStrip;

if (inverseAndProgram) {
if (decorator) {
throw new Exception('Unexpected inverse block on decorator', inverseAndProgram);
}

if (inverseAndProgram.chain) {
inverseAndProgram.program.body[0].closeStrip = close.strip;
}
Expand All @@ -145,7 +152,7 @@ export function prepareBlock(openBlock, program, inverseAndProgram, close, inver
}

return {
type: 'BlockStatement',
type: decorator ? 'DecoratorBlock' : 'BlockStatement',
path: openBlock.path,
params: openBlock.params,
hash: openBlock.hash,
Expand Down
71 changes: 67 additions & 4 deletions lib/handlebars/compiler/javascript-compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ JavaScriptCompiler.prototype = {
this.name = this.environment.name;
this.isChild = !!context;
this.context = context || {
decorators: [],
programs: [],
environments: []
};
Expand All @@ -81,7 +82,7 @@ JavaScriptCompiler.prototype = {

this.compileChildren(environment, options);

this.useDepths = this.useDepths || environment.useDepths || this.options.compat;
this.useDepths = this.useDepths || environment.useDepths || environment.useDecorators || this.options.compat;
this.useBlockParams = this.useBlockParams || environment.useBlockParams;

let opcodes = environment.opcodes,
Expand All @@ -107,16 +108,43 @@ JavaScriptCompiler.prototype = {
throw new Exception('Compile completed with content left on stack');
}

if (!this.decorators.isEmpty()) {
this.useDecorators = true;

this.decorators.prepend('var decorators = container.decorators;\n');
this.decorators.push('return fn;');

if (asObject) {
this.decorators = Function.apply(this, ['fn', 'props', 'container', 'depth0', 'data', 'blockParams', 'depths', this.decorators.merge()]);
} else {
this.decorators.prepend('function(fn, props, container, depth0, data, blockParams, depths) {\n');
this.decorators.push('}\n');
this.decorators = this.decorators.merge();
}
} else {
this.decorators = undefined;
}

let fn = this.createFunctionContext(asObject);
if (!this.isChild) {
let ret = {
compiler: this.compilerInfo(),
main: fn
};
let programs = this.context.programs;

if (this.decorators) {
ret.main_d = this.decorators; // eslint-disable-line camelcase
ret.useDecorators = true;
}

let {programs, decorators} = this.context;
for (i = 0, l = programs.length; i < l; i++) {
if (programs[i]) {
ret[i] = programs[i];
if (decorators[i]) {
ret[i + '_d'] = decorators[i];
ret.useDecorators = true;
}
}
}

Expand Down Expand Up @@ -163,6 +191,7 @@ JavaScriptCompiler.prototype = {
// getContext opcode when it would be a noop
this.lastContext = 0;
this.source = new CodeGen(this.options.srcName);
this.decorators = new CodeGen(this.options.srcName);
},

createFunctionContext: function(asObject) {
Expand Down Expand Up @@ -561,6 +590,24 @@ JavaScriptCompiler.prototype = {
}
},

// [registerDecorator]
//
// On stack, before: hash, program, params..., ...
// On stack, after: ...
//
// Pops off the decorator's parameters, invokes the decorator,
// and inserts the decorator into the decorators list.
registerDecorator(paramSize, name) {
let foundDecorator = this.nameLookup('decorators', name, 'decorator'),
options = this.setupHelperArgs(name, paramSize);

this.decorators.push([
'fn = ',
this.decorators.functionCall(foundDecorator, '', ['fn', 'props', 'container', options]),
' || fn;'
]);
},

// [invokeHelper]
//
// On stack, before: hash, inverse, program, params..., ...
Expand Down Expand Up @@ -738,6 +785,7 @@ JavaScriptCompiler.prototype = {
child.index = index;
child.name = 'program' + index;
this.context.programs[index] = compiler.compile(child, options, this.context, !this.precompile);
this.context.decorators[index] = compiler.decorators;
this.context.environments[index] = child;

this.useDepths = this.useDepths || compiler.useDepths;
Expand Down Expand Up @@ -946,7 +994,16 @@ JavaScriptCompiler.prototype = {
},

setupParams: function(helper, paramSize, params) {
let options = {}, contexts = [], types = [], ids = [], param;
let options = {},
contexts = [],
types = [],
ids = [],
objectArgs = !params,
param;

if (objectArgs) {
params = [];
}

options.name = this.quotedString(helper);
options.hash = this.popStack();
Expand Down Expand Up @@ -985,6 +1042,10 @@ JavaScriptCompiler.prototype = {
}
}

if (objectArgs) {
options.args = this.source.generateArray(params);
}

if (this.trackIds) {
options.ids = this.source.generateArray(ids);
}
Expand All @@ -1009,9 +1070,11 @@ JavaScriptCompiler.prototype = {
this.useRegister('options');
params.push('options');
return ['options=', options];
} else {
} else if (params) {
params.push(options);
return '';
} else {
return options;
}
}
};
Expand Down
8 changes: 6 additions & 2 deletions lib/handlebars/compiler/printer.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,15 @@ PrintVisitor.prototype.Program = function(program) {
PrintVisitor.prototype.MustacheStatement = function(mustache) {
return this.pad('{{ ' + this.SubExpression(mustache) + ' }}');
};
PrintVisitor.prototype.Decorator = function(mustache) {
return this.pad('{{ DIRECTIVE ' + this.SubExpression(mustache) + ' }}');
};

PrintVisitor.prototype.BlockStatement = function(block) {
PrintVisitor.prototype.BlockStatement =
PrintVisitor.prototype.DecoratorBlock = function(block) {
let out = '';

out += this.pad('BLOCK:');
out += this.pad((block.type === 'DecoratorBlock' ? 'DIRECTIVE ' : '') + 'BLOCK:');
this.padding++;
out += this.pad(this.SubExpression(block));
if (block.program) {
Expand Down
2 changes: 2 additions & 0 deletions lib/handlebars/compiler/visitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,10 @@ Visitor.prototype = {
},

MustacheStatement: visitSubExpression,
Decorator: visitSubExpression,

BlockStatement: visitBlock,
DecoratorBlock: visitBlock,

PartialStatement: visitPartial,
PartialBlockStatement: function(partial) {
Expand Down
2 changes: 2 additions & 0 deletions lib/handlebars/compiler/whitespace-control.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ WhitespaceControl.prototype.Program = function(program) {
};

WhitespaceControl.prototype.BlockStatement =
WhitespaceControl.prototype.DecoratorBlock =
WhitespaceControl.prototype.PartialBlockStatement = function(block) {
this.accept(block.program);
this.accept(block.inverse);
Expand Down Expand Up @@ -124,6 +125,7 @@ WhitespaceControl.prototype.PartialBlockStatement = function(block) {
return strip;
};

WhitespaceControl.prototype.Decorator =
WhitespaceControl.prototype.MustacheStatement = function(mustache) {
return mustache.strip;
};
Expand Down
6 changes: 6 additions & 0 deletions lib/handlebars/decorators.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import registerInline from './decorators/inline';

export function registerDefaultDecorators(instance) {
registerInline(instance);
}

22 changes: 22 additions & 0 deletions lib/handlebars/decorators/inline.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {extend} from '../utils';

export default function(instance) {
instance.registerDecorator('inline', function(fn, props, container, options) {
let ret = fn;
if (!props.partials) {
props.partials = {};
ret = function(context, options) {
// Create a new partials stack frame prior to exec.
let original = container.partials;
container.partials = extend({}, original, props.partials);
let ret = fn(context, options);
container.partials = original;
return ret;
};
}

props.partials[options.args[0]] = options.fn;

return ret;
});
}
Loading

0 comments on commit 1aae321

Please sign in to comment.