├── .gitignore ├── .travis.yml ├── README.md ├── bench ├── fixture.json └── object.js ├── index.js ├── object.js ├── object_test.js ├── package.json ├── string.js ├── string_test.js ├── test └── mocha.opts └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | - 8 5 | - 9 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # json-templater [![Build Status](https://travis-ci.org/lightsofapollo/json-templater.svg?branch=master)](https://travis-ci.org/lightsofapollo/json-templater) 2 | 3 | JSON template(r) is an opinionated simple way to do mustache style 4 | template replacements on your js and json objects (and of course 5 | strings too)!. 6 | 7 | 8 | ## Usage: json-templater/string 9 | 10 | The string submodule is a very simple mustache like variable replacement with no special features: 11 | 12 | ```js 13 | var render = require('json-templater/string'); 14 | render('{{xfoo}} {{say.what}}', { xfoo: 'yep', say: { what: 'yep' } }); 15 | // yep yep 16 | ``` 17 | 18 | ## Usage: json-templater/object 19 | 20 | The much more interesting part of this module is the object sub-module which does a deep clone and runs strings through json-templater/string (including keys!) 21 | 22 | `template.json:` 23 | ```json 24 | { 25 | "magic_key_{{magic}}": { 26 | "key": "interpolation is nice {{value}}" 27 | } 28 | } 29 | ``` 30 | 31 | ```js 32 | var object = require('json-templater/object'); 33 | object( 34 | require('./template.json'), 35 | { magic: 'key', value: 'value' } 36 | ); 37 | 38 | // result 39 | 40 | { 41 | magic_key_key: { 42 | key: 'interpolation is nice value' 43 | } 44 | } 45 | 46 | ``` 47 | 48 | ### Custom render 49 | 50 | You can override default renderer using the third argument in `object` (`json-templater/string` is default): 51 | 52 | `template.json:` 53 | ```json 54 | { 55 | "magic": { 56 | "key": "interpolation is nice {{value}}" 57 | } 58 | } 59 | ``` 60 | 61 | ```js 62 | var object = require('json-templater/object'); 63 | 64 | object( 65 | require('./template.json'), 66 | { magic: 'key', value: 'value' }, 67 | function (value, data, key) { 68 | return value; 69 | } 70 | ); 71 | 72 | // result 73 | 74 | { 75 | "magic": { 76 | "key": "interpolation is nice {{value}}" 77 | } 78 | } 79 | ``` 80 | 81 | #### key reference 82 | 83 | Handler function gets three arguments: 84 | 85 | - `value`: value which is about to be handled 86 | - `data`: initial template data object 87 | - `key`: key corresponding to the value 88 | 89 | Using this data some complex logic could be implemented, for instance: 90 | 91 | ```js 92 | var object = require('json-templater/object'); 93 | var string = require('json-templater/string'); 94 | 95 | object( 96 | require('./template.json'), 97 | { magic: 'key', value: 'value' }, 98 | function (value, data, key) { 99 | // custom renderer for some special value 100 | if (key === 'specialKey') { 101 | return 'foo'; 102 | } 103 | // usual string renderer 104 | return string(value, data); 105 | } 106 | ); 107 | 108 | // result 109 | 110 | { 111 | magic: { 112 | specialKey: "foo", 113 | key: "interpolation is nice value" 114 | } 115 | } 116 | ``` 117 | 118 | ## LICENSE 119 | 120 | Copyright (c) 2014 Mozilla Foundation 121 | 122 | Permission is hereby granted, free of charge, to any person obtaining a copy 123 | of this software and associated documentation files (the "Software"), to deal 124 | in the Software without restriction, including without limitation the rights 125 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 126 | copies of the Software, and to permit persons to whom the Software is 127 | furnished to do so, subject to the following conditions: 128 | 129 | The above copyright notice and this permission notice shall be included in 130 | all copies or substantial portions of the Software. 131 | 132 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 133 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 134 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 135 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 136 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 137 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 138 | THE SOFTWARE. 139 | -------------------------------------------------------------------------------- /bench/fixture.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "provisionerId": "aws-provisioner", 4 | "workerType": "ami-1ad0bc2a", 5 | "routing": "task-testing.task-creator", 6 | "retries": 5, 7 | "priority": 5, 8 | "created": "2014-03-03T15:49:40.701Z", 9 | "deadline": "2014-03-04T15:49:40.701Z", 10 | "parameters": { 11 | "xfoo": "wat" 12 | }, 13 | "payload": { 14 | "image": "ubuntu", 15 | "command": [ 16 | "/bin/bash", 17 | "-c", 18 | "echo 'Hello World'" 19 | ], 20 | "features": { 21 | "azureLivelog": true, 22 | "bufferLog": false 23 | } 24 | }, 25 | "metadata": { 26 | "name": "A _custom_ {{xfoo}} {{xfoo}} {{xfoo}} {{xfoo}} {{xfoo}} {{xfoo}} {{xfoo}} {{xfoo}} {{xfoo}} task", 27 | "description": "A **super** quick test task from task creator tool.", 28 | "owner": "jlal@mozilla.com", 29 | "source": "http://docs.taskcluster.net/tools/task-creator/" 30 | }, 31 | "tags": { 32 | "kind": "one-time-task" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /bench/object.js: -------------------------------------------------------------------------------- 1 | var tplString = require('../string'); 2 | var objectRender = require('../object'); 3 | var mustache = require('mustache'); 4 | 5 | var fixture = require('./fixture.json'); 6 | 7 | module.exports.compare = { 8 | "json-template/string": function() { 9 | return objectRender(fixture, fixture.parameters); 10 | }, 11 | 12 | "mustache": function() { 13 | return objectRender(fixture, fixture.parameters, mustache.render); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports.string = require('./string'); 2 | module.exports.object = require('./object'); 3 | -------------------------------------------------------------------------------- /object.js: -------------------------------------------------------------------------------- 1 | var renderString = require('./string'); 2 | 3 | function walkObject(object, handler) { 4 | if (Array.isArray(object)) return walkArray(object, handler); 5 | var result = {}; 6 | 7 | for (var key in object) { 8 | result[walk(key, handler, null)] = walk(object[key], handler, key); 9 | } 10 | 11 | return result; 12 | } 13 | 14 | function walkArray(array, handler) { 15 | return array.map(function(input) { 16 | return walk(input, handler, null); 17 | }); 18 | } 19 | 20 | /** 21 | Walk the object and invoke the function on string types. 22 | 23 | Why write yet-another cloner/walker? The primary reason is we also want to run 24 | template functions on keys _and_ values which most clone things don't do. 25 | 26 | @param {Object} input object to walk and duplicate. 27 | @param {Function} handler handler to invoke on string types. 28 | @param {?String} [key] key corresponding to input, if the latter is a value in object 29 | */ 30 | function walk(input, handler, key) { 31 | switch (typeof input) { 32 | // object is slightly special if null we move on 33 | case 'object': 34 | if (!input) return input; 35 | return walkObject(input, handler); 36 | 37 | case 'string': 38 | return handler(input, key); 39 | // all other types cannot be mutated 40 | default: 41 | return input; 42 | } 43 | } 44 | 45 | function render(object, view, handler) { 46 | handler = handler || renderString; 47 | 48 | return walk(object, function(value, key) { 49 | return handler(value, view, key); 50 | }, null); 51 | } 52 | 53 | module.exports = render; 54 | -------------------------------------------------------------------------------- /object_test.js: -------------------------------------------------------------------------------- 1 | suite('object', function() { 2 | var subject = require('./object'); 3 | var renderString = require('./string'); 4 | var assert = require('assert'); 5 | 6 | function verify(title, input, output, handler) { 7 | test(title, function() { 8 | var result = subject.apply(subject, input); 9 | assert.deepEqual(output, result); 10 | }); 11 | } 12 | 13 | 14 | verify( 15 | 'replace value in nested object', 16 | [ 17 | { 18 | foo: { 19 | bar: '{{say.what}}' 20 | } 21 | }, 22 | { 23 | say: { what: 'yeah' } 24 | } 25 | ], 26 | { 27 | foo: { 28 | bar: 'yeah' 29 | } 30 | } 31 | ); 32 | 33 | verify( 34 | 'replace keys', 35 | [ 36 | { 37 | 'mykeywins{{yes}}': 'value' 38 | }, 39 | { yes: 'no' } 40 | ], 41 | { 'mykeywinsno': 'value' } 42 | ); 43 | 44 | verify( 45 | 'boolean', 46 | [{ 'woot': true }, {}], 47 | { woot: true } 48 | ); 49 | 50 | verify( 51 | 'undefined', 52 | [ 53 | { 54 | key: 'value', 55 | object: { 56 | nested: undefined 57 | } 58 | }, 59 | [] 60 | ], 61 | { 62 | key: 'value', 63 | object: { 64 | nested: undefined 65 | } 66 | } 67 | ); 68 | 69 | verify( 70 | 'array', 71 | [ 72 | ['foo', 'bar{{1}}', 'baz{{2}}'], 73 | ['ignore me', '-first', '-second'] 74 | ], 75 | [ 76 | 'foo', 77 | 'bar-first', 78 | 'baz-second' 79 | ] 80 | ); 81 | 82 | verify( 83 | 'number', 84 | [{ xfoo: 1 }, {}], 85 | { xfoo: 1 } 86 | ); 87 | 88 | verify( 89 | 'nested arrays', 90 | [ 91 | '{{some.arrays}}', 92 | { some: { arrays: [1, [2, [{3: 4}]]] } } 93 | ], 94 | [1, [2, [{3: 4}]]] 95 | ); 96 | 97 | verify( 98 | 'nested objects', [ 99 | '{{some.object}}', 100 | { some: { object: {3: {4: 5}} } } 101 | ], 102 | {3: {4: 5}} 103 | ); 104 | 105 | verify( 106 | 'nested arrays in objects and objects in arrays', 107 | [ 108 | { 109 | 'arrays{{some.arrays.0}}': '{{some.arrays}}', 110 | 'object{{some.arrays.1.0}}': '{{some.object}}' 111 | }, { 112 | some: { 113 | arrays: [1, [2, [{ 3: 4 }]]], 114 | object: { 3: { 4: 5 }} 115 | } 116 | } 117 | ], { 118 | arrays1: [1, [2, [{ 3: 4 }]]], 119 | object2: { 3: { 4: 5 }} 120 | } 121 | ); 122 | 123 | verify( 124 | 'custom handler', 125 | [ 126 | { 127 | foo: 'hello from {{foo}}', 128 | bar: 'hello from {{bar}}' 129 | }, 130 | { 131 | foo: 'foo', 132 | bar: 'bar' 133 | }, 134 | function(value, view, key) { 135 | // let's render the corresponding value in a different way 136 | if (key === 'foo') { 137 | return value; 138 | } 139 | return renderString(value, view); 140 | } 141 | ], 142 | { 143 | foo: 'hello from {{foo}}', 144 | bar: 'hello from bar' 145 | } 146 | ); 147 | 148 | }); 149 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-templater", 3 | "version": "1.2.0", 4 | "description": "Simple json/js object template strings", 5 | "main": "template.js", 6 | "scripts": { 7 | "test": "mocha *_test.js", 8 | "bench": "node-bench bench/object.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/lightsofapollo/json-templater.git" 13 | }, 14 | "keywords": [ 15 | "template", 16 | "json", 17 | "json", 18 | "template" 19 | ], 20 | "author": "James Lal [:lightsofapollo]", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/lightsofapollo/json-templater/issues" 24 | }, 25 | "homepage": "https://github.com/lightsofapollo/json-templater", 26 | "devDependencies": { 27 | "mocha": "~1.18.2", 28 | "bench": "~0.3.5", 29 | "mustache": "~2.2.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /string.js: -------------------------------------------------------------------------------- 1 | /** 2 | Convert a dotted path to a location inside an object. 3 | 4 | @private 5 | @example 6 | 7 | // returns xfoo 8 | extractValue('wow.it.works', { 9 | wow: { 10 | it: { 11 | works: 'xfoo' 12 | } 13 | } 14 | }); 15 | 16 | // returns undefined 17 | extractValue('xfoo.bar', { nope: 1 }); 18 | 19 | @param {String} path dotted to indicate levels in an object. 20 | @param {Object} view for the data. 21 | */ 22 | function extractValue(path, view) { 23 | // Short circuit for direct matches. 24 | if (view && view[path]) return view[path]; 25 | 26 | var parts = path.split('.'); 27 | 28 | while ( 29 | // view should always be truthy as all objects are. 30 | view && 31 | // must have a part in the dotted path 32 | (part = parts.shift()) 33 | ) { 34 | view = (typeof view === 'object' && part in view) ? 35 | view[part] : 36 | undefined; 37 | } 38 | 39 | return view; 40 | } 41 | 42 | var REGEX = new RegExp('{{([a-zA-Z.-_0-9]+)}}', 'g'); 43 | var TEMPLATE_OPEN = '{{'; 44 | 45 | /** 46 | NOTE: I also wrote an implementation that does not use regex but it is actually slower 47 | in real world usage and this is easier to understand. 48 | 49 | @param {String} input template. 50 | @param {Object} view details. 51 | */ 52 | function replace(input, view) { 53 | // optimization to avoid regex calls (indexOf is strictly faster) 54 | if (input.indexOf(TEMPLATE_OPEN) === -1) return input; 55 | var result; 56 | var replaced = input.replace(REGEX, function(original, path) { 57 | var value = extractValue(path, view); 58 | if (undefined === value || null === value) { 59 | return original; 60 | } 61 | 62 | if (typeof value === 'object') { 63 | result = value; 64 | return; 65 | } 66 | 67 | return value; 68 | }); 69 | return (undefined !== result) ? result : replaced; 70 | } 71 | 72 | module.exports = replace; 73 | -------------------------------------------------------------------------------- /string_test.js: -------------------------------------------------------------------------------- 1 | suite('string', function() { 2 | var assert = require('assert'); 3 | var subject = require('./string'); 4 | 5 | function verify(title, input, output) { 6 | test(title, function() { 7 | var result = subject.apply(subject, input); 8 | assert.equal(output, result); 9 | }); 10 | } 11 | 12 | verify('no token', ['woot', {}], 'woot'); 13 | 14 | verify( 15 | 'token no value', 16 | ['{{token}}', {}], 17 | '{{token}}' 18 | ); 19 | 20 | verify( 21 | 'one token', 22 | ['{{token}}', { token: 'v' }], 23 | 'v' 24 | ); 25 | 26 | verify( 27 | 'nested token', 28 | ['{{foo.bar.baz}}', { foo: { bar: { baz: 'baz' } } }], 29 | 'baz' 30 | ); 31 | 32 | verify( 33 | 'nested token no value', 34 | ['{{foo.bar}}', { foo: {} }], 35 | '{{foo.bar}}' 36 | ); 37 | 38 | verify( 39 | 'nested token false value', 40 | ['{{foo.bar}}', { foo: { bar: false} }], 41 | 'false' 42 | ); 43 | 44 | verify( 45 | 'replace in the middle of string', 46 | [ 47 | 'foo bar {{baz}} qux', 48 | { baz: 'what?' } 49 | ], 50 | 'foo bar what? qux' 51 | ); 52 | 53 | verify( 54 | 'multiple replacements', 55 | [ 56 | '{{a}} {{woot}} bar baz {{yeah}}', 57 | { a: 'foo', yeah: true } 58 | ], 59 | 'foo {{woot}} bar baz true' 60 | ); 61 | 62 | verify( 63 | 'object notation for non-object', 64 | [ 65 | '{{a.b.c}} what', 66 | { a: { b: 'iam b' } } 67 | ], 68 | '{{a.b.c}} what' 69 | ); 70 | 71 | verify( 72 | 'dots as key names', 73 | ['{{woot.bar.baz}}', { 'woot.bar.baz': 'yup' }], 74 | 'yup' 75 | ); 76 | 77 | verify( 78 | 'array view', 79 | [ 80 | 'do what {{fields.0}}', 81 | { 82 | fields: ['first'] 83 | } 84 | ], 85 | 'do what first' 86 | ); 87 | 88 | }); 89 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --ui tdd 2 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | bench@~0.3.5: 6 | version "0.3.6" 7 | resolved "https://registry.yarnpkg.com/bench/-/bench-0.3.6.tgz#2f204bd2c0e3cc71c3ab4b6a0adbbc5c0ecad096" 8 | 9 | commander@0.6.1: 10 | version "0.6.1" 11 | resolved "https://registry.yarnpkg.com/commander/-/commander-0.6.1.tgz#fa68a14f6a945d54dbbe50d8cdb3320e9e3b1a06" 12 | 13 | commander@2.0.0: 14 | version "2.0.0" 15 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.0.0.tgz#d1b86f901f8b64bd941bdeadaf924530393be928" 16 | 17 | debug@*: 18 | version "3.1.0" 19 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" 20 | dependencies: 21 | ms "2.0.0" 22 | 23 | diff@1.0.7: 24 | version "1.0.7" 25 | resolved "https://registry.yarnpkg.com/diff/-/diff-1.0.7.tgz#24bbb001c4a7d5522169e7cabdb2c2814ed91cf4" 26 | 27 | glob@3.2.3: 28 | version "3.2.3" 29 | resolved "https://registry.yarnpkg.com/glob/-/glob-3.2.3.tgz#e313eeb249c7affaa5c475286b0e115b59839467" 30 | dependencies: 31 | graceful-fs "~2.0.0" 32 | inherits "2" 33 | minimatch "~0.2.11" 34 | 35 | graceful-fs@~2.0.0: 36 | version "2.0.3" 37 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-2.0.3.tgz#7cd2cdb228a4a3f36e95efa6cc142de7d1a136d0" 38 | 39 | growl@1.7.x: 40 | version "1.7.0" 41 | resolved "https://registry.yarnpkg.com/growl/-/growl-1.7.0.tgz#de2d66136d002e112ba70f3f10c31cf7c350b2da" 42 | 43 | inherits@2: 44 | version "2.0.3" 45 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 46 | 47 | jade@0.26.3: 48 | version "0.26.3" 49 | resolved "https://registry.yarnpkg.com/jade/-/jade-0.26.3.tgz#8f10d7977d8d79f2f6ff862a81b0513ccb25686c" 50 | dependencies: 51 | commander "0.6.1" 52 | mkdirp "0.3.0" 53 | 54 | lru-cache@2: 55 | version "2.7.3" 56 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" 57 | 58 | minimatch@~0.2.11: 59 | version "0.2.14" 60 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.2.14.tgz#c74e780574f63c6f9a090e90efbe6ef53a6a756a" 61 | dependencies: 62 | lru-cache "2" 63 | sigmund "~1.0.0" 64 | 65 | mkdirp@0.3.0: 66 | version "0.3.0" 67 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e" 68 | 69 | mkdirp@0.3.5: 70 | version "0.3.5" 71 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.5.tgz#de3e5f8961c88c787ee1368df849ac4413eca8d7" 72 | 73 | mocha@~1.18.2: 74 | version "1.18.2" 75 | resolved "https://registry.yarnpkg.com/mocha/-/mocha-1.18.2.tgz#800848f8f7884c61eefcfa2a27304ba9e5446d0b" 76 | dependencies: 77 | commander "2.0.0" 78 | debug "*" 79 | diff "1.0.7" 80 | glob "3.2.3" 81 | growl "1.7.x" 82 | jade "0.26.3" 83 | mkdirp "0.3.5" 84 | 85 | ms@2.0.0: 86 | version "2.0.0" 87 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 88 | 89 | mustache@~2.2.1: 90 | version "2.2.1" 91 | resolved "https://registry.yarnpkg.com/mustache/-/mustache-2.2.1.tgz#2c40ca21c278f53150682bcf9090e41a3339b876" 92 | 93 | sigmund@~1.0.0: 94 | version "1.0.1" 95 | resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" 96 | --------------------------------------------------------------------------------