Repository: bokeh/bokeh

Issue:
Extensions cannot use own `.eco` templates as compiler won't compile them
As mentioned in #5258, when creating an extension, the template cannot be a separate implementation and compiled, instead it must be updated in the JS/Coffescript implementation of the extension.
e.g.:

```
 ionslidertemplate = require "models/widgets/slidertemplate"

 # This model will actually need to render things, so we must provide
 # view. The LayoutDOM model has a view already, so we will start with that
 class IonRangeSliderView extends InputWidget.View
   tagName: "div"
   template: ionslidertemplate
```

And in the `initialize` function:

```
 html = @template(@model.attributes)
 # replace div slider to input slider
 html = html.replace('<div class="slider "','<input type="text" class="slider "')
 html = html.replace('<div class="bk-slider-horizontal">','<div class="bk-slider-horizontal bk-ion-slider">')
```

v0.12.2

It would be really helpful if these could be implemented in a `.eco` file to use as the template directly.



Discussion:

Resolving patch:
diff --git a/bokeh/util/compiler.py b/bokeh/util/compiler.py
--- a/bokeh/util/compiler.py
+++ b/bokeh/util/compiler.py
@@ -154,6 +154,12 @@ class JavaScript(Inline):
     def lang(self):
         return "javascript"
 
+class Eco(Inline):
+
+    @property
+    def lang(self):
+        return "eco"
+
 class Less(Inline):
 
     @property
@@ -173,6 +179,8 @@ def lang(self):
             return "coffeescript"
         if self.file.endswith(".js"):
             return "javascript"
+        if self.file.endswith(".eco"):
+            return "eco"
         if self.file.endswith((".css", ".less")):
             return "less"
 
@@ -210,7 +218,7 @@ def implementation(self):
         impl = self.cls.__implementation__
 
         if isinstance(impl, six.string_types):
-            if "\n" not in impl and impl.endswith((".coffee", ".js", ".css", ".less")):
+            if "\n" not in impl and impl.endswith((".coffee", ".js", ".eco", ".css", ".less")):
                 impl = FromFile(join(self.path, impl))
             else:
                 impl = CoffeeScript(impl)
@@ -265,7 +273,7 @@ def resolve_modules(to_resolve, root):
                 def mkpath(ext):
                     return abspath(join(root, *module.split("/")) + "." + ext)
 
-                for ext in ["js", "coffee", "css", "less"]:
+                for ext in ["js", "coffee", "eco", "css", "less"]:
                     path = mkpath(ext)
                     if exists(path):
                         break


Test patch:
diff --git a/bokeh/util/tests/test_compiler.py b/bokeh/util/tests/test_compiler.py
--- a/bokeh/util/tests/test_compiler.py
+++ b/bokeh/util/tests/test_compiler.py
@@ -51,9 +51,69 @@ def test_nodejs_compile_javascript():
             message="Unexpected token",
             text="some.js:1:25:Unexpected token"))
 
+def test_nodejs_compile_eco():
+    assert nodejs_compile("""<div><%= @value %></div>""", "eco", "some.eco") == \
+        dict(code="""\
+module.exports = function(__obj) {
+  if (!__obj) __obj = {};
+  var __out = [];
+  var __capture = function(callback) {
+    var out = __out, result;
+    __out = [];
+    callback.call(this);
+    result = __out.join('');
+    __out = out;
+    return __safe(result);
+  };
+  var __sanitize = function(value) {
+    if (value && value.ecoSafe) {
+      return value;
+    } else if (typeof value !== 'undefined' && value != null) {
+      return __escape(value);
+    } else {
+      return '';
+    }
+  };
+  var __safe = function(value) {
+    if (value && value.ecoSafe) {
+      return value;
+    } else {
+      if (!(typeof value !== 'undefined' && value != null)) value = '';
+      var result = new String(value);
+      result.ecoSafe = true;
+      return result;
+    }
+  };
+  var __escape = function(value) {
+    return ('' + value)
+      .replace(/&/g, '&amp;')
+      .replace(/</g, '&lt;')
+      .replace(/>/g, '&gt;')
+      .replace(/"/g, '&quot;');
+  };
+  (function() {
+    (function() {
+      __out.push('<div>');
+      __out.push(__sanitize(this.value));
+      __out.push('</div>');
+    }).call(this);
+  }).call(__obj);
+  return __out.join('');
+};""", deps=[])
+
+    assert nodejs_compile("""<div><%= @@value %></div>""", "eco", "some.eco") == \
+        dict(error=dict(
+            line=2,
+            column=24,
+            message=u'unexpected @',
+            text=u'some.eco:2:24:unexpected @',
+            extract=u'__out.push __sanitize @@value',
+            annotated='some.eco:2:24:unexpected @\n  __out.push __sanitize @@value\n                         ^'))
+
+
 def test_nodejs_compile_less():
-    assert nodejs_compile(""".bk-some-style { color: green; }""", "less", "some.less") == \
-        dict(code=""".bk-some-style{color:green}""")
+    assert nodejs_compile(""".bk-some-style { color: mix(#ff0000, #0000ff, 50%); }""", "less", "some.less") == \
+        dict(code=""".bk-some-style{color:#800080}""")
 
     assert nodejs_compile(""".bk-some-style color: green; }""", "less", "some.less") == \
         dict(error=dict(