Move the widget-hbs compiler to js from es6

Move the widget-hbs compiler to js from es6

diff --git a/lib/discourse_js_processor.rb b/lib/discourse_js_processor.rb
index 4060b16..fdf61e9 100644
--- a/lib/discourse_js_processor.rb
+++ b/lib/discourse_js_processor.rb
@@ -55,8 +55,8 @@ class DiscourseJsProcessor
       embed-application
     ).any? { |f| relative_path == "#{js_root}/#{f}.js" }
 
-    relative_path =~ /^#{js_root}\/[^\/]+\// ||
-      relative_path =~ /^#{test_root}\/[^\/]+\//
+    !!(relative_path =~ /^#{js_root}\/[^\/]+\// ||
+      relative_path =~ /^#{test_root}\/[^\/]+\//)
   end
 
   def self.skip_module?(data)
@@ -87,7 +87,7 @@ class DiscourseJsProcessor
       }
 
 JS
-      source = File.read("#{Rails.root}/lib/javascripts/widget-hbs-compiler.js.es6")
+      source = File.read("#{Rails.root}/lib/javascripts/widget-hbs-compiler.js")
       js_source = ::JSON.generate(source, quirks_mode: true)
       js = ctx.eval("Babel.transform(#{js_source}, { ast: false, plugins: ['check-es2015-constants', 'transform-es2015-arrow-functions', 'transform-es2015-block-scoped-functions', 'transform-es2015-block-scoping', 'transform-es2015-classes', 'transform-es2015-computed-properties', 'transform-es2015-destructuring', 'transform-es2015-duplicate-keys', 'transform-es2015-for-of', 'transform-es2015-function-name', 'transform-es2015-literals', 'transform-es2015-object-super', 'transform-es2015-parameters', 'transform-es2015-shorthand-properties', 'transform-es2015-spread', 'transform-es2015-sticky-regex', 'transform-es2015-template-literals', 'transform-es2015-typeof-symbol', 'transform-es2015-unicode-regex'] }).code")
       ctx.eval(js)
diff --git a/lib/javascripts/widget-hbs-compiler.js b/lib/javascripts/widget-hbs-compiler.js
new file mode 100644
index 0000000..907ca44
--- /dev/null
+++ b/lib/javascripts/widget-hbs-compiler.js
@@ -0,0 +1,349 @@
+function resolve(path) {
+  if (path.indexOf("settings") === 0 || path.indexOf("transformed") === 0) {
+    return `this.${path}`;
+  }
+  return path;
+}
+
+function sexpValue(value) {
+  if (!value) {
+    return;
+  }
+
+  let pValue = value.original;
+  if (value.type === "StringLiteral") {
+    return JSON.stringify(pValue);
+  } else if (value.type === "SubExpression") {
+    return sexp(value);
+  }
+  return pValue;
+}
+
+function pairsToObj(pairs) {
+  let result = [];
+
+  pairs.forEach(p => {
+    result.push(`"${p.key}": ${sexpValue(p.value)}`);
+  });
+
+  return `{ ${result.join(", ")} }`;
+}
+
+function i18n(node) {
+  let key = sexpValue(node.params[0]);
+
+  let hash = node.hash;
+  if (hash.pairs.length) {
+    return `I18n.t(${key}, ${pairsToObj(hash.pairs)})`;
+  }
+
+  return `I18n.t(${key})`;
+}
+
+function sexp(value) {
+  if (value.path.original === "hash") {
+    return pairsToObj(value.hash.pairs);
+  }
+
+  if (value.path.original === "concat") {
+    let result = [];
+    value.params.forEach(p => {
+      result.push(sexpValue(p));
+    });
+    return result.join(" + ");
+  }
+
+  if (value.path.original === "i18n") {
+    return i18n(value);
+  }
+}
+
+function valueOf(value) {
+  if (value.type === "SubExpression") {
+    return sexp(value);
+  } else if (value.type === "PathExpression") {
+    return value.original;
+  } else if (value.type === "StringLiteral") {
+    return JSON.stringify(value.value);
+  }
+}
+
+function argValue(arg) {
+  return valueOf(arg.value);
+}
+
+function useHelper(state, name) {
+  let id = state.helpersUsed[name];
+  if (!id) {
+    id = ++state.helperNumber;
+    state.helpersUsed[name] = id;
+  }
+  return `__h${id}`;
+}
+
+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":
+      return i18n(node);
+      break;
+    case "avatar":
+      let template = argValue(node.hash.pairs.find(p => p.key === "template"));
+      let username = argValue(node.hash.pairs.find(p => p.key === "username"));
+      let size = argValue(node.hash.pairs.find(p => p.key === "size"));
+      return `${useHelper(
+        state,
+        "avatar"
+      )}(${size}, { template: ${template}, username: ${username} })`;
+      break;
+    case "date":
+      return `${useHelper(state, "dateNode")}(${valueOf(node.params[0])})`;
+      break;
+    case "d-icon":
+      return `${useHelper(state, "iconNode")}(${valueOf(node.params[0])})`;
+      break;
+    default:
+      // Shortcut: If our mustach has hash arguments, we can assume it's attaching.
+      // For example `{{home-logo count=123}}` can become `this.attach('home-logo, { "count": 123 });`
+      let hash = node.hash;
+      if (hash.pairs.length) {
+        let widgetString = JSON.stringify(path);
+        // magic: support applying of attrs. This is commonly done like `{{home-logo attrs=attrs}}`
+        let firstPair = hash.pairs[0];
+        if (firstPair.key === "attrs") {
+          return `this.attach(${widgetString}, ${firstPair.value.original})`;
+        }
+
+        return `this.attach(${widgetString}, ${pairsToObj(hash.pairs)})`;
+      }
+
+      if (node.escaped) {
+        return `${resolve(path)}`;
+      } else {
+        return `new ${useHelper(state, "rawHtml")}({ html: '<span>' + ${resolve(
+          path
+        )} + '</span>'})`;
+      }
+      break;
+  }
+}
+
+class Compiler {
+  constructor(ast) {
+    this.idx = 0;
+    this.ast = ast;
+
+    this.state = {
+      helpersUsed: {},
+      helperNumber: 0
+    };
+  }
+
+  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)
+              );
+            });
+

[... diff too long, it was truncated ...]

GitHub sha: 5d66a2c1