function resolve(path) { if (path.indexOf("settings") === 0 || path.indexOf("transformed") === 0) { return `this.${path}`; } return path; } function sexp(value) { if (value.path.original === "hash") { let result = []; value.hash.pairs.forEach(p => { let pValue = p.value.original; if (p.value.type === "StringLiteral") { pValue = JSON.stringify(pValue); } result.push(`"${p.key}": ${pValue}`); }); return `{ ${result.join(", ")} }`; } } function argValue(arg) { let value = arg.value; if (value.type === "SubExpression") { return sexp(arg.value); } else if (value.type === "PathExpression") { return value.original; } else if (value.type === "StringLiteral") { return JSON.stringify(value.value); } } function mustacheValue(node, state) { let path = node.path.original; switch (path) { case "attach": let widgetName = argValue(node.hash.pairs.find(p => p.key === "widget")); let attrs = node.hash.pairs.find(p => p.key === "attrs"); if (attrs) { return `this.attach(${widgetName}, ${argValue(attrs)})`; } return `this.attach(${widgetName}, attrs)`; break; case "yield": return `this.attrs.contents()`; break; case "i18n": let value; if (node.params[0].type === "StringLiteral") { value = `"${node.params[0].value}"`; } else if (node.params[0].type === "PathExpression") { value = resolve(node.params[0].original); } if (value) { return `I18n.t(${value})`; } break; case "fa-icon": case "d-icon": state.helpersUsed.iconNode = true; let icon = node.params[0].value; return `__iN("${icon}")`; break; default: if (node.escaped) { return `${resolve(path)}`; } else { state.helpersUsed.rawHtml = true; return `new __rH({ html: '' + ${resolve(path)} + ''})`; } break; } } class Compiler { constructor(ast) { this.idx = 0; this.ast = ast; this.state = { helpersUsed: {} }; } newAcc() { return `_a${this.idx++}`; } processNode(parentAcc, node) { let instructions = []; let innerAcc; switch (node.type) { case "Program": node.body.forEach(bodyNode => { instructions = instructions.concat( this.processNode(parentAcc, bodyNode) ); }); break; case "ElementNode": innerAcc = this.newAcc(); instructions.push(`var ${innerAcc} = [];`); node.children.forEach(child => { instructions = instructions.concat(this.processNode(innerAcc, child)); }); if (node.attributes.length) { let attributes = []; node.attributes.forEach(a => { const name = a.name === "class" ? "className" : a.name; if (a.value.type === "MustacheStatement") { attributes.push( `"${name}":${mustacheValue(a.value, this.state)}` ); } else { attributes.push(`"${name}":"${a.value.chars}"`); } }); const attrString = `{${attributes.join(", ")}}`; instructions.push( `${parentAcc}.push(virtualDom.h('${ node.tag }', ${attrString}, ${innerAcc}));` ); } else { instructions.push( `${parentAcc}.push(virtualDom.h('${node.tag}', ${innerAcc}));` ); } break; case "TextNode": return `${parentAcc}.push(${JSON.stringify(node.chars)});`; case "MustacheStatement": const value = mustacheValue(node, this.state); if (value) { instructions.push(`${parentAcc}.push(${value})`); } break; case "BlockStatement": let negate = ""; switch (node.path.original) { case "unless": negate = "!"; case "if": instructions.push( `if (${negate}${resolve(node.params[0].original)}) {` ); node.program.body.forEach(child => { instructions = instructions.concat( this.processNode(parentAcc, child) ); }); if (node.inverse) { instructions.push(`} else {`); node.inverse.body.forEach(child => { instructions = instructions.concat( this.processNode(parentAcc, child) ); }); } instructions.push(`}`); break; case "each": const collection = resolve(node.params[0].original); instructions.push(`if (${collection} && ${collection}.length) {`); instructions.push( ` ${collection}.forEach(${node.program.blockParams[0]} => {` ); node.program.body.forEach(child => { instructions = instructions.concat( this.processNode(parentAcc, child) ); }); instructions.push(` });`); instructions.push("}"); break; } break; default: break; } return instructions.join("\n"); } compile() { return this.processNode("_r", this.ast); } } function compile(template) { const preprocessor = Ember.__loader.require("@glimmer/syntax"); const compiled = preprocessor.preprocess(template); const compiler = new Compiler(compiled); let code = compiler.compile(); let imports = ""; if (compiler.state.helpersUsed.iconNode) { imports += "var __iN = Discourse.__widget_helpers.iconNode; "; } if (compiler.state.helpersUsed.rawHtml) { imports += "var __rH = Discourse.__widget_helpers.rawHtml; "; } return `function(attrs, state) { ${imports}var _r = [];\n${code}\nreturn _r; }`; } exports.compile = compile; function error(path, state, msg) { const filename = state.file.opts.filename; return path.replaceWithSourceString( `function() { console.error("${filename}: ${msg}"); }` ); } exports.WidgetHbsCompiler = function(babel) { let t = babel.types; return { visitor: { ImportDeclaration(path, state) { let node = path.node; if ( t.isLiteral(node.source, { value: "discourse/widgets/hbs-compiler" }) ) { let first = node.specifiers && node.specifiers[0]; if (!t.isImportDefaultSpecifier(first)) { let input = state.file.code; let usedImportStatement = input.slice(node.start, node.end); let msg = `Only \`import hbs from 'discourse/widgets/hbs-compiler'\` is supported. You used: \`${usedImportStatement}\``; throw path.buildCodeFrameError(msg); } state.importId = state.importId || path.scope.generateUidIdentifierBasedOnNode(path.node.id); path.scope.rename(first.local.name, state.importId.name); path.remove(); } }, TaggedTemplateExpression(path, state) { if (!state.importId) { return; } let tagPath = path.get("tag"); if (tagPath.node.name !== state.importId.name) { return; } if (path.node.quasi.expressions.length) { return error( path, state, "placeholders inside a tagged template string are not supported" ); } let template = path.node.quasi.quasis .map(quasi => quasi.value.cooked) .join(""); try { path.replaceWithSourceString(compile(template)); } catch (e) { return error(path, state, e.toString()); } } } }; };