Skip to content

Instantly share code, notes, and snippets.

@morrelinko
Last active June 8, 2022 08:15
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save morrelinko/84b391964914dd3e7761d62629eb029c to your computer and use it in GitHub Desktop.
Save morrelinko/84b391964914dd3e7761d62629eb029c to your computer and use it in GitHub Desktop.
Nunjucks Components
'use strict'
const { nodes, runtime } = require('nunjucks')
module.exports = function compileComponent (node, frame) {
let componentId = this._tmpid()
let templateId = this._tmpid()
let templateId2 = this._tmpid()
let componentVar = `component_${componentId}`
this._emitLine(`var ${componentVar} = {};`)
node.slots.forEach(slot => {
let slotName = `${componentVar}_slot_${slot.name.value}`
this._emitLine(`function ${slotName}(frame) {`)
this._emitLine(`var lineno = null;`)
this._emitLine(`var colno = null;`)
this._emitLine(`var ${this.buffer} = "";`)
this._emitLine('var frame = frame.push(true);')
let slotFrame = new runtime.Frame()
if (slot.body instanceof nodes.Literal) {
this._emitLine(`${this.buffer} = '${slot.body.value}';`)
} else {
this.compile(slot.body, slotFrame)
}
this._emitLine(`return new runtime.SafeString(${this.buffer});`)
this._emitLine('}')
this._emitLine(`${componentVar}.${slot.name.value} = ${slotName}(frame);`)
})
this._emitLine('var tasks = [];')
this._emitLine('tasks.push(function (callback) {')
this._emit('env.getTemplate(')
this._compileExpression(node.template, frame)
this._emit(
`, true, ${this._templateName()}, false, ${this._makeCallback(templateId)}`
)
this._emitLine(`callback(null, ${templateId});`)
this._emitLine('});')
this._emitLine('});')
this._emitLine('tasks.push(function (template, callback) {')
this._emitLine(`
template.render(
Object.assign(
context.getVariables(),
${componentVar}
),
frame,
${this._makeCallback(templateId2)}
`)
this._emitLine(`callback(null, ${templateId2});`)
this._emitLine('});')
this._emitLine('});')
this._emitLine('tasks.push(function (result, callback) {')
this._emitLine(`${this.buffer} += result;`)
this._emitLine('callback();')
this._emitLine('});')
this._emitLine('env.waterfall(tasks, function () {')
this._addScopeLevel()
}
nodes.Component = nodes.Node.extend('Component', {
fields: ['template', 'slots']
})
nodes.Slot = nodes.Node.extend('Slot', {
fields: ['name', 'body']
})
'use strict'
const flatten = require('lodash/flatten')
const first = require('lodash/first')
class ComponentExtension {
get tags () {
return ['component', 'slot']
}
parse (parser, nodes, lexer) {
let tag = parser.peekToken()
if (
!parser.skipSymbol('component') &&
!parser.skipSymbol('slot') &&
!parser.skipSymbol('endslot')
) {
this.fail(
'ComponentExtension: expected "component" or "slot"',
tag.lineno,
tag.colno
)
}
// parse the component file
let template = parser.parseExpression()
let content = new nodes.NodeList(0, 0)
let slots = []
let args = []
// Compile inline arguments as slots
if ((args = parser.parseSignature(null, true))) {
// Pass inline args as slots
if (args.children) {
args = first(args.children)
}
if (args && args.children) {
args = args.children
}
if (args) {
args.forEach(arg => {
slots.push(new nodes.Slot(
arg.lineno,
arg.colno,
arg.key,
new nodes.Literal(
arg.lineno,
arg.colno,
arg.value.value
)))
})
}
}
// advance until we visit a 'slot' or 'endcomponent'
parser.advanceAfterBlockEnd('component')
mergeChildren(
content,
parser.parseUntilBlocks('slot', 'endslot', 'endcomponent')
)
let tok = parser.peekToken()
while (tok && tok.value === 'slot') {
// skip the slot symbol and get the expression if any
parser.skipSymbol('slot')
let name = parser.parsePrimary()
let valueToken = parser.peekToken()
let slot = new nodes.Slot(tok.lineno, tok.colno, name)
if (
valueToken.type !== lexer.TOKEN_STRING &&
valueToken.type !== lexer.TOKEN_SYMBOL
) {
parser.skip(valueToken.type)
}
// get the body of the slot and add to slot list
if (valueToken.type === lexer.TOKEN_STRING) {
// case: inline slot
slot.body = new nodes.Literal(
valueToken.lineno,
valueToken.colno,
parser.parseExpression().value
)
parser.advanceAfterBlockEnd('component')
} else {
// case: block slot
slot.body = parser.parseUntilBlocks('endslot')
parser.advanceAfterBlockEnd()
}
mergeChildren(
content,
parser.parseUntilBlocks('slot', 'endslot', 'endcomponent')
)
slots.push(slot)
// Move on to next slot
tok = parser.peekToken()
}
// Remove 'content' arg entirely
// if no data is available
if (content.children.length > 0) {
// create content slot
slots.push(
new nodes.Slot(
tag.lineno,
tag.colno,
new nodes.Value(0, 0, 'content'),
content
)
)
}
switch (tok.value) {
case 'endcomponent':
parser.advanceAfterBlockEnd()
break
}
return new nodes.Component(tag.lineno, tag.colno, template, slots)
}
}
function mergeChildren (targetList, sourceList) {
let children = flatten(sourceList.children)
children.forEach(child => {
targetList.addChild(child)
})
}
module.exports = ComponentExtension
@monochromer
Copy link

Trouble with nested components (SyntaxError: missing ) after argument list):

{% component "some-component.njk" prop="value1" %}
  {% component "another-component.njk" prop="value2" %}
  {% endcomponent %}
{% endcomponent %}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment