├── .gitignore ├── .travis.yml ├── AUTHORS ├── README.md ├── bower.json ├── examples ├── demo.css ├── demo.html └── simple.js ├── index.js ├── lib └── jsurl.js ├── package.json └── test ├── common └── jsurlTest.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.testlog 2 | /node_modules/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.2" 4 | - "0.12" 5 | - "0.10" 6 | sudo: false 7 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Authors ordered by first contribution. 2 | 3 | Bruno Jouhier 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## JSURL 2 | 3 | JSURL is an alternative to JSON + URL encoding (or JSON + base64 encoding). 4 | It makes it handy to pass complex values via URL query parameters. 5 | 6 | JSURL has been designed to be: 7 | 8 | * Compact: its output is much more compact than JSON + URL encoding (except in pathological cases). 9 | It is even often slightly more compact than regular JSON! 10 | * Readable: its output is much more readable than JSON + URL encoding. 11 | * Foolproof: its output only contains characters that are unaffected by URL encoding/decoding. 12 | There is no risk of missing a URL encoding/decoding pass, or of messing up a JSURL string by applying 13 | an extra URL encoding or decoding pass. 14 | * Easy to generate and parse 15 | 16 | ## Syntax 17 | 18 | Think of it as JSON with the following changes: 19 | 20 | * Curly braces (`{` and `}`) replaced by parentheses (`(` and `)`) 21 | * Square brackets (`[` and `]`) replaced by `(~` and `)` 22 | * Property names unquoted (but escaped -- see below). 23 | * String values prefixed by a single quote (`'`) and escaped 24 | * All other JSON punctuation (colon `:` and comma `,`) replaced by tildes (`~`) 25 | * An extra tilde (`~`) at the very beginning. 26 | 27 | Property names and string values are escaped as follows: 28 | 29 | * Letters, digits, underscore (`_`), hyphen (`-`) and dot (`.`) are preserved. 30 | * Dollar sign (`$`) is replaced by exclamation mark (`!`) 31 | * Other characters with UNICODE value <= `0xff` are encoded as `*XX` 32 | * Characters with UNICODE value > `0xff` are encoded as `**XXXX` 33 | 34 | ## Examples 35 | 36 | JSON: 37 | 38 | ``` json 39 | {"name":"John Doe","age":42,"children":["Mary","Bill"]} 40 | ``` 41 | 42 | JSON + URL encoding: 43 | 44 | ``` 45 | %7B%22name%22%3A%22John%20Doe%22%2C%22age%22%3A42%2C%22children%22%3A%5B%22Mary%22%2C%22Bill%22%5D%7D 46 | ``` 47 | 48 | JSURL: 49 | 50 | ``` jsurl 51 | ~(name~'John*20Doe~age~42~children~(~'Mary~'Bill)) 52 | ``` 53 | 54 | ## API 55 | 56 | ``` javascript 57 | var JSURL = require("jsurl"); 58 | 59 | str = JSURL.stringify(obj); 60 | obj = JSURL.parse(str); 61 | 62 | // return def instead of throwing on error 63 | obj = JSURL.tryParse(str[, def]); 64 | ``` 65 | 66 | # Installation 67 | 68 | The easiest way to install `jsurl` is with NPM: 69 | 70 | ```sh 71 | npm install jsurl 72 | ``` 73 | 74 | ## License 75 | 76 | This work is licensed under the [MIT license](http://en.wikipedia.org/wiki/MIT_License). 77 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsurl", 3 | "main": "index.js", 4 | "version": "0.1.0", 5 | "homepage": "https://github.com/Sage/jsurl", 6 | "authors": [ 7 | "Bruno Jouhier" 8 | ], 9 | "description": "URL friendly JSON-like formatting and parsing", 10 | "license": "MIT", 11 | "ignore": [ 12 | "**/.*", 13 | "node_modules", 14 | "bower_components", 15 | "test", 16 | "tests" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /examples/demo.css: -------------------------------------------------------------------------------- 1 | h2 { 2 | padding: 0 0 0.3em 0; 3 | margin: 0; 4 | } 5 | 6 | input[type="button"] { 7 | margin: 0.5em; 8 | } 9 | 10 | textarea { 11 | font-family: Courier; 12 | font-size: small; 13 | margin: 0.5em; 14 | padding: 0.1em; 15 | width: 100%; 16 | } 17 | 18 | table { 19 | background-color: #D2E0E6; 20 | border: 0; 21 | } 22 | 23 | tr { 24 | margin: 0; 25 | padding: 0; 26 | border: 0; 27 | } 28 | 29 | td { 30 | font-family: Calibri, Helvetica, Arial; 31 | margin: 0; 32 | padding: 1.0em 1.0em; 33 | border: 0; 34 | text-align: center; 35 | font-size: small; 36 | color: #2b81af; 37 | } 38 | 39 | td.input { 40 | } 41 | -------------------------------------------------------------------------------- /examples/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | JSURL Test 5 | 6 | 7 | 28 | 29 | 30 |
31 | 32 | 33 | 39 | 44 | 48 | 49 |
34 |

JSON

35 | 36 |
37 | 38 |
40 | 41 |
42 | 43 |
45 |

JSURL

46 | 47 |
50 |
51 | 52 | -------------------------------------------------------------------------------- /examples/simple.js: -------------------------------------------------------------------------------- 1 | var JSURL = require('jsurl'); 2 | 3 | var obj = { 4 | name: "John Doe", 5 | age: 42, 6 | children: ["Mary", "Bill"] 7 | }; 8 | 9 | console.log("JSON =" + JSON.stringify(obj)); 10 | console.log("JSURL =" + JSURL.stringify(obj)); 11 | console.log("JSON+URL=" + encodeURIComponent(JSON.stringify(obj))); 12 | console.log("RTRIP =" + JSON.stringify(JSURL.parse(JSURL.stringify(obj)))); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/jsurl'); -------------------------------------------------------------------------------- /lib/jsurl.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2011 Bruno Jouhier 3 | * 4 | * Permission is hereby granted, free of charge, to any person 5 | * obtaining a copy of this software and associated documentation 6 | * files (the "Software"), to deal in the Software without 7 | * restriction, including without limitation the rights to use, 8 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the 10 | * Software is furnished to do so, subject to the following 11 | * conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be 14 | * included in all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | * OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | // 26 | (function(exports) { 27 | "use strict"; 28 | exports.stringify = function stringify(v) { 29 | function encode(s) { 30 | return !/[^\w-.]/.test(s) ? s : s.replace(/[^\w-.]/g, function(ch) { 31 | if (ch === '$') return '!'; 32 | ch = ch.charCodeAt(0); 33 | // thanks to Douglas Crockford for the negative slice trick 34 | return ch < 0x100 ? '*' + ('00' + ch.toString(16)).slice(-2) : '**' + ('0000' + ch.toString(16)).slice(-4); 35 | }); 36 | } 37 | 38 | var tmpAry; 39 | 40 | switch (typeof v) { 41 | case 'number': 42 | return isFinite(v) ? '~' + v : '~null'; 43 | case 'boolean': 44 | return '~' + v; 45 | case 'string': 46 | return "~'" + encode(v); 47 | case 'object': 48 | if (!v) return '~null'; 49 | 50 | tmpAry = []; 51 | 52 | if (Array.isArray(v)) { 53 | for (var i = 0; i < v.length; i++) { 54 | tmpAry[i] = stringify(v[i]) || '~null'; 55 | } 56 | 57 | return '~(' + (tmpAry.join('') || '~') + ')'; 58 | } else { 59 | for (var key in v) { 60 | if (v.hasOwnProperty(key)) { 61 | var val = stringify(v[key]); 62 | 63 | // skip undefined and functions 64 | if (val) { 65 | tmpAry.push(encode(key) + val); 66 | } 67 | } 68 | } 69 | 70 | return '~(' + tmpAry.join('~') + ')'; 71 | } 72 | default: 73 | // function, undefined 74 | return; 75 | } 76 | }; 77 | 78 | var reserved = { 79 | "true": true, 80 | "false": false, 81 | "null": null 82 | }; 83 | 84 | exports.parse = function(s) { 85 | if (!s) return s; 86 | s = s.replace(/%(25)*27/g, "'"); 87 | var i = 0, 88 | len = s.length; 89 | 90 | function eat(expected) { 91 | if (s.charAt(i) !== expected) throw new Error("bad JSURL syntax: expected " + expected + ", got " + (s && s.charAt(i))); 92 | i++; 93 | } 94 | 95 | function decode() { 96 | var beg = i, 97 | ch, r = ""; 98 | while (i < len && (ch = s.charAt(i)) !== '~' && ch !== ')') { 99 | switch (ch) { 100 | case '*': 101 | if (beg < i) r += s.substring(beg, i); 102 | if (s.charAt(i + 1) === '*') r += String.fromCharCode(parseInt(s.substring(i + 2, i + 6), 16)), beg = (i += 6); 103 | else r += String.fromCharCode(parseInt(s.substring(i + 1, i + 3), 16)), beg = (i += 3); 104 | break; 105 | case '!': 106 | if (beg < i) r += s.substring(beg, i); 107 | r += '$', beg = ++i; 108 | break; 109 | default: 110 | i++; 111 | } 112 | } 113 | return r + s.substring(beg, i); 114 | } 115 | 116 | return (function parseOne() { 117 | var result, ch, beg; 118 | eat('~'); 119 | switch (ch = s.charAt(i)) { 120 | case '(': 121 | i++; 122 | if (s.charAt(i) === '~') { 123 | result = []; 124 | if (s.charAt(i + 1) === ')') i++; 125 | else { 126 | do { 127 | result.push(parseOne()); 128 | } while (s.charAt(i) === '~'); 129 | } 130 | } else { 131 | result = {}; 132 | if (s.charAt(i) !== ')') { 133 | do { 134 | var key = decode(); 135 | result[key] = parseOne(); 136 | } while (s.charAt(i) === '~' && ++i); 137 | } 138 | } 139 | eat(')'); 140 | break; 141 | case "'": 142 | i++; 143 | result = decode(); 144 | break; 145 | default: 146 | beg = i++; 147 | while (i < len && /[^)~]/.test(s.charAt(i))) 148 | i++; 149 | var sub = s.substring(beg, i); 150 | if (/[\d\-]/.test(ch)) { 151 | result = parseFloat(sub); 152 | } else { 153 | result = reserved[sub]; 154 | if (typeof result === "undefined") throw new Error("bad value keyword: " + sub); 155 | } 156 | } 157 | return result; 158 | })(); 159 | } 160 | 161 | exports.tryParse = function(s, def) { 162 | try { 163 | return exports.parse(s); 164 | } catch (ex) { 165 | return def; 166 | } 167 | } 168 | 169 | })(typeof exports !== 'undefined' ? exports : (window.JSURL = window.JSURL || {})); 170 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsurl", 3 | "description": "URL friendly JSON-like formatting and parsing", 4 | "version": "0.1.4", 5 | "license": "MIT", 6 | "homepage": "http://github.com/Sage/jsurl", 7 | "author": "Bruno Jouhier", 8 | "repository": { 9 | "type": "git", 10 | "url": "git://github.com/Sage/jsurl.git" 11 | }, 12 | "devDependencies": { 13 | "qunit": "^0.7.7" 14 | }, 15 | "directories": {"lib": "./lib" }, 16 | "scripts": { 17 | "test": "node test" 18 | }, 19 | "main": "index.js" 20 | } 21 | -------------------------------------------------------------------------------- /test/common/jsurlTest.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | QUnit.module(module.id); 3 | 4 | var JSURL = require("../.."); 5 | var undefined; 6 | 7 | function t(v, r) { 8 | strictEqual(JSURL.stringify(v), r, "stringify " + (typeof v !== 'object' ? v : JSON.stringify(v))); 9 | strictEqual(JSURL.stringify(JSURL.parse(JSURL.stringify(v))), r, "roundtrip " + (typeof v !== 'object' ? v : JSON.stringify(v))); 10 | } 11 | 12 | test('basic values', 26, function() { 13 | t(undefined, undefined); 14 | t(function() { 15 | foo(); 16 | }, undefined); 17 | t(null, "~null"); 18 | t(false, "~false"); 19 | t(true, "~true"); 20 | t(0, "~0"); 21 | t(1, "~1"); 22 | t(-1.5, "~-1.5"); 23 | t("hello world\u203c", "~'hello*20world**203c"); 24 | t(" !\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~", "~'*20*21*22*23!*25*26*27*28*29*2a*2b*2c-.*2f09*3a*3b*3c*3d*3e*3f*40AZ*5b*5c*5d*5e_*60az*7b*7c*7d*7e"); 25 | // JSON.stringify converts special numeric values to null 26 | t(NaN, "~null"); 27 | t(Infinity, "~null"); 28 | t(-Infinity, "~null"); 29 | }); 30 | test('arrays', 4, function() { 31 | t([], "~(~)"); 32 | t([undefined, function() { 33 | foo(); 34 | }, 35 | null, false, 0, "hello world\u203c"], "~(~null~null~null~false~0~'hello*20world**203c)"); 36 | }); 37 | test('objects', 4, function() { 38 | t({}, "~()"); 39 | t({ 40 | a: undefined, 41 | b: function() { 42 | foo(); 43 | }, 44 | c: null, 45 | d: false, 46 | e: 0, 47 | f: "hello world\u203c" 48 | }, "~(c~null~d~false~e~0~f~'hello*20world**203c)"); 49 | }); 50 | test('mix', 2, function() { 51 | t({ 52 | a: [ 53 | [1, 2], 54 | [], {}], 55 | b: [], 56 | c: { 57 | d: "hello", 58 | e: {}, 59 | f: [] 60 | } 61 | }, "~(a~(~(~1~2)~(~)~())~b~(~)~c~(d~'hello~e~()~f~(~)))"); 62 | }); 63 | 64 | test('percent-escaped single quotes', 1, function() { 65 | deepEqual(JSURL.parse("~(a~%27hello~b~%27world)"), { 66 | a: 'hello', 67 | b: 'world' 68 | }); 69 | }); 70 | 71 | test('percent-escaped percent-escaped single quotes', 1, function() { 72 | deepEqual(JSURL.parse("~(a~%2527hello~b~%2525252527world)"), { 73 | a: 'hello', 74 | b: 'world' 75 | }); 76 | }); 77 | 78 | test('tryParse', 4, function() { 79 | strictEqual(JSURL.tryParse("~null"), null); 80 | strictEqual(JSURL.tryParse("~1", 2), 1); 81 | strictEqual(JSURL.tryParse("1"), undefined); 82 | strictEqual(JSURL.tryParse("1", 0), 0); 83 | }); 84 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require('fs'); 4 | var fsp = require('path'); 5 | 6 | var root = fsp.join(__dirname, 'common'); 7 | var tests = fs.readdirSync(root).filter(function(file) { 8 | return /\.js$/.test(file); 9 | }).map(function(file) { 10 | return fsp.join(root, file); 11 | }); 12 | 13 | var testrunner = require("qunit"); 14 | 15 | process.on('uncaughtException', function(err) { 16 | console.error(err.stack); 17 | process.exit(1); 18 | }); 19 | 20 | testrunner.run({ 21 | code: '', 22 | tests: tests, 23 | }, function(err) { 24 | if (err) throw err; 25 | }); 26 | --------------------------------------------------------------------------------