diff --git a/src/compiler/util/parseJavaScript.js b/src/compiler/util/parseJavaScript.js index 73b830d8e5..ec13d56158 100644 --- a/src/compiler/util/parseJavaScript.js +++ b/src/compiler/util/parseJavaScript.js @@ -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); @@ -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 @@ -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; } diff --git a/src/taglibs/core/invoke-tag.js b/src/taglibs/core/invoke-tag.js index 566f058d4c..dce4219f0e 100644 --- a/src/taglibs/core/invoke-tag.js +++ b/src/taglibs/core/invoke-tag.js @@ -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. --> <${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. --> <${{ render:data.barRenderer }} ...{} w-id="barTest"/> - // 2. --> <${{ render: data.template.render }} ...{} w-id="barTest"/> - // 3. --> <${{ 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. --> console.log(arguement/s) - - functionCallExpression = functionName + "(" + args + ");"; - newNode = context.builder.scriptlet({ - value: functionCallExpression - }); - } + const functionArgs = functionAttr.argument; context.deprecate( 'The "" tag is deprecated. Please use "$ " for JavaScript in the template. See: https://github.com/marko-js/marko/wiki/Deprecation:-var-assign-invoke-tags' @@ -77,13 +16,28 @@ module.exports = function codeGenerator(elNode, context) { return; } - if (args === undefined) { + if (functionArgs === undefined) { context.addError( 'Invalid tag. Missing function arguments. Expected: { + if (attr !== functionAttr) { + replacement.addAttribute(attr); + } + }); + } else { + replacement = builder.scriptlet({ + value: functionCallExpression + }); + } + + elNode.replaceWith(replacement); }; diff --git a/src/taglibs/core/marko.json b/src/taglibs/core/marko.json index 62653dd2c8..66c9513eb9 100644 --- a/src/taglibs/core/marko.json +++ b/src/taglibs/core/marko.json @@ -1,4 +1,5 @@ { + "transformer": "./root-transformer", "": { "transformer": "./assign-tag", "open-tag-only": true, diff --git a/src/taglibs/core/root-transformer.js b/src/taglibs/core/root-transformer.js new file mode 100644 index 0000000000..0de87c124b --- /dev/null +++ b/src/taglibs/core/root-transformer.js @@ -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; +} diff --git a/src/taglibs/core/util/renderCallToDynamicTag.js b/src/taglibs/core/util/renderCallToDynamicTag.js new file mode 100644 index 0000000000..61eb6f45ad --- /dev/null +++ b/src/taglibs/core/util/renderCallToDynamicTag.js @@ -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 + } + ]; +} diff --git a/test/compiler/fixtures-html/invoke-if/expected.js b/test/compiler/fixtures-html/invoke-if/expected.js index cf66196309..2c5510854e 100644 --- a/test/compiler/fixtures-html/invoke-if/expected.js +++ b/test/compiler/fixtures-html/invoke-if/expected.js @@ -10,7 +10,7 @@ function render(input, out, __component, component, state) { var data = input; if (true) { - console.log("hello"); + console.log('hello') } } diff --git a/test/compiler/fixtures-html/invoke/expected.js b/test/compiler/fixtures-html/invoke/expected.js index 5a3635c268..1eb7065896 100644 --- a/test/compiler/fixtures-html/invoke/expected.js +++ b/test/compiler/fixtures-html/invoke/expected.js @@ -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, { diff --git a/test/compiler/fixtures-html/invoke/template.marko b/test/compiler/fixtures-html/invoke/template.marko index 292a434273..58e986a643 100644 --- a/test/compiler/fixtures-html/invoke/template.marko +++ b/test/compiler/fixtures-html/invoke/template.marko @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/test/compiler/fixtures-html/render-body-call/expected.js b/test/compiler/fixtures-html/render-body-call/expected.js new file mode 100644 index 0000000000..e19c3e0225 --- /dev/null +++ b/test/compiler/fixtures-html/render-body-call/expected.js @@ -0,0 +1,76 @@ +"use strict"; + +var marko_template = module.exports = require("marko/src/html").t(__filename), + marko_componentType = "/marko-test$1.0.0/compiler/fixtures-html/render-body-call/template.marko", + components_helpers = require("marko/src/components/helpers"), + marko_renderer = components_helpers.r, + marko_defineComponent = components_helpers.c, + marko_helpers = require("marko/src/runtime/html/helpers"), + marko_dynamicTag = marko_helpers.d; + +function render(input, out, __component, component, state) { + var data = input; + + marko_dynamicTag(input, {}, out, __component, "0"); + + marko_dynamicTag(input.renderThing, {}, out, __component, "1"); + + marko_dynamicTag(input, attrs, out, __component, "2"); + + marko_dynamicTag(renderBody, {}, out, __component, "3"); + + marko_dynamicTag(input.template, { + x: 1 + }, out, __component, "4"); + + marko_dynamicTag(input.template, { + y: function() {} + }, out, __component, "5"); + + marko_dynamicTag({ + render: input.barRenderer + }, {}, out, __component, "6"); + + marko_dynamicTag(function(out) { + input.barRenderer({}, true, out); + }, {}, out, __component, "7"); + + if (x) { + marko_dynamicTag(renderA, {}, out, __component, "8"); + } else if (y) { + marko_dynamicTag(renderB, {}, out, __component, "9"); + } else { + marko_dynamicTag(renderC, {}, out, __component, "10"); + } + + if (x) { + marko_dynamicTag(render, {}, out, __component, "11"); + } + + if (!x) { + marko_dynamicTag(render, {}, out, __component, "12"); + } + + var for__13 = 0; + + for (let i = 0; i < 10; i++) { + var keyscope__14 = "[" + ((for__13++) + "]"); + + marko_dynamicTag(input.items[i], {}, out, __component, "15" + keyscope__14); + } + + let i = 10; + + while (i--) marko_dynamicTag(input, {}, out, __component, "16") +} + +marko_template._ = marko_renderer(render, { + ___implicit: true, + ___type: marko_componentType + }); + +marko_template.Component = marko_defineComponent({}, marko_template._); + +marko_template.meta = { + id: "/marko-test$1.0.0/compiler/fixtures-html/render-body-call/template.marko" + }; diff --git a/test/compiler/fixtures-html/render-body-call/template.marko b/test/compiler/fixtures-html/render-body-call/template.marko new file mode 100644 index 0000000000..5c18c3045f --- /dev/null +++ b/test/compiler/fixtures-html/render-body-call/template.marko @@ -0,0 +1,32 @@ +$ { + input.renderBody(out); + input.renderThing(out); + input.renderBody(out, attrs); + renderBody(out); +} + +$ input.template.render({ x: 1 }, out); +$ input.template.renderer({ y() {} }, out); +$ input.barRenderer(null, out); + +$ input.barRenderer({}, true, out); + +$ if (x) { + renderA(out); +} else if (y) { + renderB(out); +} else { + renderC(out); +} + +$ x && render(out); +$ x || render(out); + +$ for (let i = 0; i < 10; i++) { + input.items[i].renderBody(out); +} + +$ let i = 10; +$ while (i--) { + input.renderBody(out); +} \ No newline at end of file