diff --git a/README.md b/README.md index 53d5d9afdc..15e215015b 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,8 @@ Though being compatible with [Ruby Liquid](https://github.com/shopify/liquid) is * Dynamic file locating (enabled by default), which means layout/partial name can be an variable in liquidjs. See [#51](https://github.com/harttle/liquidjs/issues/51). * Truthy and Falsy. All values except `undefined`, `null`, `false` are truthy, whereas in Ruby Liquid all except `nil` and `false` are truthy. See [#26](https://github.com/harttle/liquidjs/pull/26). * Number Rendering. Since JavaScript do not distinguish `float` and `integer`, we cannot either convert between them nor render regarding to their type. See [#59](https://github.com/harttle/liquidjs/issues/59). -* Along with [.to_liquid()](https://github.com/Shopify/liquid/wiki/Introduction-to-Drops), we provide an alias `.toLiquid()` to align with your code styles. +* [.to_liquid()](https://github.com/Shopify/liquid/wiki/Introduction-to-Drops) has a `.toLiquid()` alias and and the JavaScript `.toString()` is aliased to `.to_s()`. +* [.to_s()](https://www.rubydoc.info/gems/liquid/Liquid/Drop) uses `JSON.prototype.stringify` as default, rather than Ruby's inspect. ## TOC diff --git a/package.json b/package.json index 096aadb4f2..63275d09de 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,10 @@ "types": "src/index.d.ts", "scripts": { "lint": "eslint src/ test/ *.js", - "test": "mocha test/unit", + "unit": "mocha test/unit", "e2e": "mocha test/e2e", - "coverage": "cross-env NODE_ENV=test nyc --reporter=html npm test", + "test": "npm run unit && npm run e2e", + "coverage": "cross-env NODE_ENV=test nyc --reporter=html npm run unit", "coveralls": "nyc report --reporter=text-lcov | coveralls", "build": "rollup -c && ls -lh dist", "version": "npm run build && git add -A dist", diff --git a/src/scope.js b/src/scope.js index 561930f709..f343c34560 100644 --- a/src/scope.js +++ b/src/scope.js @@ -60,16 +60,10 @@ const Scope = { if (_.isNil(obj)) { val = undefined } else { - if (typeof obj.to_liquid === 'function') { - obj = obj.to_liquid() - } else if (typeof obj.toLiquid === 'function') { - obj = obj.toLiquid() - } - - if (key === 'size' && (_.isArray(obj) || _.isString(obj))) { - val = obj.length - } else { - val = obj[key] + obj = toLiquid(obj) + val = key === 'size' ? readSize(obj) : obj[key] + if (_.isFunction(obj.liquid_method_missing)) { + val = obj.liquid_method_missing(key) } } if (_.isNil(val) && this.opts.strict_variables) { @@ -138,6 +132,22 @@ const Scope = { } } +function toLiquid (obj) { + if (_.isFunction(obj.to_liquid)) { + return obj.to_liquid() + } + if (_.isFunction(obj.toLiquid)) { + return obj.toLiquid() + } + return obj +} + +function readSize (obj) { + if (!_.isNil(obj.size)) return obj.size + if (_.isArray(obj) || _.isString(obj)) return obj.length + return obj.size +} + function matchRightBracket (str, begin) { let stack = 1 // count of '[' - count of ']' for (let i = begin; i < str.length; i++) { diff --git a/src/util/underscore.js b/src/util/underscore.js index c70429f742..cb51feb468 100644 --- a/src/util/underscore.js +++ b/src/util/underscore.js @@ -1,4 +1,5 @@ const toStr = Object.prototype.toString +const arrToStr = Array.prototype.toString /* * Checks if value is classified as a String primitive or object. @@ -9,6 +10,10 @@ export function isString (value) { return toStr.call(value) === '[object String]' } +export function isFunction (value) { + return typeof value === 'function' +} + export function promisify (fn) { return function () { return new Promise((resolve, reject) => { @@ -20,19 +25,16 @@ export function promisify (fn) { } export function stringify (value) { - if (isNil(value)) { - return String(value) - } - if (typeof value.to_liquid === 'function') { - return stringify(value.to_liquid()) - } - if (typeof value.toLiquid === 'function') { - return stringify(value.toLiquid()) - } - if (isString(value) || value instanceof RegExp || value instanceof Date) { - return value.toString() - } + if (isNil(value)) return String(value) + if (isFunction(value.to_liquid)) return stringify(value.to_liquid()) + if (isFunction(value.toLiquid)) return stringify(value.toLiquid()) + if (isFunction(value.to_s)) return value.to_s() + if ([toStr, arrToStr].indexOf(value.toString) > -1) return defaultToString(value) + if (isFunction(value.toString)) return value.toString() + return toStr.call(value) +} +function defaultToString (value) { const cache = [] return JSON.stringify(value, (key, value) => { if (isObject(value)) { diff --git a/test/unit/render.js b/test/unit/render.js index 5b50b50948..24aa442aca 100644 --- a/test/unit/render.js +++ b/test/unit/render.js @@ -63,6 +63,24 @@ describe('render', function () { const tpl = Template.parseValue('obj') return expect(render.renderValue(tpl, scope)).to.eventually.equal('{"foo":"foo"}') }) + it('should respect to .toString()', async () => { + const scope = scopeFactory({ obj: { toString: () => 'FOO' } }) + const tpl = Template.parseValue('obj') + const str = await render.renderValue(tpl, scope) + return expect(str).to.equal('FOO') + }) + it('should respect to .to_s()', async () => { + const scope = scopeFactory({ obj: { to_s: () => 'FOO' } }) + const tpl = Template.parseValue('obj') + const str = await render.renderValue(tpl, scope) + return expect(str).to.equal('FOO') + }) + it('should respect to .liquid_method_missing()', async () => { + const scope = scopeFactory({ obj: { liquid_method_missing: x => x.toUpperCase() } }) + const tpl = Template.parseValue('obj.foo') + const str = await render.renderValue(tpl, scope) + return expect(str).to.equal('FOO') + }) }) describe('.evalValue()', function () {