diff --git a/index.js b/index.js index ae010b7..5258e36 100644 --- a/index.js +++ b/index.js @@ -9,11 +9,6 @@ void function(global) { return err; } - // defaultTo :: a,a? -> a - function defaultTo(x, y) { - return y == null ? x : y; - } - // create :: Object -> String,*... -> String function create(transformers) { return function(template) { @@ -43,7 +38,23 @@ void function(global) { key = String(idx); idx += 1; } - var value = defaultTo('', lookup(args, key.split('.'))); + + // 1. Split the key into a lookup path. + // 2. If the first path component is not an index, prepend '0'. + // 3. Reduce the lookup path to a single result. If the lookup + // succeeds the result is a singleton array containing the + // value at the lookup path; otherwise the result is []. + // 4. Unwrap the result by reducing with '' as the default value. + var path = key.split('.'); + var value = (/^\d+$/.test(path[0]) ? path : ['0'].concat(path)) + .reduce(function(maybe, key) { + return maybe.reduce(function(_, x) { + return x != null && key in Object(x) ? + [typeof x[key] === 'function' ? x[key]() : x[key]] : + []; + }, []); + }, [args]) + .reduce(function(_, x) { return x; }, ''); if (xf == null) { return value; @@ -57,19 +68,6 @@ void function(global) { }; } - function lookup(_obj, _path) { - var obj = _obj; - var path = _path; - if (!/^\d+$/.test(path[0])) { - path = ['0'].concat(path); - } - for (var idx = 0; idx < path.length; idx += 1) { - var key = path[idx]; - obj = typeof obj[key] === 'function' ? obj[key]() : obj[key]; - } - return obj; - } - // format :: String,*... -> String var format = create({}); diff --git a/test/index.js b/test/index.js index 2560046..b644e38 100644 --- a/test/index.js +++ b/test/index.js @@ -59,6 +59,13 @@ suite('format', function() { ); }); + test('uses default string representations', function() { + eq(format('result: {}', null), 'result: null'); + eq(format('result: {}', undefined), 'result: undefined'); + eq(format('result: {}', [1, 2, 3]), 'result: 1,2,3'); + eq(format('result: {}', {foo: 42}), 'result: [object Object]'); + }); + test('treats "{{" and "}}" as "{" and "}"', function() { eq(format('{{ {}: "{}" }}', 'foo', 'bar'), '{ foo: "bar" }'); }); @@ -75,6 +82,21 @@ suite('format', function() { eq(format('{first} {last}', bobby), 'Bobby Fischer'); }); + test('defaults to "" if lookup fails', function() { + eq(format('result: {foo.bar.baz}', null), 'result: '); + eq(format('result: {foo.bar.baz}', 'x'), 'result: '); + eq(format('result: {foo.bar.baz}', {}), 'result: '); + eq(format('result: {foo.bar.baz}', {foo: null}), 'result: '); + eq(format('result: {foo.bar.baz}', {foo: 'x'}), 'result: '); + eq(format('result: {foo.bar.baz}', {foo: {}}), 'result: '); + eq(format('result: {foo.bar.baz}', {foo: {bar: null}}), 'result: '); + eq(format('result: {foo.bar.baz}', {foo: {bar: 'x'}}), 'result: '); + eq(format('result: {foo.bar.baz}', {foo: {bar: {}}}), 'result: '); + eq(format('result: {foo.bar.baz}', {foo: {bar: {baz: null}}}), 'result: null'); + eq(format('result: {foo.bar.baz}', {foo: {bar: {baz: 'x'}}}), 'result: x'); + eq(format('result: {foo.bar.baz}', {foo: {bar: {baz: {}}}}), 'result: [object Object]'); + }); + test('invokes methods', function() { eq(format('{0.toLowerCase}', 'III'), 'iii'); eq(format('{0.toUpperCase}', 'iii'), 'III');