Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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