Repository: shepherdwind/velocity.js
Pull request #56 (2015-12-10 10:48:34)

proposal: toString like org.apache.velocity when render
Java package [org.apache.velocity](https://github.com/apache/velocity-engine) convert HashMap/ArrayList to string as described below

- HashMap => `{k1=v1, k2=v2}`
- ArrayList => `[e1, e2]`

This PR do it. Probably, it is not the VTL specification. I suppose it is just a java's implementation of toString.
But I think this PR improve compatibility to original. (like  `.keySet()`, `.entrySet()`, `.size()`)
And this is useful on debugging VTL.

## org.apache.velocity's behavior

```java
package mt;

import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;

import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;

public class Main {
  public static void main(String[] args) {
    HashMap<String, Object> data = new HashMap<String, Object>() {{
      put("name", "ToQoz");
      put("age", 999);
      put("jobHistories", new ArrayList<Object>() {{
        add(new HashMap<String, Object>() {{
          put("company", "foo");
          put("title", "programmer");
        }});
        add(new HashMap<String, Object>() {{
          put("company", "bar");
          put("title", "programmer");
        }});
      }});
    }};

    // context
    VelocityContext context = new VelocityContext();
    context.put("data", data);
    // template
    String templateStr = "$data";
    // render/print
    StringWriter out = new StringWriter();
    Velocity.evaluate(context, out, "", templateStr);
    System.out.println(out);
  }
}
```

outputs `{jobHistories=[{company=foo, title=programmer}, {company=bar, title=programmer}], name=ToQoz, age=999}`


Patch:
diff --git a/src/compile/compile.js b/src/compile/compile.js
index 269d6f2..36c1675 100644
--- a/src/compile/compile.js
+++ b/src/compile/compile.js
@@ -77,7 +77,7 @@ module.exports = function(Velocity, utils) {
 
         switch (ast.type) {
           case 'references':
-            str += this.getReferences(ast, true);
+            str += this.format(this.getReferences(ast, true));
           break;
 
           case 'set':
@@ -106,6 +106,22 @@ module.exports = function(Velocity, utils) {
       }, this);
 
       return str;
+    },
+    format: function(value) {
+      if (utils.isArray(value)) {
+        return "[" + value.map(this.format.bind(this)).join(", ") + "]";
+      }
+
+      if (utils.isObject(value)) {
+        if (value.hasOwnProperty('toString')) {
+          return value.toString();
+        }
+
+        var kvJoin = function(k) { return k + "=" + this.format(value[k]); }.bind(this);
+        return "{" + Object.keys(value).map(kvJoin).join(", ") + "}";
+      }
+
+      return value;
     }
   });
 };


Test patch:
diff --git a/tests/compile.js b/tests/compile.js
index 07805f7..988799f 100644
--- a/tests/compile.js
+++ b/tests/compile.js
@@ -824,5 +824,61 @@ describe('Compile', function() {
       }));
     });
   });
+
+  describe('Object|Array#toString', function() {
+    it('simple object', function() {
+      var vm = '$data';
+      var expected = '{k=v, k2=v2}';
+      assert.equal(render(vm, {data: {k: "v", k2: "v2"}}), expected)
+    });
+
+    it('object.keySet()', function() {
+      var vm = '$data.keySet()';
+      var expected = '[k, k2]';
+      assert.equal(render(vm, {data: {k: "v", k2: "v2"}}), expected)
+    });
+
+    it('object.entrySet()', function() {
+      var vm = '$data.entrySet()';
+      var expected = '[{key=k, value=v}, {key=k2, value=v2}]';
+      assert.equal(render(vm, {data: {k: "v", k2: "v2"}}), expected)
+    });
+
+    it('nested object', function() {
+      var vm = '$data';
+      var expected = '{k={k2=v2}, kk={k3=v3}}';
+      assert.equal(render(vm, {data: {k: {k2: "v2"}, kk: {k3: "v3"}}}), expected)
+    });
+
+    it('object that has toString as own property', function() {
+      var vm = '$data';
+      var expected = 'custom';
+      assert.equal(render(vm, {data: {toString: function() { return 'custom'; }, key: "value", key2: "value2", key3: {key4: "value4"}}}), expected)
+    });
+
+    it('simple array', function() {
+      var vm = '$data';
+      var expected = '[a, b]';
+      assert.equal(render(vm, {data: ["a", "b"]}), expected)
+    });
+
+    it('nested array', function() {
+      var vm = '$data';
+      var expected = '[a, [b]]';
+      assert.equal(render(vm, {data: ["a", ["b"]]}), expected)
+    });
+
+    it('object in array', function() {
+      var vm = '$data';
+      var expected = '[a, {k=v}]';
+      assert.equal(render(vm, {data: ["a", {k: "v"}]}), expected)
+    });
+
+    it('array in object', function() {
+      var vm = '$data';
+      var expected = '{k=[a, b]}';
+      assert.equal(render(vm, {data: {k: ["a", "b"]}}), expected)
+    });
+  });
 })