Skip to content

Commit

Permalink
Convert imperative rendering inside scriptlets to dynamic tags
Browse files Browse the repository at this point in the history
  • Loading branch information
DylanPiercey committed Nov 26, 2018
1 parent 3e6b954 commit 2f042ef
Show file tree
Hide file tree
Showing 9 changed files with 356 additions and 71 deletions.
57 changes: 56 additions & 1 deletion src/compiler/util/parseJavaScript.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,15 @@ function parseExpression(src, builder, isExpression) {
if (node.body && node.body.length === 1) {
return convert(node.body[0]);
}
return null;

let container = builder.containerNode();
for (let child of node.body) {
let convertedChild = convert(child);
if (convertedChild) {
container.appendChild(convertedChild);
}
}
return container;
}
case "ObjectExpression": {
let properties = convert(node.properties);
Expand All @@ -209,6 +217,10 @@ function parseExpression(src, builder, isExpression) {
return null;
}

if (node.kind === "get" || node.kind === "set") {
return null;
}

if (!computed && key.type === "Identifier") {
// Favor using a Literal AST node to represent
// the key instead of an Identifier
Expand Down Expand Up @@ -288,6 +300,49 @@ function parseExpression(src, builder, isExpression) {
}
return builder.vars(declarations, kind);
}
case "IfStatement": {
const ifNode = builder.ifStatement(
convert(node.test),
convert(node.consequent)
);

let alternate = node.alternate;

if (!alternate) {
return ifNode;
}

const container = builder.containerNode();
container.appendChild(ifNode);

do {
container.appendChild(
alternate.consequent
? builder.elseIfStatement(
convert(alternate.test),
convert(alternate.consequent)
)
: builder.elseStatement(convert(alternate))
);
alternate = alternate.alternate;
} while (alternate);

return container;
}
case "ForStatement": {
return builder.forStatement(
convert(node.init),
convert(node.test),
convert(node.update),
convert(node.body)
);
}
case "WhileStatement": {
return builder.whileStatement(
convert(node.test),
convert(node.body)
);
}
default:
return null;
}
Expand Down
90 changes: 22 additions & 68 deletions src/taglibs/core/invoke-tag.js
Original file line number Diff line number Diff line change
@@ -1,70 +1,9 @@
const renderCallToDynamicTag = require("./util/renderCallToDynamicTag");

