Skip to content

Instantly share code, notes, and snippets.

@romannurik
Last active September 21, 2022 12:58
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save romannurik/a5bd5875d7cf648387ebfea0c0b9bdb7 to your computer and use it in GitHub Desktop.
Save romannurik/a5bd5875d7cf648387ebfea0c0b9bdb7 to your computer and use it in GitHub Desktop.
Nunjucks Block Call Extension (call a macro, pass content of sub-blocks as keyword args)
const TAG_NAME = 'blockcall';
const ARG_TAG_NAME = 'argblock';
class BlockCallExtension {
constructor(nunjucks) {
this.tags = [TAG_NAME];
this.nunjucks = nunjucks;
}
parse(parser, nodes, lexer) {
//setupDebuggableNodeNames(nodes);
let token = parser.nextToken();
// parse the call, i.e. foo.blah(1, 2, a=3)
let macroCall = parser.parsePrimary();
if (!macroCall instanceof nodes.FunCall) {
parser.fail('Expected a function call');
return;
}
// make the function name the first argument, to ensure
// it gets evaluated w/ CallExtension
macroCall.args.children.unshift(macroCall.name);
let parsedArgs = macroCall.args;
// let parsedArgs = parser.parseSignature(false, false);
parser.advanceAfterBlockEnd(token.value);
let parsedBodies = [];
let bodyArgNames = [];
let currentArgName = null;
while (true) {
let parsedBody = parser.parseUntilBlocks(`end${TAG_NAME}`, ARG_TAG_NAME);
let nextToken = parser.peekToken();
if (currentArgName) {
bodyArgNames.push(currentArgName);
parsedBodies.push(parsedBody);
}
if (nextToken.value == ARG_TAG_NAME) {
parser.nextToken();
// another arg, keep parsing
let argBlockArgs = parser.parseSignature(false, true);
currentArgName = argBlockArgs.children[0].value;
parser.advanceAfterBlockEnd(nextToken.value);
} else {
// no more args, stop here
parser.advanceAfterBlockEnd();
break;
}
}
let numBodies = parsedBodies.length;
// create a temporary field on this instance
// to handle this specific run when CallExtension is compiled
// and run.
// find a random field name
let tempMethodName;
while (!tempMethodName || this[tempMethodName]) {
tempMethodName = 'run_ ' + Math.ceil(Math.random() * 10000);
}
// create and register the field
this[tempMethodName] = (context, method, ...compiledArgsAndBodies) => {
// unregister
delete this[tempMethodName];
if (!method || typeof method !== 'function') {
parser.fail('Could not resolve function call: ' + nodeToStr(nodes, macroCall.name));
return;
}
let compiledArgs = compiledArgsAndBodies.slice(0, -numBodies);
let compiledBodies = compiledArgsAndBodies.slice(-numBodies);
// create a dict of keyword args that come from bodies
let bodyArgs = {};
for (let i = 0; i < numBodies; i++) {
// TODO: don't always mark safe?
bodyArgs[bodyArgNames[i]] = new this.nunjucks.runtime.SafeString(compiledBodies[i]());
}
// if (!(args[args.length-1] instanceof nodes.KeywordArgs)) {
// args.push(new nodes.KeywordArgs());
// }
// internal nunjucks-ism
bodyArgs.__keywords = true;
// TODO: switch to nodes.KeywordArgs?
// bodies become keyword args
let lastCompiledArg = compiledArgs.slice(-1);
if (lastCompiledArg.length && lastCompiledArg[0].__keywords) {
// last arg is already a keyword arg dict, simply extend it
Object.assign(lastCompiledArg[0], bodyArgs);
} else {
// last arg is not a keyword arg, push a new keyword arg
compiledArgs.push(bodyArgs);
}
//let body = args.pop();
let val = method(...compiledArgs);
return val;
};
// See above for notes about CallExtension
return new nodes.CallExtension(this, tempMethodName, parsedArgs, parsedBodies);
}
}
// make console.log() on classes in 'nodes' print real names instead of 'new_cls'
function setupDebuggableNodeNames(nodes) {
Object.keys(nodes).forEach(t => {
Object.defineProperty(nodes[t], 'name', {
get: () => t
});
});
}
// super limited functionality, currently only useful for printing LookupVal, Symbol, or Literal
function nodeToStr(nodes, node) {
if (node instanceof nodes.LookupVal) {
return nodeToStr(nodes, node.target) + '.' + nodeToStr(nodes, node.val);
} else if (node instanceof nodes.Symbol || node instanceof nodes.Literal) {
return node.value;
}
return node;
}
module.exports = BlockCallExtension;
<div>
<h1>Can pass in args as normal</h1>
<aside>
This is an example arg <em>sent as a block</em>.
</aside>
<main>
<p>And this is the main content</p>
</main>
</div>
const nunjucks = require('nunjucks');
const BlockCallExtension = require('../../local_node_modules/nunjucks-block-call-extension');
let template = `
{#
as a reminder, this can be imported
#}
{% macro myLayout(title, side, content) %}
<div>
<h1>{{ title }}</h1>
{% if side %}
<aside>
{{ side }}
</aside>
{% endif %}
<main>
{{ content }}
</main>
</div>
{% endmacro %}
{#
if importing, you'd use {% blockcall foo.myLayout ... %}
#}
{% blockcall myLayout(title='Can pass in args as normal') %}
{% argblock side %}
This is an example arg <em>sent as a block</em>.
{% argblock content %}
<p>And this is the main content</p>
{% endblockcall %}
`;
let env = new nunjucks.Environment();
env.addExtension('BlockCallExtension', new BlockCallExtension(nunjucks));
console.log(env.renderString(template, {}));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment