package/package.json000644 000765 000024 0000001130 13034235465013015 0ustar00000000 000000 { "name": "cjson", "description": "cjson - Commented JavaScript Object Notation. It is a json loader, which parses only valid json files, but with comments enabled. Useful for loading configs.", "version": "0.5.0", "repository": "git://github.com/kof/node-cjson.git", "keywords": [ "json", "parser", "comments", "config", "loader" ], "author": "Oleg Slobodskoi ", "engines": { "node": ">= 0.3.0" }, "dependencies": { "json-parse-helpfulerror": "^1.0.3" }, "scripts": { "test": "node ./test/test.js" }, "license": "MIT" } package/.npmignore000644 000765 000024 0000000023 12645405516012531 0ustar00000000 000000 node_modules *.log package/LICENSE000644 000765 000024 0000002067 12645405516011551 0ustar00000000 000000 Copyright (c) 2015 Oleg Slobodskoi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. package/index.js000644 000765 000024 0000014771 13034235547012214 0ustar00000000 000000 var fs = require('fs'), Path = require('path'), jph = require('json-parse-helpfulerror'); /** * Default options. * * @type {Object} */ exports.options = { // merge all passed/found config files, see `cjson.extend` merge: false, // allows you to do some string replacements, see `cjson.replace`. replace: null, // freeze config recursively, see `cjson.freeze` freeze: false, // you can use any other extension for your config files, f.e. *.cjson ext: '.json', // you can use any parser you want. the default uses JSON.parse for maximum // speed, if it throws it uses uses an alternative parser to give more // helpful errors parse: jph.parse } /** * Remove single and multilie comments. Make sure to * leave them inside of strings. * * @param {String} json file. * @return {String} json without comments. */ exports.decomment = function(str) { var i, curChar, nextChar, inString = false, inComment = false, newStr = ''; for (i = 0; i < str.length; ++i) { curChar = str.charAt(i); nextChar = str.charAt(i + 1); // it's either closing or opening inString and it is not escaped if (!inComment && curChar === '"' && str.charAt(i - 1) !== '\\') { inString = !inString; } // we are not inside of a string if (!inString) { // singleline comment start if (!inComment && curChar + nextChar === '//') { ++i; inComment = 1; // singleline comment end } else if (inComment === 1 && curChar === '\n') { inComment = false; // multiline comment start } else if (!inComment && curChar + nextChar === '/*') { ++i; inComment = 2; curChar = ''; // multiline comment end } else if (inComment === 2 && curChar + nextChar === '*/') { ++i; inComment = false; curChar = ''; } if (inComment) { curChar = ''; } } newStr += curChar; } return newStr; }; /** * Decomment the string and parse json. * * @param {String} json. * @param {Function} [reviver] will be called for every key and value at every * level of the final result. * @return {Object} parsed json object. */ exports.parse = function(str, reviver) { return exports.options.parse(exports.decomment(str), reviver); }; /** * Replace templates with data. {{toReplace}} * * @param {String} json. * @param {Object} data data hash. * @return {String} json string with replaced data. */ exports.replace = function(str, data) { return str.replace(/\{\{([^}]+)\}\}/g, function(match, search) { if (data.hasOwnProperty(search)) { // If the variable is an object, stringify it before replacement. // The false positive of "null" is fine in this case. if (typeof data[search] === 'object') { return JSON.stringify(data[search]); } return data[search]; } return match; }); }; /** * Merge objects to the first one * * @param {Boolean|Object} deep if set true, deep merge will be done. * @param {Object} obj1 any object. * @param {Object} obj2 any object. * @return {Object} target merged object. */ exports.extend = (function() { var toString = Object.prototype.toString, obj = '[object Object]'; return function extend(deep, obj1, obj2 /*, obj1, obj2, obj3 */) { // take first argument, if its not a boolean var args = arguments, i = deep === true ? 1 : 0, key, target = args[i]; for (++i; i < args.length; ++i) { for (key in args[i]) { if (deep === true && target[key] && // if not doing this check you may end in // endless loop if using deep option toString.call(args[i][key]) === obj && toString.call(target[key]) === obj) { // create a copy of target object to avoid subobjects changes target[key] = extend(deep, {}, target[key]); extend(deep, target[key], args[i][key]); } else { target[key] = args[i][key]; } } } return target; }; }()); /** * Freeze the object recursively. * * @param {Object} obj. * @return {Object} */ exports.freeze = function freeze(obj) { var key; if (obj instanceof Object) { for (key in obj) { freeze(obj[key]); } Object.freeze(obj); } }; /** * Load and parse a config file/files. * * @param {String|Array} path absolute path/paths to the file/files or dir. * @param {Object|Boolean} [options] if true, extend all jsons to the first one, * it can be also object {merge: true, replace: {key: 'value'}} * @return {Object} conf parsed json object. */ exports.load = function load(path, options) { var data, paths, conf; if (options === true) { options = {merge: true}; } options = exports.extend({}, exports.options, options); if (Array.isArray(path)) { conf = {}; path.forEach(function(path) { if (fs.existsSync(path)) { var data = load(path, options), filename; if (options.merge) { exports.extend(true, conf, data); } else { filename = Path.basename(path, options.ext); conf[filename] = data; } } }); return conf; } if (fs.statSync(path).isDirectory()) { paths = []; fs.readdirSync(path).forEach(function(filename) { var file = Path.join(path, filename); if (Path.extname(file) == options.ext && fs.statSync(file).isFile()) { paths.push(file); } }); return load(paths, options); } data = fs.readFileSync(path, 'utf-8'); // replace BOM Character data = data.replace(/\ufeff/g, ''); if (options.replace) { data = exports.replace(data, options.replace); } try { data = exports.parse(data); } catch(err) { err.message += '\nFile: "' + path + '"'; throw err; } if (options.freeze) { exports.freeze(data); } return data; }; package/.travis.yml000644 000765 000024 0000000052 12602217164012636 0ustar00000000 000000 language: node_js node_js: - 0.6 - 0.8package/Makefile000644 000765 000024 0000000051 12602217164012164 0ustar00000000 000000 test: node ./test/test.js .PHONY: test package/readme.md000644 000765 000024 0000007655 12645405516012333 0ustar00000000 000000 [![build status](https://secure.travis-ci.org/kof/node-cjson.png)](http://travis-ci.org/kof/node-cjson) ## CJSON (Commented JavaScript Object Notation) is a comments enabled json config loader. JSON has a good spec, is implemented in every language, has easy to read syntax and is much more powerful than ini files. JSON is perfect for writing config files, except of one problem - there is no comments, but sometimes config files get large and need to be commented. Well, you could just evaluate json file as a JavaScript using one-liner, right? The purpose of this module is to avoid dirty JavaScript configs and to enable clean, consistent, secure, portable and JSON valid notation. CJSON supports JavaScript style comments: singleline "//" and multiline "/**/". It takes care about comments inside of strings. Example of such shiny config file: ```javascript /* * This is my app configuration file. * */ { "host": "localhost", // app is listening on this port "port": 8888 } ``` ## API ### load the module var cjson = require('cjson'); ### cjson.load(path, [options]); Load config file from given path, array of paths or directory. Second parameter is optional and can be a boolean or object. - `path` {String|Array} absolute path to the file, array of paths or directory - `options` {Boolean|Object} optional options. If you pass `true` as second param, its the same like ` {merge: true}` and will merge all configs together. `options` defaults: ```javascript { // merge all passed/found config files, see `cjson.extend` merge: false, // allows you to do some string replacements, see `cjson.replace`. replace: null, // freeze config recursively, see `cjson.freeze` freeze: false, // you can use any other extension for your config files, f.e. .cjson ext: '.json', // you can use any parser you want. the default uses JSON.parse for maximum // speed, if it throws it uses uses an alternative parser to give more // helpful errors parse: jph.parse } ``` Examples: ```javascript // just one config var conf = cjson.load('/path/to/your/config.json'); // array of configs var conf = cjson.load(['/path/to/your/config1.json', '/path/to/your/config2.json']); //output { config1: {key1: 'value1'} config2: {key2: 'value2'} } // use optional merge parameter // array of configs var conf = cjson.load(['/path/to/your/config1.json', '/path/to/your/config2.json'], true); // output { key1: 'value1', key2: 'value2' } // load all config files from a directory var conf = cjson.load('/path/to/your/configs'); // overwriting dev config with production var paths = ['/path/to/conf.json']; if (process.env.NODE_ENV ==='production') { paths.push('/path/to/conf-prod.json'); } var conf = cjson.load(paths, true); ``` ### cjson.extend([deep], target, object1, [objectN]) Merge the contents of two or more objects together into the first object. - `deep` If true, the merge becomes recursive. - `target` The object to extend. It will receive the new properties. - `object1` An object containing additional properties to merge in. - `objectN` Additional objects containing properties to merge in. Example: ```javascript var object = cjson.extend({}, object1, object2); ``` ### cjson.decomment(str) Remove JavaScript style comments, singleline - '//' and multiline - '/**/'. It takes care about comments inside of strings and escaping. ### cjson.parse(str, [reviver]) Like `JSON.parse`, but it takes care about comments. Optional `reviver` argument is for `JSON.parse` method and will be called for every key and value at every level of the final result ### cjson.replace(str, obj) Replace all strings `{{key}}` contained in `{key: 'value'}`, where `key` can be any property of passed `obj`. Example: ```javascript var str = '{"path": "{{root}}/src"}'; // json file contents cjson.replace(str, {root: '/usr'}); // '{"path": "/usr/src"}' ``` ### cjson.freeze(obj) Recursively freeze an object. ## Installation npm install cjson package/test/test.js000644 000765 000024 0000011351 13034235547013032 0ustar00000000 000000 var a = require('assert'); var cjson = require('../'); var fixtures = __dirname + '/fixtures'; var data = { conf1: {key: 'value'}, conf2: {key: 'value'}, conf3: {key: 'value'}, conf4: { "//key" : "value", "key": "//value", "/*key": "value", "key": "/*value*/" }, conf5: {"'key/*test*/'": "'value//test'"}, conf6: {"key\"/*test*/": "value\"//test"}, conf7: {"key": "{{root}}/src"}, conf8: {}, conf10: {"test":"valid JSON, except for the the hidden BOM character"}, conf11: { "subobject": { "test": 5 }, "subarray": [ { "foo": 7 } ] }, conf12: { "num": 1, "str": "moo", "bool": false, "arr": [], "obj": {}, "null": null } }; a.doesNotThrow(function() { cjson.load(fixtures + '/conf1.json'); }, 'valid config loaded'); a.deepEqual(cjson.load(fixtures + '/conf1.json'), data.conf1, 'data is correct'); a.deepEqual(cjson.load(fixtures + '/conf2.json'), data.conf2, 'singleline comment'); a.deepEqual(cjson.load(fixtures + '/conf3.json'), data.conf3, 'multiline comment'); a.deepEqual(cjson.load(fixtures + '/conf4.json'), data.conf4, 'comments inside of a string'); a.deepEqual(cjson.load(fixtures + '/conf5.json'), data.conf5, 'single and double quotes mix'); a.deepEqual(cjson.load(fixtures + '/conf6.json'), data.conf6, 'escaped double quote inside of a string'); a.deepEqual(cjson.load(fixtures + '/conf7.json', {replace: {root: '/usr'}}), {"key": "/usr/src"}, 'tmpl replacement'); a.deepEqual(cjson.load(fixtures + '/conf8.json'), data.conf8, 'string-like comment'); a.deepEqual(cjson.load(fixtures + '/conf10.json'), data.conf10, 'BOM character'); var conf11 = cjson.load(fixtures + '/conf11.json'); a.deepEqual(cjson.load(fixtures + '/templates/conf11tmpl.json', {replace: conf11}), data.conf11, 'object and array as template variables'); var conf12 = cjson.load(fixtures + '/conf12.json'); a.deepEqual(cjson.load(fixtures + '/templates/conf12tmpl.json', {replace: conf12}), data.conf12, 'JSON types as template variables'); var data1 = { conf1: {key: 'value'}, conf6: data.conf6 }; a.deepEqual(cjson.load([fixtures + '/conf1.json', fixtures + '/conf6.json']), data1, 'load array of jsons'); var data2 = { key: 'value', "key\"/*test*/": "value\"//test" }; a.deepEqual(cjson.load([fixtures + '/conf1.json', fixtures + '/conf6.json'], true), data2, 'load array of jsons and merge them'); a.deepEqual(cjson.load(fixtures), data, 'load all and merge them'); a.deepEqual( cjson.load([ fixtures + '/conf1.json', 'missing-conf.json', fixtures + '/conf6.json' ], true), data2, 'load a missing config file among an array of config files and merge them' ); a.deepEqual(cjson.load(fixtures, {ext: '.cjson'}), {conf9: {a: 1}}, 'use custom ext'); var str = require('fs').readFileSync(fixtures + '/conf2.json').toString(); a.deepEqual(cjson.parse(str), data.conf2, '.parse method with comments'); (function errors() { try { cjson.load(fixtures + '/errors/invalid.cjson'); } catch (e) { // Taking the first line from the error message var message = e.message.split('\n')[0]; // The error we expect is that the value ended with \n instead of quotes a.equal( message.trim().toLowerCase(), "unexpected token '}' at 2:17", 'Assert that the error message is properly formatted.' ); } }()); (function extend() { a.deepEqual(cjson.extend({test1: 1}, {test2: 2}), {test1: 1, test2: 2}, 'extend 2 simple objects'); a.deepEqual(cjson.extend({test1: 1}, {test2: 2}, {test3: 3}), {test1: 1, test2: 2, test3: 3}, 'extend 3 simple objects'); a.deepEqual(cjson.extend({test1: 1}, true), {test1: 1}, '2 arg is not an object'); a.deepEqual(cjson.extend( true, {test1: {test1: 1}}, {test1: {test2: 2} } ), { test1: {test1: 1, test2: 2} }, 'deep extend' ); a.deepEqual(cjson.extend( true, {test: {test: 'test'}}, {test: {test: 'test'} } ), {test: {test: 'test'} }, 'deep extend, check endless lop' ); var data1 = {a: {b: 1}}, data2 = {a: {b: 2}}; cjson.extend(true, {}, data1, data2); a.notDeepEqual(data1, data2, 'original deep object is not mangled'); }()); (function freeze() { var data1 = {a: {b: 1}}, data2 = {a: {b: 1}}; cjson.freeze(data1); data1.abc = 123; data1.a = 123; a.deepEqual(data1, data2, 'data1 wasn\'t changed'); data1 = cjson.load(fixtures + '/conf1.json', {freeze: true}), data2 = cjson.load(fixtures + '/conf1.json', {freeze: true}); data1.abc = 123; data1.a = 123; a.deepEqual(data1, data2, 'data1 wasn\'t changed'); }()); console.log('All tests passed.'); package/test/fixtures/conf5.json000644 000765 000024 0000000050 12674052646015273 0ustar00000000 000000 { "'key/*test*/'": "'value//test'" }package/test/fixtures/conf1.json000644 000765 000024 0000000027 12674052646015273 0ustar00000000 000000 { "key": "value" } package/test/fixtures/conf11.json000644 000765 000024 0000000157 12674052657015362 0ustar00000000 000000 { "subobject": { "test": 5 }, "subarray": [ { "foo": 7 } ] }package/test/fixtures/conf12.json000644 000765 000024 0000000145 12674052657015360 0ustar00000000 000000 { "num": 1, "str": "moo", "bool": false, "arr": [], "obj": {}, "null": null }package/test/fixtures/conf2.json000644 000765 000024 0000000062 12674052646015273 0ustar00000000 000000 { // single line comment "key": "value" } package/test/fixtures/conf3.json000644 000765 000024 0000000131 12674052646015271 0ustar00000000 000000 { /* multiline comment */ "key": "value" /* multiline comment */ } package/test/fixtures/conf4.json000644 000765 000024 0000000136 12674052646015277 0ustar00000000 000000 { "//key" : "value", "key": "//value", "/*key": "value", "key": "/*value*/" } package/test/fixtures/conf10.json000644 000765 000024 0000000163 12674052646015354 0ustar00000000 000000 { // First character of this file is BOM "test": "valid JSON, except for the the hidden BOM character" }package/test/fixtures/conf6.json000644 000765 000024 0000000050 12674052646015274 0ustar00000000 000000 { "key\"/*test*/": "value\"//test" }package/test/fixtures/conf7.json000644 000765 000024 0000000036 12674052646015301 0ustar00000000 000000 { "key": "{{root}}/src" } package/test/fixtures/conf8.json000644 000765 000024 0000000022 12674052646015275 0ustar00000000 000000 { // "foo": 1 } package/test/fixtures/conf9.cjson000644 000765 000024 0000000017 12674052646015445 0ustar00000000 000000 { "a": 1 } package/test/fixtures/errors/invalid.cjson000644 000765 000024 0000000024 12674052646017367 0ustar00000000 000000 { "foo": "bar } package/test/fixtures/templates/conf11tmpl.json000644 000765 000024 0000000100 12674052657020241 0ustar00000000 000000 { "subobject": {{subobject}}, "subarray": {{subarray}} }package/test/fixtures/templates/conf12tmpl.json000644 000765 000024 0000000200 12674052657020243 0ustar00000000 000000 { "num": {{num}}, "str": "{{str}}", "bool": {{bool}}, "arr": {{arr}}, "obj": {{obj}}, "null": {{null}} }