Last active
April 13, 2025 18:23
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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