module.exports = function codeGenerator(elNode, context) {
const builder = context.builder;
const functionAttr = elNode.attributes[0];
const attrs = elNode.attributes;
const args = context.builder.parseJavaScriptArgs(functionAttr.argument);
const argsLength = args.length;
const functionName = functionAttr.name;

let outIsFirstIndex = false;
let argsContainsOut = false;
let functionCallExpression = null;
let newNode = null;

args.map((arg, i) => {
if (arg.name === "out") {
if (i === 0) {
outIsFirstIndex = true;
}

argsContainsOut = true;
}
});

// Removes HtmlAtrribute
attrs.splice(0, 1);

if (
(functionName === "data.renderBody" ||
functionName === "input.renderBody") &&
argsContainsOut &&
outIsFirstIndex
) {
// Handles cases for the following:
// 1. <invoke data.renderBody(out) w-id="barTest"/> --> <${data.renderBody} w-id="barTest"/>

attrs.unshift({
value: args[0],
spread: true
});

newNode = context.createNodeForEl(
context.builder.parseExpression(functionName),
attrs
);
} else if (argsLength > 1 && argsContainsOut && !outIsFirstIndex) {
// Handles cases for the following:
// 1. <invoke data.barRenderer({}, out) w-id="barTest"/> --> <${{ render:data.barRenderer }} ...{} w-id="barTest"/>
// 2. <invoke data.template.render({}, out) w-id="barTest"/> --> <${{ render: data.template.render }} ...{} w-id="barTest"/>
// 3. <invoke data.template.renderer({}, out) w-id="barTest"/> --> <${{ render: data.template.renderer }} ...{} w-id="barTest"/>

attrs.unshift({
value: args[0],
spread: true
});

newNode = context.createNodeForEl(
context.builder.parseExpression("{renderer:" + functionName + "}"),
attrs
);
} else {
// Handles all other cases:
// 1. e.g. <invoke console.log('hello') /> --> console.log(arguement/s)

functionCallExpression = functionName + "(" + args + ");";
newNode = context.builder.scriptlet({
value: functionCallExpression
});
}
const functionArgs = functionAttr.argument;

context.deprecate(
'The "<invoke>" tag is deprecated. Please use "$ <js_code>" for JavaScript in the template. See: https://github.com/marko-js/marko/wiki/Deprecation:-var-assign-invoke-tags'
Expand All @@ -77,13 +16,28 @@ module.exports = function codeGenerator(elNode, context) {
return;
}

if (args === undefined) {
if (functionArgs === undefined) {
context.addError(
'Invalid <invoke> tag. Missing function arguments. Expected: <invoke console.log("Hello World")'
);
return;
}

elNode.insertSiblingBefore(newNode);
elNode.detach();
const functionCallExpression = `${functionAttr.name}(${functionArgs})`;
const functionAst = context.builder.parseExpression(functionCallExpression);
let replacement = renderCallToDynamicTag(functionAst, context);

if (replacement) {
elNode.forEachAttribute(attr => {
if (attr !== functionAttr) {
replacement.addAttribute(attr);
}
});
} else {
replacement = builder.scriptlet({
value: functionCallExpression
});
}

elNode.replaceWith(replacement);
};
1 change: 1 addition & 0 deletions src/taglibs/core/marko.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"transformer": "./root-transformer",
"<assign>": {
"transformer": "./assign-tag",
"open-tag-only": true,
Expand Down
76 changes: 76 additions & 0 deletions src/taglibs/core/root-transformer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"use strict";

const OUT_IDENTIFIER_REG = /[(,] *out *[,)]/;
const renderCallToDynamicTag = require("./util/renderCallToDynamicTag");

module.exports = function transform(el, context) {
const walker = context.createWalker({
enter(node) {
if (
node.type !== "Scriptlet" ||
!OUT_IDENTIFIER_REG.test(node.code)
) {
return;
}

const replacement = replaceScriptlets(
context.builder.parseStatement(node.code),
context
);

node.replaceWith(replacement);
}
});
walker.walk(el);
};

function replaceScriptlets(node, context) {
const builder = context.builder;
if (!node.type) {
if (node.replaceChild) {
node.forEach(child => {
const replacement = replaceScriptlets(child, context);
if (child !== replacement) {
node.replaceChild(replacement, child);
}
});
} else if (node.body) {
node.body.forEach(child => {
const replacement = replaceScriptlets(child, context);
if (child !== replacement) {
node.body.replaceChild(replacement, child);
}
});
}

return node;
}

switch (node.type) {
case "LogicalExpression":
node = builder.ifStatement(
node.operator === "&&" ? node.left : builder.negate(node.left),
[replaceScriptlets(node.right, context)]
);
break;
case "FunctionCall":
node = renderCallToDynamicTag(node, context) || node;
break;
case "If":
case "ElseIf":
node.body = replaceScriptlets(node.body, context);
if (node.else) {
replaceScriptlets(node.else, context);
}
break;
case "Else":
case "ForStatement":
case "WhileStatement":
node.body = replaceScriptlets(node.body, context);
break;
default:
break;
}

return node;
}
89 changes: 89 additions & 0 deletions src/taglibs/core/util/renderCallToDynamicTag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
module.exports = function renderCallToDynamicTag(ast, context) {
const builder = context.builder;
const args = ast.args;
const callee = ast.callee;
const argsLength = args.length;
const outIndex = args.findIndex(arg => arg.name === "out");
const calleeProperty = callee.property && callee.property.name;

if (outIndex === -1) {
return false;
}

let tagName;
let tagAttrs;

if (argsLength <= 2) {
if (outIndex === 0) {
// Handles cases for the following:
// 1. input.renderBody(out) --> <${input}/>
// 2. input.renderThing(out) --> <${input.renderThing}/>
// 3. input.renderBody(out, attrs) --> <${input} ...attrs/>
// 4. renderBody(out) --> <${renderBody}/>
if (argsLength === 2) {
tagName = callee;
tagAttrs = toAttributesOrSpread(args[1]);
}

// Removes `.renderBody` which is optional.
if (calleeProperty === "renderBody") {
tagName = callee.object;
} else {
tagName = callee;
}
} else if (outIndex === 1) {
// Handles cases for the following:
// 1. input.template.render({}, out) --> <${input.template} ...{}/>
// 2. input.template.renderer({}, out) --> <${input.template} ...{}/>
// 3. input.barRenderer({}, out) --> <${{ render:input.barRenderer }} ...{}/>

tagAttrs = toAttributesOrSpread(args[0]);

// Removes `.render` or `.renderer` which are optional.
if (calleeProperty === "render" || calleeProperty === "renderer") {
tagName = callee.object;
} else {
tagName = builder.objectExpression({
render: callee
});
}
}
} else {
// Handles worst case scenario:
// 1. input.barRenderer({}, true, out) --> <${(out) => input.barRenderer({}, true, out)}/>
tagName = builder.functionDeclaration(
null,
[builder.identifier("out")],
[ast]
);
}

return context.createNodeForEl(tagName, tagAttrs, null, true, true);
};

function toAttributesOrSpread(val) {
if (
!val ||
(val.type === "Literal" && val.value === null) ||
(val.type === "Identifier" && val.name === "undefined")
) {
return [];
}

if (
val.type === "ObjectExpression" &&
val.properties.every(prop => !prop.computed)
) {
return val.properties.map(prop => ({
name: prop.literalKeyValue,
value: prop.value
}));
}

return [
{
value: val,
spread: true
}
];
}
4 changes: 3 additions & 1 deletion test/compiler/fixtures-html/invoke/expected.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ var marko_template = module.exports = require("marko/src/html").t(__filename),
function render(input, out, __component, component, state) {
var data = input;

marko_dynamicTag(input.renderBody, out, out, __component, "0");
marko_dynamicTag(input, {
x: 1
}, out, __component, "hi");
}

marko_template._ = marko_renderer(render, {
Expand Down
2 changes: 1 addition & 1 deletion test/compiler/fixtures-html/invoke/template.marko
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<invoke input.renderBody(out)/>
<invoke input.renderBody(out) w-id="hi" x=1/>
Loading

0 comments on commit 2f042ef

Please sign in to comment.